Godwoken is a layer 2 rollup framework for Nervos CKB. It provides scaling capability, as well as an account based programming model to CKB. Please refer to this post for a general introduction on Godwoken. This post here dives into the internals of godwoken, explaining how godwoken works, and how each individual pieces are fit together.
Fundamentally, Godwoken is a layer 2 solution for layer 1 Nervos CKB blockchain. Here shows one deployment of Godwoken on CKB's testnet. New layer 2 Godwoken blocks are submitted to layer 1 CKB in block submission transactions. The current state of the layer 2 Godwoken blockchain is committed in the cell data part of the CKB cell generated by the block submission transaction. This transaction is an example of a block submission transaction. The chain state, as of this layer 2 block, is:
0xa0cf6037bfc238b179b74a30a9b12e15a4fbdd8881aebc8e5a66a8b5b5c95f0a4ca62a4c652697c96b7e02eea3a1bc21c5369bcb30626b9aee1cbb1babea11ff4f16000055a255f47f22b6f8ed1a025f971d60528f5407b7c71745c8c493a51a631bb4bfab6f000000000000000000000000000000000000000000000000000000000000000000000000000047ee34be61c51e026838c5348ab0ca3566eed50dc9b3dc676fe16a7f7d72f80fcfe4f3d680010000466f0000000000000001
This is actually a GlobalState
data structure serialized in molecule's format:
struct GlobalState {
rollup_config_hash: Byte32,
account: AccountMerkleState,
block: BlockMerkleState,
reverted_block_root: Byte32,
tip_block_hash: Byte32,
tip_block_timestamp: Uint64,
last_finalized_block_number: Uint64,
// 0: running, 1: halting
status: byte,
version: byte,
}
Please refer to this file for more definitions.
The actual layer 2 Godwoken block, is also contained in the block submission transaction. The first witness of the above mentioned transaction, contains the following data:
0x600300001000000069000000690000005500000055000000100000005500000055000000410000004082de966ef59f3777d5c6b447d7912a457cb6bffc6317f336b03f39930c4c0f474779d431f4832a4779a804880f0d1ff4e9a48554ca8e85b56511c982c2094400f302000000000000ef02000010000000e7020000eb020000d70200001c0000006c010000700100007401000078010000d3020000500100002c000000340000005400000074000000940000009c000000c0000000e4000000e80000000c010000aa6f0000000000001c0000000200000014000000715ab282b873b79a7be8b0e8c13c4e8966a52040c822d671a8097366aacd98746a00407cd378b54b2facb8ac8fe8970e447ced1ff245705db4fe72be953e4f9ee3808a1700a578341aa80a8b2349c236c4af64e5cfe4f3d6800100004ca62a4c652697c96b7e02eea3a1bc21c5369bcb30626b9aee1cbb1babea11ff4f1600004ca62a4c652697c96b7e02eea3a1bc21c5369bcb30626b9aee1cbb1babea11ff4f1600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000011b7aeea1224e8e80173fe93db2187d0251d4b41f00e61fe3c3433f0e6953800000000000000000004000000570100004c4f0150f4dad550e8b95696636f041140059724eacb55453d70a18072520c97411ae6b64f01501e5a5863aa3e7d66c1e19a26b1eaac8183aa59e9ce8d77db4e2cb30b65d95b534f01505677593a7fbb2a4ecf82f3b1f7bee953c0199d4c9d6552154a95d844635901e64f0150870913864cfddf50dc51a88b5ca5b8bdf95c476a068fbd2eaa19398318703af05060f8c8c082fafd59eca27762ccbebc580271e0bac92f7a381c739d1954f1c6c9508c4c5f7fb230bee4a9877f3039feb2675dd44d04e8ac0e41dd9f9e13f277119f503e6ee57e32132b778b0e14a9a06b185c73c4a4d01b6cab9370991c3897f90f7050dc2f0723654791da20e1dc7713e167d58c71ceac4839364a6e5033f07f00efc14f0150ad18935a95613207b9baec1fb60f35eab69f273e3423ddb58cb37d886205488650b32cd6babf0fd7a5506a00365d252bc7162f527f4123ec22dcb51ad9336797294ff1040000000000000000000000
This is actually a WitnessArgs
data structure described here, and serialized in molecule's format. In the output_type
field of WitnessArgs, the actual L2Block
data structure is stored, also serialized in molecule's format:
table L2Block {
raw: RawL2Block,
kv_state: KVPairVec,
kv_state_proof: Bytes,
transactions: L2TransactionVec,
block_proof: Bytes,
withdrawals: WithdrawalRequestVec,
}
Also, please refer to this file for more definitions.
The output cell containing the GlobalState
, is named rollup cell, since this cell contains all the latest information for a particular Godwoken rollup deployment. Careful readers here might notice that Godwoken has no restrictions on how many rollup cells are permitted in CKB, which is correct. Multiple Godwoken deployments can be setup in a single CKB blockchain for different purposes. Each Godwoken deployment, is thus identified by the rollup cell.
Godwoken requires the rollup cell for each Godwoken deployments to use a particular Godwoken type script, named state validator script. This script will perform the following duties:
- Enforce type ID logic, this gives each rollup cell an ID, so one won't need to refer to rollup cell's OutPoint, which is changed whenever the rollup cell is updated. The type script hash of the rollup cell, is also named rollup type hash of current Godwoken deployment. Rollup type hash can also be viewed as a chain ID, uniquely identifying each layer 2 Godwoken blockchain.
- Enforce Godwoken's own checking logic. The state validator type script ensures that the
GlobalState
is indeed calculated from the current L2 block. (TODO: state validator validation rules).
State validator type script is the backbone of Godwoken, it ensures all the security checks are performed on chain, and all the layer 2 funds stay secure in a decentralized environment.
Godwoken provides an account model to Nervos CKB:
- A 256-bit key-value storage is provided to applications running on Godwoken.
- Conceptually, Godwoken provides a sequential model: each layer 2 transaction is applied to the Godwoken storage in sequence. Smart contracts in layer 2 transactions are free to read from and write into the storage space.
One might wonder where all the data are stored given constant storage requirement in the above transactions. Godwoken leverages Sparse Merkle Tree to build the storage space. Only the root hash of the Sparse Merkle Tree(SMT) is kept in the rollup cell. State validator validates that the sparse merkle tree is correctly updated in each layer 2 block. We actually took a step further here, and build an optimized SMT, which can save a lot of space and calculation for CKB's use case. Please refer to here for more details.
The following workflow is used by Godwoken:
- Individual parties, named aggregators can collect layer 2 Godwoken transactions(more details on layer 2 transactions will be explained below), then pack them into layer 2 blocks, and submit them to CKB. Aggregators will need to stake a certain amount of CKB in order to submit a layer 2 block.
- Each layer 2 block submitted to the chain is first marked as unfinalized. An unfinalized layer 2 block might be challenged by others(named challengers), if the layer 2 block appears malicious.
- A challenger starts a challenge request by marking one layer 2 transaction in the layer 2 block as invalid. Some CKB will need to be staked in order to create a challenge. Once a challenge request is included in CKB, the challenge phase is started.
- In a challenge phase(details will be explained below), the original aggregator submitting the block will need to prove that the marked layer 2 transaction is indeed correct. The aggregator can do this by submitting a cancel challenge request, which executes the marked layer 2 transaction on chain. CKB only accepts valid transaction on chain, which means the layer 2 transaction must succeed for the cancel challenge request to be committed on chain. In a cancel challenge request, aggregator can claim staked tokens from the challenger.
- If an aggregator fails to prove the validity of the layer 2 transaction via a cancel challenge request, the challenger can claim tokens staked by the aggregator, and revert the chain to the state just before the invalid layer 2 block.
- After a certain amount of time, an unfinalized layer 2 block will become finalized. Finalized layer 2 blocks will be freezed, and cannot be challenged by anyone.
Note this is slightly different from a typical optimistic rollup: here a transaction is executed, only to prove the transaction is valid, while a timeout indicates challenge success in this case.
In this design, an aggregator bears liquidity costs for staking CKBs, as well as layer 1 transaction fees for the layer 1 transactions containing layer 2 block. In exchange, the aggregator can charge layer 2 transaction fees from layer 2 users. As explained below, layer 2 transaction fees can be paid either in CKB or in any kind of sUDT. In fact, Godwoken treat CKB and sUDTs as exactly the same thing in layer 2.
Godwoken is designed based on the assumption that anyone shall be able to propose layer2 blocks. But in the current deployment, Godwoken has only one block producer to sequence transactions and propose new blocks. To support multiple block producers (or sequencers) is still an open question for different rollups. In the future, we may introduce a consensus through upgrading to support multiple block producer coordination.
In this sections we will explain all the actions one can perform on Godwoken, together with technical details related to each action.
To use Godwoken, one must first deposit some tokens(either CKB or sUDTs) from layer 1 to layer 2. This is named as a deposit action. A deposit action is represented as a layer 1 CKB transaction, it must create a special output cell named a deposit cell. Here is an example of a deposit action. This transaction deposits 10000 CKB to Godwoken. What is relavant here, is the lock script of output cell #0:
{
"code_hash": "0x50704b84ecb4c4b12b43c7acb260ddd69171c21b4c0ba15f3c469b7d143f6f18",
"args": "0x702359ea7f073558921eb50d8c1c77e92f760c8f8656bde4995f26b8963e2dd8a900000014000000340000009d000000a5000000e13f4a9c1642a6c59766eb5509580808bf3917bba104a616df0d207c93050e596900000010000000300000003100000007521d0aa8e66ef441ebc31204d86bb23fc83e9edc58c19dbb1b0ebe64336ec00134000000702359ea7f073558921eb50d8c1c77e92f760c8f8656bde4995f26b8963e2dd8e8ae579256c3b84efb76bbb69cb6bcbef1375f00813a0900000000c002000000",
"hash_type": "type"
}
code_hash
and hash_type
is pre-determined by each Godwoken deployment. args
contains 2 parts: the first 32 bytes of args
, contain the rollup type hash of the current Godwoken deployment(remember we mentioned earlier, that rollup type hash works like a chain ID?). The second part, is the DepositLockArgs data structure, serialized in molecule format:
table DepositLockArgs {
// layer1 lock hash
owner_lock_hash: Byte32,
layer2_lock: Script,
cancel_timeout: Uint64,
registry_id: Uint32,
}
This data structure contains 2 parts of information:
layer2_lock
specifies the lock script to use, when Godwoken transfers the tokens to layer 2.owner_lock_hash
andcancel_timeout
provide a way to redeem the token in the case Godwoken ignores this request(e.g., when the network becomes bloated).cancel_timeout
specifies a timeout parameter in CKB's since format. When the timeout has reached, the user can create another request to cancel the deposit action, and redeem his/her tokens for other use.owner_lock_hash
is used to provide the token owner's identity in case of a cancel deposit action.
Godwoken will periodically collect all live deposit cells, and include them in layer 2 blocks. Each deposit cell will be transformed to custodian cell by Godwoken, correspondingly, Godwoken will create(if one does not exist) a layer 2 account based on the layer2_lock
used in DepositLockArgs
, then put the newly deposited tokens inside this account.
Custodian cells contain all the tokens that are managed internally in Godwoken. This transaction contains a custodian cell in its output cell #2. Like a deposit cell, a custodian cell is represented by its lock script:
{
"code_hash": "0x85ae4db0dd83f428a31deb342e4000af37ce2c9645d9e619df00096e3c50a2bb",
"args": "0x702359ea7f073558921eb50d8c1c77e92f760c8f8656bde4995f26b8963e2dd8e1000000100000003000000038000000595a1405676547c8ddc083f3413d58d65a1bb9275b5a923ec61dce4e572647cc4c6f000000000000a900000014000000340000009d000000a5000000e13f4a9c1642a6c59766eb5509580808bf3917bba104a616df0d207c93050e596900000010000000300000003100000007521d0aa8e66ef441ebc31204d86bb23fc83e9edc58c19dbb1b0ebe64336ec00134000000702359ea7f073558921eb50d8c1c77e92f760c8f8656bde4995f26b8963e2dd8e8ae579256c3b84efb76bbb69cb6bcbef1375f00813a0900000000c002000000",
"hash_type": "type"
}
Like deposit cells, custodian cells have pre-determined code_hash
and hash_type
based on Godwoken deployments. The first 32 bytes in args
contain the rollup type hash as well. What's different here, is that CustodianLockArgs is used instead to fill the remaining part of args
:
table CustodianLockArgs {
deposit_block_hash: Byte32,
deposit_finalized_timepoint: Uint64,
// used for revert this cell to deposit request cell
// after finalize, this lock is meaningless
deposit_lock_args: DepositLockArgs,
}
As noted, custodian cell contains the original deposit information, as well the layer 2 block info in which the original deposit cell is processed.
Once tokens have been deposited and processed by Godwoken, they can be used in layer 2 Godwoken blockchain. A layer 2 Godwoken transaction uses totally different structure from a layer 1 CKB transaction:
table RawL2Transaction {
// chain id
chain_id: Uint64,
from_id: Uint32,
to_id: Uint32,
nonce: Uint32,
args: Bytes,
}
table L2Transaction {
raw: RawL2Transaction,
signature: Bytes,
}
Please refer to this file for a more complete definition and related types.
For those of you who are already familiar with CKB's transaction structure, this might actually surprise you. While the signature part might ring a bell, there are no cells in this transaction structure, there are only 32-bit integers value representing accounts, 32-bit nonce which is not used in layer 1 CKB, as well as a single variable length args
part. But there is no need to panic! We will explain the details on this layer 2 transaction structure piece by piece.
While all operations on layer 2 Godwoken can be represented as a L2Transaction
in the above format, let's start from a simple transferring operation:
{
"raw": {
"chain_id": "0x116e9",
"from_id": "0x2",
"to_id": "0x1",
"nonce": "0x3",
"args": "0x01000000040000009001000000000000000000000000000064000000000000000000000000000000"
},
"signature": "0x306d9240d6b18eaa29c83b5f4603c9cbd04402d09c3c38063a87c35b9bfe4ce16ffe64b9f20e306a59819cecfd9b72d3f62888305155bd3e48e40742a3cd8a8901"
}
This is in fact the JSON representation of the above L2Transaction
data structure. args
contains a variable length, free formatted transaction argument data that is interpreted depending on the values of to_id
. For this particular example, args
contains SUDTTransfer data structure in molecule serialization format:
table SUDTTransfer {
// Godwoken registry address: (registry_id (4 bytes) | address len(4 bytes) | address)
to_address: Bytes,
amount: Uint256,
// paid fee(ckb)
fee: Fee,
}
A JSON representation for this data structure is:
{
"type": "SUDTTransfer",
"value": {
"to_address": "0x0200000014000000bb1d13450cfa630728d0390c99957c6948bf7d19",
"amount": "0x190",
"fee": "0x64"
}
}
In this example, we are transferring 400 shannons(denoted by to_id
0x1) from account 0x2(denoted by from_id
) to account 0x0200000014000000bb1d13450cfa630728d0390c99957c6948bf7d19(denoted by to_address
in SUDTTransfer
), we would also love to pay for 100 shannons as layer 2 transaction fees. The next 2 sections shall explain the details for layer 2 transaction.
Account lock controls how a signature for a layer 2 transaction is validated. Recall that a deposit cell actually includes a layer 2 lock script in its DepositLockArgs
:
table DepositLockArgs {
layer2_lock: Script,
// ...
// other fields are omitted
}
When Godwoken sees a layer2_lock
in a DepositLockArgs
data structure, it first queries its internal storage to locate an account using the particular layer 2 lock script. If there is not one, Godwoken will create a new account for this lock script, and assign an associated 32-bit integer account ID. Note that Godwoken enforces one-to-one mapping between layer 2 lock script, and account ID:
- Given an account ID, one can look up for the layer 2 lock script in Godwoken
- Given a layer 2 lock script, there can be at most one account ID using that lock script in current Godwoken deployment
This provides some handy consequences for us:
- A user does not have to remember his/her account ID in a Godwoken deployment. One can only remember his/her wallet address, which can be used to query the correct account ID when translated to layer 2 lock script;
- A layer 2 transaction can only include the account ID, which can be used to query the actual layer 2 lock script to use. This results in smaller layer 2 transaction and bigger throughput.
Now we can have some harmony here: a user can pick any lock script to use in layer 2, while still minimizing layer 2 transaction size. The advantage here, is that we can preserve the same flexibility in layer 1 CKB as well in layer 2 Godwoken: one can freely pick any lock script to use.
This actually have greater usage here: some of you might already know that we build godwoken together with polyjuice, our Ethereum compatible solution. Naturally, we have an Ethereum compatible lock, that enables you to use Metamask together with polyjuice. But the story does not stop here: with a different layer 2 lock that implements, for example, EOS signature validation logic, an EOS wallet can also be empowered to call Ethereum dapps with polyjuice. The whole interoperability power of Nervos CKB, is preserved in Godwoken as well.
Now we can derive the signature validation rule for a layer 2 Godwoken transaction:
- Using
from_id
in the layer 2 transaction, Godwoken locates the corresponding layer 2 lock script - The layer 2 lock script is executed to validate the layer 2 transaction
There is one quirk to the above flow here: recall that current Godwoken uses optimistic rollup design. Due to the "optimistic" nature, the layer 2 lock script is not typically executed on chain. It is only executed in the case that a challenger starts a challenge request on chain, and an aggregator proves the validity of the layer 2 transaction via a cancel challenge request. Hence the way to build a layer 2 lock script, is also slightly different here. An example for such a script, can be found here.
With signature verified, another problem arises: how does Godwoken compute the next on-layer2-chain state? Backends in Godwoken handles this task.
Godwoken actually has 2 kinds of accounts:
- Typical user accounts denoted by an account lock
- Contract accounts denoted by a backend script
Ethereum developers would recognize that this design resembles a lot like EOA vs contract account in Ethereum, which is true since Godwoken is inspired from Ethereum here.
While a typical user account is a balance owned by a user, contract account, on the other hand, provides a storage for an on-chain smart contract. Similar to the way that a user account is created from a general lock script with a user's public key hash, a smart contract for a contract account is created from a backend with some special script args. The smart contract is also represented using the unified Script data structure in the Nervos ecosystem: the combination of code_hash
and hash_type
in a script identifies the backend, while args
provide contract account specific arguments.
Given the above background, the rule for executing backends can be introduced:
- Using
to_id
in the layer 2 transaction, Godwoken locates the corresponding backend script - The backend script is executed to calculate the state after applying current transaction.
Similar to account locks, the above rule is more of a conceptual rule. The actual backend script is only executed on-chain in a cancel challenge request.
As examples, several different backends will be introduced here:
sUDT is the most commonly used backend in Godwoken. All tokens, whether they are CKB or layer 1 sUDTs, will be represented as a layer 2 sUDT type in Godwoken. A layer 2 sUDT backend script is defined via the following specification:
code_hash
andhash_type
are pre-determined by each Godwoken deploymentargs
contains the script hash of layer 1 sUDT type script. In case CKB is used,args
will be0x0000000000000000000000000000000000000000000000000000000000000000
.
This way we can correlate layer 1 sUDT type script, with its corresponding layer 2 sUDT backend script.
The layer 2 sUDT backend provides ERC20 compatible interface, even though you are not using Solidity and EVM, a similar API will be available for you to use.
Layer 2 transaction using a sUDT backend must provided a SUDTArgs data structure serialized in molecule format in layer 2 transaction args. The 2 supported action for sUDT backend, is SUDTQuery and SUDTTransfer. Later versions might expand this list to include other ERC20 operations.
Notice sUDT contract account is typically created by Godwoken automatically when processing deposit cells. One don't typically create new contract account using sUDT as backend script.
An implementation for the layer 2 sUDT backend can be found here.
MetaContract is a special backend in Godwoken:
- It is reserved as account ID 0
- You cannot create a new contract account using MetaContract as the backend
The sole purpose of MetaContract now, is to create contract account given a particular backend.
Here is the JSON representation for a layer 2 transaction, which invokes MetaContract to create a polyjuice root contract account:
{
"raw": {
"chain_id": "0x116e9",
"from_id": "0x2",
"to_id": "0x0",
"nonce": "0x4",
"args": "0x0000000041000000080000003900000010000000300000003100000020814f4f3ebaf8a297d452aa38dbf0f9cb0b2988a87cb6119c2497de817e7de9000400000001000000"
},
"signature": "0xc62d332f398323b972c5ee5c4481661ca9d17125af6f61e5220e2fbfe3bd325a0cc6c3ac174950dc1282d5e6059fc08838b9040ed7eca0ad13474af869f25a8701"
}
The args
part in this transaction, contains a MetaContractArgs data structure serialized in molecule format. A JSON representation for the args
, is as follows:
{
"type": "CreateAccount",
"value": {
"script": {
"code_hash": "0x20814f4f3ebaf8a297d452aa38dbf0f9cb0b2988a87cb6119c2497de817e7de9",
"hash_type": "data",
"args": "0x01000000"
}
}
}
The details of this transactioon will be explained in Life of a Polyjuice Transaction.
Polyjuice is the main backend we use now in Godwoken. It allows us to create a contract account using EVM bytecode, the resulting account, will be able to execute smart contracts written for Ethereum. Polyjuice aims at 100% compatibility in EVM level, meaning all programs runnable on Ethereum, can be run on Godwoken powered by Polyjuice.
For more details on polyjuice, please refer to Life of a Polyjuice Transaction.
While one is free to create as many accounts as possible, Godwoken now only supports whitelisted account locks & backends for a security reasons. The list now include:
- Ethereum compatible account lock
- MetaContract backend
- Layer 2 sUDT backend
- ETH Registry backend
- Polyjuice backend
If there is one particular account lock or backend you like, please do not hesistate to let us know. We are interested in a future where an EOS wallet can control an Ethereum app, or a BTC wallet can control a Diem app. Those are all feasible on Godwoken with the composability of account locks, and backends.
Withdraw action enables one to withdraw tokens from layer 2 Godwoken back to layer 1 CKB. A withdraw action uses the following data structure:
struct RawWithdrawalRequest {
nonce: Uint32,
// chain id
chain_id: Uint64,
// CKB amount
capacity: Uint64,
// SUDT amount
amount: Uint128,
sudt_script_hash: Byte32,
// layer2 account_script_hash
account_script_hash: Byte32,
// withdrawal registry ID
registry_id: Uint32,
// layer1 lock to withdraw after challenge period
owner_lock_hash: Byte32,
// withdrawal fee, paid to block producer
fee: Uint128,
}
vector WithdrawalRequestVec <WithdrawalRequest>;
struct WithdrawalRequest {
raw: RawWithdrawalRequest,
signature: Bytes,
}
Please refer to this file for a more complete definition and related types.
A WithdrawalRequest
uses account_script_hash
as a key to locate the account lock, perform the same signature verification flow as layer 2 transaction. Withdraw action can be used to withdraw CKB and a layer 1 sUDT type simutaneously. Due to the constraint of CKB's cell model, currently Godwoken requires each withdraw request to withdraw at least 400 CKBytes as an expedient.
You might notice more fields are included in WithdrawalRequest
, those are used to fulfill a special feature of Godwoken: selling of withdrawed tokens. This is explained in the next section.
Here is the JSON representation of a WithdrawalRequest
:
{
"raw": {
"nonce": "0x3",
"chain_id": "0x116e9",
"capacity": "0x9502f9000",
"amount": "0x0",
"registry_id": "0x2",
"sudt_script_hash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"account_script_hash": "0x167d7855f41c343e2064fc5b62f7d640d2d74a8255766bff62f1f5fc63ab4c58",
"owner_lock_hash": "0xa35eda3e71e86e4e22b7924012b6a6e90809dc7a68621d5f7a7c40eea01be45e",
},
"signature": "0xef1a36bb7cbd3884bf404811ed0e534b4ef3f87abe52f44838018b3af7c7a5534cd1b0b5c1cb55a32b122278ebc9fe37a92e2442ba83160e778163eafd444b9700"
}