Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update bip-taro.mediawiki #14

Open
wants to merge 2 commits into
base: bip-taro
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 101 additions & 0 deletions bip-taro-addr.mediawiki
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-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==
273 changes: 273 additions & 0 deletions bip-taro-ms-smt.mediawiki
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
Loading