Skip to main content

zkBNB Protocol Design

Glossary

  • L1: layer 1 blockchain, it is BNB Smart Chain.
  • Rollup: Zk Rollup based layer-2 network, it is zkBNB.
  • Owner: A user get a L2 account.
  • Committer: Entity executing transactions and producing consecutive blocks on L2.
  • Eventually: happening within finite time.
  • Assets in L2: Assets in L2 smart contract controlled by owners.
  • L2 Key: Owner's private key used to send transaction on L2.
  • MiMC Signature: The result of signing the owner's message, using his private key, used in L2 internal transactions.

The current implementation we still use EDDSA as the signature scheme, we will soon support switch to EDCSA.

Design

Overview

zkBNB implements a zk-rollup protocol (in short "rollup" below) for:

  • BNB and BEP20 fungible token deposit and transfer
  • BEP721 non-fungible token deposit and transfer
  • mint BEP721 non-fungible tokens on L2
  • NFT-marketplace on L2

General rollup workflow is as follows:

  • Users can become owners in rollup by calling registerZNS in L1 to register a short name for L2;
  • Owners can transfer assets to each other, mint NFT on L2;
  • Owners can withdraw assets under their control to any L1 address.

Rollup operation requires the assistance of a committer, who rolls transactions together, also a prover who computes a zero-knowledge proof of the correct state transition, and affects the state transition by interacting with the rollup contract.

Data format

Data types

We assume that 1 Chunk = 32 bytes.

TypeSize(Byte)TypeComment
AccountIndex4uint32Incremented number of accounts in Rollup. New account will have the next free id. Max value is 2^32 - 1 = 4.294967295 × 10^9
AssetId2uint16Incremented number of tokens in Rollup, max value is 65535
PackedTxAmount5int64Packed 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.
PackedFee2uint16Packed fees must be represented with 2 bytes: 5 bit for exponent, 11 bit for mantissa.
StateAmount16*big.IntState 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.
Nonce4uint32Nonce 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.
EthAddress20stringTo make an BNB Smart Chain address from the BNB Smart Chain'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.
Signature64[]byteBased on EDDSA.
HashValue32stringhash value based on MiMC

Amount packing

Mantissa and exponent parameters used in zkBNB:

amount = mantissa * radix^{exponent}

TypeExponent bit widthMantissa bit widthRadix
PackedTxAmount53510
PackedFee51110

State Merkle Tree(height)

We have 3 unique trees: AccountTree(32), NftTree(40) and one sub-tree AssetTree(16) which belongs to AccountTree(32). The empty leaf for all the trees is just set every attribute as 0 for every node.

AccountTree

AccountTree is used for storing all accounts info and the node of the account tree is:

type AccountNode struct{
AccountNameHash string // bytes32
PubKey string // bytes32
Nonce int64
CollectionNonce int64
AssetRoot string // bytes32
}

Leaf hash computation:

func ComputeAccountLeafHash(
accountNameHash string,
pk string,
nonce int64,
collectionNonce int64,
assetRoot []byte,
) (hashVal []byte, err error) {
hFunc := mimc.NewMiMC()
var buf bytes.Buffer
buf.Write(common.FromHex(accountNameHash))
err = util.PaddingPkIntoBuf(&buf, pk)
if err != nil {
logx.Errorf("[ComputeAccountAssetLeafHash] unable to write pk into buf: %s", err.Error())
return nil, err
}
util.PaddingInt64IntoBuf(&buf, nonce)
util.PaddingInt64IntoBuf(&buf, collectionNonce)
buf.Write(assetRoot)
hFunc.Reset()
hFunc.Write(buf.Bytes())
hashVal = hFunc.Sum(nil)
return hashVal, nil
}
AssetTree

AssetTree is sub-tree of AccountTree and it stores all the assets balance, and offerCanceledOrFinalized. The node of asset tree is:

type AssetNode struct {
Balance string
OfferCanceledOrFinalized string // uint128
}

Leaf hash computation:

func ComputeAccountAssetLeafHash(
balance string,
offerCanceledOrFinalized string,
) (hashVal []byte, err error) {
hFunc := mimc.NewMiMC()
var buf bytes.Buffer
err = util.PaddingStringBigIntIntoBuf(&buf, balance)
if err != nil {
logx.Errorf("[ComputeAccountAssetLeafHash] invalid balance: %s", err.Error())
return nil, err
}
err = util.PaddingStringBigIntIntoBuf(&buf, offerCanceledOrFinalized)
if err != nil {
logx.Errorf("[ComputeAccountAssetLeafHash] invalid balance: %s", err.Error())
return nil, err
}
hFunc.Write(buf.Bytes())
return hFunc.Sum(nil), nil
}

NftTree

NftTree is used for storing all the NFTs and the node info is:

type NftNode struct {
CreatorAccountIndex int64
OwnerAccountIndex int64
NftContentHash string
NftL1Address string
NftL1TokenId string
CreatorTreasuryRate int64
CollectionId int64 // 32 bit
}

Leaf hash computation:

func ComputeNftAssetLeafHash(
creatorAccountIndex int64,
ownerAccountIndex int64,
nftContentHash string,
nftL1Address string,
nftL1TokenId string,
creatorTreasuryRate int64,
collectionId int64,
) (hashVal []byte, err error) {
hFunc := mimc.NewMiMC()
var buf bytes.Buffer
util.PaddingInt64IntoBuf(&buf, creatorAccountIndex)
util.PaddingInt64IntoBuf(&buf, ownerAccountIndex)
buf.Write(ffmath.Mod(new(big.Int).SetBytes(common.FromHex(nftContentHash)), curve.Modulus).FillBytes(make([]byte, 32)))
err = util.PaddingAddressIntoBuf(&buf, nftL1Address)
if err != nil {
logx.Errorf("[ComputeNftAssetLeafHash] unable to write address to buf: %s", err.Error())
return nil, err
}
err = util.PaddingStringBigIntIntoBuf(&buf, nftL1TokenId)
if err != nil {
logx.Errorf("[ComputeNftAssetLeafHash] unable to write big int to buf: %s", err.Error())
return nil, err
}
util.PaddingInt64IntoBuf(&buf, creatorTreasuryRate)
util.PaddingInt64IntoBuf(&buf, collectionId)
hFunc.Write(buf.Bytes())
hashVal = hFunc.Sum(nil)
return hashVal, nil
}

StateRoot

StateRoot is the final root that shows the final layer-2 state and will be stored on L1. It is computed by the root of AccountTree and NftTree. The computation of StateRoot works as follows:

StateRoot = MiMC(AccountRoot || NftRoot)

func ComputeStateRootHash(
accountRoot []byte,
nftRoot []byte,
) []byte {
hFunc := mimc.NewMiMC()
hFunc.Write(accountRoot)
hFunc.Write(nftRoot)
return hFunc.Sum(nil)
}

zkBNB Transactions

zkBNB transactions are divided into Rollup transactions (initiated inside Rollup by a Rollup account) and Priority operations (initiated on the BSC by an BNB Smart Chain account).

Rollup transactions:

  • EmptyTx
  • Transfer
  • Withdraw
  • CreateCollection
  • MintNft
  • TransferNft
  • AtomicMatch
  • CancelOffer
  • WithdrawNft

Priority operations:

  • RegisterZNS
  • Deposit
  • DepositNft
  • FullExit
  • FullExitNft

Rollup transaction lifecycle

  1. User creates a Transaction or a Priority operation.
  2. After processing this request, committer creates a Rollup operation and adds it to the block.
  3. Once the block is complete, sender submits it to the zkBNB smart contract as a block commitment. Part of the logic of some Rollup transaction is checked by the smart contract.
  4. The proof for the block is submitted to the zkBNB smart contract as the block verification. If the verification succeeds, the new state is considered finalized.

EmptyTx

Description

No effects.

On-Chain operation

Size
ChunksSignificant bytes
11
Structure
FieldSize(byte)Value/typeDescription
TxType10x00Transaction type

User transaction

No user transaction

RegisterZNS

Description

This is a layer-1 transaction and a user needs to call this method first to register a layer-2 account.

On-Chain operation

Size
ChunksSignificant bytes
6101
Structure
NameSize(byte)Comment
TxType1transaction type
AccountIndex4unique account index
AccountName32account name
AccountNameHash32hash value of the account name
PubKeyX32layer-2 account's public key X
PubKeyY32layer-2 account's public key Y
func ConvertTxToRegisterZNSPubData(oTx *tx.Tx) (pubData []byte, err error) {
if oTx.TxType != commonTx.TxTypeRegisterZns {
logx.Errorf("[ConvertTxToRegisterZNSPubData] invalid tx type")
return nil, errors.New("[ConvertTxToRegisterZNSPubData] invalid tx type")
}
// parse tx
txInfo, err := commonTx.ParseRegisterZnsTxInfo(oTx.TxInfo)
if err != nil {
logx.Errorf("[ConvertTxToRegisterZNSPubData] unable to parse tx info: %s", err.Error())
return nil, err
}
var buf bytes.Buffer
buf.WriteByte(uint8(oTx.TxType))
buf.Write(Uint32ToBytes(uint32(txInfo.AccountIndex)))
chunk := SuffixPaddingBufToChunkSize(buf.Bytes())
buf.Reset()
buf.Write(chunk)
buf.Write(PrefixPaddingBufToChunkSize(AccountNameToBytes32(txInfo.AccountName)))
buf.Write(PrefixPaddingBufToChunkSize(txInfo.AccountNameHash))
pk, err := ParsePubKey(txInfo.PubKey)
if err != nil {
logx.Errorf("[ConvertTxToRegisterZNSPubData] unable to parse pub key: %s", err.Error())
return nil, err
}
// because we can get Y from X, so we only need to store X is enough
buf.Write(PrefixPaddingBufToChunkSize(pk.A.X.Marshal()))
buf.Write(PrefixPaddingBufToChunkSize(pk.A.Y.Marshal()))
buf.Write(PrefixPaddingBufToChunkSize([]byte{}))
return buf.Bytes(), nil
}

