-
Notifications
You must be signed in to change notification settings - Fork 23
Commit
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
<pre> | ||
BIP: ??? | ||
Layer: Applications | ||
Title: Taro On Chain Addresses | ||
Author: Olaoluwa Osuntokun <[email protected]> | ||
Comments-Summary: No comments yet. | ||
Comments-URI: https://git | ||
Status: Draft | ||
Type: Standards Track | ||
Created: 2021-12-10 | ||
License: BSD-2-Clause | ||
</pre> | ||
|
||
==Abstract== | ||
|
||
This document describes a way to map a single-asset Taro send to a familiar | ||
<code>bech32</code> address, as well as a way to map that address into a valid | ||
Taro script tree that can be included in a broadcast transaction to complete a | ||
transfer. Once the transaction has been broadcast, the receiver can use the | ||
previous outpoint of the confirmed transaction to lookup the complete asset | ||
proof in their chosen Universe. | ||
|
||
==Copyright== | ||
|
||
This document is licensed under the 2-clause BSD license. | ||
|
||
==Motivation== | ||
|
||
The Taro protocol needs an easy way to allow users to send each other assets | ||
on-chain, without requiring several rounds of interaction to exchange and | ||
validate proofs. By using the existing <code>bech32</code> address | ||
serialization standard, such addresses look distinct, while also looking | ||
familiar enough based on the character set encoding. The described address | ||
format also addresses a number of possible foot guns, by making it impossible | ||
to send the wrong asset (based on an address) amongst other protections. | ||
|
||
==Specification== | ||
|
||
A Taro asset is uniquely defined by its <code>asset_id</code> as well as the | ||
<code>asset_script_hash</code> that serves as a predicate that must be | ||
satisfied for transfers. These values, along with an internal Taproot key used | ||
when creating the Bitcoin output that holds the Taro asset, are encoded into a | ||
single address. | ||
|
||
===Encoding an Address=== | ||
|
||
Let the human readable prefix (as specified by BIP 173) be: | ||
|
||
* <code>taro</code> for mainnet | ||
* <code>tarot</code> for testnet | ||
Given the 32-byte <code>asset_id</code>, 32-byte | ||
<code>asset_script_hash</code>, and 32-byte x-only BIP 340/341 internal public | ||
key, 8-byte amount to send, an address is encoded as: | ||
* <code>bech32(hrp=taroHrp, asset_id || asset_script_hash || internal_key || amt)</code> | ||
This comment has been minimized.
Sorry, something went wrong.
This comment has been minimized.
Sorry, something went wrong.
Roasbeef
Author
Owner
|
||
===Decoding and Sending To An Address=== | ||
|
||
Given a valid Taro address, decompose the contents into the referenced | ||
<code>asset_id</code>, <code>asset_script_hash</code>, and | ||
<code>internal_key</code>. | ||
|
||
Construct a new blank Taro leaf with the following TLV values: | ||
* <code>taro_version</code>: <code>0</code> | ||
* <code>asset_id</code>: <code>asset_id</code> | ||
* <code>asset_type</code>: <code>0</code> | ||
* <code>amt</code>: <code>amt_to_send</code> | ||
* <code>taro_version</code>: <code>0</code> | ||
* <code>asset_script_version</code>: <code>0</code> | ||
* <code>asset_script_hash</code>: <code>asset_script_hash</code> | ||
Create a valid tapscript root, using leaf version <code>0x0c</code> with the | ||
sole leaf being the serialized TLV blob specified above. | ||
|
||
Create the top-level taproot public key script, as a segwit v1 witness | ||
program, as specified in BIP 341, using the included key as the internal key. | ||
|
||
With the target taproot public key script constructed, the asset is sent to the | ||
receiver with the execution of the following steps: | ||
# Construct a valid transaction that spends an input that holds the referenced <code>asset_id</code> and ''at least'' <code>amt</code> units of the asset. | ||
# Create a new Taro output commitment based on the input commitment (this will be the change output), that now only commits to <code>S-A</code> units of <code>asset_id</code>, where <code>S</code> is the input amount, and <code>A</code> is the amount specified in the encoded Taro address. | ||
## This new leaf MUST have a <code>split_commitment</code> specified that commits to the position (keyed by <code>sha256(output_index || asset_id || asset_script_hash)</code> within the transaction of the newly created asset leaf for the receiver. | ||
## Add an additional output that sends a de minimis (in practice this MUST be above dust) amount to the top-level taproot public key computed earlier. | ||
## Broadcast and sign the transaction, submitting the resulting Taro state transition proof to a Universe of choice, also known by the receiver. | ||
# Post the resulting state transition proof to the specified Universe. The submitted proof ''must'' contain the optional auxiliary value of the full <code>split_commitment</code> the receiver requires to spend the asset. | ||
===Spending The Received Asset=== | ||
|
||
In order to spend (or simply confirm receipt) of the received asset, the | ||
receiver should: | ||
# Re-derive the taproot public key script created above that sends to their specified Taro asset leaf. | ||
# Wait for a transaction creating the output to be confirmed in the blockchain. | ||
## In practice this may be via light client protocols such as BIP 157/158, or simply a full node with an address index, or import public key. | ||
# For each previous outpoint referenced in the transaction: | ||
## Look up the previous outpoint as a key into the chosen canonical Universe/Multiverse. | ||
### If the key is found, verify the inclusion proof of the value (as described in [[../master/bip-cmyk-proof-file.mediawiki|bip-cmyk-proof-file]]), and extract the <code>split_commitment</code> inclusion proof for the output. | ||
# Walk the Universe tree backwards in time to incrementally construct the full provenance proof needed to spend the asset. | ||
==Test Vectors== | ||
|
||
==Reference Implementation== |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,273 @@ | ||
<pre> | ||
BIP: ??? | ||
Layer: Applications | ||
Title: Merkle Sum Sparse Merkle Trees | ||
Author: Olaoluwa Osuntokun <[email protected]> | ||
Comments-Summary: No comments yet. | ||
Comments-URI: https://git | ||
Status: Draft | ||
Type: Standards Track | ||
Created: 2021-12-10 | ||
License: BSD-2-Clause | ||
</pre> | ||
|
||
==Abstract== | ||
|
||
This document describes a merkle-sum sparse merkle tree (MS-SMT) data | ||
structure. This is an augmented version of a sparse merkle tree that includes | ||
a sum value which is combined during the internal branch hashing operation. | ||
Such trees permit efficient proofs of non-inclusion, while also supporting | ||
efficient fault proofs of invalid merkle sum commitments. | ||
|
||
==Copyright== | ||
|
||
This document is licensed under the 2-clause BSD license. | ||
|
||
==Motivation== | ||
|
||
Taro is a Taproot-native asset overlay protocol. Rather than post all asset | ||
related data on-chain in <code>OP_RETURN</code> outputs, the protocol instead | ||
uses a series of commitments anchored within the Taproot script tree. When | ||
handling unique assets, it's important to be able to prove that the former | ||
owner (or seller) of the asset is no longer committing to it within their tree. | ||
Additionally, when carrying out multi-asset swaps, verifiers need to be able to | ||
efficiently verify that no new assets are being created (inflation check). The | ||
MS-SMT supports both non-inclusion proofs, and non-inflation proofs. | ||
|
||
==Design== | ||
|
||
A merkle sum tree is a merkalized key-value map simulated over a particia | ||
merkle tree of depth 256 (as we use sha256 as our hash function). The "base" | ||
state of the tree, is a merkle tree with 256 leaves, storing an "empty hash". | ||
This comment has been minimized.
Sorry, something went wrong.
This comment has been minimized.
Sorry, something went wrong. |
||
Within this tree, the digests of an empty leaf, and empty internal nodes for | ||
each level can be computed ahead of time. The "value" of an empty leaf is zero. | ||
|
||
In addition to storing the hash digest of a leaf/branch, an 8-byte value is | ||
also stored along side the entry, making each entry 40 bytes in length. The | ||
root hash therefore commits to the digest of all items in the leaf, as well as | ||
the sum of all the "sum values" in the set of leaves. When combining two | ||
branches/leaves, the sum of the left and right leaf/branch is serialized along | ||
with the hash digest of the nodes. | ||
|
||
When inserting a new key into the tree, at each level, the ith bit of the key | ||
is used to traverse left or right down the tree. Due to this traversal, every | ||
possible key has a unique location (position wise) within the set of leaves. A | ||
non-inclusion proof is the proof that the value at the unique position for a | ||
key is empty. | ||
|
||
Due to the nature of the mapping, sparse merkle trees are ''history | ||
independent'' meaning no matter the inserting order, given the same set of keys | ||
and values, the same root hash will always be produced. As the size of the | ||
tree is intractable, a series of techniques are used to maintain a relevant set | ||
of branches and leaves in memory, using a persistent key-value store to store | ||
the relevant unique items of the tree. Proofs can be compressed by using a | ||
bitmap to indicate if the next node in the proof is an empty hash for that | ||
level, or the parent of the item being proved. | ||
|
||
===Specification=== | ||
|
||
We use <code>sha256</code> as our hash function, and 8-byte sum values. | ||
|
||
====Building the Empty Hash Map==== | ||
|
||
The map of all empty hashes by level <code>empty_hashes</code> can be | ||
pre-computed ahead of time, as: | ||
* The hash of an empty leaf is <code>empty_hash_1 = sha256(nil, nil)</code> | ||
* The hash of an empty branch at the second level is <code>empty_hash_2 = sha256(empty_hash_1, empty_hash_1)</code> | ||
* and so on... | ||
We refer to the map resulting from this route as the | ||
<code>empty_hash_map</code>: | ||
<source lang="python"> | ||
build_empty_hash_map() -> map[int][32]byte: | ||
|
||
empty_hash_map = make(map[int][32]byte) | ||
prior_level_hash = None | ||
for i in range(256): | ||
if prior_level_hash is None: | ||
prior_level_hash = sha256(nil, nil, 0) | ||
empty_hash_map[i] = prior_level_hash | ||
continue | ||
empty_hash_map[i] = sha256(prior_level_hash, prior_level_hash, 0) | ||
return empty_hash_map | ||
</source> | ||
|
||
====Looking Up Items==== | ||
|
||
Looking up an item in the tree requires traversal down the tree based on the | ||
next bit position of the key itself. We assume the existence of a persistent | ||
key-value store that maps the hash of a node to the left and right digests of | ||
its children. | ||
|
||
The following routine specifies the insertion algorithm: | ||
This comment has been minimized.
Sorry, something went wrong. |
||
<source lang="python"> | ||
lookup_item(key [32]byte, db KVStore) -> MerkleSumLeaf: | ||
|
||
root_hash, _ = db.fetch_root_hash() | ||
current_branch = root_hash | ||
value_hash, value_sum = None | ||
for i in range(256): | ||
if bit_index(i, key) == 0: | ||
current_branch, _ = db.get_children(current_branch) | ||
else: | ||
_, current_branch = db.get_children(current_branch) | ||
return MerkleSumLeaf(current_branch.hash, current_branch) | ||
</source> | ||
|
||
====Inserting Items==== | ||
|
||
Inserting items into the tree entails traversing the tree until we arrive at | ||
the position for the leaf, then bubbling up (hashing and summing) the change | ||
all the way up the tree. | ||
|
||
<source lang="python"> | ||
insert_item(key [32]byte, value []byte, sum_value int64, db KVStore) -> None: | ||
root_hash, _ = db.fetch_root_hash() | ||
current_branch = root_hash | ||
|
||
insertion_path = [] | ||
value_hash, value_sum = None | ||
for i in range(256): | ||
if bit_index(i, key) == 0: | ||
current_branch, sibling = db.get_children(current_branch) | ||
insertion_path.append(sibling) | ||
else: | ||
sibling, current_branch, = db.get_children(current_branch) | ||
insertion_path.append(sibling) | ||
|
||
db.insert(current_branch.parent_hash, MerkleSumLeaf(key, value, sum_value)) | ||
|
||
for i in range(256): | ||
updated_sum = sum_value + inclusion_path[-1].value | ||
|
||
sibling_node = insertion_path[-1] | ||
if bit_index(i, key) == 0: | ||
updated_value = sha256(value, sibling_node.sum_value, updated_sum) | ||
|
||
db.insert(key=updated_value, value=(sibling_node, value)) | ||
else: | ||
updated_value = sha256(insertion_path[-1].hash, value, updated_sum) | ||
|
||
db.insert(key=updated_value, value=(value, sibling_node)) | ||
|
||
value = updated_value | ||
sum_value = updated_sum | ||
|
||
insertion_path.pop() | ||
|
||
return None | ||
</source> | ||
|
||
|
||
====Deleting Items==== | ||
|
||
Deleting an item is identical to insertion, but we delete the item in the tree | ||
by setting its value to the empty hash. | ||
<source lang="python"> | ||
delete_item(key [32]byte, db KVStore) -> None: | ||
return insert_item(key, nil, 0, db) | ||
</source> | ||
|
||
====Creating Inclusion & Non-Inclusion Proofs==== | ||
|
||
An inclusion proof of an item proves that the item is found in the tree, and | ||
has a certain sum value. A non-inclusion tree proves the opposite: that an item | ||
is not found within the tree. | ||
|
||
Generating an inclusion or non inclusion proof entails walking down the tree | ||
and obtaining all the sibling hashes and their sum values: | ||
<source lang="python"> | ||
gen_merkle_proof(key [32]byte, db KVStore) -> []MerkleSumNode | ||
root_hash, _ = db.fetch_root_hash() | ||
current_branch = root_hash | ||
|
||
proof_nodes = [] | ||
value_hash, value_sum = None | ||
for i in range(256): | ||
if bit_index(i, key) == 0: | ||
current_branch, sibling = db.get_children(current_branch) | ||
proof_nodes.append(sibling) | ||
else: | ||
sibling, current_branch, = db.get_children(current_branch) | ||
proof_nodes.append(sibling) | ||
return proof_nodes | ||
</source> | ||
|
||
A plain proof is always a series of 256 merkle sum elements. However we can | ||
compress proofs by using an extra bitmap that indicates if the proof contents | ||
are an empty hash or not. | ||
<source lang="python"> | ||
compress_merkle_proof(proof []MerkleSumNode) -> CompressedProof: | ||
compressed_proof = new_compressed_proof( | ||
compression_bits=new_bit_vector(256), | ||
proof=[]MerkleSumNode{}, | ||
) | ||
|
||
for i, proof_node in proof: | ||
if proof_node == empty_hash_map[i]: | ||
compressed_proof.compression_bits.append(1) | ||
else: | ||
compressed_proof.proof.append(proof_node) | ||
return compressed_proof | ||
</source> | ||
|
||
====Verifying Inclusion & Non-Inclusion Proofs==== | ||
|
||
In order to verify a proof, we need to confirm that if starting at the proof, | ||
if we hash and sum up the tree, then we'll end up at the same root hash and sum | ||
value. | ||
|
||
Before proofs are verified, the proof should first be decompressed: | ||
<source lang="python"> | ||
decompress_merkle_proof(compressed_proof CompressedProof) -> []MerkleSumNode: | ||
proof = []MerkleSumNode{} | ||
|
||
for i in range(256): | ||
if compressed_proof.bit_at(index=i) == 1: | ||
proof.append(empty_hash_map[i]) | ||
else: | ||
proof.append(compressed_proof.proof) | ||
compressed_proof = drop_1(compressed_proof.proof) | ||
return proof | ||
</source> | ||
|
||
With the proof decompressed, we verify the proof by hashing and summing up each | ||
level. | ||
<source lang="python"> | ||
verify_merkle_proof(proof []MerkleSumNode, root MerkleSumNode, | ||
key [32]byte, value []byte, value_sum int64) -> bool: | ||
|
||
for i in range(256): | ||
if bit_index(i, key) == 0: | ||
value = sha256(proof[-1-i], value, proof[1-i].sum, value_sum) | ||
else: | ||
value = sha256(value, proof[-1-i], value.sum, value_sum) | ||
return root.hash == value && root.sum == value_sum | ||
</source> | ||
|
||
|
||
====Caching Optimizations==== | ||
|
||
TODO(roasbeef): | ||
|
||
|
||
==Test Vectors== | ||
|
||
TBD | ||
|
||
==Backwards Compatibility== | ||
|
||
==Reference Implementation== | ||
|
||
github.com/lightninglabs/taro |
Shouldn't you include the taro version in here? Or is it unnecessary because the asset actually commits to a given taro version?