BIP: ??? Layer: Applications Title: Taproot Asset Flat File Proof Format Author: Olaoluwa Osuntokun <laolu32@gmail.com> Comments-Summary: No comments yet. Comments-URI: https://git Status: Draft Type: Standards Track Created: 2021-12-10 License: BSD-2-Clause
This document defines a flat file proof format as a standardized way to package Taproot Asset proofs. The proof format itself is an append-only log of the prior lineage of a given asset. Proofs are anchored at the initial "genesis output" for a given asset. A proof of a single Taproot Asset state transition includes a Bitcoin merkle proof, a Taproot Asset merkle-sum sparse merkle tree (MS-SMT) inclusion proof, and finally a set of valid witnesses for the state transition.
This document is licensed under the 2-clause BSD license.
The Taproot Asset protocol is an overlay protocol that enables the representation and transfer of assets on the base Bitcoin blockchain. As Bitcoin is a UTXO-based system each asset is itself rooted at an initial "genesis" transaction, which marks the creation of said asset. An asset is therefore defined by its genesis output, as this marks its lineage. To ensure implementations are able to verify provenance proofs across the ecosystem, a standardized proof format is proposed. The proof format is a linear log of state transitions, allowing new transfers/transition to simply be append to the end of the fail.
Proving provenance of an asset requires the following arguments at each point in the past history of the asset:
- The very first snapshot of an asset is rooted at the genesis outpoint as dictated by the canonical Universe.
- A valid merkle proof that proves the inclusion of the genesis outpoint and resulting created asset.
- At each step/transition beyond the genesis outpoint:
- A valid merkle proof of a transaction which spends the outpoint referenced in the prior step.
- A valid MS-SMT opening proving the commitment of the new location of the asset.
- A valid asset witness state transition from the prior outpoint to the new location.
- A valid canonical Taproot Asset commitment exists for the given asset.
- If the transaction anchoring the state transition has other Taproot (P2TR) outputs, then a valid tapscript exclusion proof to prove that the commitment isn't duplicated elsewhere.
The Taproot Asset proof file is a flat file that records each relevant state transition for a given asset to be verified. The file is verified incrementally, with verification halting if an invalid state transition or proof is encountered.
A file is a series of inclusion and state transition proofs rooted at a given genesis outpoint.
A single inclusion and state transition proof has the following format is a TLV blob with the following format:
- type: 0 (
version
)- value:
- [
uint32
:version
]
- [
- value:
- type: 1 (
prev_out
)- value:
- [
36*byte
:txid || output_index
]
- [
- value:
- type: 2 (
block_header
)- value:
- [
80*byte
:bitcoin_header
]
- [
- value:
- type: 3 (
anchor_tx
)- value:
- [
...*byte
:serialized_bitcoin_tx
]
- [
- value:
- type: 4 (
anchor_tx_merkle_proof
)- value:
- [
...*byte
:merkle_inclusion_proof
]
- [
- value:
- type: 5 (
taproot_asset_asset_leaf
)- value:
- [
tlv_blob
:serialized_tlv_leaf
]
- [
- value:
- type: 6 (
taproot_asset_inclusion_proofs
)- value:
- [
...*byte
:taproot_asset_taproot_proof
]- type: 0 (
output_index
- value: [
int32
:index
]
- value: [
- type: 1 (
internal_key
- value: [
33*byte
:y_parity_byte || schnorr_x_only_key
]
- value: [
- type: 2 (
taproot_asset_proof
)- value: [
...*byte
:asset_proof
]- type: 0 (
taproot_asset_proof
)- value: [
...*byte
:asset_inclusion_proof
] - type: 0
- value: [
uint32
:proof_version
]
- value: [
- type: 1
- value: [
32*byte
:asset_id
]
- value: [
- type: 2
- value: [
...*byte
:ms_smt_inclusion_proof
]
- value: [
- value: [
- type: 1 (
taproot_asset_inclusion_proof
)- value: [
...*byte
:taproot_asset_inclusion_proof
] - type: 0
- value: [
uint32
:proof_version
]
- value: [
- type: 1
- value: [
...*byte
:ms_smt_inclusion_proof
]
- value: [
- type: 2 (
taproot_sibling_preimage
)- value: [
byte
:sibling_type
][varint
:num_bytes
][...*byte
:tapscript_preimage
]
- value: [
- value: [
- type: 0 (
- value: [
- type: 3 (
taproot_asset_commitment_exclusion_proof
- value: [
...*byte
:taproot_exclusion_proof
]- type: 0 (
tap_image_1
)- value: [
...*byte
:tapscript_preimage
]
- value: [
- type: 1 (
tap_image_2
)- value: [
...*byte
:tapscript_preimage
]
- value: [
- type: 2 (
bip_86
)- value: [
byte 0x00/0x01
:bip_86
]
- value: [
- type: 0 (
- value: [
- type: 0 (
- [
- value:
- type: 7 (
taproot_exclusion_proofs
)- value:
- [
uint16
:num_proofs
][...*byte
:taproot_asset_taproot_proof
]
- [
- value:
- type: 8 (
split_root_proof
)- value:
- [
...*byte
:taproot_asset_taproot_proof
]
- [
- value:
- type: 9 (
meta_reveal
)- value:
- [
...*byte
:asset_meta_reveal
]- type: 0 (
meta_type
- value: [
uint8
:type
]
- value: [
- type: 1 (
meta_data
- value: [
*byte
:meta_data_bytes
]
- value: [
- type: 0 (
- [
- value:
- type: 10 (
taproot_asset_input_splits
)- value:
- [
...*byte
:nested_proof_map
]
- [
- value:
- type: 11 (
challenge_witness
)- value:
- [
...*byte
:challenge_witness
]
- [
- value:
- type: 12 (
block_height
)- value:
- [
uint32
:block_height
]
- [
- value:
- type: 12 (
asset_genesis
)- value:
- [
32*byte
:first_prev_out_hash
] - [
u32
:first_prev_out_index
] - [
BigSize
:tag_len
] - [
tag_len*byte
:tag
] - [
BigSize
:metadata_len
] - [
metadata_len*byte
:metadata
] - [
u32
:output_index
] - [
u8
:type
]
- [
- value:
- type: 13 (
asset_group_key_reveal
)- value:
- [
64*byte
:pub_key || asset_group_script_root
]
- [
- value:
version
: is the version of the single mint or transition proof, currently fixed to value0
.prev_out
: is the 36-byte outpoint of the Taproot Asset committed output being spent. If this is the very first proof, then this value will be the "genesis outpoint" for the given asset.block_header
: is the 80-byte block header that includes a spend of the above outpoint.merkle_inclusion_proof
: is the merkle inclusion proof of the transaction spending theprevious_outpoint
. This is serialized with aBigSize
length prefix as:proof_node_count || serialized_proof || proof_direction_bits
- where:
proof_node_count
is aBigSize
integer specifying the number of nodes in the proof.serialized_proof
isproof_node_count*32
bytes for the proof path.proof_direction_bits
is a bitfield of sizelength_of_proof
with a value of0
indicating a left direction, and1
indicating a right direction.
anchor_tx
: is the transaction spending theprevious_outpoint
. This transaction commits to at least a single Taproot Asset tree within one of its outputs.taproot_asset_taproot_proof
: is a nested TLV that can be used to prove either inclusion or a Taproot Asset, or the lack of a Taproot Asset commitment via thetaproot_asset_commitment_exclusion_proof
.taproot_exclusion_proofs
: is a series of _exclusion_ proofs that prove that the other outputs in a transaction don't commit to a valid Taproot Asset. This re-uses thetaproot_asset_taproot_proof
structure, but will only contain antaproot_asset_commitment_exclusion_proof
value and not also ataproot_asset_taproot_proof
value.split_root_proof
: is an optionaltaproot_asset_taproot_proof
that proves the inclusion of the split commitment's root asset in case of an asset split.taproot_asset_input_splits
: is an optional list of nested full proofs for any additional inputs found within the resulting asset.asset_meta_reveal
: is an mandatory field (for genesis assets) that reveals the pre-image of theasset_meta_hash
contained in the asset TLV.- The
meta_type
field can be used to indicate how to parse/render the meta data pre-image.- The meta type currently defined are:
0
: no true type, just designates an opaque data blob.
- The meta type currently defined are:
- The
meta_data
is the raw meta data itself.- If the contained asset is a genesis asset (has a valid genesis witness), then a verifier SHOULD verify that: `sha256(tlv_encode(meta_reveal)) == asset_meta_hash`.
- This field MUST only be present for genesis asset proofs.
- The
challenge_witness
is an optional asset witness over a well-defined asset state transition that proves ownership of thescript_key
the asset currently resides at.block_height
: is the block height of the block that includes a spend of theprev_out
outpoint.asset_genesis
: is a mandatory field for genesis assets. Theasset_genesis
represents the preimage of theasset_id
.- This field MUST only be present for genesis asset proofs.
asset_group_key_reveal
: is a mandatory field (for genesis assets having a group key) that reveals the raw internal signing key and script root needed to derive the asset group key:asset_raw_group_key = asset_group_key_reveal[0:32]
,asset_group_script_root = asset_group_key_reveal[32:64]
.asset_raw_group_key
: is a 32-byte public key as defined by BIP-340. The raw public key is revealed at genesis in order for verifiers to to check the group key derivation. The owner of the corresponding private key is able to sign for the group key using the key spend path when minting an asset.asset_group_script_root
: is a 32-byte tapscript root as defined by BIP-341. The root is revealed at genesis in order for verifiers to to check the group key derivation. The root can hold arbitrary scripts that enforces conditions for minting the asset.- If the contained asset is a genesis asset (has a valid genesis witness), then a verifier SHOULD verify that:
asset_group_key == GroupKey(asset_id, asset_raw_group_key, asset_group_script_root)
according to [bip-tap]. - This field MUST only be present for genesis asset proofs having a group key.
- [
u32
:file_version
] version of proof file format, currently fixed to0
. - [
BigSize
:num_proofs
] number of proofs contained in the file - [
num_proof*proof
:proofs
] encoded proofs- [
BigSize
:proof_len
] length of encoded proof - [
proof_len*byte
:proof_tlv_bytes
] a single proof encoded as a TLV stream as defined above - [
32*byte
:proof_checksum
] the checksum of the proof, which isSHA256(prev_hash || proof_tlv_bytes)
whereprev_hash
is the checksum of the previous proof or a zero hash for the first proof.
- [
Verification of a proof file starts at the first entry (the genesis output creation) and walks forward, validating each state transition and inclusion proof in series. If any state transition is found to be invalid, then the asset proof is invalid. Otherwise, if the file is consumed in full without any violations, the proof is said to be valid.
Given a proof file for a given asset f_proof
, genesis outpoint
g
verification is defined as follows:
- Verify the integrity of the proof file:
- For each proof, extract the
proof_len
,proof_len
number of bytes asproof_tlv_bytes
and 32 bytesproof_checksum
. - Compute
SHA256(prev_hash || proof_tlv_bytes)
whereprev_hash
is theproof_checksum
of the previous proof or a 32-byte zero hash for the first proof in a file. - If this computed value doesn't match
proof_checksum
, verification fails.
- For each proof, extract the
- Verify each inclusion proof and state transition:
- Parse the next proof block from the flat file.
- If this is the first proof to be verified:
- Store the
previous_outpoint
as the genesis outpoint.
- Store the
- Otherwise, verify that the
anchor_transaction
has an inputs that spends the priorprevious_outpoint
- Given the
anchor_transaction
verify that the includedmerkle_inclusion_proof
rooted at the merkle root of theblock_header
is valid. - Parse the
tlv_proof_map
. - If the
anchor_transaction
does not have at leastasset_output_pos
outputs, verification fails. - Verify that the
asset_leaf_proof
embeds thetaproot_asset_leaf
at the outpout rooted at theasset_output_pos
using the specifiedinternal_key
to compute the taproot commitment. - Verify that the asset witness included at the
prev_asset_witness
field of thetaproot_asset_leaf
is valid based on the specificasset_script_version
- If a
split_commitment_opening
is present, verify that the included leaf is a valid opening rooted at thetaproot_asset_leaf
'ssplit_commitment_root
field. - If a
split_commitment_opening
is present, verify that an inclusion proof for thesplit_commitment_root
's leaf is present insplit_root_proof
. - If the asset is a genesis asset, and the
asset_meta
field is present, then verify thatsha256(asset_meta) == asset.asset_meta_hash
verify_asset_file_proof(file_proof []byte, genesis_outpoint OutPoint,
assetID [32]byte) -> bool
genesis_outpoint, prev_outpoint = None
file_reader = new_bytes_reader(file_proof)
prev_hash = bytes(32)
while file_reader.len() != 0:
proof_block = parse_proof_block(file_reader)
sha_sum = sh256(prev_hash + proof_block.bytes())
if proof_block.proof_checksum != sha_sum:
return false
if genesis_outpoint is None:
genesis_outpoint = proof_block.previous_outpoint
txn = proof_block.txn
if genesis_outpoint is not None:
if !spends_prev_out(txn):
return false
if !verify_merkle_proof(
proof_block.block_header, proof_block.merkle_inclusion_proof, txn,
):
return false
proof_tlv_map = proof_block.tlv_map
if len(txn.outputs) < proof_tlv_map.asset_output_pos:
return false
if !verify_asset_tree_proof(
txn, proof_tlv_map.taproot_asset_leaf, proof_tlv_map.asset_leaf_proof,
):
return false
if !verify_taproot_asset_state_transition(proof_tlv_map.taproot_asset_leaf):
return false
if proof_tlv_map.challenge_witness is not None:
new_leaf = clone_unique_leaf(proof_tlv_map.taproot_asset_leaf)
new_leaf.script_key = NUMS_key
new_leaf.prev_witnesses = {{
prev_id: {
asset_id: proof_tlv_map.taproot_asset_leaf.asset_id
outpoint: 00000000...0000000:0
script_key: proof_tlv_map.taproot_asset_leaf.script_key
}
tx_witness: proof_tlv_map.challenge_witness
}}
if !verify_taproot_asset_state_transition(new_leaf):
return false
if proof_tlv_map.split_commitment_opening is not None:
if !verify_split_commitment(
proof_tlv_map.taproot_asset_leaf,
proof_tlv_map.split_commitment_opening,
):
return false
if !verify_asset_tree_proof(
txn,
proof_tlv_map.split_commitment_opening.split_commitment_root,
proof_tlv_map.split_root_proof,
):
return false
has_meta_reveal = proof_tlv_map.meta_reveal is not None
has_meta_hash = proof_tlv_map.asset.meta_hash is not None
is_genesis_asset = is_genesis_asset(proof_tlv_map.asset)
match:
case has_meta_reveal && !is_genesis_asset:
return false
case has_meta_reveal && is_genesis_asset:
meta_hash := sha256(meta_reveal)
if meta_hash != proof_tlv_map.asset.meta_hash:
return false
case has_meta_hash && is_genesis_asset && !has_meta_reveal:
return false
case !has_meta_reveal && is_genesis_asset:
return false
return true
An optional ownership proof can be added to a proof through the
challenge_witness
field. That witness must be a valid asset
tx_witness
over a well-defined asset state transition that spends
the full amount of the asset to the NUMS key.
The state transition can be created with the following steps:
- Create a deep copy of the asset to prove ownership of.
- Truncate the
prev_witnesses
list to just a single element. - Set the
prev_witnesses[0].prev_id.out_point
to the empty outpoint (all zero hash and zero index). - Set the
prev_witnesses[0].prev_id.script_key
to the asset's script key. - Set the asset's
script_key
to the NUMS key. - Create a signature for the asset state transition, using the interactive flow (no split tomb stone).
- Extract just the
prev_witnesses[0].tx_witness
from the signed state transition and append that to the proof as thechallenge_witness
.
Test vectors for the File Serialization can be found here:
- Proof TLV encoding test vectors
- Proof TLV encoding error test vectors
- Fully valid regtest proof TLV encoding test vectors
github.com/lightninglabs/taproot-assets/tree/main/proof