User transaction

NameSize(byte)Comment
AccountName32account name
Owner20account layer-1 address
PubKey32layer-2 account's public key

Circuit

func VerifyRegisterZNSTx(
api API, flag Variable,
tx RegisterZnsTxConstraints,
accountsBefore [NbAccountsPerTx]AccountConstraints,
) (pubData [PubDataSizePerTx]Variable) {
pubData = CollectPubDataFromRegisterZNS(api, tx)
CheckEmptyAccountNode(api, flag, accountsBefore[0])
return pubData
}

Deposit

Description

This is a layer-1 transaction and is used for depositing assets into the layer-2 account.

On-Chain operation

Size
ChunksSignificant bytes
655
Structure
NameSize(byte)Comment
TxType1transaction type
AccountIndex4account index
AssetId2asset index
AssetAmount16state amount
AccountNameHash32account name hash
func ConvertTxToDepositPubData(oTx *tx.Tx) (pubData []byte, err error) {
if oTx.TxType != commonTx.TxTypeDeposit {
logx.Errorf("[ConvertTxToDepositPubData] invalid tx type")
return nil, errors.New("[ConvertTxToDepositPubData] invalid tx type")
}
// parse tx
txInfo, err := commonTx.ParseDepositTxInfo(oTx.TxInfo)
if err != nil {
logx.Errorf("[ConvertTxToDepositPubData] unable to parse tx info: %s", err.Error())
return nil, err
}
var buf bytes.Buffer
buf.WriteByte(uint8(oTx.TxType))
buf.Write(Uint32ToBytes(uint32(txInfo.AccountIndex)))
buf.Write(Uint16ToBytes(uint16(txInfo.AssetId)))
buf.Write(Uint128ToBytes(txInfo.AssetAmount))
chunk1 := SuffixPaddingBufToChunkSize(buf.Bytes())
buf.Reset()
buf.Write(chunk1)
buf.Write(PrefixPaddingBufToChunkSize(txInfo.AccountNameHash))
buf.Write(PrefixPaddingBufToChunkSize([]byte{}))
buf.Write(PrefixPaddingBufToChunkSize([]byte{}))
buf.Write(PrefixPaddingBufToChunkSize([]byte{}))
buf.Write(PrefixPaddingBufToChunkSize([]byte{}))
return buf.Bytes(), nil
}

User transaction

DepositBNB
NameSize(byte)Comment
AccountNameHash32account name hash
DepositBEP20
NameSize(byte)Comment
AssetAddress20asset layer-1 address
Amount13asset layer-1 amount
AccountNameHash32account name hash

Circuit

func VerifyDepositTx(
api API, flag Variable,
tx DepositTxConstraints,
accountsBefore [NbAccountsPerTx]AccountConstraints,
) (pubData [PubDataSizePerTx]Variable) {
pubData = CollectPubDataFromDeposit(api, tx)
// verify params
IsVariableEqual(api, flag, tx.AccountNameHash, accountsBefore[0].AccountNameHash)
IsVariableEqual(api, flag, tx.AccountIndex, accountsBefore[0].AccountIndex)
IsVariableEqual(api, flag, tx.AssetId, accountsBefore[0].AssetsInfo[0].AssetId)
return pubData
}

DepositNft

Description

This is a layer-1 transaction and is used for depositing NFTs into the layer-2 account.

On-Chain operation

Size
ChunksSignificant bytes
6134
Structure
NameSize(byte)Comment
TxType1transaction type
AccountIndex4account index
NftIndex5unique index of a nft
NftL1Address20nft layer-1 address
CreatorAccountIndex4creator account index
CreatorTreasuryRate2creator treasury rate
CollectionId2collection id
NftContentHash32nft content hash
NftL1TokenId32nft layer-1 token id
AccountNameHash32account name hash
func ConvertTxToDepositNftPubData(oTx *tx.Tx) (pubData []byte, err error) {
if oTx.TxType != commonTx.TxTypeDepositNft {
logx.Errorf("[ConvertTxToDepositNftPubData] invalid tx type")
return nil, errors.New("[ConvertTxToDepositNftPubData] invalid tx type")
}
// parse tx
txInfo, err := commonTx.ParseDepositNftTxInfo(oTx.TxInfo)
if err != nil {
logx.Errorf("[ConvertTxToDepositNftPubData] unable to parse tx info: %s", err.Error())
return nil, err
}
var buf bytes.Buffer
buf.WriteByte(uint8(oTx.TxType))
buf.Write(Uint32ToBytes(uint32(txInfo.AccountIndex)))
buf.Write(Uint40ToBytes(txInfo.NftIndex))
buf.Write(AddressStrToBytes(txInfo.NftL1Address))
chunk1 := SuffixPaddingBufToChunkSize(buf.Bytes())
buf.Reset()
buf.Write(Uint32ToBytes(uint32(txInfo.CreatorAccountIndex)))
buf.Write(Uint16ToBytes(uint16(txInfo.CreatorTreasuryRate)))
buf.Write(Uint16ToBytes(uint16(txInfo.CollectionId)))
chunk2 := PrefixPaddingBufToChunkSize(buf.Bytes())
buf.Reset()
buf.Write(chunk1)
buf.Write(chunk2)
buf.Write(PrefixPaddingBufToChunkSize(txInfo.NftContentHash))
buf.Write(Uint256ToBytes(txInfo.NftL1TokenId))
buf.Write(PrefixPaddingBufToChunkSize(txInfo.AccountNameHash))
buf.Write(PrefixPaddingBufToChunkSize([]byte{}))
return buf.Bytes(), nil
}

User transaction

NameSize(byte)Comment
AccountNameHash32account name hash
AssetAddress20nft contract layer-1 address
NftTokenId32nft layer-1 token id

Circuit

func VerifyDepositNftTx(
api API,
flag Variable,
tx DepositNftTxConstraints,
accountsBefore [NbAccountsPerTx]AccountConstraints,
nftBefore NftConstraints,
) (pubData [PubDataSizePerTx]Variable) {
pubData = CollectPubDataFromDepositNft(api, tx)
// verify params
// check empty nft
CheckEmptyNftNode(api, flag, nftBefore)
// account index
IsVariableEqual(api, flag, tx.AccountIndex, accountsBefore[0].AccountIndex)
// account name hash
IsVariableEqual(api, flag, tx.AccountNameHash, accountsBefore[0].AccountNameHash)
return pubData
}

Transfer

Description

This is a layer-2 transaction and is used for transferring assets in the layer-2 network.

On-Chain operation

Size
ChunksSignificant bytes
656
Structure
NameSize(byte)Comment
TxType1transaction type
FromAccountIndex4from account index
ToAccountIndex4receiver account index
AssetId2asset index
AssetAmount5packed asset amount
GasFeeAccountIndex4gas fee account index
GasFeeAssetId2gas fee asset id
GasFeeAssetAmount2packed fee amount
CallDataHash32call data hash
func ConvertTxToTransferPubData(oTx *tx.Tx) (pubData []byte, err error) {
if oTx.TxType != commonTx.TxTypeTransfer {
logx.Errorf("[ConvertTxToTransferPubData] invalid tx type")
return nil, errors.New("[ConvertTxToTransferPubData] invalid tx type")
}
// parse tx
txInfo, err := commonTx.ParseTransferTxInfo(oTx.TxInfo)
if err != nil {
logx.Errorf("[ConvertTxToDepositPubData] unable to parse tx info: %s", err.Error())
return nil, err
}
var buf bytes.Buffer
buf.WriteByte(uint8(oTx.TxType))
buf.Write(Uint32ToBytes(uint32(txInfo.FromAccountIndex)))
buf.Write(Uint32ToBytes(uint32(txInfo.ToAccountIndex)))
buf.Write(Uint16ToBytes(uint16(txInfo.AssetId)))
packedAmountBytes, err := AmountToPackedAmountBytes(txInfo.AssetAmount)
if err != nil {
logx.Errorf("[ConvertTxToDepositPubData] unable to convert amount to packed amount: %s", err.Error())
return nil, err
}
buf.Write(packedAmountBytes)
buf.Write(Uint32ToBytes(uint32(txInfo.GasAccountIndex)))
buf.Write(Uint16ToBytes(uint16(txInfo.GasFeeAssetId)))
packedFeeBytes, err := FeeToPackedFeeBytes(txInfo.GasFeeAssetAmount)
if err != nil {
logx.Errorf("[ConvertTxToDepositPubData] unable to convert amount to packed fee amount: %s", err.Error())
return nil, err
}
buf.Write(packedFeeBytes)
chunk := SuffixPaddingBufToChunkSize(buf.Bytes())
buf.Reset()
buf.Write(chunk)
buf.Write(PrefixPaddingBufToChunkSize(txInfo.CallDataHash))
buf.Write(PrefixPaddingBufToChunkSize([]byte{}))
buf.Write(PrefixPaddingBufToChunkSize([]byte{}))
buf.Write(PrefixPaddingBufToChunkSize([]byte{}))
buf.Write(PrefixPaddingBufToChunkSize([]byte{}))
pubData = buf.Bytes()
return pubData, nil
}

