Commit-execution split
Suggest an editSome actions cannot be executed in the moment they are committed. A bet needs a random number that nobody knows yet, and a market order needs a price that nobody has quoted yet. On Solana a transaction either does everything now or nothing, and the sender sees the result before sending it. An outcome that the sender must not choose cannot be decided in the sender's transaction.
The transaction that only wins
Suppose we write the simplest coin flip. The user sends one transaction with the bet, the program reads something that looks random, say the hash of the current slot, and pays the win in the same instruction.
The problem is that the user does not have to send this transaction blindly. Anyone can simulate a transaction before sending it. The simulation is free, and it shows exactly what the transaction would do. So the user simulates the bet. If the simulation shows a loss, they do not send it. A slot later they simulate again, and they send the transaction only when it shows a win.
The program did nothing wrong: the randomness is real, and the check is correct. The user still wins every time, because the user decides which transactions happen and the same user can see what will happen after their transaction before they send it.
Commit now, execute later
The fix is the commit-execution split: the action becomes two transactions.
In the first transaction the user commits. They place the bet, the stake is locked, and the program records that a round is open. At this moment nobody knows the outcome, neither the user nor the program, because the thing that decides it does not exist yet.
In the second transaction the action is executed. The random number arrives, or the price is set, and the program settles the round against the commitment that is already locked. The user cannot back out, because the stake is already in.
Two things have to be true for this to work. First, the outcome must not exist at commit time. If it did, the user would simulate the commit transaction instead. Second, the user must not be the one who decides whether the second transaction happens. If they were, they would execute only the rounds they win. So the second transaction is sent by someone else: an oracle, a maker, or any user who is paid for it. If the user has to send it themselves, they can cheat by never sending a losing one.
Switchboard: the seed is fixed before the number exists
A coin flip on Switchboard's randomness does this in two transactions.
In the first transaction the player picks a side and pays, and the program asks Switchboard to commit. Switchboard writes the hash of the previous slot into a randomness account. This hash is the seed. The random value will be computed from this seed and the oracle's key, inside the oracle's secure hardware. This means the oracle cannot change the value, and the player cannot compute it.
In the second transaction the oracle's signed value is revealed, and the program settles the bet right there. The player cannot simulate this transaction into a win, because nothing the player controls decides the value.
The program also keeps the round open until the reveal settles it. Otherwise the player could leave a losing round behind and start a new one.
Drift: the order is the commit, the fill is the execution
Drift is a perpetuals exchange, and a market order on Drift is not filled in the transaction that places it.
Instead, the order is written into the trader's account together with an auction: a start price, an end price, and a duration of a few seconds. During the auction, makers send their own transactions and fill the order at a price inside that range. Whatever is left at the end of the auction goes to the order book and then to the AMM.
The trader committed to the order and to the price range. The fills are executed by other people. There is nothing for the trader to simulate, because the fill price does not exist when they sign.
What the split costs
Between the commit and the execution, the protocol has to remember that something is open: a round, an order, a request. That means an account for each of them, and someone pays rent for it.
Someone also has to send the second transaction. The design has to say who that is and why they will do it.
And every open commit needs an exit for the case when nobody executes it. Usually this is a deadline after which the stake returns to the user. The exit must not leak the outcome. If a player could cancel after the reveal, they would cancel every loss. So a cancel is allowed only while the outcome does not exist yet, or after a timeout when the oracle did not answer at all.
Assume the user looks first
The user always looks at the result before sending a transaction. So every action gets the same question: would the user still send it after seeing the result? For a transfer or a deposit the answer is yes, because the result is the same either way. For a bet or a fill the answer is no, because the user would send only the good results. Those actions are committed first and decided later, in a transaction the user does not choose.