forked from bitcoin/bips
-
Notifications
You must be signed in to change notification settings - Fork 23
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
1,762 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
===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-taro-proof-file.mediawiki|bip-taro-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== |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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". | ||
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: | ||
<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 |
Oops, something went wrong.