User transaction

type TransferTxInfo struct {
FromAccountIndex int64
ToAccountIndex int64
ToAccountNameHash string
AssetId int64
AssetAmount *big.Int
GasAccountIndex int64
GasFeeAssetId int64
GasFeeAssetAmount *big.Int
Memo string
CallData string
CallDataHash []byte
ExpiredAt int64
Nonce int64
Sig []byte
}

Circuit

func VerifyTransferTx(
api API, flag Variable,
tx *TransferTxConstraints,
accountsBefore [NbAccountsPerTx]AccountConstraints,
) (pubData [PubDataSizePerTx]Variable) {
// collect pub-data
pubData = CollectPubDataFromTransfer(api, *tx)
// verify params
// account index
IsVariableEqual(api, flag, tx.FromAccountIndex, accountsBefore[0].AccountIndex)
IsVariableEqual(api, flag, tx.ToAccountIndex, accountsBefore[1].AccountIndex)
IsVariableEqual(api, flag, tx.GasAccountIndex, accountsBefore[2].AccountIndex)
// account name hash
IsVariableEqual(api, flag, tx.ToAccountNameHash, accountsBefore[1].AccountNameHash)
// asset id
IsVariableEqual(api, flag, tx.AssetId, accountsBefore[0].AssetsInfo[0].AssetId)
IsVariableEqual(api, flag, tx.AssetId, accountsBefore[1].AssetsInfo[0].AssetId)
// gas asset id
IsVariableEqual(api, flag, tx.GasFeeAssetId, accountsBefore[0].AssetsInfo[1].AssetId)
IsVariableEqual(api, flag, tx.GasFeeAssetId, accountsBefore[2].AssetsInfo[0].AssetId)
// should have enough balance
tx.AssetAmount = UnpackAmount(api, tx.AssetAmount)
tx.GasFeeAssetAmount = UnpackFee(api, tx.GasFeeAssetAmount)
//tx.GasFeeAssetAmount = UnpackFee(api, tx.GasFeeAssetAmount)
IsVariableLessOrEqual(api, flag, tx.AssetAmount, accountsBefore[0].AssetsInfo[0].Balance)
IsVariableLessOrEqual(api, flag, tx.GasFeeAssetAmount, accountsBefore[0].AssetsInfo[1].Balance)
return pubData
}

Withdraw

Description

This is a layer-2 transaction and is used for withdrawing assets from the layer-2 to the layer-1.

On-Chain operation

Size
ChunksSignificant bytes
651
Structure
NameSize(byte)Comment
TxType1transaction type
AccountIndex4from account index
ToAddress20layer-1 receiver address
AssetId2asset index
AssetAmount16state amount
GasFeeAccountIndex4gas fee account index
GasFeeAssetId2gas fee asset id
GasFeeAssetAmount2packed fee amount
func ConvertTxToWithdrawPubData(oTx *tx.Tx) (pubData []byte, err error) {
if oTx.TxType != commonTx.TxTypeWithdraw {
logx.Errorf("[ConvertTxToWithdrawPubData] invalid tx type")
return nil, errors.New("[ConvertTxToWithdrawPubData] invalid tx type")
}
// parse tx
txInfo, err := commonTx.ParseWithdrawTxInfo(oTx.TxInfo)
if err != nil {
logx.Errorf("[ConvertTxToWithdrawPubData] unable to parse tx info: %s", err.Error())
return nil, err
}
var buf bytes.Buffer
buf.WriteByte(uint8(oTx.TxType))
buf.Write(Uint32ToBytes(uint32(txInfo.FromAccountIndex)))
buf.Write(AddressStrToBytes(txInfo.ToAddress))
buf.Write(Uint16ToBytes(uint16(txInfo.AssetId)))
chunk1 := SuffixPaddingBufToChunkSize(buf.Bytes())
buf.Reset()
buf.Write(Uint128ToBytes(txInfo.AssetAmount))
buf.Write(Uint32ToBytes(uint32(txInfo.GasAccountIndex)))
buf.Write(Uint16ToBytes(uint16(txInfo.GasFeeAssetId)))
packedFeeBytes, err := FeeToPackedFeeBytes(txInfo.GasFeeAssetAmount)
if err != nil {
logx.Errorf("[ConvertTxToDepositPubData] unable to convert amount to packed fee amount: %s", err.Error())
return nil, err
}
buf.Write(packedFeeBytes)
chunk2 := PrefixPaddingBufToChunkSize(buf.Bytes())
buf.Reset()
buf.Write(chunk1)
buf.Write(chunk2)
buf.Write(PrefixPaddingBufToChunkSize([]byte{}))
buf.Write(PrefixPaddingBufToChunkSize([]byte{}))
buf.Write(PrefixPaddingBufToChunkSize([]byte{}))
buf.Write(PrefixPaddingBufToChunkSize([]byte{}))
return buf.Bytes(), nil
}

User transaction

type WithdrawTxInfo struct {
FromAccountIndex int64
AssetId int64
AssetAmount *big.Int
GasAccountIndex int64
GasFeeAssetId int64
GasFeeAssetAmount *big.Int
ToAddress string
ExpiredAt int64
Nonce int64
Sig []byte
}

Circuit

func VerifyWithdrawTx(
api API, flag Variable,
tx *WithdrawTxConstraints,
accountsBefore [NbAccountsPerTx]AccountConstraints,
) (pubData [PubDataSizePerTx]Variable) {
pubData = CollectPubDataFromWithdraw(api, *tx)
// verify params
// account index
IsVariableEqual(api, flag, tx.FromAccountIndex, accountsBefore[0].AccountIndex)
IsVariableEqual(api, flag, tx.GasAccountIndex, accountsBefore[1].AccountIndex)
// asset id
IsVariableEqual(api, flag, tx.AssetId, accountsBefore[0].AssetsInfo[0].AssetId)
IsVariableEqual(api, flag, tx.GasFeeAssetId, accountsBefore[0].AssetsInfo[1].AssetId)
IsVariableEqual(api, flag, tx.GasFeeAssetId, accountsBefore[1].AssetsInfo[0].AssetId)
// should have enough assets
tx.GasFeeAssetAmount = UnpackFee(api, tx.GasFeeAssetAmount)
IsVariableLessOrEqual(api, flag, tx.AssetAmount, accountsBefore[0].AssetsInfo[0].Balance)
IsVariableLessOrEqual(api, flag, tx.GasFeeAssetAmount, accountsBefore[0].AssetsInfo[1].Balance)
return pubData
}

CreateCollection

Description

This is a layer-2 transaction and is used for creating a new collection

On-Chain operation

Size
ChunksSignificant bytes
615
Structure
NameSize(byte)Comment
TxType1transaction type
AccountIndex4account index
CollectionId2collection index
GasAccountIndex4gas account index
GasFeeAssetId2asset id
GasFeeAssetAmount2packed fee amount
func ConvertTxToCreateCollectionPubData(oTx *tx.Tx) (pubData []byte, err error) {
if oTx.TxType != commonTx.TxTypeCreateCollection {
logx.Errorf("[ConvertTxToCreateCollectionPubData] invalid tx type")
return nil, errors.New("[ConvertTxToCreateCollectionPubData] invalid tx type")
}
// parse tx
txInfo, err := commonTx.ParseCreateCollectionTxInfo(oTx.TxInfo)
if err != nil {
logx.Errorf("[ConvertTxToCreateCollectionPubData] unable to parse tx info: %s", err.Error())
return nil, err
}
var buf bytes.Buffer
buf.WriteByte(uint8(oTx.TxType))
buf.Write(Uint32ToBytes(uint32(txInfo.AccountIndex)))
buf.Write(Uint16ToBytes(uint16(txInfo.CollectionId)))
buf.Write(Uint32ToBytes(uint32(txInfo.GasAccountIndex)))
buf.Write(Uint16ToBytes(uint16(txInfo.GasFeeAssetId)))
packedFeeBytes, err := FeeToPackedFeeBytes(txInfo.GasFeeAssetAmount)
if err != nil {
logx.Errorf("[ConvertTxToDepositPubData] unable to convert amount to packed fee amount: %s", err.Error())
return nil, err
}
buf.Write(packedFeeBytes)
chunk := SuffixPaddingBufToChunkSize(buf.Bytes())
buf.Reset()
buf.Write(chunk)
buf.Write(PrefixPaddingBufToChunkSize([]byte{}))
buf.Write(PrefixPaddingBufToChunkSize([]byte{}))
buf.Write(PrefixPaddingBufToChunkSize([]byte{}))
buf.Write(PrefixPaddingBufToChunkSize([]byte{}))
buf.Write(PrefixPaddingBufToChunkSize([]byte{}))
return buf.Bytes(), nil
}

User transaction

