Sign in

Commit-execution split

Suggest an edit

Some actions that users commit cannot be executed in the same instant, whether for security or architectural reasons. For those cases, the best practical approach is to split the actions into commit and execute phases.

The best example is your typical program with a random outcome. You can't implement a secure program that lets a user get the random outcome in the same transaction as they're submitting. Because if you did, the user could simply simulate transactions many times and only send them those few times when they actually succeed to produce the winning outcome for them.

In fact, because of it, you also can't let them have the authority of being the sender of that "execute" part of the transaction at all. So naturally, you want to split it into "commit" and "execute" types of transactions, because you can't let them have the "execute" part.

Commit-execution splitTwo transactions on a timeline. The first, from the user, locks the bet with a padlock while the outcome is still unknown, shown as a question mark. The second, from someone else, holds the dice and settles the result. Commit-execution split time user tx 1: commit ? unknown someone else tx 2: execute The user commits before the outcome exists. The outcome arrives in a transaction the user does not choose.

Examples

Switchboard VRF (coinflip). In the first transaction players pick a side, pay, and the program creates a request for a random number. Nobody knows the number yet, not the player, not the operator. In the second transaction the oracle (not the player!) writes the number into that request, and the program settles the bet the resolution of which is already locked. The player cannot start a new round until this one is settled, so there is no way to abandon a losing bet.

The same pattern can be seen in other protocols too. In Parcl, orders collect over an interval and clear at one price, so nobody gains from being first. In Drift, before the pool fills an order, makers get a short window to bid for it.

In code

The action is two instructions. The player signs the commit, and a different signer brings the result in the execute.

rust
// transaction 1: the player commits and the stake is locked; the outcome does not exist yet
pub fn commit(ctx: Context<Commit>, side: Side, stake: u64) -> Result<()> {
    lock_stake(&ctx, stake)?;
    ctx.accounts.round.side = side;
    Ok(())
}

// transaction 2: the oracle, not the player, brings the result and settles the round
pub fn execute(ctx: Context<Execute>, random: u64) -> Result<()> {
    require!(ctx.accounts.signer.key() == ctx.accounts.round.oracle, Error::NotOracle);
    settle(&ctx, random)
}