diff --git a/README.md b/README.md index 5f5b5d7b..46f956db 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,66 @@ # OPinit CosmosSDK Modules -This repository provides CosmosSDK modules for OPinit. Any app chain can use these modules to integrate OPinit. +Initia Layer 2 solution with Optimistic Rollup. -## How to Integrate +## Optimistic Rollup Architecture -### Host(L1) Chain +![architecture](./specs/architecture.png) -### Child(L2) Chain +### L1 Components + +#### [Bridge Module](./specs/l1_bridge.md) + +The bridge module triggers the deposit event for the bridge executor, which acts as a relayer between L1 and L2. It has two interfaces: `initialize_deposit` for users and `finalize_withdrawal` for the bridge executor. Both interfaces can be executed by anyone who wants to move the tokens between L1 and L2. + +A deposit does not require any proving or confirmation period, but a withdrawal requires [withdrawal proving](./specs/withdrawal_proving.md) and a finalized output root which contains the withdrawal transaction. + +#### BatchInbox Module + +The batch inbox is the data availability (DA) layer, which can be replaced by other solutions like `Celestia`. The rollup chain can be deterministically derived using the data from the DA layer. This ability to derive the entire rollup chain based on the DA layer is what makes Minitia a rollup. + +To reduce gas costs, the batch inbox only exposes an empty function interface that receives arbitrary bytes as an argument. This trick ensures that L2 data is not a part of the state but instead resides in tx db (= calldata). + +#### [L2OutputOracle Module](./specs/l2_output_oracle.md) + +The L2 output oracle is the component to store the L2 output root for block finalization. The users who withdraw the tokens from L2 to L1 also need to use this output root to prove the withdraw transaction is in the finalized L2 output root. + +The challenger always monitor the oracle output and do challenge when the output is different from the value computed from challenger side. + +### L2 Components + +#### BridgeExecutor + +The bridge executor is the core component in minitia rollup, which is charge of following operations via [L2 Bridge Module](./specs/l2_bridge.md): + +* Finalize L1 deposit transaction to L2. +* Construct withdraw tx storage Merkle Tree. +* Compute L2 output root. +* Provide the withdrawal proofs (Merkle Proofs) to users. + +#### [Minitia](./specs/minitia.md) + +The L2 app chain implementation provides rollup-specific interfaces for a bridge executor. The minitia is a minimized version of the initia app chain, so it does not include staking-related modules such as `staking`, `distribution`, `crisis`, and `evidence`. Instead, it has a new module called `rollup`, which provides a permissioned interface for adding and removing validators, as well as executing [bridge messages](./specs/l2_bridge.md) that can be executed by the bridge executor. + +#### BatchSubmitter + +A background process that submits transaction batches to the `BatchInbox` module of L1. + +#### Challenger + +A challenger is an entity capable of deleting invalid output proposals from the output oracle. It mimics the output root generation process that a bridge executor does to check the validity of the proposed output root on the oracle module. This process confirms that the proposed output root contains a valid app hash, and all withdrawal transactions are properly relayed to L1. + +Additionally, a challenger monitors deposit transactions from L1 to L2 to ensure censorship resistance. If the transactions are not properly relayed to L2 within the timeout (L2 block numbers), the challenger deletes the output root. + +In the initia optimistic rollup spec, a challenger is supposed to run an IBC relayer between L1 and L2 to support instant bridge operation. It is the entity that can monitor an invalid state first, so it can prevent invalid IBC operation by stopping the relayer process. To accomplish this, initia is using [a new ibc middleware](https://github.com/initia-labs/initia/pull/86) on the L1 side to restrict the relayer permission to a specific address for each channel. + +### Dispute Process + +Initia's optimistic rollup uses a simplified version of the dispute mechanism with L1 governance security. This approach is very similar to Cosmos's shared security, but it does not require all validators to run a whole L2 node. Instead, the validators are only required to run an L2 node to decide the valid entity between the `proposer` and `challenger` when a dispute is opened. They do not need to run whole L2 blocks but only need to run a dispute block with the last L2 state on L1. + +The dispute process works as follows: + +1. A `challenger` deletes the invalid output root from the output oracle module. +2. Both a `challenger` and a `proposer` make a governance proposal to update invalid operator addresses: + * The `challenger` make a governance proposal to change the `proposer` to another address if the `proposer` keeps submitting an invalid output root. + * The `proposer` make a governance proposal to change the `challenger` to another address if the `challenger` keeps deleting a valid output root. +3. L1 validators make a validity decision by running an L2 node with L2 state and data inputs. diff --git a/specs/architecture.png b/specs/architecture.png new file mode 100644 index 00000000..951e4687 Binary files /dev/null and b/specs/architecture.png differ diff --git a/specs/l1_bridge.md b/specs/l1_bridge.md new file mode 100644 index 00000000..f504cd48 --- /dev/null +++ b/specs/l1_bridge.md @@ -0,0 +1,73 @@ +# L1 Bridge Module + +## Events + +### `TokenRegisteredEvent` + +The event is emitted when a new token support is added to bridge contract. + +* In v1 spec, the bridge executor should add a new token support manually. + +```rust +/// Emitted when deposit store is registered. +struct TokenRegisteredEvent has drop, store { + l2_id: String, + l1_token: String, + l2_token: vector, // sha3_256(type_name(`L2ID`) || type_name(`l1_token`)) +} +``` + +### `TokenBridgeInitiatedEvent` + +The event is emitted when a user executes `deposit_token` function to move a token from L1 to L2. + +* The bridge module maintain `sequence` number to give an unique identifier for each relaying operation. +* In v1, `l2_id` + `l1_sequence` is the unique identifier. + +```rust +/// Emitted when a token bridge is initiated to the l2 chain. +struct TokenBridgeInitiatedEvent has drop, store { + from: address, // l1 address + to: address, // l2 address + l2_id: String, + l1_token: String, + l2_token: vector, + amount: u64, + l1_sequence: u64, +} +``` + +### `TokenBridgeFinalizedEvent` + +The event is emitted when a withdrawal transaction is proved and finalized. + +* In v1, `sha3(bcs(l2_sequence) + bcs(from) + bcs(to) + bcs(amount) + bytes(l1_token))` is the unique identifier for each withdrawal operation. + +```rust +/// Emitted when a token bridge is finalized on l1 chain. +struct TokenBridgeFinalizedEvent has drop, store { + from: address, // l2 address + to: address, // l1 address + l2_id: String, + l1_token: String, + l2_token: vector, + amount: u64, + l2_sequence: u64, // the sequence number which is assigned from the l2 bridge +} +``` + +## Operations + +### Register Token + +This function is for the bridge executor, who controls the bridge relaying operations. In version 1, only the bridge executor can add support for a new token type. After registration, the `TokenRegisteredEvent` event is emitted to deploy a new coin type module on L2. + +The bridge executor should monitor the `TokenRegisteredEvent` and deploy a new coin type module on L2. They should also execute the L2 bridge's `/minitia.rollup.v1.MsgCreateToken` function for initialization. + +### Initiate Token Bridge + +This function enables a user to transfer their asset from L1 to L2. The deposited token will be locked in the bridge's `DepositStore` and can only be released using the `withdraw` operation in L2. When executed, this operation emits a `TokenBridgeInitiatedEvent`. A bridge executor should subscribe to this event and transfer the token to L2 by executing the `finalize_token_bridge` function in the L2 bridge. + +### Finalize Token Bridge + +This function is used to prove and finalize the withdrawal transaction from L2. The proving process is described [here](https://www.notion.so/Withdrawal-Proving-a49f7c26467044489731048f68ed584b?pvs=21). Once the proving is complete, the deposited tokens are withdrawn to the recipient address, and the `TokenBridgeFinalizedEvent` event is emitted. To prevent duplicate withdrawal attempts, the bridge uses a unique identifier calculated as `sha3(bcs(l2_sequence) + bcs(from) + bcs(to) + bcs(amount) + bytes(l1_token))`. diff --git a/specs/l2_bridge.md b/specs/l2_bridge.md new file mode 100644 index 00000000..571342f5 --- /dev/null +++ b/specs/l2_bridge.md @@ -0,0 +1,52 @@ +# L2 Bridge + +## Events + +### `TokenBridgeFinalizedEvent` + +The event is emitted when a executor finalized the token transfer from the L1 to L2. + +```rust +// Emitted when a token bridge is finalized on l2 chain. +struct TokenBridgeFinalizedEvent has drop, store { + from: address, // l1 address + to: address, // l2 address + l2_token: vector, + amount: u64, + l1_sequence: u64, // the sequence number which is assigned from the l1 bridge +} +``` + +### `TokenBridgeInitiatedEvent` + +The event is emitted when a user executes `withdraw_token` function to move token from L2 to L1. + +- The bridge module maintain `sequence` number to give unique identifier to each relay operation. +- In v1, `l2_sequence` is the unique identifier. + +```rust +// Emitted when a token bridge is initiated to the l1 chain. +struct TokenBridgeInitiatedEvent has drop, store { + from: address, // l2 address + to: address, // l1 address + l2_token: vector, + amount: u64, + l2_sequence: u64, // the operation sequence number +} +``` + +## Operations + +### Register Token + +This function allows the block executor to initialize a new token type with registration on the bridge module. The name of the newly deployed module should follow the L1 bridge contract’s event message `l2_token`, such as `01::l2_${l2_token}::Coin`. + +### Finalize Token Bridge + +This function finalizes the token transfer from L1 to L2. Only the block executor is allowed to execute this operation. + +### Initiate Token Bridge + +This function initiates the token bridge from L2 to L1. Users can execute `withdraw_token` to send tokens from L2 to L1. This operation emits the `TokenBridgeInitiatedEvent` with an `l2_sequence` number to prevent duplicate execution on L1. + +The block executor should monitor this event to build withdraw storage for withdrawal proofs. diff --git a/specs/l2_output_oracle.md b/specs/l2_output_oracle.md new file mode 100644 index 00000000..332a232f --- /dev/null +++ b/specs/l2_output_oracle.md @@ -0,0 +1,32 @@ +# L2 Output Oracle + +In version 1, output oracle maintain `proposer` and `challenger` addresses on its config store. The `proposer` can submit the `output_proposal` and the `challenger` can delete the output when the proposed output state is wrong. + +The first version of the implementation does not include a dispute system, but uses permissioned propose and challenge mechanisms. In version 2, anyone can propose the output with a certain amount of `stake`, and disputes will be resolved based on the governance of L1. + +## Operations + +### Propose L2 Output + +L2 output oracle receives `output_root` with L2 block number to check the checkpoint of L2. The checkpoints are the multiple of `submission_interval`. A proposer must submit the `output_root` at the every checkpoints. + +The followings are the components of `output_root`. + +- `version`: the version of output root +- `state_root`: l2 state root +- `storage_root`: withdrawal storage root +- `latest_block_hash`: l2 latest block hash + +To build the `output_root`, concatenate all the components in sequence and apply `sha3_256`. + +### Delete L2 Output + +A challenger can delete the output without dispute in version 1 with output index. + +### Update Proposer + +The operation is to update proposer to another address when a proposer keeps submitting a invalid output root. The operation is supposed to be executed by `0x1` via L1 governance. + +### Update Challenger + +The operation is to update challenger to another address when a challenger keeps deleting a valid output root. The operation is supposed to be executed by `0x1` via L1 governance. diff --git a/specs/minitia.md b/specs/minitia.md new file mode 100644 index 00000000..5c45358d --- /dev/null +++ b/specs/minitia.md @@ -0,0 +1,237 @@ +# Minitia + +## Messages + +There are three categories of message in `x/rollup`` module. + +* Bridge Executor messages + * [`MsgCreateToken`](#msgcreatetoken) + * [`MsgDeposit`](#msgdeposit) +* Validator messages + * [`MsgExecuteMessages`](#msgexecutemessages) + * [`MsgExecuteLegacyContents`](#msgexecutelegacycontents) +* Authority messages + * [`MsgAddValidator`](#msgaddvalidator) + * [`MsgRemoveValidator`](#msgremovevalidator) + * [`MsgUpdateParams`](#msgupdateparams) + * [`MsgWhitelist`](#msgwhitelist) + * [`MsgSpendFeePool`](#msgspendfeepool) + +### `MsgCreateToken` + +The message is for a bridge executor to publish a new coin struct tag `0x1::native_${denom}::Coin` and initialize a new coin with that struct tag. + +```proto +// MsgCreateToken is the message to create a new token from L1 +message MsgCreateToken { + option (cosmos.msg.v1.signer) = "sender"; + option (amino.name) = "rollup/MsgCreateToken"; + + // the sender address + string sender = 1 [ + (gogoproto.moretags) = "yaml:\"sender\"", + (cosmos_proto.scalar) = "cosmos.AddressString" + ]; + + // denom is the denom of the token to create. + string denom = 2; + string name = 3; + string symbol = 4; + int64 decimals = 5; +} +``` + +### `MsgDeposit` + +The message is for a bridge executor to finalize a deposit request from L1. The message handler internally executes [`finalize_token_bridge`](./l2_bridge.md#finalize-token-bridge) of `l2_bridge`. + +```proto +// MsgDeposit is the message to submit deposit funds from upper layer +message MsgDeposit { + option (cosmos.msg.v1.signer) = "sender"; + option (amino.name) = "rollup/MsgDeposit"; + + // the sender address + string sender = 1 [ + (gogoproto.moretags) = "yaml:\"sender\"", + (cosmos_proto.scalar) = "cosmos.AddressString" + ]; + + // from is l1 sender address + string from = 2 [(cosmos_proto.scalar) = "cosmos.AddressString"]; + + // to is l2 recipient address + string to = 3 [(cosmos_proto.scalar) = "cosmos.AddressString"]; + + // amount is the coin amount to deposit. + cosmos.base.v1beta1.Coin amount = 4 [ + (gogoproto.moretags) = "yaml:\"amount\"", + (gogoproto.nullable) = false, + (amino.dont_omitempty) = true + ]; + + // sequence is the sequence number of l1 bridge + uint64 sequence = 5; +} +``` + +### `MsgExecuteMessages` + +The message is to execute authority messages with validator permission like `x/gov` module of cosmos-sdk. Any validator can execute the message with various authority messages. + +```proto +// MsgExecuteMessages is the message to execute the given +// authority messages with validator permission. +message MsgExecuteMessages { + option (cosmos.msg.v1.signer) = "sender"; + option (amino.name) = "rollup/MsgExecuteMessages"; + + // Sender is the that actor that signed the messages + string sender = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; + + // messages are the arbitrary messages to be executed. + repeated google.protobuf.Any messages = 2; +} +``` + +### `MsgExecuteLegacyContents` + +The message is also copied from `x/gov` module of cosmos-sdk to support legacy param update of ibc modules. The execution permission is given to validators. + +```proto + +// MsgExecuteLegacyContents is the message to execute legacy +// (gov) contents with validator permission. +message MsgExecuteLegacyContents { + option (cosmos.msg.v1.signer) = "sender"; + option (amino.name) = "rollup/MsgExecuteLegacyContents"; + + // Sender is the that actor that signed the messages + string sender = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; + + // contents are the arbitrary legacy (gov) contents to be executed. + repeated google.protobuf.Any contents = 2; +} +``` + +### `MsgAddValidator` + +The message is to add a new validator to the comet-bft validator set. The execution permission is given to authority, which is `rollup` module account. + +```proto +// MsgAddValidator defines a SDK message for adding a new validator. +message MsgAddValidator { + option (cosmos.msg.v1.signer) = "authority"; + option (amino.name) = "rollup/MsgAddValidator"; + + option (gogoproto.equal) = false; + option (gogoproto.goproto_getters) = false; + + // authority is the address that controls the module + // (defaults to x/rollup unless overwritten). + string authority = 1 [ + (gogoproto.moretags) = "yaml:\"authority\"", + (cosmos_proto.scalar) = "cosmos.AddressString" + ]; + + string moniker = 2; + string validator_address = 3 [(cosmos_proto.scalar) = "cosmos.AddressString"]; + google.protobuf.Any pubkey = 4 [(cosmos_proto.accepts_interface) = "cosmos.crypto.PubKey"]; +} +``` + +### `MsgRemoveValidator` + +The message is to remove a validator from the comet-bft validator set. The execution permission is given to authority, which is `rollup` module account. + +```proto +// MsgAddValidator is the message to remove a validator from designated list +message MsgRemoveValidator { + option (cosmos.msg.v1.signer) = "authority"; + option (amino.name) = "rollup/MsgRemoveValidator"; + + // authority is the address that controls the module + // (defaults to x/rollup unless overwritten). + string authority = 1 [ + (gogoproto.moretags) = "yaml:\"authority\"", + (cosmos_proto.scalar) = "cosmos.AddressString" + ]; + + // validator is the validator to remove. + string validator_address = 2 [(cosmos_proto.scalar) = "cosmos.AddressString"]; +} +``` + +### `MsgUpdateParams` + +The message is to update the rollup module params. The execution permission is given to authority, which is `rollup` module account. + +```proto +// MsgUpdateParams is the message to update legacy parameters +message MsgUpdateParams { + option (cosmos.msg.v1.signer) = "authority"; + option (amino.name) = "rollup/MsgUpdateParams"; + + // authority is the address that controls the module + // (defaults to x/rollup unless overwritten). + string authority = 1 [ + (gogoproto.moretags) = "yaml:\"authority\"", + (cosmos_proto.scalar) = "cosmos.AddressString" + ]; + + // params are the arbitrary parameters to be updated. + Params params = 2; +} +``` + +### `MsgWhitelist` + +The message is to add a coin type to whitelist for auto register. The execution permission is given to authority, which is `rollup` module account. + +```proto +// whitelist a coin type to enable auto coin module register. +message MsgWhitelist { + option (cosmos.msg.v1.signer) = "authority"; + option (amino.name) = "rollup/MsgWhitelist"; + + // authority is the address that controls the module + // (defaults to x/rollup unless overwritten). + string authority = 1 [ + (gogoproto.moretags) = "yaml:\"authority\"", + (cosmos_proto.scalar) = "cosmos.AddressString" + ]; + + // coin_type is the struct tag to whitelist. + string coin_type = 2; +} +``` + +### `MsgSpendFeePool` + +The message is to spend collected fee. The execution permission is given to authority, which is `rollup` module account. + +```proto +// MsgSpendFeePool is the message to withdraw collected fees from the module account to the recipient address. +message MsgSpendFeePool { + option (cosmos.msg.v1.signer) = "authority"; + option (amino.name) = "rollup/MsgSpendFeePool"; + + // authority is the address that controls the module + // (defaults to x/rollup unless overwritten). + string authority = 1 [ + (gogoproto.moretags) = "yaml:\"authority\"", + (cosmos_proto.scalar) = "cosmos.AddressString" + ]; + + // recipient is address to receive the coins. + string recipient = 2 [(cosmos_proto.scalar) = "cosmos.AddressString"]; + + // the coin amount to spend. + repeated cosmos.base.v1beta1.Coin amount = 3 [ + (gogoproto.moretags) = "yaml:\"amount\"", + (gogoproto.nullable) = false, + (amino.dont_omitempty) = true, + (gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins" + ]; +} +``` diff --git a/specs/withdrawal_proving.md b/specs/withdrawal_proving.md new file mode 100644 index 00000000..ac8bad79 --- /dev/null +++ b/specs/withdrawal_proving.md @@ -0,0 +1,64 @@ +# Withdrawal Proving + +`ominitia` defines `submission_interval`, which is the L2 block number at which checkpoints must be submitted. At each `submission_interval`, the bridge executor should build the withdraw storage, which is the Merkle Tree for the withdrawal verification process on L1. + +`ominitia` uses a sorted Merkle Tree to reduce verifying cost, and each tree node is referred to as a `withdrawal_hash`. + +The following are the components of `withdrawal_hash`: + +- `sequence`: The L2 bridge sequence assigned to each withdrawal operation. +- `sender`: The address from which the withdrawal operation is made. +- `receiver`: The address to which the withdrawal operation is made. +- `amount`: The token amount of the withdrawal operation. +- `coin_type`: The L1 token struct tag. + +To build the `withdrawal_hash`, concatenate all the components and apply `sha3_256` after serializing the values with `bcs`, except for `coin_type`, because the `coin_type` is already a string that can be converted to bytes. + +```rust +fun verify( + withdrawal_proofs: vector>, + sequence: u64, + sender: address, + receiver: address, + amount: u64, + coin_type: String, +): bool { + let withdrawal_hash = { + let withdraw_tx_data = vector::empty(); + vector::append(&mut withdraw_tx_data, bcs::to_bytes(&sequence)); + vector::append(&mut withdraw_tx_data, bcs::to_bytes(&sender)); + vector::append(&mut withdraw_tx_data, bcs::to_bytes(&receiver)); + vector::append(&mut withdraw_tx_data, bcs::to_bytes(&amount)); + vector::append(&mut withdraw_tx_data, *string::bytes(&type_info::type_name())); + + sha3_256(withdraw_tx_data) + }; + + let i = 0; + let len = vector::length(&withdrawal_proofs); + let root_seed = withdrawal_hash; + while (i < len) { + let proof = vector::borrow(&withdrawal_proofs, i); + let cmp = bytes_cmp(&root_seed, proof); + root_seed = if (cmp == 2 /* less */) { + let tmp = vector::empty(); + vector::append(&mut tmp, root_seed); + vector::append(&mut tmp, *proof); + + sha3_256(tmp) + } else /* greator or equals */ { + let tmp = vector::empty(); + vector::append(&mut tmp, *proof); + vector::append(&mut tmp, root_seed); + + sha3_256(tmp) + }; + i = i + 1; + }; + + let root_hash = root_seed; + assert!(storage_root == root_hash, error::invalid_argument(EINVALID_STORAGE_ROOT_PROOFS)); +} +``` + +The example implementation of building the Merkle Tree can be found [here](https://github.com/initia-labs/op-bridge-executor).