type CreateCollectionTxInfo struct {
AccountIndex int64
CollectionId int64
Name string
Introduction string
GasAccountIndex int64
GasFeeAssetId int64
GasFeeAssetAmount *big.Int
ExpiredAt int64
Nonce int64
Sig []byte
}

Circuit

func VerifyCreateCollectionTx(
api API, flag Variable,
tx *CreateCollectionTxConstraints,
accountsBefore [NbAccountsPerTx]AccountConstraints,
) (pubData [PubDataSizePerTx]Variable) {
pubData = CollectPubDataFromCreateCollection(api, *tx)
// verify params
IsVariableLessOrEqual(api, flag, tx.CollectionId, 65535)
// account index
IsVariableEqual(api, flag, tx.AccountIndex, accountsBefore[0].AccountIndex)
IsVariableEqual(api, flag, tx.GasAccountIndex, accountsBefore[1].AccountIndex)
// asset id
IsVariableEqual(api, flag, tx.GasFeeAssetId, accountsBefore[0].AssetsInfo[0].AssetId)
IsVariableEqual(api, flag, tx.GasFeeAssetId, accountsBefore[1].AssetsInfo[0].AssetId)
// collection id
IsVariableEqual(api, flag, tx.CollectionId, api.Add(accountsBefore[0].CollectionNonce, 1))
// should have enough assets
tx.GasFeeAssetAmount = UnpackAmount(api, tx.GasFeeAssetAmount)
IsVariableLessOrEqual(api, flag, tx.GasFeeAssetAmount, accountsBefore[0].AssetsInfo[0].Balance)
return pubData
}

MintNft

Description

This is a layer-2 transaction and is used for minting NFTs in the layer-2 network.

On-Chain operation

Size
ChunksSignificant bytes
658
Structure
NameSize(byte)Comment
TxType1transaction type
CreatorAccountIndex4creator account index
ToAccountIndex4receiver account index
NftIndex5unique nft index
GasFeeAccountIndex4gas fee account index
GasFeeAssetId2gas fee asset id
GasFeeAssetAmount2packed fee amount
CreatorTreasuryRate2creator treasury rate
CollectionId2collection index
NftContentHash32nft content hash
func ConvertTxToMintNftPubData(oTx *tx.Tx) (pubData []byte, err error) {
if oTx.TxType != commonTx.TxTypeMintNft {
logx.Errorf("[ConvertTxToMintNftPubData] invalid tx type")
return nil, errors.New("[ConvertTxToMintNftPubData] invalid tx type")
}
// parse tx
txInfo, err := commonTx.ParseMintNftTxInfo(oTx.TxInfo)
if err != nil {
logx.Errorf("[ConvertTxToMintNftPubData] unable to parse tx info: %s", err.Error())
return nil, err
}
var buf bytes.Buffer
buf.WriteByte(uint8(oTx.TxType))
buf.Write(Uint32ToBytes(uint32(txInfo.CreatorAccountIndex)))
buf.Write(Uint32ToBytes(uint32(txInfo.ToAccountIndex)))
buf.Write(Uint40ToBytes(txInfo.NftIndex))
buf.Write(Uint32ToBytes(uint32(txInfo.GasAccountIndex)))
buf.Write(Uint16ToBytes(uint16(txInfo.GasFeeAssetId)))
packedFeeBytes, err := FeeToPackedFeeBytes(txInfo.GasFeeAssetAmount)
if err != nil {
logx.Errorf("[ConvertTxToDepositPubData] unable to convert amount to packed fee amount: %s", err.Error())
return nil, err
}
buf.Write(packedFeeBytes)
buf.Write(Uint16ToBytes(uint16(txInfo.CreatorTreasuryRate)))
buf.Write(Uint16ToBytes(uint16(txInfo.NftCollectionId)))
chunk := SuffixPaddingBufToChunkSize(buf.Bytes())
buf.Reset()
buf.Write(chunk)
buf.Write(PrefixPaddingBufToChunkSize(common.FromHex(txInfo.NftContentHash)))
buf.Write(PrefixPaddingBufToChunkSize([]byte{}))
buf.Write(PrefixPaddingBufToChunkSize([]byte{}))
buf.Write(PrefixPaddingBufToChunkSize([]byte{}))
buf.Write(PrefixPaddingBufToChunkSize([]byte{}))
return buf.Bytes(), nil
}

User transaction

type MintNftTxInfo struct {
CreatorAccountIndex int64
ToAccountIndex int64
ToAccountNameHash string
NftIndex int64
NftContentHash string
NftCollectionId int64
CreatorTreasuryRate int64
GasAccountIndex int64
GasFeeAssetId int64
GasFeeAssetAmount *big.Int
ExpiredAt int64
Nonce int64
Sig []byte
}

Circuit

func VerifyMintNftTx(
api API, flag Variable,
tx *MintNftTxConstraints,
accountsBefore [NbAccountsPerTx]AccountConstraints, nftBefore NftConstraints,
) (pubData [PubDataSizePerTx]Variable) {
pubData = CollectPubDataFromMintNft(api, *tx)
// verify params
// check empty nft
CheckEmptyNftNode(api, flag, nftBefore)
// account index
IsVariableEqual(api, flag, tx.CreatorAccountIndex, accountsBefore[0].AccountIndex)
IsVariableEqual(api, flag, tx.ToAccountIndex, accountsBefore[1].AccountIndex)
IsVariableEqual(api, flag, tx.GasAccountIndex, accountsBefore[2].AccountIndex)
// account name hash
IsVariableEqual(api, flag, tx.ToAccountNameHash, accountsBefore[1].AccountNameHash)
// content hash
isZero := api.IsZero(tx.NftContentHash)
IsVariableEqual(api, flag, isZero, 0)
// gas asset id
IsVariableEqual(api, flag, tx.GasFeeAssetId, accountsBefore[0].AssetsInfo[0].AssetId)
IsVariableEqual(api, flag, tx.GasFeeAssetId, accountsBefore[2].AssetsInfo[0].AssetId)
// should have enough balance
tx.GasFeeAssetAmount = UnpackFee(api, tx.GasFeeAssetAmount)
IsVariableLessOrEqual(api, flag, tx.GasFeeAssetAmount, accountsBefore[0].AssetsInfo[0].Balance)
return pubData
}

TransferNft

Description

This is a layer-2 transaction and is used for transferring NFTs to others in the layer-2 network.

On-Chain operation

Size
ChunksSignificant bytes
654
Structure
NameSize(byte)Comment
TxType1transaction type
FromAccountIndex4from account index
ToAccountIndex4receiver account index
NftIndex5unique nft index
GasFeeAccountIndex4gas fee account index
GasFeeAssetId2gas fee asset id
GasFeeAssetAmount2packed fee amount
CallDataHash32call data hash
func ConvertTxToTransferNftPubData(oTx *tx.Tx) (pubData []byte, err error) {
if oTx.TxType != commonTx.TxTypeTransferNft {
logx.Errorf("[ConvertTxToMintNftPubData] invalid tx type")
return nil, errors.New("[ConvertTxToMintNftPubData] invalid tx type")
}
// parse tx
txInfo, err := commonTx.ParseTransferNftTxInfo(oTx.TxInfo)
if err != nil {
logx.Errorf("[ConvertTxToMintNftPubData] unable to parse tx info: %s", err.Error())
return nil, err
}
var buf bytes.Buffer
buf.WriteByte(uint8(oTx.TxType))
buf.Write(Uint32ToBytes(uint32(txInfo.FromAccountIndex)))
buf.Write(Uint32ToBytes(uint32(txInfo.ToAccountIndex)))
buf.Write(Uint40ToBytes(txInfo.NftIndex))
buf.Write(Uint32ToBytes(uint32(txInfo.GasAccountIndex)))
buf.Write(Uint16ToBytes(uint16(txInfo.GasFeeAssetId)))
packedFeeBytes, err := FeeToPackedFeeBytes(txInfo.GasFeeAssetAmount)
if err != nil {
logx.Errorf("[ConvertTxToDepositPubData] unable to convert amount to packed fee amount: %s", err.Error())
return nil, err
}
buf.Write(packedFeeBytes)
chunk := SuffixPaddingBufToChunkSize(buf.Bytes())
buf.Reset()
buf.Write(chunk)
buf.Write(PrefixPaddingBufToChunkSize(txInfo.CallDataHash))
buf.Write(PrefixPaddingBufToChunkSize([]byte{}))
buf.Write(PrefixPaddingBufToChunkSize([]byte{}))
buf.Write(PrefixPaddingBufToChunkSize([]byte{}))
buf.Write(PrefixPaddingBufToChunkSize([]byte{}))
return buf.Bytes(), nil
}

User transaction

type TransferNftTxInfo struct {
FromAccountIndex int64
ToAccountIndex int64
ToAccountNameHash string
NftIndex int64
GasAccountIndex int64
GasFeeAssetId int64
GasFeeAssetAmount *big.Int
CallData string
CallDataHash []byte
ExpiredAt int64
Nonce int64
Sig []byte
}

Circuit

