-
Notifications
You must be signed in to change notification settings - Fork 39
Migration Guide (v1→v2)
Not all breaking changes affect all applications.
To identify which breaking changes might affect your application, see if any of the following cases apply.
- handles ERC-20 token deposit inputs. See the ERC-20 token deposit inputs section.
- handles application address relay inputs. See the Application address section.
- generates Ether withdrawal vouchers. See the Ether withdrawal vouchers section.
- validates notices. See the Outputs section.
- executes vouchers. See the Outputs section.
- listens to voucher execution events. See the Outputs section.
- checks if a voucher was executed. See the Outputs section.
In SDK v1, ERC-20 token deposit inputs start with a 1-byte Boolean field which indicates whether the transfer was successful or not.
{
success: Byte[1],
tokenAddress: Byte[20],
senderAddress: Byte[20],
amount: Byte[32],
execLayerData: Byte[],
}
We realized this design could lead to programming errors, in which this field is not checked, and failed transactions could be wrongfully accepted.
To solve this issue, in SDK v2, we modified the ERC-20 portal to only accept successful transactions. With this change, the success
field would always be true. So, to avoid confusion and save space, we removed it.
{
tokenAddress: Byte[20],
senderAddress: Byte[20],
amount: Byte[32],
execLayerData: Byte[],
}
In SDK v1, Ether withdrawals were issued as vouchers targeting the application contract, and calling the withdrawEther
function.
{
destination: applicationAddress,
payload: abi.encodeWithSignature("withdrawEther(address,uint256)", recipient, value),
}
In SDK v2, we have removed this function in favor of a simpler way, which uses the newly-added value
field.
{
destination: recipient,
payload: "0x",
value: value,
}
Note
This value field can be used to pass Ether to payable functions.
In SDK v1, the application address could be sent to the machine via an input through the DAppAddressRelay
contract.
In SDK v2, however, the application address has been added to the input metadata. Therefore, it's available in every input.
This change allowed us to remove the DAppAddressRelay
contract.
Here is an example of a finish request response, according to the OpenAPI interface of the Rollup HTTP server (source). Here, the field of interest is data.metadata.app_contract
.
{
"request_type": "advance_state",
"data": {
"metadata": {
"chain_id": 42,
"app_contract": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
"msg_sender": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
"input_index": 123,
"block_number": 10000000,
"block_timestamp": 1588598533000,
"prev_randao": "0x0000000000000000000000000000000000000000000000000000000000000001"
},
"payload": "0xdeadbeef"
}
}
In SDK v1, the only types of verifiable outputs the machine could generate were notices and vouchers. Each output type was stored in a different buffer inside the machine, and in a different tree in the proof structure. With this design, adding or modifying output types would require changes that would cascade in all levels of the SDK.
In SDK v2, vouchers and notices are now stored in the same buffer inside the machine, and in the same tree of the proof structure. They are essentially arbitrary byte arrays now! To distinguish between outputs of different types, they are encoded as Solidity function calls, each with its own signature. We are very careful with hash collisions, since Solidity only uses 32 bits of entropy for function selectors.
Now, any output can be validated, not just notices.
We've also added a new type of executable output: DELEGATECALL
vouchers.
With this refactoring, which we call "Output Unification", the Output API has changed dramatically. Adapting applications to the new API, however, should be possible nevertheless. In the following subsections, we will go into more detail.
Outputs are encoded as Solidity function calls.
For supported signatures, see the Outputs
interface.
Encoding and decoding Solidity function call data is widely supported by Ethereum libraries.
For JavaScript/TypeScript, we personally recommend the ones from wevm (viem
, wagmi
, ABIType
). Here are some examples:
// Notice with payload "Hello, World!" encoded using ASCII
Notice(hex"48656c6c6f2c20576f726c6421") // 0xc258d6e50000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000d48656c6c6f2c20576f726c642100000000000000000000000000000000000000
// Voucher to WETH token contract (on Mainnet), passing 1 ETH, calling the deposit() function
Voucher(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2, 1000000000000000000, hex"d0e30db0") // 0x237a816f000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000004d0e30db000000000000000000000000000000000000000000000000000000000
// DELEGATECALL voucher to Safe ERC-20 library (on Sepolia), calling the safeTransfer(address,address,uint256) function,
// passing as argument the address of the CTSI token contract,
// the address that the ENS name "vitalik.eth" resolves to (on Mainnet, as of this writing),
// and the value of 1 CTSI
DelegateCallVoucher(0x817b126F242B5F184Fa685b4f2F91DC99D8115F9, hex"d1660f99000000000000000000000000491604c0fdf08347dd1fa4ee062a822a5dd06b5d000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa960450000000000000000000000000000000000000000000000000de0b6b3a7640000") // 0x10321e8b000000000000000000000000817b126f242b5f184fa685b4f2f91dc99d8115f900000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000064d1660f99000000000000000000000000491604c0fdf08347dd1fa4ee062a822a5dd06b5d000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa960450000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000
The proof structure has changed significantly. However, those are internal changes which should not impact application developers. So, as long as you use the proofs as provided by the GraphQL server, you should be fine. :-)
If you're still curious as to how the proofs have changed, here is how the proof structure looked like in SDK v1.
We used to use the Proof
structure in all functions that required a proof.
struct Proof {
OutputValidityProof validity;
bytes context;
}
struct OutputValidityProof {
uint64 inputIndexWithinEpoch;
uint64 outputIndexWithinInput;
bytes32 outputHashesRootHash;
bytes32 vouchersEpochRootHash;
bytes32 noticesEpochRootHash;
bytes32 machineStateHash;
bytes32[] outputHashInOutputHashesSiblings;
bytes32[] outputHashesInEpochSiblings;
}
In SDK2, we now use the following OutputValidityProof
structure instead,
which is a lot simpler!
struct OutputValidityProof {
uint64 outputIndex;
bytes32[] outputHashesSiblings;
}
In SDK v1, only notices could be validated through the validateNotice
function.
function validateNotice(
bytes calldata notice,
Proof calldata proof
) external view returns (bool success);
In SDK v2, any output can be validated with the function validateOutput
.
function validateOutput(
bytes calldata output,
OutputValidityProof calldata proof
) external view;
Here, note that the output
parameter is a Solidity-encoded function call.
We removed the Boolean return value to avoid confusion, as it would either return true
or revert.
In SDK v1, only vouchers could be executed through the executeVoucher
function.
function executeVoucher(
address destination,
bytes calldata payload,
Proof calldata proof
) external returns (bool success);
In SDK v2, besides the traditional CALL
vouchers, we've added support to DELEGATECALL
vouchers.
These can be executed through the executeOutput
function.
function executeOutput(
bytes calldata output,
OutputValidityProof calldata proof
) external;
Here, the output
parameter is also the Solidity-encoded function call.
We have also removed the Boolean return value for the same reason as for the validation entry point.
In SDK v1, whenever a voucher was executed, a VoucherExecution
event would be emitted.
event VoucherExecuted(uint256 voucherId);
This voucherId
was kind of opaque. In reality, it was a combination of the input index and the output index.
In SDK v2, this event was renamed as OutputExecuted
, and the parameters have also changed.
event OutputExecuted(uint64 outputIndex, bytes output);
Instead of a "voucher ID", the new event includes the output index, and the output itself.
Notice how outputs are now fully identified by the output index, and don't rely on the input index to be unique. This means that output indices are ever-increasing.
In SDK v1, one could check whether a voucher has been executed already by calling the wasVoucherExecuted
function.
function wasVoucherExecuted(
uint256 inputIndex,
uint256 outputIndexWithinInput
) external view returns (bool executed);
In SDK v2, outputs are no longer attached to the inputs that generated them.
So, the now-called wasOutputExecuted
function just receives the output index.
function wasOutputExecuted(
uint256 outputIndex
) external view returns (bool executed);