Compact Extension for m-NFT Protocol: The Key to Mass Adoption

Compact m-NFT uses Sparse Merkle Tree (SMT) to reduce the on-chain storage cost of NFT issuance. It compresses multiple m-NFTs into one single cell for individual user. Token transfer happens in an asynchronous way to avoid the Anyone-can-pay (ACP) requirement for the receiver’s lock as well as the cell competition problems.

Basic Ideas

There are generally two schemes to store data into SMT for users, User-shared SMT (USS) and Token-shared SMT (TSS). USS uses a single cell for different users, so it requires aggregator to combine users’ transactions. TSS needs a seperated cell for individual user, and it manages all kinds of tokens for the specific user. Compact UDT protocol adopts the USS mode, it’s more space efficient, also more complicated on implementation. As a comparison, TSS mode is simple yet powerful.

USS vs TSS design scheme comparision

The reason why we choose TSS

To be honest, TSS has several drawbacks.

  • It requires a seperated storage space for every user, which costs much more than USS, and it’s not friendly to ask user to create the cell.
  • We have to face the cross cell operation problem, and figure out a solution simalar to cheque cell and anyone-can-pay.

However, TSS has some unique advantages that lead us to make the final decision.

  • It would be super difficult to handle various user locks in the USS typescript. For example, Unipass L3 uses SMT to store the user name to address map. USS has to design a perfect protocol to make the 3rd-part lockscripts to follow the data and witness structure. There will also be lots of potential security vulnerabilities.
  • The implementation of USS aggregator is a big challenge. You need to open an RPC for all uses, because of all the users need to operate the USS cell, it’s almost impossible to operate by multiplue hosts, it must be some centralized service to avoid the cell collision.
  • TSS’s lockscript and typescript is decoupled. You don’t care about the lockscript logics when implement the TSS script.
  • TSS does not require a heavy-load centralized aggregator, because everyone handles their own SMT in seperated cell.
  • TSS is incentive compatible for the token transfer, since everyone spends their own money to pay the tx fee. On the contrary, USS model spends some public pool to pay the fee.

Two-step transfer

Since everyone has an SMT cell for the NFT safe box, there is a naturally question that how to transfer NFTs from one box to another? We notice that cheque cell and anyone-can-pay solution were introduced to resolve the same question for SUDT payment. But cheque cell adds an extral cell for payment, which makes it more expensive, and acp forces the receiver’s lockscript to adopt acp logics and coupled with SUDT typescript.

Here we introduce a new method to handle this problem. The principle is simple, the sender first minus his/her token and leave a proof in his/her own cell, and then the receiver restore the token in his/her own cell according to the proof. With this method, the sender has no needs to operate the receiver’s cell like that required in acp mode, and the receiver could immediately confirm the token when he/she scans the block to get the proof. More over, there is no Intermediate cell needed.

To acheive it, we must follow two rules. Rule number 1, there is only one account cell for one specific cell to ensure only one deposit proof could be claimed once. Rule number 2, the account cell cannot be destroyed or transfered, or the receiver may not claim their NFTs. So there has to be a global account cell registry to ensure the uniqueness.

User experience

For the user side, it would remain almost the same as mNFT usages. The sender needs not wait for the receiver’s account cell ready, because the sender doesn’t modify the receiver’s state. The receiver needn’t claim the incoming tokens until he/she sends out them.

Data Structure

To support compact NFT (cNFT), we have to make the following data structure upgrade for previous mNFT protocol.

  • Introduce a new type of cell compact_nft_cell to hold cNFT for users
  • Add nft_smt_root field in NFT Class cell for directly cNFT minting
  • Update version from 0x00 ⇒ 0x01 in NFT Class Cell and NFT Cell to keep compatibility

Global account registry

The main purpose of global account registry is to make sure every lockscript maintains only one cell.

# global_compact_registry_cell data structure
data:
    version: byte
    registry_smt_root: bytes32
type:
    code_hash: global_compact_registry_type
    args: type_id     # type_id == lock_hash[0..20]
lock:
    always_success lock

Compact NFT Cell / Account Cell

compact_nft_cell is used to store user’s token states, including those the user holds, sent, and claimed. To keep the cell unique and untransferable, its typescript.args must match its lockscript.

# compact_nft_cell data structure
data:
    version: byte     # must be 0
    nft_smt_root: byte32
type:
    code_hash: compact_nft_type
    args: lockscript_hash[0..20]   # must match self.lockscript
lock:
    user-defined

cNFT SMT data structure

The nft_smt_root is the most important data field in the design. It appears in both nft_class_cell and compact_nft_cell. Data stored in the SMT is managed by key-value pair. There are mainly three kinds of data stored and compressed in this hash root. We use smt.key.smt_type to identify different data types.

owned_nft only presents in the compact_nft_cell, it maintains the token list that the current user holds. The smt_type is set to 0x01.

# owned_nft data structure
key:
hash of {
    smt_type: uint8       # 0x01
    issuer_id: Byte[20]
    class_id: uint32
    token_idx: uint32
}
value:
hash of {
    characteristic: Byte[8]
    configure: Byte[1]
    state: Byte[1]
}

claimed_nft only presents in the compact_nft_cell too. It is used to detect the replay attack to make sure the cell claim deposit only once. Here we use the deposit outpoint hash to identify different deposits. The smt_type is set to 0x02.

# claimed_nft data structure
key: 
hash of {
    smt_type: uint8       # 0x02
    out_point: OutPoint[12..36] // Outpoint field recorded in the proof
}
value:
    0x00...00 for nonclaimed
    0xFF...FF for claimed

withdrawal_nft exists in both nft_class_cell and compact_nft_cell. It’s used to generate a deposit proof for the receiver to claim. The smt_type is set to 0x03.

# withdrawal_nft data structure
key: 
hash of {
    smt_type: uint8       # 0x03
    issuer_id: Byte[20]
    class_id: uint32
    token_idx: uint32
}
value:
hash of {
    characteristic: Byte[8]
    configure: Byte[1]
    state: Byte[1]
    to: lockscript_hash[0..20]
    out_point: OutPoint[12..36]    # Outpoint of previous input cell with SMT
}

General Token Extension in the Future

Now we have a general SMT state framework for end users. We could also propose an user friendly fungible token protocol based on the same framework. For example, we may use owned_ft, claimed_ft, and withdrawal_ft k-v pair in the smt_root field to process both fungible and non-fungible tokens.

# owned_ft data structure
key:
hash of {
    smt_type: uint8       # 0x11
    issuer_id: Byte[20]
    class_id: uint32
    token_idx: uint32
}
value:
hash of {
		amount: uint128
}
# claimed_ft data structure
key: 
hash of {
    smt_type: uint8       # 0x12
    out_point: OutPoint[12..36] // Outpoint field recorded in the proof
}
value:
    0x00...00 for nonclaimed
    0xFF...FF for claimed
# withdrawal_ft data structure
key: 
hash of {
    smt_type: uint8       # 0x13
    issuer_id: Byte[20]
    class_id: uint32
    token_idx: uint32
}
value:
hash of {
    amount: uint128
    to: lockscript_hash[0..20]
    out_point: OutPoint[12..36]    // Outpoint of previous input cell with SMT
}
5 Likes