func VerifyTransferNftTx(
api API,
flag Variable,
tx *TransferNftTxConstraints,
accountsBefore [NbAccountsPerTx]AccountConstraints,
nftBefore NftConstraints,
) (pubData [PubDataSizePerTx]Variable) {
pubData = CollectPubDataFromTransferNft(api, *tx)
// verify params
// account index
IsVariableEqual(api, flag, tx.FromAccountIndex, accountsBefore[0].AccountIndex)
IsVariableEqual(api, flag, tx.ToAccountIndex, accountsBefore[1].AccountIndex)
IsVariableEqual(api, flag, tx.GasAccountIndex, accountsBefore[2].AccountIndex)
// account name
IsVariableEqual(api, flag, tx.ToAccountNameHash, accountsBefore[1].AccountNameHash)
// asset id
IsVariableEqual(api, flag, tx.GasFeeAssetId, accountsBefore[0].AssetsInfo[0].AssetId)
IsVariableEqual(api, flag, tx.GasFeeAssetId, accountsBefore[2].AssetsInfo[0].AssetId)
// nft info
IsVariableEqual(api, flag, tx.NftIndex, nftBefore.NftIndex)
IsVariableEqual(api, flag, tx.FromAccountIndex, nftBefore.OwnerAccountIndex)
// should have enough balance
tx.GasFeeAssetAmount = UnpackFee(api, tx.GasFeeAssetAmount)
IsVariableLessOrEqual(api, flag, tx.GasFeeAssetAmount, accountsBefore[0].AssetsInfo[0].Balance)
return pubData
}

AtomicMatch

Description

This is a layer-2 transaction that will be used for buying or selling Nft in the layer-2 network.

On-Chain operation

Size
ChunksSignificant bytes
644
Structure

Offer:

NameSize(byte)Comment
Type1transaction type, 0 indicates this is a BuyNftOffer , 1 indicate this is a SellNftOffer
OfferId3used to identify the offer
AccountIndex4who want to buy/sell nft
AssetId2the asset id which buyer/seller want to use pay for nft
AssetAmount5the asset amount
ListedAt8timestamp when the order is signed
ExpiredAt8timestamp after which the order is invalid
Sig64signature generated by buyer/seller_account_index's private key

AtomicMatch(below is the only info that will be uploaded on-chain):

NameSize(byte)Comment
TxType1transaction type
SubmitterAccountIndex4submitter account index
BuyerAccountIndex4buyer account index
BuyerOfferId3used to identify the offer
SellerAccountIndex4seller account index
SellerOfferId3used to identify the offer
AssetId2asset id
AssetAmount5packed asset amount
CreatorAmount5packed creator amount
TreasuryAmount5packed treasury amount
GasFeeAccountIndex4gas fee account index
GasFeeAssetId2gas fee asset id
GasFeeAssetAmount2packed fee amount
func ConvertTxToAtomicMatchPubData(oTx *tx.Tx) (pubData []byte, err error) {
if oTx.TxType != commonTx.TxTypeAtomicMatch {
logx.Errorf("[ConvertTxToAtomicMatchPubData] invalid tx type")
return nil, errors.New("[ConvertTxToAtomicMatchPubData] invalid tx type")
}
// parse tx
txInfo, err := commonTx.ParseAtomicMatchTxInfo(oTx.TxInfo)
if err != nil {
logx.Errorf("[ConvertTxToAtomicMatchPubData] unable to parse tx info: %s", err.Error())
return nil, err
}
var buf bytes.Buffer
buf.WriteByte(uint8(oTx.TxType))
buf.Write(Uint32ToBytes(uint32(txInfo.AccountIndex)))
buf.Write(Uint32ToBytes(uint32(txInfo.BuyOffer.AccountIndex)))
buf.Write(Uint24ToBytes(txInfo.BuyOffer.OfferId))
buf.Write(Uint32ToBytes(uint32(txInfo.SellOffer.AccountIndex)))
buf.Write(Uint24ToBytes(txInfo.SellOffer.OfferId))
buf.Write(Uint40ToBytes(txInfo.BuyOffer.NftIndex))
buf.Write(Uint16ToBytes(uint16(txInfo.SellOffer.AssetId)))
chunk1 := SuffixPaddingBufToChunkSize(buf.Bytes())
buf.Reset()
packedAmountBytes, err := AmountToPackedAmountBytes(txInfo.BuyOffer.AssetAmount)
if err != nil {
logx.Errorf("[ConvertTxToDepositPubData] unable to convert amount to packed amount: %s", err.Error())
return nil, err
}
buf.Write(packedAmountBytes)
creatorAmountBytes, err := AmountToPackedAmountBytes(txInfo.CreatorAmount)
if err != nil {
logx.Errorf("[ConvertTxToDepositPubData] unable to convert amount to packed amount: %s", err.Error())
return nil, err
}
buf.Write(creatorAmountBytes)
treasuryAmountBytes, err := AmountToPackedAmountBytes(txInfo.TreasuryAmount)
if err != nil {
logx.Errorf("[ConvertTxToDepositPubData] unable to convert amount to packed amount: %s", err.Error())
return nil, err
}
buf.Write(treasuryAmountBytes)
buf.Write(Uint32ToBytes(uint32(txInfo.GasAccountIndex)))
buf.Write(Uint16ToBytes(uint16(txInfo.GasFeeAssetId)))
packedFeeBytes, err := FeeToPackedFeeBytes(txInfo.GasFeeAssetAmount)
if err != nil {
logx.Errorf("[ConvertTxToDepositPubData] unable to convert amount to packed fee amount: %s", err.Error())
return nil, err
}
buf.Write(packedFeeBytes)
chunk2 := PrefixPaddingBufToChunkSize(buf.Bytes())
buf.Reset()
buf.Write(chunk1)
buf.Write(chunk2)
buf.Write(PrefixPaddingBufToChunkSize([]byte{}))
buf.Write(PrefixPaddingBufToChunkSize([]byte{}))
buf.Write(PrefixPaddingBufToChunkSize([]byte{}))
buf.Write(PrefixPaddingBufToChunkSize([]byte{}))
return buf.Bytes(), nil
}

User transaction

type OfferTxInfo struct {
Type int64
OfferId int64
AccountIndex int64
NftIndex int64
AssetId int64
AssetAmount *big.Int
ListedAt int64
ExpiredAt int64
TreasuryRate int64
Sig []byte
}

type AtomicMatchTxInfo struct {
AccountIndex int64
BuyOffer *OfferTxInfo
SellOffer *OfferTxInfo
GasAccountIndex int64
GasFeeAssetId int64
GasFeeAssetAmount *big.Int
CreatorAmount *big.Int
TreasuryAmount *big.Int
Nonce int64
ExpiredAt int64
Sig []byte
}

Circuit

