- Table of contents
- Glossary
- Design
- Data format
- zkSync operations
- Smart contracts API
- Block state transition circuit
- Appendix I: Cryptographic primitives
- L1: layer-1 blockchain (Ethereum)
- Rollup: layer-2 blockchain (zkSync)
- Owner: a user who controls some assets in L2.
- Operator: entity operating the Rollup.
- Eventually: happening within finite time.
- Assets in rollup: assets in L2 smart contract controlled by owners.
- Rollup key: owner's private key used to control deposited assets.
- Rescue signature: the result of signing the owner's message, using his private key, used in Rollup internal transactions.
zkSync implements a ZK rollup protocol (in short "rollup" below) for ETH and ERC20 fungible token transfers.
General rollup workflow is as follows:
- Users can become owners in rollup by depositing assets from L1 or receiving a transfer from other owners.
- Owners can transfer assets to each other.
- Owners can withdraw assets under their control to an L1 address.
Rollup operation requires the assistance of an operator, who rolls transactions together, computes a zero-knowledge proof of the correct state transition, and affects the state transition by interacting with the rollup contract.
Cryptography assumptions:
- DLP is unbroken.
- Rescue hash and sha256 are collision-resistant.
- ZKP scheme used in the construction is secure (subject to a separate formal proof).
L1 blockchain assumptions:
- L1 protocol is secure.
- L1 is eventually censorship-resistant: a sufficiently highly priced L1 tx will be mined in a block within finite time.
- Owners have access to the full L1 archive (can at any time retrieve all block bodies of the L1 chain).
Operational assumptions:
- Rollup key is controlled by the owner and not compromised at all times.
-
1. Continuous ownership: assets deposited in rollup are immediately under control of the specified, unique owner.
-
2. Control: assets in rollup can not be transferred (change owner), change in value, disappear or be moved out of rollup, unless the owner initiates a corresponding action.
-
3. Eventual retrievability: assets in rollup controlled by an owner can eventually be withdrawn to the L1 address of the owner's choice, without the cooperation of any other party except L1 miners.
This includes, in particular, the following claims:
- 3.1. Double-spends in Rollup are not possible.
- 3.2. Rollup always operates under full reserve: the total supply of each asset type in rollup is equal to the sum of all its deposited amounts minus sum of all its withdrawn amounts.
- 3.3. Root hash is always correct and L2 state can not be corrupted
- 3.4. A state can always be restored from the calldata published to L1
- 3.5. The smart contract can not be locked out
Type | Byte len | Encoding | Comment |
---|---|---|---|
AccountId | 4 | BE integer | Incremented number of accounts in Rollup. New account will have the next free id. Max value is 2^32 - 1 = 4.294967295 × 10^9 |
TokenId | 2 | BE integer | Incremented number of tokens in Rollup, max value is 65535 |
PackedTxAmount | 5 | Parameters | Packed transactions amounts are represented with 40 bit (5 byte) values, encoded as mantissa × 10^exponent where mantissa is represented with 35 bits, exponent is represented with 5 bits. This gives a range from 0 to 34359738368 × 10^31, providing 10 full decimal digit precision. |
PackedFee | 2 | Parameters | Packed fees must be represented with 2 bytes: 5 bit for exponent, 11 bit for mantissa. |
StateAmount | 16 | BE integer | State amount is represented as uint128 with a range from 0 to ~3.4 × 10^38. It allows to represent up to 3.4 × 10^20 "units" if standard Ethereum's 18 decimal symbols are used. This should be a sufficient range. |
Nonce | 4 | BE integer | Nonce is the total number of executed transactions of the account. In order to apply the update of this state, it is necessary to indicate the current account nonce in the corresponding transaction, after which it will be automatically incremented. If you specify the wrong nonce, the changes will not occur. |
RollupPubkeyHash | 20 | LE integer | To make a public key hash from a Rollup public key apply Rescue hash function to the [x,y] points of the key and then take the last 20 bytes of the result. |
EthAddress | 20 | LE integer | To make an Ethereum address from the Etherum's public key, all we need to do is to apply Keccak-256 hash function to the key and then take the last 20 bytes of the result. |
PackedRollupPubkey | 32 | LE integer | A Rollup public key is the first 32 bytes of a Rollup public key |
TxHash | 32 | LE integer | To get hash for transaction apply SHA256 function to concatenated bytes of transaction fields |
Signature | 64 | LE integer | Read transaction signature |
BlockNumber | 4 | BE integer | Incremented number of Rollup blocks, max number is 4294967295 |
RootHash | 32 | LE integer | Merkle tree root hash |
Timestamp | 64 | BE integer | unix timestamp |
Amounts and fees are compressed in zkSync using simple fundamentals of floating point arithmetic.
A floating-point number has the following parts: a mantissa, a radix, and an exponent. The mantissa (always non-negative in our case) holds the significant digits of the floating-point number. The exponent indicates the power of the radix that the mantissa and sign should be multiplied by. The components are combined as follows to get the floating-point value:
sign * mantissa * (radix ^ exponent)
Mantissa and exponent parameters used in zkSync:
Type | Exponent bit width | Mantissa bit width | Radix |
---|---|---|---|
PackedTxAmount | 5 | 35 | 10 |
PackedFee | 5 | 11 | 10 |
Accounts and Balances trees representation:
Legend:
- Ha is account tree height. (24)
- Hb is balance tree height. (8)
We have directly one main Accounts tree
and its leaves Accounts
also have subtrees Balances tree
with their own
Balances
leaves.
The leaf hash is the rescue hash of its bit string representation described below. To get bit string representation each filed is encoded as bits starting from least significant (LE order for bits) and concatenated in the order they are present in the structure.
Each account is inserted into the Accounts tree as a leaf with the least free id (AccountId
).
Field | Type |
---|---|
nonce | Nonce |
pubkey_hash | RollupPubkeyHash |
address | EthAddress |
state_tree_root | RootHash |
state_tree_root
is combined using Rescue hash of the root hash of the balance_tree_root
(as a field
element) and zero field element reserved for future subtree root hash and then padded to 256 bits (by adding 0 bits in
LE bit representation)
An empty leaf contains: state_tree_root
computed using empty balances subtree. Empty balances subtree of the account
has zero in each leaf.
Field | Type |
---|---|
value | StateAmount |
And empty leaf contains value
equal to zero.
Rollup block pub data consists of Rollup operations pub data sequence. The maximum block size is a constant value. If the size of the operations included in the block is not enough to fill it completely, the remainder will be filled with empty Noop operations.
zkSync operations are divided into Rollup transactions (initiated inside Rollup by a Rollup account) and Priority operations (initiated on the mainchain by an Ethereum account).
Rollup transactions:
- Noop
- Transfer
- Transfer to new
- Withdraw (Partial exit)
- Change pubkey
- Forced Exit
- MintNFT
- WithdrawNFT
- Swap
Priority operations:
- Deposit
- Full exit
Full list: https://docs.google.com/spreadsheets/d/1ejK1MJfVehcwjgjVDFD3E2k1EZ7auqbG_y0DKidS9nA/edit#gid=0
Legend:
- Transaction: what users can submit to the operator directly.
- Priority operation: what users can submit to the zkSync smart contract.
- Rollup operation: part of the rollup block representing
Transaction
orPriority operation
. - Onchain operation: what the operator can put into the rollup block pubdata (operation pubdata).
- Node implementation: node model that describes an operation.
- Circuit implementation: circuit model that describes the operation and its witness.
- Chunk: the dimension of the operation. Each chunk has its own part of the public data (10 bytes) given through witnesses.
- Significant bytes: how many bytes, of all bytes occupied by the operation, are significant (including operation number).
- Hash: the result of SHA-256 function with operation's pubdata as input. Used for operation identification.
- User creates a
Transaction
or aPriority operation
. - After processing this request, operator creates a
Rollup operation
and adds it to the block. - Once the block is complete, operator submits it to the zkSync smart contract as a block commitment. Part of the logic
of some
Rollup operations
is checked by the smart contract. - The proof for the block is submitted to the zkSync smart contract as the block verification. If the verification succeeds, the new state is considered finalized.
Critical circuit logic is described below in a python-like pseudocode for simplicity of comprehension. It describes invariants that should be preserved after rollup operation execution and account tree updates that should be performed if this invariants are true.
There are two main invariant functions: tree_invariants
and pubdata_invariants
. Each predicate in these functions
should evaluate to true.
Functions and data structures used in this pseudocode language:
# Returns account in the account tree given its id; it always returns something, since account tree is filled by empty account by default.
get_account_tree(account_id) -> Account
# Unpacks packed amount or fee; these functions implicitly enforce the invariant that amount and fee is packable
unpack_amount(packed_amount) -> StateAmount
unpack_fee(packed_fee) -> StateAmount
# Checks if account is empty: address, pubkey_hash, nonce and all balances are 0.
is_account_empty(Account) -> bool
# Returns signer for the given transaction
recover_signer_pubkey_hash(tx) -> RollupPubkeyHash
# Account data structure
Account.balance[token_id] -> StateAmount # returns account balance of the given token
Account.nonce -> Nonce # Nonce of the account
Account.address -> EthAddress # Address of the account
Account.pubkey_hash -> RollupPubkeyHash # Hash of the public key set for the account
# Constants
MAX_TOKENS = 2**32 # maximum number of tokens in the Rollup(including "ETH" token)
MAX_FUNGIBLE_TOKENS = 2**16 # maximum number of fungible tokens in the Rollup(including "ETH" token)
MAX_NONCE = 2**32 # max possible nonce
No effects.
Chunks | Significant bytes |
---|---|
1 | 1 |
Field | Byte len | Value/type | Description |
---|---|---|---|
Opcode | 1 | 0x00 |
Operation code |
000000000000000000
No user transaction.
No effects. This operation is used for padding the block with zero bytes (cheap in calldata) to the full block capacity.
Transfers funds between Rollup accounts.
Chunks | Significant bytes |
---|---|
2 | 20 |
Field | Byte len | Value/type | Description |
---|---|---|---|
opcode | 1 | 0x05 |
Operation code |
from_account | 4 | AccountId | Unique identifier of the rollup account from which funds will be withdrawn (sender) |
token | 4 | TokenId | Unique token identifier in the rollup |
to_account | 4 | AccountId | Unique identifier of the rollup account that will receive the funds (recipient) |
packed_amount | 5 | PackedTxAmount | Packed amount of funds sent |
packed_fee | 2 | PackedFee | Packed amount of fee paid |
050000000400000002000000030000001ad30012
Reads as: transfer from account #4 token #2 to account #3 amount in packed representation 0x0000001ad3 for fee in packed representation 0x0012.
Field | Value/type | Description |
---|---|---|
type | 0xfa |
Operation code |
account_id | AccountId | Unique id of the sender rollup account in the state tree |
from | ETHAddress | Unique address of the rollup account from which funds will be withdrawn (sender) |
to | ETHAddress | Unique address of the rollup account that will receive the funds (recipient) |
token | TokenId | Unique token identifier in the rollup |
amount | PackedTxAmount | Amount of funds sent |
fee | PackedFee | Amount of fee paid |
nonce | Nonce | A one-time code that specifies the order of transactions |
valid_from | Timestamp | Unix timestamp from which the block with this transaction can be processed |
valid_until | Timestamp | Unix timestamp until which the block with this transaction can be processed |
signature | Signature | Signature of previous fields, see the spec below |
User transaction representation. (NOTE: tx bytecode differs slightly from this representation due to data packing, see the spec below).
{
"type": "Transfer",
"accountId": 2061,
"from": "0x1f04204dba8e9e8bf90f5889fe4bdc0f37265dbb",
"to": "0x05e3066450dfcd4ee9ca4f2039d58883631f0460",
"token": 1023,
"amount": "12340000000000",
"fee": "56700000000",
"nonce": 784793056,
"validFrom": 0,
"validUntil": 18446744073709551615,
"signature": {
"pubKey": "0e1390d3e86881117979db2b37e40eaf46b6f8f38d2509ff3ecfaf229c717b9d",
"signature": "817c866e71a0b3e6d412ac56524557d368c11332db93554693787e89c9813310adeda68314fc833a4f73323eca00e2cc774e78db88921dc230db7dae691fe500"
}
}
Signed transaction representation.
Signer:
Private key: Fs(0x057afe7e950189b17eedfd749f5537a88eb3ed4981467636a115e5c3efcce0f4)
Public key: x: Fr(0x0e63e65569365f7d2db43642f9cb15781120364f5e993cd6822cbab3f86be4d3), y: Fr(0x1d7b719c22afcf3eff09258df3f8b646af0ee4372bdb7979118168e8d390130e)
Signed transaction fields:
type: 0xfa
version: 0x01
accountId: 0x0000080d
from: 0x1f04204dba8e9e8bf90f5889fe4bdc0f37265dbb
to: 0x05e3066450dfcd4ee9ca4f2039d58883631f0460
token: 0x000003ff
amount: 0x5bf0aea003
fee: 0x46e8
nonce: 0x2ec6fde0
time_range: 0x0000000000000000ffffffffffffffff
Signed bytes: 0xfa010000080d1f04204dba8e9e8bf90f5889fe4bdc0f37265dbb05e3066450dfcd4ee9ca4f2039d58883631f0460000003ff5bf0aea00346e82ec6fde00000000000000000ffffffffffffffff
Field | Value/type | Description |
---|---|---|
tx | TransferTx | Signed transfer transaction defined above |
from_account_id | AccountId | Unique id of the sender rollup account in the state tree |
to_account_id | AccountId | Unique id of the recipient rollup account in the state tree |
# TransferOp - Rollup operation described above
# Block - block where this Rollup operation is executed
# OnchainOp - public data created after executing this rollup operation and posted to the Ethereum
from_account = get_account_tree(TransferOp.from_account_id)
to_account = get_account_tree(TransferOp.to_account_id)
fee_account = get_account_tree(Block.fee_account)
amount = unpack_amount(TransferOp.tx.packed_amount)
fee = unpack_fee(TransferOp.tx.packed_fee)
def tree_invariants():
TransferOp.token < MAX_TOKENS
from_account.id == TransferOp.tx.from_account_id;
from_account.nonce == TransferOp.tx.nonce
from_account.nonce < MAX_NONCE
from_account.balance(TransferOp.tx.token) >= (amount + fee)
from_account.pubkey_hash == recover_signer_pubkey_hash(TransferOp.tx)
from_account.address == TransferOp.tx.from_address
to_account.address == TransferOp.tx.to_address
def tree_updates():
from_account.balance[TransferOp.tx.token] -= (amount + fee)
from_account.nonce += 1
to_acccount.balance[TransferOp.tx.token] += amount
fee_account.balance[TransferOp.tx.token] += fee
def pubdata_invariants():
OnhcainOp.opcode == 0x05
OnchainOp.from_account == TransferOp.from_account_id
OnchainOp.token == TransferOp.tx.token
OnchainOp.to_account == TransferOp.to_account_id
OnhcainOp.packed_amount == TransferOp.tx.packed_amount
OnhcainOp.packed_fee == TransferOp.tx.packed_fee
Transfers funds from Rollup account to a new Rollup account (dynamically assigns a free account number to a Rollup address). So, the "account creation" will be performed first, that is, the correspondence RollupAddress - AccountId is assigned. And then the usual funds' Transfer between Rollup accounts will occur.
Chunks | Significant bytes |
---|---|
6 | 40 |
Field | Byte len | Value/type | Description |
---|---|---|---|
opcode | 1 | 0x02 |
Operation code |
from_account | 4 | AccountId | Unique identifier of the rollup account from which funds will be withdrawn (sender) |
token | 4 | TokenId | Unique token identifier in the rollup |
packed_amount | 5 | PackedTxAmount | Packed amount of funds sent |
to_address | 20 | ETHAddress | The address of the rollup account that will receive the funds (recipient) |
to_account | 4 | AccountId | Unique identifier of the rollup account that will receive the funds (recipient) |
packed_fee | 2 | PackedFee | Packed amount of fee paid |
0200000004000000020000001ad3080910111213141516171819202122233425262800000003001200000000
Reads as: transfer from account #4 token #2 amount in packed representation 0x0000001ad3 to account with address 0x0809101112131415161718192021222334252628 and id #3 for fee in packed representation 0x0012.
Same as Transfer
Field | Value/type | Description |
---|---|---|
tx | TransferTx | Signed transfer transaction defined above |
from_account_id | AccountId | Unique id of the sender rollup account in the state tree |
to_account_id | AccountId | Unique id of the recipient rollup account in the state tree |
# TransferToNewOp - Rollup operation described above
# Block - block where this Rollup operation is executed
# OnchainOp - public data created after executing this rollup operation and posted to the Ethereum
from_account = get_account_tree(TransferToNewOp.from_account_id)
to_account = get_account_tree(TransferToNewOp.to_account_id)
fee_account = get_account_tree(Block.fee_account)
amount = unpack_amount(TransferToNewOp.tx.packed_amount)
fee = unpack_fee(TransferToNewOp.tx.packed_fee)
def tree_invariants():
TransferToNewOp.token < MAX_TOKENS
from_account.id == TransferToNewOp.tx.from_account_id;
from_account.nonce == TransferToNewOp.tx.nonce
from_account.nonce < MAX_NONCE
from_account.balance[TransferToNewOp.tx.token] >= (amount + fee)
from_account.pubkey_hash == recover_signer_pubkey_hash(TransferToNewOp.tx)
from_account.address == TransferToNewOp.tx.from_address
is_account_empty(to_account) == True
def tree_updates():
from_account.balance[TransferOp.tx.token] -= (amount + fee)
from_account.nonce += 1
to_account.address = TransferToNewOp.to_address
to_account.balance[TransferOp.tx.token] += amount
fee_account.balance[TransferOp.tx.token] += fee
def pubdata_invariants():
OnhcainOp.opcode == 0x02
OnchainOp.from_account == TransferToNewOp.from_account_id
OnchainOp.token == TransferToNewOp.tx.token
OnhcainOp.packed_amount == TransferToNewOp.tx.packed_amount
OnchainOp.to_address == TransferToNewOp.tx.to_address
OnchainOp.to_account == TransferToNewOp.to_account_id
OnhcainOp.packed_fee == TransferToNewOp.tx.packed_fee
Withdraws funds from Rollup account to appropriate balance of the indicated Ethereum address.
Chunks | Significant bytes |
---|---|
6 | 47 |
Field | Byte len | Value/type | Description |
---|---|---|---|
opcode | 1 | 0x03 |
Operation code |
from_account | 4 | AccountId | Unique identifier of the rollup account from which funds will be withdrawn (sender) |
token | 4 | TokenId | Unique token identifier in the rollup |
full_amount | 16 | StateAmount | Full amount of funds sent |
packed_fee | 2 | PackedFee | Packed amount of fee paid |
to_address | 20 | EthAddress | The address of Ethereum account, to the balance of which funds will be accrued (recipient) |
030000000400000002000000000000000002c68af0bb1400000012080910111213141516171819202122233425262800000000
Reads as: transfer from account #4 token #2 amount 0x000000000000000002c68af0bb140000 for fee packed in representation 0x0012 to ethereum account with address 0x0809101112131415161718192021222334252628.
Field | Value/type | Description |
---|---|---|
type | 0xfc |
Operation code |
account_id | AccountId | Unique id of the sender rollup account in the state tree |
from_address | ETHAddress | Unique address of the rollup account from which funds will be withdrawn (sender) |
to_address | EthAddress | The address of Ethereum account, to the balance of which the funds will be accrued(recipient) |
token | TokenId | Unique token identifier in the rollup |
amount | StateAmount | Full amount of funds sent |
fee | PackedFee | Packed amount of fee paid |
nonce | Nonce | A one-time code that specifies the order of transactions |
valid_from | Timestamp | Unix timestamp from which the block with this transaction can be processed |
valid_until | Timestamp | Unix timestamp until which the block with this transaction can be processed |
signature | Signature | Signature of previous fields, see the spec below |
User transaction representation. (NOTE: tx bytecode differs slightly from this representation due to data packing, see the spec below)..
{
"type": "Withdraw",
"accountId": 4118,
"from": "0x041f3b8db956854839d7434f3e53c7141a236b16",
"to": "0xdc8f1d4d7b5b4cde2dbc793c1d458f8916cb0513",
"token": 1023,
"amount": "12340000000000",
"fee": "56700000000",
"nonce": 352676723,
"signature": {
"pubKey": "0e1390d3e86881117979db2b37e40eaf46b6f8f38d2509ff3ecfaf229c717b9d",
"signature": "e29781125bdd3c1d0a4e863e7d364979dd3e3a36c2cd31fcf18d3853ae417201632c5c6ce4ac24da6a3ca99ed7e2005a74e6d6e2a3378d4102d68efb02d28d03"
},
"fast": false,
"validFrom": 0,
"validUntil": 18446744073709551615
}
Signed transaction representation.
Signer:
Private key: Fs(0x057afe7e950189b17eedfd749f5537a88eb3ed4981467636a115e5c3efcce0f4)
Public key: x: Fr(0x0e63e65569365f7d2db43642f9cb15781120364f5e993cd6822cbab3f86be4d3), y: Fr(0x1d7b719c22afcf3eff09258df3f8b646af0ee4372bdb7979118168e8d390130e)
Signed transaction fields:
type: 0xfc
version: 0x01
accountId: 0x00001016
from: 0x041f3b8db956854839d7434f3e53c7141a236b16
to: 0xdc8f1d4d7b5b4cde2dbc793c1d458f8916cb0513
token: 0x000003ff
fullAmount: 0x000000000000000000000b3921510800
fee: 0x46e8
nonce: 0x15056b73
time_range: 0x0000000000000000ffffffffffffffff
Signed bytes: 0xfc0100001016041f3b8db956854839d7434f3e53c7141a236b16dc8f1d4d7b5b4cde2dbc793c1d458f8916cb0513000003ff000000000000000000000b392151080046e815056b730000000000000000ffffffffffffffff
Field | Value/type | Description |
---|---|---|
tx | WithdrawTx | Signed withdraw transaction defined above |
# WithdrawOp - Rollup operation described above
# Block - block where this Rollup operation is executed
# OnchainOp - public data created after executing this rollup operation and posted to the Ethereum
account = get_tree_account(WithdrawOp.tx.account_id)
fee_account = get_tree_account(Block.fee_account)
fee = unpack_fee(WithdrawOp.tx.packed_fee)
def tree_invariants():
WithdrawOp.token < MAX_FUNGIBLE_TOKENS
account.nonce == WithdrawOp.nonce
account.nonce < MAX_NONCE
account.balance[WithdrawOp.tx.token] >= (amount + fee)
account.pubkey_hash == recover_signer_pubkey_hash(WithdrawOp.tx)
def tree_updates():
account.balance[WithdrawOp.tx.token] -= (amount + fee)
account.nonce += 1
fee_account.balance[WithdrawOp.token] += fee
def pubdata_invariants():
OnhcainOp.opcode == 0x03
OnchainOp.from_account == WithdrawOp.tx.account_id
OnchainOp.token == WithdrawOp.tx.token
OnhcainOp.full_amount == WithdrawOp.tx.amount
OnhcainOp.packed_fee == WithdrawOp.tx.packed_fee
OnchainOp.to_address == WithdrawOp.tx.to_address
Withdraws NFT from Rollup account to appropriate ethereum account.
Chunks | Significant bytes |
---|---|
10 | 95 |
Field | Byte len | Value/type | Description |
---|---|---|---|
opcode | 1 | 0x0a |
Operation code |
from_account | 4 | AccountId | Unique identifier of the rollup account from which funds will be withdrawn (sender) |
creator_account | 4 | AccountId | Unique identifier of the rollup account which create the NFT (creator) |
creator_address | 20 | EthAddress | The address of Ethereum account, to the balance of which create the NFT (creator) |
serial_id | 4 | Int | Special id for NFT for enforcing uniqueness |
content_hash | 32 | H256 | Content hash of NFT |
to_address | 20 | EthAddress | The address of Ethereum account, to the balance of which funds will be accrued (recipient) |
token | 4 | TokenId | Unique token identifier in the rollup |
fee_token | 4 | TokenId | Token for paying fees |
packed_fee | 2 | PackedFee | Packed amount of fee paid |
0a0000002a0000002b21abaed8712072e918632259780e587698ef58da00000000000000000000000000000000000000000000000000000000000000000000000021abaed8712072e918632259780e587698ef58da000100000000002a05400000000000
Reads as: Withdraw NFT from account #4 token #2 for fee packed in representation and paying fee in token 0x0012 to ethereum account with address 0x0809101112131415161718192021222334252628.
Field | Value/type | Description |
---|---|---|
type | 0xf5 |
Operation code |
account_id | AccountId | Unique id of the sender rollup account in the state tree |
from_address | ETHAddress | Unique address of the rollup account from which funds will be withdrawn (sender) |
to_address | EthAddress | The address of Ethereum account, to the balance of which the funds will be accrued(recipient) |
token | TokenId | Unique token identifier in the rollup |
fee_token | TokenId | Fee token identifier in the rollup |
fee | PackedFee | Packed amount of fee paid |
nonce | Nonce | A one-time code that specifies the order of transactions |
valid_from | Timestamp | Unix timestamp from which the block with this transaction can be processed |
valid_until | Timestamp | Unix timestamp until which the block with this transaction can be processed |
signature | Signature | Signature of previous fields, see the spec below |
User transaction representation. (NOTE: tx bytecode differs slightly from this representation due to data packing, see the spec below)..
{
"type": "WithdrawNFT",
"accountId": 4118,
"from": "0x041f3b8db956854839d7434f3e53c7141a236b16",
"to": "0xdc8f1d4d7b5b4cde2dbc793c1d458f8916cb0513",
"token": 274474656,
"feeToken": 1023,
"fee": "12340000000000",
"nonce": 3924379879,
"signature": {
"pubKey": "0e1390d3e86881117979db2b37e40eaf46b6f8f38d2509ff3ecfaf229c717b9d",
"signature": "55aea80f1359cbedf68f86509a51ab721c7656263b221f8afa38a6c794ba181d119396c3d82e93feb61d2a5bef44069a3c1a7086206d7fab923abcc16e30b501"
},
"fast": false,
"validFrom": 0,
"validUntil": 18446744073709551615
}
Signed transaction representation.
Signer:
Private key: Fs(0x057afe7e950189b17eedfd749f5537a88eb3ed4981467636a115e5c3efcce0f4)
Public key: x: Fr(0x0e63e65569365f7d2db43642f9cb15781120364f5e993cd6822cbab3f86be4d3), y: Fr(0x1d7b719c22afcf3eff09258df3f8b646af0ee4372bdb7979118168e8d390130e)
Signed transaction fields:
type: 0xf5
version: 0x01
accountId: 0x00001016
from: 0x041f3b8db956854839d7434f3e53c7141a236b16
to: 0xdc8f1d4d7b5b4cde2dbc793c1d458f8916cb0513
token: 0x105c26a0
fee_token: 0x000003ff
fee: 0x9a4a
nonce: 0xe9e948e7
time_range: 0x0000000000000000ffffffffffffffff
Signed bytes: 0xf50100001016041f3b8db956854839d7434f3e53c7141a236b16dc8f1d4d7b5b4cde2dbc793c1d458f8916cb0513105c26a0000003ff9a4ae9e948e70000000000000000ffffffffffffffff
Field | Value/type | Description |
---|---|---|
tx | WithdrawNFTTx | Signed withdraw transaction defined above |
# WithdrawNFTOp - Rollup operation described above
# Block - block where this Rollup operation is executed
# OnchainOp - public data created after executing this rollup operation and posted to the Ethereum
account = get_tree_account(WithdrawNFTOp.tx.account_id)
fee_account = get_tree_account(Block.fee_account)
fee = unpack_fee(WithdrawNFTOp.tx.packed_fee)
def tree_invariants():
WithdrawNFTOp.token < MAX_TOKENS
WithdrawNFTOp.token > MAX_FUNGIBLE_TOKENS
WithdrawNFTOp.fee_token < MAX_FUNGIBLE_TOKENS
account.nonce == WithdrawNFTOp.nonce
account.nonce < MAX_NONCE
account.balance[WithdrawNFTOp.tx.token] == 1
account.balance[WithdrawNFTOp.tx.fee_token] >= fee
account.pubkey_hash == recover_signer_pubkey_hash(WithdrawNFTOp.tx)
def tree_updates():
account.balance[WithdrawNFTOp.tx.token] = 0
account.balance[WithdrawNFTOp.tx.fee_token] -= fee
account.nonce += 1
fee_account.balance[WithdrawNFTOp.token] += fee
def pubdata_invariants():
OnhcainOp.opcode == 0x0a
OnchainOp.from_account == WithdrawNFTOp.tx.account_id
OnchainOp.token == WithdrawNFTOp.tx.token
OnchainOp.fee_token == WithdrawNFTOp.tx.fee_token
OnhcainOp.packed_fee == WithdrawNFTOp.tx.packed_fee
OnchainOp.to_address == WithdrawNFTOp.tx.to_address
Mints an NFT token inside Rollup
Chunks | Significant bytes |
---|---|
5 | 47 |
Field | Byte len | Value/type | Description |
---|---|---|---|
opcode | 1 | 0x09 |
Operation code |
creator_account | 4 | AccountId | Unique identifier of the rollup account will mint nft (creator) |
recipient | 4 | AccountId | Recipient of NFT |
content_hash | 32 | ContentHash | Content for NFT |
fee_token | 4 | TokenId | Fee for paying fee |
packed_fee | 2 | PackedFee | Packed amount of fee paid |
090000000a0000000b0000000000000000000000000000000000000000000000000000000000000000000000000140000000
Reads as: Mint NFT from account to recipient with content hash and pay packed fee in fee_token
Field | Value/type | Description |
---|---|---|
type | 0xf6 |
Operation code |
account_id | AccountId | Unique id of the sender rollup account in the state tree |
from_address | ETHAddress | Unique address of the rollup account from which funds will be withdrawn (sender) |
to_address | EthAddress | The address of Ethereum account, to the balance of which the funds will be accrued(recipient) |
token | TokenId | Unique token identifier in the rollup |
fee_token | TokenId | Fee token identifier in the rollup |
fee | PackedFee | Packed amount of fee paid |
nonce | Nonce | A one-time code that specifies the order of transactions |
signature | Signature | Signature of previous fields, see the spec below |
User transaction representation. (NOTE: tx bytecode differs slightly from this representation due to data packing, see the spec below)..
{
"type": "MintNFT",
"creatorId": 4118,
"creatorAddress": "0x041f3b8db956854839d7434f3e53c7141a236b16",
"contentHash": "0x0d185e587f0a80e93cd0f311cf9c7a0071ae63d088cfc810de55c9f6d9fb4bcc",
"recipient": "0xdc8f1d4d7b5b4cde2dbc793c1d458f8916cb0513",
"fee": "12340000000000",
"feeToken": 1023,
"nonce": 352676723,
"signature": {
"pubKey": "0e1390d3e86881117979db2b37e40eaf46b6f8f38d2509ff3ecfaf229c717b9d",
"signature": "59aff7f0f3dff5c6b0efc1f919599e62214bb99933438b74acb96076598b41ae4976b68328492978b349e907aebfc9dce2b1298debda589686517780afae7203"
}
}
Signed transaction representation.
Signer:
Private key: Fs(0x057afe7e950189b17eedfd749f5537a88eb3ed4981467636a115e5c3efcce0f4)
Public key: x: Fr(0x0e63e65569365f7d2db43642f9cb15781120364f5e993cd6822cbab3f86be4d3), y: Fr(0x1d7b719c22afcf3eff09258df3f8b646af0ee4372bdb7979118168e8d390130e)
Signed transaction fields:
type: 0xf6
version: 0x01
creatorId: 0x00001016
creatorAddress: 0x041f3b8db956854839d7434f3e53c7141a236b16
contentHash: 0x0d185e587f0a80e93cd0f311cf9c7a0071ae63d088cfc810de55c9f6d9fb4bcc
recipient: 0xdc8f1d4d7b5b4cde2dbc793c1d458f8916cb0513
fee_token: 0x000003ff
fee: 0x9a4a
nonce: 0x15056b73
Signed bytes: 0xf60100001016041f3b8db956854839d7434f3e53c7141a236b160d185e587f0a80e93cd0f311cf9c7a0071ae63d088cfc810de55c9f6d9fb4bccdc8f1d4d7b5b4cde2dbc793c1d458f8916cb0513000003ff9a4a15056b73
Field | Value/type | Description |
---|---|---|
tx | MintNFTTx | Signed mint nft transaction defined above |
# MintNFTOp - Rollup operation described above
# Block - block where this Rollup operation is executed
# OnchainOp - public data created after executing this rollup operation and posted to the Ethereum
creator_account = get_tree_account(MintNFTOp.tx.creator_id)
recipient_account = get_tree_account(MintNFTOp.tx.recipient)
fee_account = get_tree_account(Block.fee_account)
fee = unpack_fee(MintNFTOp.tx.packed_fee)
def tree_invariants():
MintNFTOp.fee_token < MAX_FUNGIBLE_TOKENS
creator_account.nonce == MintNFTOp.nonce
creator_account.nonce < MAX_NONCE
creator_account.balance[MintNFTOp.tx.fee_token] >= fee
creator_account.pubkey_hash == recover_signer_pubkey_hash(MintNFTOp.tx)
def tree_updates():
creator_account.balance[MintNFTOp.tx.fee_token] -= fee
creator_account.balance[SPECIAL_NFT_TOKEN] += 1
special_nft_account.balance[SPECIAL_NFT_TOKEN] += 1
creator_account.nonce += 1
recipient_account[minted_token] = 1
fee_account.balance[MintNFTOp.token] += fee
def pubdata_invariants():
OnhcainOp.opcode == 0x09
OnchainOp.creator_account == MintNFTOp.tx.account_id
OnchainOp.recipient == MintNFTOp.tx.recipient
OnchainOp.fee_token == MintNFTOp.tx.fee_token
OnhcainOp.packed_fee == MintNFTOp.tx.packed_fee
Deposits funds from Ethereum account to the specified Rollup account. Deposit starts as priority operation - user calls
contract method depositEth
to deposit ethereum, or depositERC20
to
deposit ERC-20 tokens. After that operator includes this operation in a block. In the account tree, the new account will
be created if needed.
Chunks | Significant bytes |
---|---|
6 | 45 |
Field | Byte len | Value/type | Description |
---|---|---|---|
opcode | 1 | 0xfe |
Operation code |
to_account | 4 | AccountId | Unique identifier of the rollup account that will receive the funds (recipient) |
token | 4 | TokenId | Unique token identifier in the rollup |
full_amount | 16 | StateAmount | Full amount of funds sent |
to_address | 20 | ETHAddress | The address of the rollup account that will receive the funds (recipient) |
fe010000000400000002000000000000000002c68af0bb1400000809101112131415161718192021222334252628000000000000
Reads as: deposit to account #4 token #2 amount 0x000000000000000002c68af0bb140000, account will have address 0x0809101112131415161718192021222334252628.
Field | Byte len | Value/type | Description |
---|---|---|---|
token | 4 | TokenId | Unique token identifier in the rollup |
full_amount | 16 | StateAmount | Full amount of funds sent |
to_address | 20 | ETHAddress | The address of the rollup account that will receive the funds (recipient) |
Field | Value/type | Description |
---|---|---|
op | DepositPriorityOp | Priority operation defined above |
to_account_id | AccountId | Unique identifier of the rollup account that will receive the funds (recipient) |
# DepositOp - Rollup operation described above
# OnchainOp - public data created after executing this rollup operation and posted to the Ethereum
account = get_account_tree(DepositOp.to_account_id)
def tree_invariants():
DepositOp.token < MAX_FUNGIBLE_TOKENS
is_account_empty(account) == True or account.address == DepositOp.op.to_address
def tree_updates():
if is_account_empty(account):
account.address = DepositOp.op.to_address
account.balance[DepositOp.op.token] += DepositOp.op.full_amount
def pubdata_invariants():
OnhcainOp.opcode == 0x01
OnchainOp.to_account == DepositOp.to_account_id
OnchainOp.token == DepositOp.op.token
OnhcainOp.full_amount == DepositOp.op.amount
OnchainOp.to_address == DepositOp.op.to_address
It is possible that the operator for some reason does not include this operation in the block. Then, through the number
of ethereum blocks set on the smart contract, the exodus mode will be launched. It will allow recipient account (i.e.
with msg.sender == Deposit.to
) to withdraw funds from zkSync contract.
The user can request this operation to withdraw funds if he thinks that his transactions are censored by validators.
It starts as a priority operation - user calls contract method fullExit
. After that operator includes this operation
in a block.
Chunks | Significant bytes |
---|---|
11 | 85 |
Field | Byte len | Value/type | Description |
---|---|---|---|
opcode | 1 | 0xf9 |
Operation code |
account_id | 4 | AccountId | Unique identifier of the rollup account from which funds will be withdrawn (sender) |
owner | 20 | EthAddress | The address of the fund owner account. Funds will be accrued to the balance of this address |
token | 4 | TokenId | Unique token identifier in the rollup |
full_amount | 16 | StateAmount | Full amount of funds that had been withdrawn |
creator_account_id (Optional) | 4 | AccountId | Unique identifier of the rollup account from which created nft |
serial_id (Optional) | 4 | Int | |
content_hash (Optional) | 32 | ContentHash | Content of NFT |
060000002a2a0a81e257a2f5d6ed4f07b81dbda09f107bd0260000002a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
Reads as: full exit from account #4 with with address 0x0809101112131415161718192021222334252628, token #2, amount is 0x000000000000000002c68af0bb140000.
Field | Byte len | Value/type | Description |
---|---|---|---|
account_id | 4 | AccountId | Unique identifier of the rollup account |
eth_address | 20 | ETHAddress | The address of the account |
token | 4 | TokenId | Unique token identifier in the rollup |
Field | Value/type | Description |
---|---|---|
op | FullExitPriorityOp | Priority operation defined above |
# FullExitOp - Rollup operation described above
# OnchainOp - public data created after executing this rollup operation and posted to the Ethereum
account = get_account_tree(FullExitOp.op.account_id)
withdrawn_amount = 0
def tree_invariants():
FullExitOp.op.token < MAX_TOKENS
account.id == FullExitOp.op.id
def tree_updates():
if account.address == FullExitOp.op.eth_address:
withdrawn_amount = account.balance[FullExitOp.op.token]
account.balance[FullExitOp.op.token] = 0
def pubdata_invariants():
OnhcainOp.opcode == 0x06
OnchainOp.account_id == FullExitOp.op.account_id
OnhcainOp.owner == FullExitOp.op.eth_address
OnchainOp.token == FullExitOp.op.token
OnchainOp.full_amount == withdrawn_amount
If something went wrong on the server side - full exit operation will be included in a block with 0 (zero) amount in pubdata.
It is possible that the operator for some reason does not include this operation in the block. Then, through the number of ethereum blocks set on the smart contract, the exodus mode will be launched. After that, a user can submit exit proof to get her funds. Read more about censorship resistance and exodus mode in special sections.
Change pubkey - changes public key of the account that is used to authorize transactions. Change pubkey is authorized with ethereum keys for which address is the same as account address.
Chunks | Significant bytes |
---|---|
6 | 49 |
Field | Byte len | Value/type | Description |
---|---|---|---|
opcode | 1 | 0x07 |
Operation code |
account_id | 4 | AccountId | Unique identifier of the rollup account |
new_pubkey_hash | 20 | RollupPubkeyHash | Hash of the new rollup public key |
account_address | 20 | ETHAddress | Address of the account |
nonce | 4 | Nonce | Account nonce |
fee_token | 4 | TokenId | Unique token identifier in the rollup used to pay fee |
packed_fee | 2 | PackedFee | Packed amount of fee paid |
0700000411036945fcc11c349c3a300f19cd87cb03c4f2ef03e69588c1f4155dec60da3bf5113e029911ce330000000300010012
Reads as: change pubkey, account #4, new pubkey hash 0x11036945fcc11c349c3a300f19cd87cb03c4f2ef, address: 0x03e69588c1f4155dec60da3bf5113e029911ce33, nonce: 3, fee paid in token #0x0001, fee amount is 0x0012.
-
Transaction can be authorized by providing signature of the message
pubkey_message(account_id, nonce, new_pubkey_hash, additional_data)
(see definition below). Transaction will be verified on the contract. -
For users that can't sign messages it is possible to authorize this operation by calling
setAuthPubkeyHash
method of the smart contract. User should provide new pubkey hash and nonce for this transaction. After this transaction succeeded transaction without signature can be sent to operator. -
For users that want to use smart contract as a wallet and don't want to deploy it unless necessary it is possible to authorize change pubkey using CREATE2-change pubkey variant where wallet address is known and is derived from
new_pubkey_hash
.We check that account address is equal to the account address derived from
new_pubkey_hash
usingcreate2_address_zksync(creator_address, salt_arg, new_pubkey_hash, code_hash)
where all arguments are supplied by user and thataccount_nonce == 0
.
function pubkey_message(account_id, nonce, new_pubkey_hash, additional_data /* arbitrary 32 bytes*/): bytes {
return concat(new_pubkey_hash, nonce, account_id, additional_data);
}
function create2_address_zksync(creator_address, salt_arg /* abitrary 32 bytes */, new_pubkey_hash, code_hash): bytes {
const salt = keccak256(salt_arg, new_pubkey_hash);
// address is derrived using EIP-1014 https://eips.ethereum.org/EIPS/eip-1014
return CREATE2_ADDRESS(creator_address, salt, code_hash);
}
Field | Value/type | Description |
---|---|---|
type | 0xf8 |
Operation code |
account_id | AccountId | Unique id of the rollup account |
account | ETHAddress | Address of the rollup account |
new_pubkey_hash | RollupPubkeyHash | Hash of the new rollup public key |
fee_token | TokenId | Unique token identifier in the rollup used to pay fee |
fee | PackedFee | Packed amount of fee paid |
nonce | Nonce | A one-time code that specifies the order of transactions |
valid_from | Timestamp | Unix timestamp from which the block with this transaction can be processed |
valid_until | Timestamp | Unix timestamp until which the block with this transaction can be processed |
signature | Signature | Signature of previous fields, see the spec below |
eth_signature (optional) | ETHSignature | Ethereum signature of the message defined above. Null if operation was authorized on contract |
{
"type": "ChangePubKey",
"accountId": 2061,
"account": "0x1f04204dba8e9e8bf90f5889fe4bdc0f37265dbb",
"newPkHash": "sync:63aa2a0efb97064e0e52a6adb63a42018bd6e72b",
"feeToken": 1023,
"fee": "56700000000",
"nonce": 155724003,
"signature": {
"pubKey": "0e1390d3e86881117979db2b37e40eaf46b6f8f38d2509ff3ecfaf229c717b9d",
"signature": "a44999d6d7962b8216ebb2c53afc159f4bd85282844b1e443f82a1e50bfe1c2b70377ac0dbe1b8c14672fac3548df77f0b7bae0eb40c553191b9b0606ef86b04"
},
"ethSignature": null,
"ethAuthData": {
"type": "Onchain"
},
"validFrom": 0,
"validUntil": 18446744073709551615
}
Signed transaction representation.
Signer:
Private key: Fs(0x057afe7e950189b17eedfd749f5537a88eb3ed4981467636a115e5c3efcce0f4)
Public key: x: Fr(0x0e63e65569365f7d2db43642f9cb15781120364f5e993cd6822cbab3f86be4d3), y: Fr(0x1d7b719c22afcf3eff09258df3f8b646af0ee4372bdb7979118168e8d390130e)
Signed transaction fields:
type: 0xf8
version: 0x01
accountId: 0x0000080d
account: 0x1f04204dba8e9e8bf90f5889fe4bdc0f37265dbb
new_pub_key_hash: 0x63aa2a0efb97064e0e52a6adb63a42018bd6e72b
token: 0x000003ff
fee: 0x46e8
nonce: 0x094828e3
time_range: 0x0000000000000000ffffffffffffffff
Signed bytes: 0xf8010000080d1f04204dba8e9e8bf90f5889fe4bdc0f37265dbb63aa2a0efb97064e0e52a6adb63a42018bd6e72b000003ff46e8094828e30000000000000000ffffffffffffffff
Field | Value/type | Description |
---|---|---|
tx | ChangePubkeyTx | Transaction defined above |
# ChangePkOp - Rollup operation described above
# OnchainOp - public data created after executing this rollup operation and posted to the Ethereum
account = get_account_tree(ChankgePkOp.tx.account_id)
fee_account = get_tree_account(Block.fee_account)
fee = unpack_fee(WithdrawOp.tx.packed_fee)
def tree_invariants():
account.id == ChangePkOp.tx.id
account.address == ChangePkOp.tx.account
account.nonce < MAX_NONCE
account.nonce == ChangPkOp.tx.nonce
account.balance[ChangPkOp.tx.fee_token] >= fee
ChangePkOp.tx.new_pubkey_hash == recover_signer_pubkey_hash(ChangePkOp.tx)
def tree_updates():
account.balance[ChangPkOp.tx.fee_token] -= fee
account.pubkey_hash = ChangePkOp.tx.new_pubkey_hash
account.nonce += 1
fee_account.balance[ChangePkOp.token] += fee
def pubdata_invariants():
OnhcainOp.opcode == 0x07
OnchainOp.account_id == ChanePkOp.tx.account_id
OnhcainOp.new_pubkey_hash == ChangePkOp.tx.new_pubkey_hash
OnchainOp.account_address == ChangePkOp.tx.account
OnchainOp.nonce == ChangePkOp.tx.nonce
OnchainOp.fee_token == ChangePkOp.tx.fee_token
OnhcainOp.packed_fee == ChangePkOp.tx.packed_fee
Signature validity is verified twice:
- Rollup signature is verified in circuit.
- Ethereum signature is verified when transaction is committed to the Ethereum.
Withdraws funds from unowned Rollup account to appropriate balance of the Ethereum address, corresponding to a Rollup account. Unowned account is an account with no signing key set.
Chunks | Significant bytes |
---|---|
6 | 51 |
Field | Byte len | Value/type | Description |
---|---|---|---|
opcode | 1 | 0x08 |
Operation code |
initiator_account_id | 4 | AccountId | Unique identifier of the rollup account which initiates a forced exit operation (sender) |
target_account_id | 4 | AccountId | Unique identifier of the rollup account to perform a forced exit on (target) |
token | 4 | TokenId | Unique token identifier in the rollup |
full_amount | 16 | StateAmount | Full amount of funds sent |
packed_fee | 2 | PackedFee | Packed amount of fee paid |
target_address | 20 | EthAddress | The address of Ethereum account, to the balance of which funds will be accrued (recipient) |
0800000004000000050002000000000000000002c68af0bb1400000012080910111213141516171819202122233425262800000000
Reads as: account #4 initiates a transfer for account #5 of token #2 amount 0x000000000000000002c68af0bb140000 for fee packed in representation 0x0012 for the Rollup account which has Ethereum account address 0x0809101112131415161718192021222334252628.
Field | Value/type | Description |
---|---|---|
type | 0x08 |
Operation code |
initiator_account_id | AccountId | Unique id of the sender rollup account in the state tree |
target | ETHAddress | Unique address of the rollup account from which funds will be withdrawn (forced exit target) |
token | TokenId | Unique token identifier in the rollup |
fee | PackedFee | Packed amount of fee paid |
nonce | Nonce | A one-time code that specifies the order of transactions |
signature | Signature | Signature of previous fields, see the spec below |
User transaction representation. (NOTE: tx bytecode differs slightly from this representation due to data packing, see the spec below).
{
"initiatorAccountId": 4118,
"target": "0xdc8f1d4d7b5b4cde2dbc793c1d458f8916cb0513",
"token": 100,
"fee": "56700000000",
"nonce": 352676723,
"signature": {
"pubKey": "0e1390d3e86881117979db2b37e40eaf46b6f8f38d2509ff3ecfaf229c717b9d",
"signature": "a8e4e6b1d90c6a7ffb7786c8560bc22c6e2668d6c7675bff2e956b7511a9139eb3068127385e4905a2d597aa2bd9d630540d069634f5c2d76c64a6a322730d00"
}
}
Signed transaction representation.
Signed using:
Private key: Fs(0x057afe7e950189b17eedfd749f5537a88eb3ed4981467636a115e5c3efcce0f4)
Public key: x: Fr(0x0e63e65569365f7d2db43642f9cb15781120364f5e993cd6822cbab3f86be4d3), y: Fr(0x1d7b719c22afcf3eff09258df3f8b646af0ee4372bdb7979118168e8d390130e)
type: 0xf7
version: 0x01
account_id: 0x00001016
target: 0xdc8f1d4d7b5b4cde2dbc793c1d458f8916cb0513
token: 0x00000064
fee: 0x46e8
nonce: 0x15056b73
Signed bytes: 0xf70100001016dc8f1d4d7b5b4cde2dbc793c1d458f8916cb05130000006446e815056b73
Field | Value/type | Description |
---|---|---|
tx | ForcedExitTx | Signed forced exit transaction defined above |
# ForcedExitOp - Rollup operation described above
# Block - block where this Rollup operation is executed
# OnchainOp - public data created after executing this rollup operation and posted to the Ethereum
# ZeroPubkey - Value determining an unset signing key of an account
initiator_account = get_tree_account(ForcedExitOp.tx.account_id)
target_account = get_account_by_address(ForcedExitOp.tx.target)
fee_account = get_tree_account(Block.fee_account)
target_account_initial_balance = target_account.balances[ForcedExitOp.tx.token]
fee = unpack_fee(ForcedExitOp.tx.packed_fee)
def tree_invariants():
ForcedExitOp.token < MAX_FUNGIBLE_TOKENS
initiator_account.nonce == ForcedExitOp.nonce
initiator_account.nonce < MAX_NONCE
initiator_account.balance[ForcedExitOp.tx.token] >= fee
initiator_account.pubkey_hash == recover_signer_pubkey_hash(ForcedExitOp.tx)
target_account.pubkey_hash == ZeroPubkey
def tree_updates():
initiator_account.balance[WithdrawOp.tx.token] -= fee
initiator_account.nonce += 1
target_account.balance[WithdrawOp.tx.token] = 0
fee_account.balance[WithdrawOp.token] += fee
def pubdata_invariants():
OnhcainOp.opcode == 0x08
OnchainOp.initiator_account_id == WithdrawOp.tx.initiator_account_id
OnchainOp.token == WithdrawOp.tx.token
OnhcainOp.full_amount == target_account_initial_balance
OnhcainOp.packed_fee == WithdrawOp.tx.packed_fee
OnchainOp.target == WithdrawOp.tx.target
Performs an atomic swap of tokens between 2 Rollup accounts at an arranged ratio.
Chunks | Significant bytes |
---|---|
5 | 46 |
Field | Byte len | Value/type | Description |
---|---|---|---|
opcode | 1 | 0x0b |
Operation code |
account_a | 4 | AccountId | Unique identifier of the first rollup account that performs a swap |
recipient_a | 4 | AccountId | Unique identifier of the rollup account which receives the funds sent by account_b |
account_b | 4 | AccountId | Unique identifier of the second rollup account that performs a swap |
recipient_b | 4 | AccountId | Unique identifier of the rollup account which receives the funds sent by account_a |
submitter | 4 | AccountId | Unique identifier of the rollup account which submits the swap transaction and pays fees |
token_a | 4 | TokenId | Unique identifier of the token that account_a is swapping |
token_b | 4 | TokenId | Unique identifier of the token that account_b is swapping |
fee_token | 4 | TokenId | Unique identifier of the token in which submitter is paying fees |
amount_a | 5 | PackedTxAmount | Full amount of funds sent by account_a (of token_a ) |
amount_b | 5 | PackedTxAmount | Full amount of funds sent by account_b (of token_b ) |
packed_fee | 2 | PackedFee | Packed amount of fee paid |
nonce_mask | 1 | 1 Byte | Nonce mask * |
* Nonce mask is an 8-bit number. 1st bit set indicates that account_a's nonce was incremented. 2nd bit set indicates that account_b's nonce was incremented. Other bits are always 0.
0b000000050000000600000007000000080000002a00000007000000010000002d00000012200000001b2005800200000000
Reads as: account #5 has swapped amount 0x0000001220 in packed representation of token #7 for account #7's amount 0x0000001b20 in packed representation of token #1. account #6 received the swapped tokens #1, and account #8 received swapped tokens #7. account #42 has submitted the swap and payed the fee of 0x0580 in packed representation with a token #45. account #5's nonce has not been incremented, while account #7's has.
Field | Value/type | Description |
---|---|---|
type | 0x6f |
Operation code |
account_id | AccountId | Unique id of the sender rollup account in the state tree |
recipient | ETHAddress | Unique address of the rollup account that will receive the funds |
nonce | Nonce | A one-time code that specifies the order of transactions |
token_sell | TokenId | Unique identifier of the token to be swapped |
token_buy | TokenId | Unique identifier of the token to be swapped for |
amount | PackedTxAmount | Amount of funds to be swapped, 0 indicates a limit order |
ratio | Ratio | Array of 2 15-byte values, acceptable sell:buy ratio |
valid_from | Timestamp | Unix timestamp from which the block with this transaction can be processed |
valid_until | Timestamp | Unix timestamp until which the block with this transaction can be processed |
signature | Signature | Signature of previous fields, see the spec below |
Field | Value/type | Description |
---|---|---|
type | 0xf4 |
Operation code |
submitter_id | AccountId | Unique id of the sender rollup account in the state tree |
submitter_address | ETHAddress | Unique address of the rollup account that submitted the transaction |
nonce | Nonce | A one-time code that specifies the order of transactions |
orders_hash | Hash | Rescue hash of 2 serialized concatenated orders * |
fee_token | TokenId | Unique identifier of the token in which submitter is paying fees |
fee | PackedFee | Packed amount of fee paid |
amount_a | PackedTxAmount | Amount of funds to be swapped by account_a |
amount_b | PackedTxAmount | Amount of funds to be swapped by account_b |
signature | Signature | Signature of previous fields, see the spec below |
* In the submitted transaction both orders are present in their full form, but only their hash is signed by the user.
User transaction representation. (NOTE: tx bytecode differs slightly from this representation due to data packing, see the spec below).
{
"submitterId": 16777214,
"submitterAddress": "0xa143fc5851d93cdc72e90d63cfb7286395ce20d5",
"nonce": 2081453911,
"orders": [
{
"accountId": 4,
"recipient": "0x204dba8e9e8bf90f5889fe4bdc0f37265dbb05e3",
"nonce": 412184582,
"tokenBuy": 1023,
"tokenSell": 1023,
"ratio": ["12", "18"],
"amount": "12000000000",
"validFrom": 0,
"validUntil": 18446744073709551615,
"signature": {
"pubKey": "0e1390d3e86881117979db2b37e40eaf46b6f8f38d2509ff3ecfaf229c717b9d",
"signature": "8bfc1b854d6cf4f6d30a98249562d34c4b2c72344498a5803817ca994f6f051b984052dd501b10d2567a24719043de92bd1d7a55a951e7be9cc0995873f88002"
}
},
{
"accountId": 16777214,
"recipient": "0x50dfcd4ee9ca4f2039d58883631f0460e0e0668f",
"nonce": 119487968,
"tokenBuy": 1023,
"tokenSell": 1023,
"ratio": ["18", "12"],
"amount": "18000000000",
"validFrom": 0,
"validUntil": 18446744073709551615,
"signature": {
"pubKey": "0e1390d3e86881117979db2b37e40eaf46b6f8f38d2509ff3ecfaf229c717b9d",
"signature": "ce385443624ae362991fd6150d950aea8143ae66388b36b4ec2ccac2c2b80a19fee8c56fcf693158f655f8612fbc3374b046b22673bd9907b31e13bdb61cc301"
}
}
],
"amounts": ["12000000000", "18000000000"],
"fee": "56000000",
"feeToken": 1023,
"signature": {
"pubKey": "0e1390d3e86881117979db2b37e40eaf46b6f8f38d2509ff3ecfaf229c717b9d",
"signature": "5b64500bf4fae9766792129808f8116a8d63a9c35dda9378e986ba708f983a9d5bd0d8ffb59aaa1b3f47f42ae8bc629cd19514f6a72fc82a144591c16105b605"
}
}
Signed transaction representation.
Signer (account_a):
Private key: Fs(0x057afe7e950189b17eedfd749f5537a88eb3ed4981467636a115e5c3efcce0f4)
Public key: x: Fr(0x0e63e65569365f7d2db43642f9cb15781120364f5e993cd6822cbab3f86be4d3), y: Fr(0x1d7b719c22afcf3eff09258df3f8b646af0ee4372bdb7979118168e8d390130e)
Signer (account_b):
Private key: Fs(0x057afe7e950189b17eedfd749f5537a88eb3ed4981467636a115e5c3efcce0f4)
Public key: x: Fr(0x0e63e65569365f7d2db43642f9cb15781120364f5e993cd6822cbab3f86be4d3), y: Fr(0x1d7b719c22afcf3eff09258df3f8b646af0ee4372bdb7979118168e8d390130e)
Signer (submitter):
Private key: Fs(0x057afe7e950189b17eedfd749f5537a88eb3ed4981467636a115e5c3efcce0f4)
Public key: x: Fr(0x0e63e65569365f7d2db43642f9cb15781120364f5e993cd6822cbab3f86be4d3), y: Fr(0x1d7b719c22afcf3eff09258df3f8b646af0ee4372bdb7979118168e8d390130e)
Signed transaction fields:
type: 0xf4
version: 0x01
submitterId: 0x00fffffe
submitterAddress: 0xa143fc5851d93cdc72e90d63cfb7286395ce20d5
nonce: 0x7c107757
orders_hash: 0x8d2f227c69cb8d3dc16e20cb1b6829d1ee5d97d276ada6c6e6f71e3bccdc17
fee_token: 0x000003ff
fee: 0x4605
amounts[0]: 0x59682f0000
amounts[1]: 0x861c468000
Signed bytes: 0xf40100fffffea143fc5851d93cdc72e90d63cfb7286395ce20d57c1077578d2f227c69cb8d3dc16e20cb1b6829d1ee5d97d276ada6c6e6f71e3bccdc17000003ff460559682f0000861c468000
Field | Value/type | Description |
---|---|---|
tx | SwapTx | Signed swap transaction defined above |
# SwapOp - Rollup operation described above
# Block - block where this Rollup operation is executed
# OnchainOp - public data created after executing this rollup operation and posted to the Ethereum
account_a = get_tree_account(SwapOp.orders.0.account_id)
account_b = get_tree_account(SwapOp.orders.1.account_id)
submitter = get_tree_account(SwapOp.submitter_id)
recipient_a = get_account_by_address(SwapOp.orders.0.recipient)
recipient_b = get_account_by_address(SwapOp.orders.1.recipient)
fee_account = get_tree_account(Block.fee_account)
fee = unpack_fee(SwapOp.tx.packed_fee)
def tree_invariants():
SwapOp.orders.0.token_sell < MAX_TOKENS
SwapOp.orders.0.token_buy < MAX_TOKENS
SwapOp.orders.1.token_sell < MAX_TOKENS
SwapOp.orders.1.token_buy < MAX_TOKENS
SwapOp.fee_token < MAX_FUNGIBLE_TOKENS
submitter.nonce == SwapOp.nonce
account_a.nonce == SwapOp.orders.0.nonce
account_b.nonce == SwapOp.orders.1.nonce
submitter.nonce < MAX_NONCE
account_a.nonce < MAX_NONCE
account_b.nonce < MAX_NONCE
submitter.pubkey_hash == recover_signer_pubkey_hash(SwapOp)
account_a.pubkey_hash == recover_signer_pubkey_hash(SwapOp.orders.0)
account_b.pubkey_hash == recover_signer_pubkey_hash(SwapOp.orders.1)
# tokens must match
SwapOp.orders.0.token_buy == SwapOp.orders.1.token_sell
SwapOp.orders.1.token_sell == SwapOp.orders.0.token_buy
SwapOp.orders.0.token_buy != SwapOp.orders.0.token_sell
# if partial order filling is not allowed, amounts must match
SwapOp.orders.0.amount == 0 or SwapOp.orders.0.amount == SwapOp.amounts.0
SwapOp.orders.1.amount == 0 or SwapOp.orders.1.amount == SwapOp.amounts.1
# either one of 2 proposed prices is accepted (or in between)
SwapOp.amounts.0 * SwapOp.orders.0.ratio.1 <= SwapOp.amounts.1 * SwapOp.orders.0.ratio.0
SwapOp.amounts.1 * SwapOp.orders.1.ratio.1 <= SwapOp.amounts.0 * SwapOp.orders.1.ratio.0
# balance checks
account_a.balance[SwapOp.orders.0.token_sell] >= SwapOp.amounts.0
account_b.balance[SwapOp.orders.1.token_sell] >= SwapOp.amounts.1
submitter.balance[SwapOp.fee_token] >= SwapOp.fee
if account_a.id == submitter.id and SwapOp.fee_token == SwapOp.orders.0.token_sell:
account_a.balance[SwapOp.orders.0.token_sell] >= SwapOp.amounts.0 + SwapOp.fee
if account_b.id == submitter.id and SwapOp.fee_token == SwapOp.orders.1.token_sell:
account_b.balance[SwapOp.orders.1.token_sell] >= SwapOp.amounts.1 + SwapOp.fee
def tree_updates():
account_a.balance[SwapOp.orders.0.token_sell] -= SwapOp.amounts.0
recipient_b.balance[SwapOp.orders.1.token_buy] += SwapOp.amounts.0
account_b.balance[SwapOp.orders.1.token_sell] -= SwapOp.amounts.1
recipient_a.balance[SwapOp.orders.0.token_buy] += SwapOp.amounts.1
submitter.balance[SwapOp.fee_token] -= SwapOp.fee
fee_account.balance[SwapOp.fee_token] += SwapOp.fee
submitter.nonce += 1
if SwapOp.orders.0.amount != 0 and submitter.id != account_a.id:
account_a.nonce += 1
if SwapOp.orders.1.amount != 0 and submitter.id != account_b.id:
account_b.nonce += 1
def pubdata_invariants():
OnhcainOp.opcode == 0x0b
OnchainOp.account_a == SwapOp.orders.0.account_id
OnchainOp.recipient_a == SwapOp.orders.0.recipient
OnchainOp.account_b == SwapOp.orders.1.account_id
OnchainOp.recipient_b == SwapOp.orders.1.recipient
OnchainOp.submitter == SwapOp.submitter_id
OnchainOp.token_a == SwapOp.orders.0.token_sell
OnchainOp.token_b == SwapOp.orders.1.token_sell
OnchainOp.fee_token == SwapOp.fee_token
OnhcainOp.packed_amount_a == SwapOp.amounts.0
OnhcainOp.packed_amount_b == SwapOp.amounts.1
OnhcainOp.packed_fee == SwapOp.fee
OnchainOp.nonce_mask == (SwapOp.orders.0.amount != 0) | (SwapOp.orders.1.amount != 0) << 1
Deposit Ether to Rollup - transfer Ether from user L1 address into Rollup address
function depositETH(address _zkSyncAddress) payable
_zkSyncAddress
: The receiver Layer 2 address
msg.value equals amount to deposit.
Deposit ERC-20 token to Rollup - transfer token from user L1 address into Rollup address
function depositERC20(IERC20 _token, uint104 _amount, address _zkSyncAddress)
_token
: Token address in L1 chain_amount
: Amount to deposit_zkSyncAddress
: The receiver Rollup address
Withdraw NFT to L1
function withdrawPendingNFTBalance(uint32 _tokenId)
_tokenId
: Token to withdraw
Withdraw ERC20 token to L1 - Transfer token from contract to owner
function withdrawPendingBalance(
address payable _owner,
address _token,
uint128 _amount
)
_owner
: Recipient_token
: Token address in L1 chain_amount
: Amount to withdraw
Authenticates pubkey hash change for new rollup public key.
function setAuthPubkeyHash(bytes calldata _pubkey_hash, uint32 _nonce)
_pubkey_hash
:RollupPubkeyHash
_nonce
: Account nonce for which this pubkey change is authorized.
Register full exit request to withdraw all token balance from the account. The user needs to call it if she believes that her transactions are censored by the validator.
function requestFullExit(uint32 _accountId, address _token)
_accountId
:AccountId
of the Rollup account_token
: Token address in L1 chain
Register full exit request to withdraw NFT tokens balance from the account. Users need to call it if they believe that their transactions are censored by the validator.
function requestFullExitNFT(uint32 _accountId, uint32 _tokenId)
_accountId
:AccountId
of the Rollup account_tokenId
: NFT Token ID
Withdraws token from Rollup to L1 in case of exodus mode. User must provide proof that she owns funds.
function performExodus(
StoredBlockInfo memory _storedBlockInfo,
address _owner,
uint32 _accountId,
uint32 _tokenId,
uint128 _amount,
uint32 _nftCreatorAccountId,
address _nftCreatorAddress,
uint32 _nftSerialId,
bytes32 _nftContentHash,
uint256[] memory _proof
)
_storedBlockInfo
: Stored block data of the last verified block_owner
: owner address in L2 chain_accountId
:AccountId
of the owner account_tokenId
: Verified token id_amount
:StateAmount
Full amount of the given token that belong toAccountId
in the last verified block_nftCreatorAccountId
: Creator id of NFT (optional, only for NFT)_nftCreatorAddress
: Creator Address of NFT (optional, only for NFT)_nftSerialId
: Serial ID of NFT (optional, only for NFT)_nftContentHash
: Identifier of the NFT represented as a 32-byte hex string_proof
: Proof that user funds are present in the account tree
Cancels open priority requests, accrues users balances from deposit priority requests in Exodus mode.
function cancelOutstandingDepositsForExodusMode(uint64 _n, bytes[] memory _depositsPubdata)
_n
: Supposed number of requests to cancel (if there are fewer requests than the provided number - all of the requests will be canceled) number of requests)_depositsPubdata
: The array of the pubdata for the deposits to be cancelled. Please note, that it should not contain any (even empty) pubdata forFullExit
priority operations.
Submit committed block data. Only active validator can make it. Onchain operations will be checked on contract and fulfilled on block verification.
struct StoredBlockInfo {
uint32 blockNumber;
uint64 priorityOperations;
bytes32 pendingOnchainOperationsHash;
uint256 timestamp;
bytes32 stateHash;
bytes32 commitment;
}
struct OnchainOperationData {
bytes ethWitness;
uint32 publicDataOffset;
}
struct CommitBlockInfo {
bytes32 newStateHash;
bytes publicData;
uint256 timestamp;
OnchainOperationData[] onchainOperations;
uint32 blockNumber;
uint32 feeAccount;
}
function commitBlocks(StoredBlockInfo memory _lastCommittedBlockData, CommitBlockInfo[] memory _newBlocksData)
_lastCommittedBlockData
: Stored info of the last committed block._newBlocksData
: Data of the new committed blocks.
StoredBlockInfo
- block data that we store on Ethereum. We store hash of this structure in storage and pass it in tx
arguments every time we need to access any of its field.
blockNumber
- rollup block numberpriorityOperations
- number of priority operations committed in this blockpendingOnchainOperationsHash
- hash of all onchain operations that have to be processed when block is finalized (executed)timestamp
- block timestampstateHash
- root hash of the rollup account tree statecommitment
- rollup block commitment.
OnchainOperationData
- data needed for the onchain operation processing.
ethWitness
- additional data that can be needed for the onchain operation processing (e.g. signature for change pubkey operation)publicDataOffset
- offset in the public data bytes after which pubdata for the onchain operation begins
CommitBlockInfo
- data needed for new block commit
newStateHash
- new rollup state root hashpublicData
- public data of the executed rollup operationstimestamp
- rollup block timestamponchainOperations
- list of onchain operations that needs to be processedblockNumber
- rollup block numberfeeAccount
- id of the fee account for rollup block
Verify aggregated proof of multiple block commitments.
function verifyAggregatedBlockProof(
uint256[] memory _recursiveInput,
uint256[] memory _proof,
uint8[] memory _vkIndexes,
uint256[] memory _individualVksInputs,
uint256[16] memory _subproofsLimbs
)
_recursiveInput
- input of the recursive aggregated circuit_proof
- recursive proof_vkIndexes
- index of the verification keys in the aggregated verification keys tree_individualVksInputs
- block commitments to be verified_subproofsLimbs
- data needed for recursive proof verification
Finishes block processing after block commitments were verified onchain. Only active validator can make it. This block onchain operations will be fulfilled.
struct ExecuteBlockInfo {
StoredBlockInfo storedBlock;
bytes[] pendingOnchainOpsPubdata;
}
function executeBlocks(ExecuteBlockInfo[] memory _blocksData)
_blocksData
- data needed to finish block execution
ExecuteBlockInfo
storedBlock
- stored block that was committedpendingOnchainOpsPubdata
- list of pubdata for onchain operations that needs to be processed
Checks if Exodus mode must be entered. Exodus mode must be entered in case of current ethereum block number is higher than the oldest of existed priority requests expiration block number.
function activateExodusMode() returns (bool)
Revert blocks that were not verified before deadline determined by EXPECT_VERIFICATION_IN
constant. The caller must be
valid operator.
function revertBlocks(StoredBlockInfo[] memory _blocksToRevert)
_blocksToRevert
: committed blocks to revert in reverse order starting from last committed.
Change current governor. The caller must be current governor.
function changeGovernor(address _newGovernor)
_newGovernor
: Address of the new governor
Add token to the list of networks tokens. The caller must be current token governance.
function addToken(address _token)
_token
: Token address
Add token to the list of paused tokens. The caller must be current governor. Its impossible to create deposits of the paused tokens.
function setTokenPaused(address _tokenAddr, bool _tokenPaused);
_tokenAddr
: Token address_tokenPaused
: Paused status of the token
Change validator status (active or not active). The caller must be current governor.
function setValidator(address _validator, bool _active)
_validator
: Validator address_active
: Active flag
function changeTokenGovernance(TokenGovernance _newTokenGovernance)
_newTokenGovernance
: New token Governance
Validate that specified address is the token governance address
function requireGovernor(address _address)
_address
: Address to check
Validate that specified address is the active validator
function requireActiveValidator(address _address)
_address
: Address to check
Validate token id (must be less than total tokens amount).
function isValidTokenId(uint32 _tokenId) returns (bool)
_tokenId
: Token id
Returns: bool flag that indicates if token id is less than total tokens amount.
Validate token address (it must be presented in tokens list).
function validateTokenAddress(address _tokenAddr) returns (uint16)
_tokenAddr
: Token address
Returns: token id.
Register custom factory for withdrawing NFT
function registerNFTFactoryCreator(uint32 _creatorAccountId, address _creatorAddress, bytes memory _signature)
_creatorAccountId
: Creator's zkSync account ID_creatorAddress
: NFT creator address_signature
: Creator's signature
The creator should sign the message
"\x19Ethereum Signed Message:\n141",
"\nCreator's account ID in zkSync: ",
Bytes.bytesToHexASCIIBytes(abi.encodePacked((_creatorAccountId))),
"\nCreator: ",
Bytes.bytesToHexASCIIBytes(abi.encodePacked((_creatorAddress))),
"\nFactory: ",
Bytes.bytesToHexASCIIBytes(abi.encodePacked((_factoryAddress)))
This signature should be sent to the governance contract by calling registerNFTFactoryCreator
from the factory
contract.
Register factory, which will be use for withdrawing NFT by default
function setDefaultNFTFactory(address _factory)
_factory
: NFT factory address
Get NFT factory which will be used for withdrawing NFT for corresponding creator
function getNFTFactory(uint32 _creatorAccountId, address _creatorAddress)
_creatorAccountId
: Creator account id_creatorAddress
: Creator address
Collects fees for adding a token and passes the call to the addToken
function in the governance contract.
function addToken(address _token)
_token
: ERC20 token address
Set new listing token and fee, can be called only by zkSync governor.
function setListingFeeToken(IERC20 _newListingFeeToken, uint256 _newListingFee)
_newListingFeeToken
: address of the token in which fees will be collected_newListingFee
: amount of tokens that will need to be paid for adding tokens
Set new listing fee, can be called only by zkSync governor.
function setListingFee(uint256 _newListingFee)
_newListingFee
: amount of tokens that will need to be paid for adding tokens
Enable or disable token lister, if enabled new tokens can be added by that address without payment, can be called only by zkSync governor.
function setLister(address _listerAddress, bool _active)
_listerAddress
: address that can list tokens without fee_active
: active flag
Change maximum amount of tokens that can be listed using this method, can be called only by zkSync governor.
function setListingCap(uint16 _newListingCap)
_newListingCap
: max number of tokens that can be listed using this contract
Change address that collects payments for listing tokens, can be called only by zkSync governor.
function setTreasury(address _newTreasury)
_newTreasury
: address that collects listing payments
Block circuit describes state transition function (STF) from previous state to the new one by applying a number of transactions.
Public inputs:
- pub_data_commitment: commitment to the state transition of the block; this is a hash that includes
old_root
,new_root
,block_number
,validator_address
,pub_data_rolling_hash
(see smart the contract code).,timestamp
,onhain_ops_offset_commitment
Witness:
- old_root,
- new_root,
- block_number,
- validator_address,
- pub_data,
- pub_data_rolling_hash,
- list of transactions,
- state Merkle trees.
If the proof is valid (the circuit is satisfied), it means that there exists a set of transactions which transitions the
state from the previous one (cryptographically fingerprinted by the Merkle root old_root
) into the new one
(cryptographically fingerprinted by the Merkle root new_root
) such that concatenated pub_data
of this transactions
in the order of application is cryptographically fingerprinted by pub_data_commitment
. oncahin_ops_offset_commitment
is array of bytes where one byte corresponds to each public data chunk and is set to 1 if chunk is first chunk for the
onchain operation and 0 otherwise.
For Merkle trees and hash invocations that require collision resistance we use Rescue hash described in AABDS19. Reference implementation examples can be found in SW19.
For our application we've chosen the following parametrization:
- rate = 2
- capacity = 1
- non-linearity of 5th degree for BN254 curve
MDS matrix and round constants are generated from the seed phrase using the following code. Seen phrases for round constants and MDS matrix consist of two parts: short human-readable and long abstract that was taken from ZCash's original "sapling-crypto" library and over which we could not have any influence. For MDS matrix seed phrase is chosen to be the first one from the series that generates matrix without eigenvalues.
If number of hashed elements it not divisible by rate then extra field element equal to 1
is appended to the hash
input.
Number of input elements if internally forbidded to be equal to zero.
Outputs of Rescue hash are expected to be uniformly distributed in the field (but their bits are not uniformly distributed).
Rate of 2 allows us to get up to two field elements from the sponge per hash round.
In test vectors we output only first element of the squeezed sponge. Such operation mode
Example 1 | |
---|---|
Msg length | 1 field element |
Msg | [0x27014c0bd27dddc8514b53831287e0ba02b26875bdcb34f0d4699681f487cf7b] |
Hash | 0x1c54bc6adef0a488caa8ef6723ae30c784ddb0659effe5c4d0ea19b5e038300a |
Example 2 | |
---|---|
Msg length | 2 field elements |
Msg | [0x27014c0bd27dddc8514b53831287e0ba02b26875bdcb34f0d4699681f487cf7b, 0x238ba289e8783d31585aa75bba8ddc2269c0c2d8c45d0769943b16f009ff5510] |
Hash | 0x1a751dc151d807fcb5269089c4d120ef318e26f2eaea983d74096f577cb45d93 |
Example 3 | |
---|---|
Msg length | 3 field elements |
Msg | [0x27014c0bd27dddc8514b53831287e0ba02b26875bdcb34f0d4699681f487cf7b, 0x238ba289e8783d31585aa75bba8ddc2269c0c2d8c45d0769943b16f009ff5510, 0x069fd7f225dd46f03e4e0059d187419eb51b5ab5a33368e4ac05e62353dda0c3] |
Hash | 0x2c3045ae4008cab38d00491870f9cb3aecb63d56c7199fd922af7b92e00722b6 |
Rescue is an algebraic hash that operates over field elements, so any binary data (packed transaction) has first to be
encode into the series of field elements. For this bit string x
is encoded as seried of field elements placing 253
bits into each element starting from the LSB.
Signature is made according to derandomized Simple Schnorr Multi-Signature(i.e., a protocol which allows a group of signers to produce a short, joint signature on a common message) called MuSig. You can find its complete description in the relevant article MPSW18. Also if you only need an algorithm for implementation read DF08 page 53. Note: you need some background in mathematics, elliptic curve cryptography and knowledge of Schnorr signature algorithm to completely understand how to implement MuSig. DF08 contains all the necessary basics for this purpose.
Signature is formed over the BabyJubjub curve in a normalized form (-x^2 + y^2 = 1 + dx^2y^2
, so a = -1
):
- Base field = Scalar field of BN254 (group order)
d = 12181644023421730124874158521699555681764249180949974110617291017600649128846
- main subgroup order =
2736030358979909402780800718157159386076813972158567259200215660948447373041
Derandomization is made via RFC6979 using SHA-256 hash function according to RFC4634 pages 4-7. Derandomization comes down to the fact that we are not generating randomness using a hash function based on some entropy and the message. Actually we generate some number k, using HMAC mechanism (read RFC2104 page 3) and SHA-256 as hash function for message in algorithm of k generation (find in RFC6979 pages 10-13) and replace randomness with this number.
For Schnorr signature (that MuSig is) we need to generate Fiat-Shamir transformation challenge that should be a random
bit string of some length (120 bits or more). We label such a challenge c
. For this we use the following procedure.
Later for message
we assume a signed message (transaction hash).
- Define
pad32(X)
pads byte stringX
to 32 bytes with zero bytes and panics on longer strings. Inside of the circuit all inputs are of the fixed length. EncodeLE(F)
encodes a field elementF
of as a byte string in little-endian orderXCoord(P)
takesX
coordinate of the pointP
on BabyJubjub curveEncodeIntoFr(X)
encodes byte stringX
as a series of field elements of the circuit base fieldFr
. Each byte is represented as a series of 8 bits (MSB first) and such full series of bits (from all the bytes) is packed into field elements using bitpacking as described above.- For shortness
enc(X)
=EncodeIntoFr(pad32(EncodeLE(X)))
- we absorb the series of field elements into the Rescue sponge as
sponge = Rescue( enc(XCoord(PubKey)), enc(XCoord(R)), enc(message))
- we draw two field elements
F0, F1
from the sponge (that hasrate = 2
) - Bottom 125 bits of
F0
are placed into bottom 125 bits ofc
(LSB ofF0
is placed into LSB ofc
) - Bottom 125 bits of
F1
are placed into next 125 bits ofc
(LSB ofF1
is placed into 126th bit ofc
)
Such procedure allows us to get c
uniformly in [0, 2^250)
.
BabyJubjub curve is a Twisted Edwards curve that always have a subgroup of even order. In the signature we require that
public key and signature's random point R
belong to the main subgroup (of prime order).
- Secret key =
0x05368b800322fad50e74d9b1eab3364570d67a56ef133de0c8dbf1deaf2a474e
- Public key X =
0x2d3801d48de21c009f4329af753cc554793c98c0c9594f4a20f7e7d23608d69d
- Public key Y =
0x2e55266f6fb271ccd351cbb10cf953adafcafe7c6785375f583611063ddf93cb
Example 1 | |
---|---|
Msg length | 0 bytes |
Msg | |
Signature R.X | 0x27b3b852c85cedfdcba33a6efe4f54207f91bd84c0b5849c4ffc9682f3c9e25f |
Signature R.Y | 0x125575f528c6df6e70f903f70b50a81bc1442a88178134c7d04b2d2f6372aec9 |
Signature S | 0x04ce6d6e21f874bd89293ea53050dbf742338d0b1513e450538fed7e56c8cdc9 |
Example 2 | |
---|---|
Msg length | 1 byte |
Msg | 0x72 |
Signature R.X | 0x024341a6854db6897eb0fc51b9612db1d1ff5c50dfcac99bf1c2396e77f047e1 |
Signature R.Y | 0x1ca7b1c1d7f54becbf3dbb96808139e6572aac85377a2bcda46b125db31d07b2 |
Signature S | 0x005694d986c7c13cb428c713b4e5f7d590d4a1561e6df64112619955c98958cb |
Example 3 | |
---|---|
Msg length | 2 bytes |
Msg | 0xaf82 |
Signature R.X | 0x0dd227c7e193e87ef488783db85100a7f8ac2f020d72e154532eea3f72b58cc5 |
Signature R.Y | 0x2abb436ba00308869e27badba36387a0981a91b4359a6bf323ae4bdf10771867 |
Signature S | 0x01dd611aaadce6fcea01094556585f9dac6b025831a0b07ed70edb9efcb2c7df |
Algorithm: SHA-256 according to RFC4634.
Example 1 | |
---|---|
Msg length | 0 bytes |
Msg | |
Hash | 0xe3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 |
Example 2 | |
---|---|
Msg length | 1 byte |
Msg | 72 |
Hash | 0x8722616204217eddb39e7df969e0698aed8e599ba62ed2de1ce49b03ade0fede |
Example 3 | |
---|---|
Msg length | 2 bytes |
Msg | 0xaf82 |
Hash | 0x2a0305714ebec7cc0cc0949aa208aa04dc7a4b43ff0d9f4f76546ae8056e2713 |
- Generic SMT description Ostersjo/Dahlberg, R.: Sparse Merkle Trees: Definitions and Space-Time Trade-Offs with Applications for Balloon. Bachelor’s thesis, Karlstad University (2016)
- Basics of SMT Fichter K.: What’s a Sparse Merkle Tree? (2018)
In zkSync we use a sparse Merkle tree with a flexible hashing strategy. We can change its depth depending on how many accounts we want to have.