func VerifyAtomicMatchTx(
api API, flag Variable,
tx *AtomicMatchTxConstraints,
accountsBefore [NbAccountsPerTx]AccountConstraints,
nftBefore NftConstraints,
blockCreatedAt Variable,
hFunc MiMC,
) (pubData [PubDataSizePerTx]Variable, err error) {
pubData = CollectPubDataFromAtomicMatch(api, *tx)
// verify params
IsVariableEqual(api, flag, tx.BuyOffer.Type, 0)
IsVariableEqual(api, flag, tx.SellOffer.Type, 1)
IsVariableEqual(api, flag, tx.BuyOffer.AssetId, tx.SellOffer.AssetId)
IsVariableEqual(api, flag, tx.BuyOffer.AssetAmount, tx.SellOffer.AssetAmount)
IsVariableEqual(api, flag, tx.BuyOffer.NftIndex, tx.SellOffer.NftIndex)
IsVariableEqual(api, flag, tx.BuyOffer.AssetId, accountsBefore[1].AssetsInfo[0].AssetId)
IsVariableEqual(api, flag, tx.SellOffer.AssetId, accountsBefore[2].AssetsInfo[0].AssetId)
IsVariableEqual(api, flag, tx.SellOffer.AssetId, accountsBefore[3].AssetsInfo[0].AssetId)
IsVariableEqual(api, flag, tx.GasAccountIndex, accountsBefore[4].AccountIndex)
IsVariableEqual(api, flag, tx.GasFeeAssetId, accountsBefore[0].AssetsInfo[0].AssetId)
IsVariableEqual(api, flag, tx.GasFeeAssetId, accountsBefore[4].AssetsInfo[1].AssetId)
IsVariableLessOrEqual(api, flag, blockCreatedAt, tx.BuyOffer.ExpiredAt)
IsVariableLessOrEqual(api, flag, blockCreatedAt, tx.SellOffer.ExpiredAt)
IsVariableEqual(api, flag, nftBefore.NftIndex, tx.SellOffer.NftIndex)
IsVariableEqual(api, flag, tx.BuyOffer.TreasuryRate, tx.SellOffer.TreasuryRate)
// verify signature
hFunc.Reset()
buyOfferHash := ComputeHashFromOfferTx(tx.BuyOffer, hFunc)
hFunc.Reset()
notBuyer := api.IsZero(api.IsZero(api.Sub(tx.AccountIndex, tx.BuyOffer.AccountIndex)))
notBuyer = api.And(flag, notBuyer)
err = VerifyEddsaSig(notBuyer, api, hFunc, buyOfferHash, accountsBefore[1].AccountPk, tx.BuyOffer.Sig)
if err != nil {
return pubData, err
}
hFunc.Reset()
sellOfferHash := ComputeHashFromOfferTx(tx.SellOffer, hFunc)
hFunc.Reset()
notSeller := api.IsZero(api.IsZero(api.Sub(tx.AccountIndex, tx.SellOffer.AccountIndex)))
notSeller = api.And(flag, notSeller)
err = VerifyEddsaSig(notSeller, api, hFunc, sellOfferHash, accountsBefore[2].AccountPk, tx.SellOffer.Sig)
if err != nil {
return pubData, err
}
// verify account index
// submitter
IsVariableEqual(api, flag, tx.AccountIndex, accountsBefore[0].AccountIndex)
// buyer
IsVariableEqual(api, flag, tx.BuyOffer.AccountIndex, accountsBefore[1].AccountIndex)
// seller
IsVariableEqual(api, flag, tx.SellOffer.AccountIndex, accountsBefore[2].AccountIndex)
// creator
IsVariableEqual(api, flag, nftBefore.CreatorAccountIndex, accountsBefore[3].AccountIndex)
// gas
IsVariableEqual(api, flag, tx.GasAccountIndex, accountsBefore[4].AccountIndex)
// verify buy offer id
buyOfferIdBits := api.ToBinary(tx.BuyOffer.OfferId, 24)
buyAssetId := api.FromBinary(buyOfferIdBits[7:]...)
buyOfferIndex := api.Sub(tx.BuyOffer.OfferId, api.Mul(buyAssetId, OfferSizePerAsset))
buyOfferIndexBits := api.ToBinary(accountsBefore[1].AssetsInfo[1].OfferCanceledOrFinalized, OfferSizePerAsset)
for i := 0; i < OfferSizePerAsset; i++ {
isZero := api.IsZero(api.Sub(buyOfferIndex, i))
isCheckVar := api.And(isZero, flag)
isCheck := api.Compiler().IsBoolean(isCheckVar)
if isCheck {
IsVariableEqual(api, 1, buyOfferIndexBits[i], 0)
}
}
// verify sell offer id
sellOfferIdBits := api.ToBinary(tx.SellOffer.OfferId, 24)
sellAssetId := api.FromBinary(sellOfferIdBits[7:]...)
sellOfferIndex := api.Sub(tx.SellOffer.OfferId, api.Mul(sellAssetId, OfferSizePerAsset))
sellOfferIndexBits := api.ToBinary(accountsBefore[2].AssetsInfo[1].OfferCanceledOrFinalized, OfferSizePerAsset)
for i := 0; i < OfferSizePerAsset; i++ {
isZero := api.IsZero(api.Sub(sellOfferIndex, i))
isCheckVar := api.And(isZero, flag)
isCheck := api.Compiler().IsBoolean(isCheckVar)
if isCheck {
IsVariableEqual(api, 1, sellOfferIndexBits[i], 0)
}
}
// buyer should have enough balance
tx.BuyOffer.AssetAmount = UnpackAmount(api, tx.BuyOffer.AssetAmount)
IsVariableLessOrEqual(api, flag, tx.BuyOffer.AssetAmount, accountsBefore[1].AssetsInfo[0].Balance)
// submitter should have enough balance
tx.GasFeeAssetAmount = UnpackFee(api, tx.GasFeeAssetAmount)
IsVariableLessOrEqual(api, flag, tx.GasFeeAssetAmount, accountsBefore[0].AssetsInfo[0].Balance)
return pubData, nil
}

CancelOffer

Description

This is a layer-2 transaction and is used for canceling nft offer.

On-Chain operation

Size
ChunksSignificant bytes
616
Structure
NameSize(byte)Comment
TxType1transaction type
AccountIndex4account index
OfferId3nft offer id
GasFeeAccountIndex4gas fee account index
GasFeeAssetId2gas fee asset id
GasFeeAssetAmount2packed fee amount
func ConvertTxToCancelOfferPubData(oTx *tx.Tx) (pubData []byte, err error) {
if oTx.TxType != commonTx.TxTypeCancelOffer {
logx.Errorf("[ConvertTxToCancelOfferPubData] invalid tx type")
return nil, errors.New("[ConvertTxToCancelOfferPubData] invalid tx type")
}
// parse tx
txInfo, err := commonTx.ParseCancelOfferTxInfo(oTx.TxInfo)
if err != nil {
logx.Errorf("[ConvertTxToCancelOfferPubData] unable to parse tx info: %s", err.Error())
return nil, err
}
var buf bytes.Buffer
buf.WriteByte(uint8(oTx.TxType))
buf.Write(Uint32ToBytes(uint32(txInfo.AccountIndex)))
buf.Write(Uint24ToBytes(txInfo.OfferId))
buf.Write(Uint32ToBytes(uint32(txInfo.GasAccountIndex)))
buf.Write(Uint16ToBytes(uint16(txInfo.GasFeeAssetId)))
packedFeeBytes, err := FeeToPackedFeeBytes(txInfo.GasFeeAssetAmount)
if err != nil {
logx.Errorf("[ConvertTxToDepositPubData] unable to convert amount to packed fee amount: %s", err.Error())
return nil, err
}
buf.Write(packedFeeBytes)
chunk := SuffixPaddingBufToChunkSize(buf.Bytes())
buf.Reset()
buf.Write(chunk)
buf.Write(PrefixPaddingBufToChunkSize([]byte{}))
buf.Write(PrefixPaddingBufToChunkSize([]byte{}))
buf.Write(PrefixPaddingBufToChunkSize([]byte{}))
buf.Write(PrefixPaddingBufToChunkSize([]byte{}))
buf.Write(PrefixPaddingBufToChunkSize([]byte{}))
return buf.Bytes(), nil
}

User transaction

type CancelOfferTxInfo struct {
AccountIndex int64
OfferId int64
GasAccountIndex int64
GasFeeAssetId int64
GasFeeAssetAmount *big.Int
ExpiredAt int64
Nonce int64
Sig []byte
}

Circuit

func VerifyCancelOfferTx(
api API, flag Variable,
tx *CancelOfferTxConstraints,
accountsBefore [NbAccountsPerTx]AccountConstraints,
) (pubData [PubDataSizePerTx]Variable) {
pubData = CollectPubDataFromCancelOffer(api, *tx)
// verify params
IsVariableEqual(api, flag, tx.AccountIndex, accountsBefore[0].AccountIndex)
IsVariableEqual(api, flag, tx.GasAccountIndex, accountsBefore[1].AccountIndex)
IsVariableEqual(api, flag, tx.GasFeeAssetId, accountsBefore[0].AssetsInfo[0].AssetId)
IsVariableEqual(api, flag, tx.GasFeeAssetId, accountsBefore[1].AssetsInfo[0].AssetId)
offerIdBits := api.ToBinary(tx.OfferId, 24)
assetId := api.FromBinary(offerIdBits[7:]...)
IsVariableEqual(api, flag, assetId, accountsBefore[0].AssetsInfo[1].AssetId)
// should have enough balance
tx.GasFeeAssetAmount = UnpackFee(api, tx.GasFeeAssetAmount)
IsVariableLessOrEqual(api, flag, tx.GasFeeAssetAmount, accountsBefore[0].AssetsInfo[1].Balance)
return pubData
}

WithdrawNft

Description

This is a layer-2 transaction and is used for withdrawing nft from the layer-2 to the layer-1.

On-Chain operation

Size
ChunksSignificant bytes
6162
Structure
NameSize(byte)Comment
TxType1transaction type
AccountIndex4account index
CreatorAccountIndex4creator account index
CreatorTreasuryRate2creator treasury rate
NftIndex5unique nft index
CollectionId2collection id
NftL1Address20nft layer-1 address
ToAddress20receiver address
GasFeeAccountIndex4gas fee account index
GasFeeAssetId2gas fee asset id
GasFeeAssetAmount2packed fee amount
NftContentHash32nft content hash
NftL1TokenId32nft layer-1 token id
CreatorAccountNameHash32creator account name hash
func ConvertTxToWithdrawNftPubData(oTx *tx.Tx) (pubData []byte, err error) {
if oTx.TxType != commonTx.TxTypeWithdrawNft {
logx.Errorf("[ConvertTxToWithdrawNftPubData] invalid tx type")
return nil, errors.New("[ConvertTxToWithdrawNftPubData] invalid tx type")
}
// parse tx
txInfo, err := commonTx.ParseWithdrawNftTxInfo(oTx.TxInfo)
if err != nil {
logx.Errorf("[ConvertTxToWithdrawNftPubData] unable to parse tx info: %s", err.Error())
return nil, err
}
var buf bytes.Buffer
buf.WriteByte(uint8(oTx.TxType))
buf.Write(Uint32ToBytes(uint32(txInfo.AccountIndex)))
buf.Write(Uint32ToBytes(uint32(txInfo.CreatorAccountIndex)))
buf.Write(Uint16ToBytes(uint16(txInfo.CreatorTreasuryRate)))
buf.Write(Uint40ToBytes(txInfo.NftIndex))
buf.Write(Uint16ToBytes(uint16(txInfo.CollectionId)))
chunk1 := SuffixPaddingBufToChunkSize(buf.Bytes())
buf.Reset()
buf.Write(AddressStrToBytes(txInfo.NftL1Address))
chunk2 := PrefixPaddingBufToChunkSize(buf.Bytes())
buf.Reset()
buf.Write(AddressStrToBytes(txInfo.ToAddress))
buf.Write(Uint32ToBytes(uint32(txInfo.GasAccountIndex)))
buf.Write(Uint16ToBytes(uint16(txInfo.GasFeeAssetId)))
packedFeeBytes, err := FeeToPackedFeeBytes(txInfo.GasFeeAssetAmount)
if err != nil {
logx.Errorf("[ConvertTxToDepositPubData] unable to convert amount to packed fee amount: %s", err.Error())
return nil, err
}
buf.Write(packedFeeBytes)
chunk3 := PrefixPaddingBufToChunkSize(buf.Bytes())
buf.Reset()
buf.Write(chunk1)
buf.Write(chunk2)
buf.Write(chunk3)
buf.Write(PrefixPaddingBufToChunkSize(txInfo.NftContentHash))
buf.Write(Uint256ToBytes(txInfo.NftL1TokenId))
buf.Write(PrefixPaddingBufToChunkSize(txInfo.CreatorAccountNameHash))
return buf.Bytes(), nil
}

User transaction

type WithdrawNftTxInfo struct {
AccountIndex int64
CreatorAccountIndex int64
CreatorAccountNameHash []byte
CreatorTreasuryRate int64
NftIndex int64
NftContentHash []byte
NftL1Address string
NftL1TokenId *big.Int
CollectionId int64
ToAddress string
GasAccountIndex int64
GasFeeAssetId int64
GasFeeAssetAmount *big.Int
ExpiredAt int64
Nonce int64
Sig []byte
}

Circuit

func VerifyWithdrawNftTx(
api API,
flag Variable,
tx *WithdrawNftTxConstraints,
accountsBefore [NbAccountsPerTx]AccountConstraints,
nftBefore NftConstraints,
) (pubData [PubDataSizePerTx]Variable) {
pubData = CollectPubDataFromWithdrawNft(api, *tx)
// verify params
// account index
IsVariableEqual(api, flag, tx.AccountIndex, accountsBefore[0].AccountIndex)
IsVariableEqual(api, flag, tx.CreatorAccountIndex, accountsBefore[1].AccountIndex)
IsVariableEqual(api, flag, tx.GasAccountIndex, accountsBefore[2].AccountIndex)
// account name hash
IsVariableEqual(api, flag, tx.CreatorAccountNameHash, accountsBefore[1].AccountNameHash)
// collection id
IsVariableEqual(api, flag, tx.CollectionId, nftBefore.CollectionId)
// asset id
IsVariableEqual(api, flag, tx.GasFeeAssetId, accountsBefore[0].AssetsInfo[0].AssetId)
IsVariableEqual(api, flag, tx.GasFeeAssetId, accountsBefore[2].AssetsInfo[0].AssetId)
// nft info
IsVariableEqual(api, flag, tx.NftIndex, nftBefore.NftIndex)
IsVariableEqual(api, flag, tx.CreatorAccountIndex, nftBefore.CreatorAccountIndex)
IsVariableEqual(api, flag, tx.CreatorTreasuryRate, nftBefore.CreatorTreasuryRate)
IsVariableEqual(api, flag, tx.AccountIndex, nftBefore.OwnerAccountIndex)
IsVariableEqual(api, flag, tx.NftContentHash, nftBefore.NftContentHash)
IsVariableEqual(api, flag, tx.NftL1TokenId, nftBefore.NftL1TokenId)
IsVariableEqual(api, flag, tx.NftL1Address, nftBefore.NftL1Address)
// have enough assets
tx.GasFeeAssetAmount = UnpackFee(api, tx.GasFeeAssetAmount)
IsVariableLessOrEqual(api, flag, tx.GasFeeAssetAmount, accountsBefore[0].AssetsInfo[0].Balance)
return pubData
}

FullExit

Description

This is a layer-1 transaction and is used for full exit assets from the layer-2 to the layer-1.

On-Chain operation

Size
ChunksSignificant bytes
655
Structure
NameSize(byte)Comment
TxType1transaction type
AccountIndex4from account index
AssetId2asset index
AssetAmount16state amount
AccountNameHash32account name hash
func ConvertTxToFullExitPubData(oTx *tx.Tx) (pubData []byte, err error) {
if oTx.TxType != commonTx.TxTypeFullExit {
logx.Errorf("[ConvertTxToFullExitPubData] invalid tx type")
return nil, errors.New("[ConvertTxToFullExitPubData] invalid tx type")
}
// parse tx
txInfo, err := commonTx.ParseFullExitTxInfo(oTx.TxInfo)
if err != nil {
logx.Errorf("[ConvertTxToFullExitPubData] unable to parse tx info: %s", err.Error())
return nil, err
}
var buf bytes.Buffer
buf.WriteByte(uint8(oTx.TxType))
buf.Write(Uint32ToBytes(uint32(txInfo.AccountIndex)))
buf.Write(Uint16ToBytes(uint16(txInfo.AssetId)))
buf.Write(Uint128ToBytes(txInfo.AssetAmount))
chunk := SuffixPaddingBufToChunkSize(buf.Bytes())
buf.Reset()
buf.Write(chunk)
buf.Write(PrefixPaddingBufToChunkSize(txInfo.AccountNameHash))
buf.Write(PrefixPaddingBufToChunkSize([]byte{}))
buf.Write(PrefixPaddingBufToChunkSize([]byte{}))
buf.Write(PrefixPaddingBufToChunkSize([]byte{}))
buf.Write(PrefixPaddingBufToChunkSize([]byte{}))
return buf.Bytes(), nil
}

User transaction

NameSize(byte)Comment
AccountNameHash32account name hash
AssetAddress20asset layer-1 address

Circuit

func VerifyFullExitTx(
api API, flag Variable,
tx FullExitTxConstraints,
accountsBefore [NbAccountsPerTx]AccountConstraints,
) (pubData [PubDataSizePerTx]Variable) {
pubData = CollectPubDataFromFullExit(api, tx)
// verify params
IsVariableEqual(api, flag, tx.AccountNameHash, accountsBefore[0].AccountNameHash)
IsVariableEqual(api, flag, tx.AccountIndex, accountsBefore[0].AccountIndex)
IsVariableEqual(api, flag, tx.AssetId, accountsBefore[0].AssetsInfo[0].AssetId)
IsVariableEqual(api, flag, tx.AssetAmount, accountsBefore[0].AssetsInfo[0].Balance)
return pubData
}

FullExitNft

Description

This is a layer-1 transaction and is used for full exit NFTs from the layer-2 to the layer-1.

On-Chain operation

Size
ChunksSignificant bytes
6164
Structure
NameSize(byte)Comment
TxType1transaction type
AccountIndex4from account index
CreatorAccountIndex2creator account index
CreatorTreasuryRate2creator treasury rate
NftIndex5unique nft index
CollectionId2collection id
NftL1Address20nft layer-1 address
AccountNameHash32account name hash
CreatorAccountNameHash32creator account name hash
NftContentHash32nft content hash
NftL1TokenId32nft layer-1 token id
func ConvertTxToFullExitNftPubData(oTx *tx.Tx) (pubData []byte, err error) {
if oTx.TxType != commonTx.TxTypeFullExitNft {
logx.Errorf("[ConvertTxToFullExitNftPubData] invalid tx type")
return nil, errors.New("[ConvertTxToFullExitNftPubData] invalid tx type")
}
// parse tx
txInfo, err := commonTx.ParseFullExitNftTxInfo(oTx.TxInfo)
if err != nil {
logx.Errorf("[ConvertTxToFullExitNftPubData] unable to parse tx info: %s", err.Error())
return nil, err
}
var buf bytes.Buffer
buf.WriteByte(uint8(oTx.TxType))
buf.Write(Uint32ToBytes(uint32(txInfo.AccountIndex)))
buf.Write(Uint32ToBytes(uint32(txInfo.CreatorAccountIndex)))
buf.Write(Uint16ToBytes(uint16(txInfo.CreatorTreasuryRate)))
buf.Write(Uint40ToBytes(txInfo.NftIndex))
buf.Write(Uint16ToBytes(uint16(txInfo.CollectionId)))
chunk1 := SuffixPaddingBufToChunkSize(buf.Bytes())
buf.Reset()
buf.Write(AddressStrToBytes(txInfo.NftL1Address))
chunk2 := PrefixPaddingBufToChunkSize(buf.Bytes())
buf.Reset()
buf.Write(chunk1)
buf.Write(chunk2)
buf.Write(PrefixPaddingBufToChunkSize(txInfo.AccountNameHash))
buf.Write(PrefixPaddingBufToChunkSize(txInfo.CreatorAccountNameHash))
buf.Write(PrefixPaddingBufToChunkSize(txInfo.NftContentHash))
buf.Write(Uint256ToBytes(txInfo.NftL1TokenId))
return buf.Bytes(), nil
}

User transaction

NameSize(byte)Comment
AccountNameHash32account name hash
NftIndex5unique nft index

Circuit

func VerifyFullExitNftTx(
api API, flag Variable,
tx FullExitNftTxConstraints,
accountsBefore [NbAccountsPerTx]AccountConstraints, nftBefore NftConstraints,
) (pubData [PubDataSizePerTx]Variable) {
pubData = CollectPubDataFromFullExitNft(api, tx)
// verify params
IsVariableEqual(api, flag, tx.AccountNameHash, accountsBefore[0].AccountNameHash)
IsVariableEqual(api, flag, tx.AccountIndex, accountsBefore[0].AccountIndex)
IsVariableEqual(api, flag, tx.NftIndex, nftBefore.NftIndex)
isCheck := api.IsZero(api.IsZero(tx.CreatorAccountNameHash))
isCheck = api.And(flag, isCheck)
IsVariableEqual(api, isCheck, tx.CreatorAccountIndex, accountsBefore[1].AccountIndex)
IsVariableEqual(api, isCheck, tx.CreatorAccountNameHash, accountsBefore[1].AccountNameHash)
IsVariableEqual(api, flag, tx.CreatorAccountIndex, nftBefore.CreatorAccountIndex)
IsVariableEqual(api, flag, tx.CreatorTreasuryRate, nftBefore.CreatorTreasuryRate)
isOwner := api.And(api.IsZero(api.Sub(tx.AccountIndex, nftBefore.OwnerAccountIndex)), flag)
IsVariableEqual(api, isOwner, tx.NftContentHash, nftBefore.NftContentHash)
IsVariableEqual(api, isOwner, tx.NftL1Address, nftBefore.NftL1Address)
IsVariableEqual(api, isOwner, tx.NftL1TokenId, nftBefore.NftL1TokenId)
tx.NftContentHash = api.Select(isOwner, tx.NftContentHash, 0)
tx.NftL1Address = api.Select(isOwner, tx.NftL1Address, 0)
tx.NftL1TokenId = api.Select(isOwner, tx.NftL1TokenId, 0)
return pubData
}

Smart contracts API

Rollup contract

RegisterZNS

Register an ZNS account which is an ENS like domain for layer-1 and a short account name for your layer-2 account.

function registerZNS(string calldata _name, address _owner, bytes32 _zkbnbPubKeyX, bytes32 _zkbnbPubKeyY) external payable nonReentrant
  • _name: your favor account name
  • _owner: account name layer-1 owner address
  • _zkbnbPubKeyX: zkBNB layer-2 public key X
  • _zkbnbPubKeyY: zkBNB layer-2 public key Y

Deposit BNB

Deposit BNB to Rollup - transfer BNB from user L1 address into Rollup account

function depositBNB(bytes32 _accountNameHash) external payable
  • _accountNameHash: The layer-2

Deposit BEP20

Deposit BEP20 assets to Rollup - transfer BEP20 assets from user L1 address into Rollup account

function depositBEP20(
IERC20 _token,
uint104 _amount,
bytes32 _accountNameHash
) external nonReentrant
  • _token: valid BEP20 address
  • _amount: deposit amount
  • _accountNameHash: ZNS account name hash

Withdraw Pending BNB/BEP20

Withdraw BNB/BEP20 token to L1 - Transfer token from contract to owner

function withdrawPendingBalance(
address payable _owner,
address _token,
uint128 _amount
) external nonReentrant
  • _owner: layer-1 address
  • _token: asset address, 0 for BNB
  • _amount: withdraw amount

Withdraw Pending Nft

Withdraw NFT to L1

function withdrawPendingNFTBalance(uint40 _nftIndex) external
  • _nftIndex: nft index

Censorship resistance

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(bytes32 _accountNameHash, address _asset) public nonReentrant
  • _accountNameHash: ZNS account name hash
  • _asset: BEP20 asset address, 0 for BNB

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(bytes32 _accountNameHash, uint32 _nftIndex) public nonReentrant
  • _accountNameHash: ZNS account name hash
  • _nftIndex: nft index

Desert mode

Withdraw funds

Withdraws token from Rollup to L1 in case of desert mode. User must provide proof that she owns funds.

// TODO

Rollup Operations

Commit block

Submit committed block data. Only active validator can make it. On-chain operations will be checked on contract and fulfilled on block verification.

struct StoredBlockInfo {
uint32 blockNumber;
uint64 priorityOperations;
bytes32 pendingOnchainOperationsHash;
uint256 timestamp;
bytes32 stateRoot;
bytes32 commitment;
}

struct CommitBlockInfo {
bytes32 newStateRoot;
bytes publicData;
uint256 timestamp;
uint32[] publicDataOffsets;
uint32 blockNumber;
}

function commitBlocks(
StoredBlockInfo memory _lastCommittedBlockData,
CommitBlockInfo[] memory _newBlocksData
)
external

StoredBlockInfo: block data that we store on BNB Smart Chain. 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 number
  • priorityOperations: priority operations count
  • pendingOnchainOperationsHash: hash of all on-chain operations that have to be processed when block is finalized (verified)
  • timestamp: block timestamp
  • stateRoot: root hash of the rollup tree state
  • commitment: rollup block commitment

CommitBlockInfo: data needed for new block commit

  • newStateRoot: new layer-2 root hash
  • publicData: public data of the executed rollup operations
  • timestamp: block timestamp
  • publicDataOffsets: list of on-chain operations offset
  • blockNumber: rollup block number

commitBlocks and commitOneBlock are used for committing layer-2 transactions data on-chain.

  • _lastCommittedBlockData: last committed block header
  • _newBlocksData: pending commit blocks
Verify and execute blocks

Submit proofs of blocks and make it verified on-chain. Only active validator can make it. This block on-chain operations will be fulfilled.

struct VerifyAndExecuteBlockInfo {
StoredBlockInfo blockHeader;
bytes[] pendingOnchainOpsPubData;
}

function verifyAndExecuteBlocks(VerifyAndExecuteBlockInfo[] memory _blocks, uint256[] memory _proofs) external

VerifyAndExecuteBlockInfo: block data that is used for verifying blocks

  • blockHeader: related block header
  • pendingOnchainOpsPubdata: public data of pending on-chain operations

verifyBlocks: is used for verifying block data and proofs

  • _blocks: pending verify blocks
  • _proofs: Groth16 proofs

Desert mode trigger

Checks if Desert mode must be entered. Desert mode must be entered in case of current BNB Smart Chain block number is higher than the oldest of existed priority requests expiration block number.

function activateDesertMode() public returns (bool)

Revert blocks

Revert blocks that were not verified before deadline determined by EXPECT_VERIFICATION_IN constant.

function revertBlocks(StoredBlockInfo[] memory _blocksToRevert) external
  • _blocksToRevert: committed blocks to revert in reverse order starting from last committed.

Set default NFT factory

Set default NFT factory, which will be used for withdrawing NFT by default

function setDefaultNFTFactory(NFTFactory _factory) external
  • _factory: NFT factory address

Register NFT factory

Register NFT factory, which will be used for withdrawing NFT.

function registerNFTFactory(
string calldata _creatorAccountName,
uint32 _collectionId,
NFTFactory _factory
) external
  • _creatorAccountName: NFT creator account name
  • _collectionId: Collection id in the layer-2
  • _factory: Address of NFTFactory

Get NFT factory for creator

Get NFT factory which will be used for withdrawing NFT for corresponding creator

function getNFTFactory(bytes32 _creatorAccountNameHash, uint32 _collectionId) public view returns (address)
  • _creatorAccountNameHash: Creator account name hash
  • _collectionId: Collection id

Governance contract

Change governor

Change current governor. The caller must be current governor.

function changeGovernor(address _newGovernor)
  • _newGovernor: Address of the new governor

Add asset

Add asset to the list of networks assets. The caller must be current asset governance.

function addAsset(address _asset) external

Set asset paused

Set asset status as paused or active. The caller must be current governor. It is impossible to create deposits of the paused assets.

function setAssetPaused(address _assetAddress, bool _assetPaused) external
  • _assetAddress: asset layer-1 address
  • _assetPausesd: status

Set validator

Change validator status (active or not active). The caller must be current governor.

function setValidator(address _validator, bool _active)
Change asset governance
function changeAssetGovernance(AssetGovernance _newAssetGovernance) external
  • _newAssetGovernance: New asset Governance

Check for governor

Validate that specified address is the token governance address

function requireGovernor(address _address)
  • _address: Address to check

Check for active validator

Validate that specified address is the active validator

function requireActiveValidator(address _address)
  • _address: Address to check

Check that asset address is valid

Validate asset address (it must be presented in assets list).

function validateAssetAddress(address _assetAddr) external view returns (uint16)
  • _assetAddr: Asset address

Returns: asset id.

Asset Governance contract

Add asset

Collecting fees for adding an asset and passing the call to the addAsset function in the governance contract.

function addAsset(address _assetAddress) external
  • _assetAddress: BEP20 asset address

Set listing fee asset

Set new listing asset and fee, can be called only by governor.

function setListingFeeAsset(IERC20 _newListingFeeAsset, uint256 _newListingFee) external
  • _newListingFeeAsset: address of the asset in which fees will be collected
  • _newListingFee: amount of tokens that will need to be paid for adding tokens

Set listing fee

Set new listing fee, can be called only by governor.

function setListingFee(uint256 _newListingFee)
  • _newListingFee: amount of assets that will need to be paid for adding tokens

Set lister

Enable or disable asset lister, if enabled new assets can be added by that address without payment, can be called only by governor.

function setLister(address _listerAddress, bool _active)
  • _listerAddress: address that can list tokens without fee
  • _active: active flag

Set listing cap

Change maximum amount of assets that can be listed using this method, can be called only by governor.

function setListingCap(uint16 _newListingCap)
  • _newListingCap: max number of assets that can be listed using this contract

Set treasury

Change address that collects payments for listing assets, can be called only by governor.

function setTreasury(address _newTreasury)
  • _newTreasury: address that collects listing payments