Skip to content

Instantly share code, notes, and snippets.

@bitjson
Last active February 2, 2024 06:30
Show Gist options
  • Save bitjson/a440232cebba8f0b2b6b9aa5db1fdb37 to your computer and use it in GitHub Desktop.
Save bitjson/a440232cebba8f0b2b6b9aa5db1fdb37 to your computer and use it in GitHub Desktop.
PMv3-Based CashTokens (Withdrawn)

Update 2022-2-22:

This was an early demo of the ideas behind the CashTokens CHIP.

For an introduction to the latest iteration of CashTokens, see:


CashToken (v0) Corporations

Read the blog post: CashTokens: Contract-Validated Tokens for Bitcoin Cash.

Review the authentication template in Bitauth IDE:

https://ide.bitauth.com/import-gist/a440232cebba8f0b2b6b9aa5db1fdb37

CashTokens template screenshot

If you have questions, feedback, or you're interested in contributing to future CashTokens or TxInt Opcodes specifications, please join CashToken Devs on Telegram or message me on Twitter.

Additional Notes

Below are some notes which may make it into a future CashTokens and/or TxInt specification.

Current draft implementation of the TxInt upgrade ➔

The current target date for a TxInt opcode upgrade is May 2022. Some TxInt opcodes could be enabled earlier, but many should only be enabled with larger integer support (see below).

520-Byte Stack Item Size Limit

While CashToken can work within the 520-byte stack item size limit, because CashToken inductive proofs require inspecting the parent and grandparent transactions' contents, stack items can easily reach 520 bytes. Flexibility in signing schemes also increases the amount of bytecode required to validate each allowed configuration, contributing to overhead which must fit within the 520 bytes for P2SH outputs. For these reasons, the token covenant in this implementation is limited to multisig security schemes with a maximum of 3 signers (m-of-n where m <= 3, n <= 3). Increasing the stack item size limit to 1000 bytes would provide space for token covenants which use additional locktime-based security features or the largest allowed multisig schemes (up to 20 keys). A stack item size limit of 2,000 to 5,000 bytes would likely provide space for validation of transactions with multiple complex covenant inputs.

Clarification: OP_OUTPOINTTXHASH

The OP_OUTPOINTTXHASH operation pushes the outpoint transaction hash in big-endian byte order. While the serialized transaction format itself specifies this value in each input in little-endian order (reversing the double-sha256 hash of the parent transaction serializations), most VM bytecode use-cases will require the value in big-endian byte order (non-reversed). As an added benefit, big-endian byte order is also used by most external systems (explorers, indexers, etc.).

Clarification: OP_UTXOBYTECODE

The OP_UTXOBYTECODE operation pushes the bytecode currently being evaluated to the stack. For standard scripts, this is the locking bytecode of the Unspent Transaction Output (UTXO) spent by the input. For P2SH scripts, the UTXO's redeem script is pushed (rather than OP_HASH160 <redeem_script_hash> OP_EQUAL). This behavior matches the existing behavior of the current VM during P2SH evaluation – once the P2SH pattern is matched, the remaining stack is copied, and the VM begins evaluation again with the new array of instructions. OP_UTXOBYTECODE provides access to the serialization of this instruction list (in derivatives of the Satoshi implementation, the script parameter provided to EvalScript).

Risk: Transaction Competition

With CashTokens, it's possible to create covenants for which multiple entities compete to create the next covenant transaction. These entities may be willing to pay higher-than-normal fees to have their version of the covenants next state mined. A non-negligible volume of these transactions may incentivize miners to develop backchannel Replace-By-Fee (RBF) infrastructure and weakening zero-confirmation reliability.

For example, when a second user attempts to outbid a previously broadcast covenant transaction, nodes which heard the first transaction may refuse to broadcast the second. If those nodes broadcast some form of Double-Spend Proof (DSP) for the new transaction, miners may be incentivized to request the "warned-against" double spend (over other channels) to maximize fees.

To avoid incentivizing the creation of double-spend backchannels, DSP strategies should account for the possibility of significant markets for fee-replacement. (Even without CashTokens, this is already true for other types of anyone-can-spend locking scripts.) Well-designed CashToken covenants should also avoid creating such scenarios, since "bidding wars" may also degrade the user experience of covenants.

Area for Research: Heterogeneous Tokens

It's possible to differentiate token types within a single CashToken corporation contract by using the tokens' satoshi values. For example, a corporation covenant can be designed to issue tokens with a base value above the current dust limit (546 satoshis) where each additional satoshi represents another unit of the token. E.g. for a base of 1000 satoshis, a 1001 satoshi CashToken is worth 1 UNIT (1001-1000), but a 2000 satoshi CashToken is worth 1000 UNITs (2000-1000).

Because the CashToken covenant places strict limitations on transfers, higher-level covenants can rely on CashTokens to only change their satoshi values in allowed ways from transfer-to-transfer. This same strategy can be used to differentiate other types of tokens like classes of stock (preferred, common, class B, etc.) or various privilege levels in a CashToken sidechain.

Complication: Big Integer Values

Because Script Numbers are currently limited to 4 bytes for mathematical operations, numbers pushed to the stack by OP_UTXOVALUE, and OP_OUTPUTVALUE are practically limited to positive int32 numbers. This limits the total operational value to 2,147,483,647 satoshis, or about 21 BCH. While this proposal doesn't yet recommend a solution for working with larger numbers in the VM, it would be valuable to adopt a big integer solution before or at the same time as the TxInt opcodes.

Complication: Fractional Satoshis

It should also be noted that future upgrades which enable fractional satoshi will complicate OP_UTXOVALUE and OP_OUTPUTVALUE. It may be valuable to plan upgrade strategies which allow these opcodes to be used with the higher specificity values. OP_OUTPUTVALUE could be modified to accept the index -1 as an indicator that a higher specificity operation should be used (future subdivisions could be specified with -2,-3, etc). To allow for future upgrades, it may be valuable to require that OP_UTXOVALUE consumes the value 0 from the stack to indicate the unit "satoshis".

(Update 2020-12-16: I'm also considering a design where each OP_UTXO* opcode consumes a number to select a particular input such that OP_INPUTINDEX OP_UTXOVALUE would do what OP_UTXOVALUE does in the initial draft implementation.)

Rationale: OP_NUM2VARINT

Contracts inspecting transactions are made simpler and safer with operations to convert between Script Numbers and Bitcoin VarInts. Without safe means of reading/writing VarInts, contracts must be carefully specified to either manually convert between formats or limit allowable input/output counts in proofs. Additionally, ease of use of VarInts in contracts might help to minimize wasted space caused by fixed-width contract values (e.g. nonces, indexes, payment amounts, rates, etc.).

Area for Research: OP_VARINT2NUM

Currently, only an implementation for OP_NUM2VARINT is proposed. This is sufficient for many types of covenants, but other covenants may be made possible or more efficient with an implementation of the reverse operation.

Area for Research: Unify Integers

Many covenant contracts utilizing transaction introspection will need to convert between Script Numbers (a.k.a. CScriptNum, usually inclusive range from -2^31 + 1 to 2^31 - 1), transaction output satoshi values (little-endian, unsigned 64-bit integers), and variable-length integers (positive integers from 0 to 2^64−1, used in transaction and signing serializations). Beyond these existing formats, some strategy is needed to perform mathematical operations on output satoshi values (and on fractional satoshi values, which may no longer fit inside a uint64).

It may be valuable to solve all these number encoding problems at one time (perhaps in a v3 transaction format). Some goals and requirements for a variable-length integer solution:

  • A single, variable-length, unsigned integer format for transaction serializations – to be used by the version, input/output count, input indexes, bytecode length, and satoshi value fields. (Version 1 and 2 transactions tend to waste bytes in fixed-width version and satoshi value integer fields, while also preventing these fields from interoperating with the VM's Script Number format in contracts. A unified integer type could solve both issues.)
  • Math support for variable-length integers – all math operations currently operate on Script Numbers, a 32-bit signed integer format used by the VM. It must be possible for math operations to either operate on the variable-length integer type or convert between Script Numbers and the variable-length integer type. This is particularly important for math operations involving satoshi values larger than the current limit of 2,147,483,647 satoshis (about 21 BCH).
  • A clear upgrade path to "fractional satoshis" – increasing the precision of value fields should be possible without breaking changes to VM operations.

Note, if a new variable-length integer format were phased in with v3 transactions, OP_NUM2VARINT could be modified to use this format when used in a v3 transaction. However, because this difference would need to be tracked along with every UTXO for proper enforcement, it would be advantageous to deploy this "upgraded OP_NUM2VARINT" such that it is only valid in v3 transactions. If OP_NUM2VARINT were to also be deployed for v1 and v2 transactions, it may be valuable to define a separate opcode for a v3-only OP_NUM2BIGINT.

Area for Research: Serialized Transaction Introspection

One or more serialized transaction introspection operations could simplify CashToken inductive proofs and other types of looping contracts. Rather than operating on the current transaction (as is the case for this proposal's set of operations), serialized transaction introspection opcodes would instead operate on the top stack item, interpreting the stack item as a serialized transaction and extracting requested data. This strategy would result in shorter contracts when validation of parent transactions is required, though it would likely require an expansion of the 520 byte stack item size limit. Notably, while the transaction serialization format is already consensus-critical for transaction identification in outpoints, "serialized TxInt" operations would further ossify the serialization format. It may be wise to only enable these types of transactions after widespread use is demonstrated, and only for the latest transaction version deployed at that time.

{
"$schema": "https://bitauth.com/schemas/authentication-template-v0.schema.json",
"description": "This template provides scripts for each stage of a simplified \"depository\" CashToken Corporation, where users may deposit 10,000 satoshis in exchange for a CashToken. This implementation supports exactly 4 homogeneous tokens. Tokens are tracked in a 2 level Merkle tree:\n rt\n / \\\n b0 b1\n / \\ / \\\n a0 a1 a2 a3\n\nThe covenant begins with no depositors (all leaves start as the hash of 0) and a dust balance. A user deposits 10,000 satoshis, receiving a CashToken (recorded at a0) secured by a single key. Then the CashToken is \"notarized\" to prove it is a descendant of the mint transaction. Once notarized, the owner transfers the CashToken to a 2-of-3 multisig for stronger security. From there, the owner transfers the token to a different owner (now single sig). Finally, the new owner redeems the CashToken with the corporation, withdrawing their 10,000 satoshi deposit, and replacing a0 in the tree with the hash of 0 (making the leaf available to other depositors).\n\nThere are 5 transaction types demonstrated:\n- tx0: Incorporation TX - the \"corporation\" is established with a dust balance\n- tx1: Token Mint TX - a CashToken is created\n- tx2: Notarization TX - the CashToken is proven to be valid\n- tx3: Token Transfer TX - the CashToken is transferred independently\n- tx4: Token Redeem TX - the CashToken is redeemed with the \"corporation\"\n\nTransaction Graph/Timeline:\n\ntx0 --- tx1 -------------------------------------- tx4\n \\--- tx2 --- tx3 --- tx3 --- ...tx3 ---/\n\nTransaction Contents:\n\ntx0: Incorporation TX - \n Input 0 - any funding output (dust amount + tx fee)\n ---\n Output 0 - Corporation Covenant (dust amount)\n Output 1 - change output\n\ntx1: Token Mint TX -\n Input 0 - tx0 output 0\n Input 1 - any funding output (10,000 satoshi deposit + tx fee)\n ---\n Output 0 - Corporation Covenant\n Output 1 - CashToken Covenant (Token ID is tx0 hash)\n Output 2 - change output\n\n[Token circulates separately from the corporation covenant]\n\n tx2: Notarization TX\n Input 0 - tx1 output 1\n Input 1 - any funding output (tx fee)\n ---\n Output 0 - CashToken covenant\n Output 1 - change output\n\n tx3: Token Transfer TX\n Input 0 - tx2 output 0\n Input 1 - any funding output (tx fee)\n ---\n Output 0 - CashToken covenant\n Output 1 - a change output\n\n[Token is redeemed with the corporation covenant]\n\ntx4: Token Redeem TX\n Input 0 - tx1 output 0\n Input 1 - tx3 output 0\n Input 2 - any funding output (for fees)\n ---\n Output 0 - Corporation covenant\n Output 1 - a change output (+10,000 satoshi withdrawal)\n\n---\n\nAdditional Notes\n\nIndestructible Dust – Once created, this CashToken corporation implementation cannot be destroyed. The covenant could be modified to allow the final dust balance to be collected from the covenant if all shares are unallocated, but the additional bytecode required over the life of the covenant is likely to be more costly than the dust amount \"saved\" when the covenant is abandoned. It's worth noting that any bitcoin user can create an unlimited number of dust outputs at any time, so dust from covenants is irrelevant from the perspective of the network.",
"name": "CashTokens (Early Preview)",
"entities": {
"public": {
"description": "",
"name": "Public",
"variables": {
"tx0_hash": {
"description": "The transaction hash of tx0 (the Incorporation TX)",
"name": "tx0 Hash",
"type": "AddressData"
},
"is_notarized": {
"description": "A boolean value set to `0` if the CashToken has not yet been notarized, and `1` if it was previously notarized.",
"name": "Is Notarized",
"type": "AddressData"
},
"parent_mint_tx": {
"description": "The full contents of the parent/mint transaction to be notarized.",
"name": "Parent Mint TX",
"type": "AddressData"
},
"grandparent_input_0_bytecode_length": {
"description": "",
"name": "Grandparent Input 0 Bytecode Length",
"type": "AddressData"
},
"grandparent_input_1_bytecode_length": {
"description": "",
"name": "Grandparent Input 1 Bytecode Length",
"type": "AddressData"
},
"grandparent_threshold": {
"description": "The grandparent's \"m\" in the m-of-n signing requirements.",
"name": "Grandparent Threshold",
"type": "AddressData"
},
"parent_key_count": {
"description": "",
"name": "Parent Key Count",
"type": "AddressData"
},
"grandparent_key_count": {
"description": "",
"name": "Grandparent Key Count",
"type": "AddressData"
}
}
},
"first_holder": {
"description": "",
"name": "First Holder",
"variables": {
"first_holder_key": {
"description": "",
"name": "First Holder's Key",
"type": "Key"
}
}
},
"second_holder": {
"description": "",
"name": "Second Holder",
"variables": {
"second_holder_key_1": {
"description": "",
"name": "Second Holder Key 1",
"type": "Key"
},
"second_holder_key_2": {
"description": "",
"name": "Second Holder Key 2",
"type": "Key"
},
"second_holder_key_3": {
"description": "",
"name": "Second Holder Key 3",
"type": "Key"
}
}
}
},
"scenarios": {
"tx0": {
"description": "",
"name": "tx0 - Incorporation TX"
},
"tx1": {
"data": {
"bytecode": {
"tx0_hash": "0x0100000000000000000000000000000000000000000000000000000000000000"
}
},
"description": "",
"name": "tx1 - Token Mint TX",
"transaction": {
"inputs": [
{
"outpointTransactionHash": "0100000000000000000000000000000000000000000000000000000000000000",
"unlockingBytecode": null
}
],
"outputs": [
{
"lockingBytecode": "a914bcc0c37b1b2a61372586fa0fc757203dbc1b6c6187",
"satoshis": 20000
},
{
"lockingBytecode": "aabbccddee",
"satoshis": 0
}
]
},
"value": 10000
},
"tx2": {
"data": {
"bytecode": {
"is_notarized": "0",
"tx0_hash": "0x0100000000000000000000000000000000000000000000000000000000000000"
}
},
"description": "",
"name": "tx2 - Notarization TX",
"transaction": {
"inputs": [
{
"outpointTransactionHash": "1f82d0cafbd8676e0cd414af090b8d48a9add84c042b243240338d29ca414dfa",
"unlockingBytecode": null
}
],
"outputs": [
{
"lockingBytecode": "a9141d3a6d229c89eab911242a9728a40f6b71ab156987",
"satoshis": 546
}
]
},
"value": 546
},
"tx3": {
"data": {
"bytecode": {
"is_notarized": "1",
"grandparent_input_0_bytecode_length": "2",
"grandparent_input_1_bytecode_length": "128",
"grandparent_threshold": "1",
"parent_key_count": "1",
"grandparent_key_count": "1"
}
},
"description": "",
"extends": "tx2",
"name": "tx3 - Token Transfer TX",
"transaction": {
"inputs": [
{
"outpointTransactionHash": "c0f936e5c82f181e81f1f621e8e0fd60b6b30a6c6b1be26a1617a7e4f658339f",
"unlockingBytecode": null
}
]
}
},
"tx4": {
"description": "",
"name": "tx4 - Token Redeem TX"
}
},
"scripts": {
"mint_token": {
"passes": [
"tx1"
],
"name": "Mint Token",
"script": "/**\n * To mint a token we must prove:\n * - TX output 0 is the next covenant state where:\n * - the new tree replaces an empty leaf (<0>) with the covenant outpoint TX hash\n * - requires: new tree root, all opposite nodes, proof path\n * - the new covenant UTXO has a value of 10,000 satoshis greater than before\n * - TX output 1 is the expected token pattern\n * - requires: m-of-n and public keys to use for the token\n * \n * rt\n * / \\\n * b0 b1\n * / \\ / \\\n * a0 a1 a2 a3\n * \n * This example verifies that a0 is `0`, then replaces\n * it with the outpoint TX hash.\n */\n\n<root_hash_after_mint> // rt\n<sibling_tier_2_hash> // b1\n<sibling_leaf_hash> // a1\n\n<1> // tier 2 is left side (requires swap)\n<1> // leaf is left side (requires swap)\n\n<1> // enter branch: Mint Token",
"unlocks": "cashtoken_corporation_covenant"
},
"notarization": {
"passes": [
"tx2"
],
"name": "Notarization",
"script": "/**\n * This is the initial \"Notarization\" spend. It can only \n * be successful if the parent (mint) transaction includes \n * the outpoint TX hash referenced by the CashToken\n * covenant (in its 0th output). \n * \n * To prove that this notarization step has been\n * completed successfully, future spends need only \n * prove that the covenant has previously been\n * successfully spent.\n * \n * This implementation does not offer a way to transfer\n * the token to a new set of holders during the notarization\n * step. Rather, the notarization transaction must lock funds\n * in a covenant with the same signing requirements as those\n * specified in the mint transaction. This is a feature:\n * notarization can be safely performed by anyone, making\n * CashTokens slightly easier to implement in some types of\n * wallets (and reducing the size of the covenant by saving\n * on validation bytecode).\n */\n\n// TODO: user's m-of-n signatures\n\n<example_tx1> // TODO: switch to `<parent_mint_tx>`, fix scenario generation error (using `example_tx1` for `bytecode.parent_mint_tx`)\n\n<0> <0> // \"dummy\"/\"checkbits\" and \"null signature\" for ",
"unlocks": "cashtoken_covenant"
},
"token_transfer_tx3": {
"passes": [
"tx3"
],
"name": "Token Transfer (tx3)",
"script": "/**\n * This script is use to transfer a notarized CashToken between\n * wallets. This implementation supports multisig wallets of\n * up to 3 keys.\n * \n * Counterintuitively, the token transfer branch of this contract\n * does not place any limitations on the outputs to which a token\n * is spent. Instead, only its parent and grandparent transactions\n * are validated to ensure the tokens lineage. However, because\n * this covenant is impossible to spend without the proper\n * \"proof-of-lineage\", if any transfer does not properly continue\n * the covenant, the token is burned.\n * \n * This means it is always possible to \"burn\" the token in one final\n * transaction – in this authentication template, the token should\n * be burned/redeemed back into the parent corporation covenant\n * (which itself then validates that the token's lineage was\n * unbroken). With this strategy, the parent covenant can validate\n * the authenticity of tokens.\n */\n\n<$(\n <example_tx1> <5> <32> <4> OP_ADD OP_ADD OP_SPLIT OP_NIP // remove length of fixed input elements\n <grandparent_input_0_bytecode_length> OP_NUM2VARINT OP_SIZE OP_NIP <grandparent_input_0_bytecode_length> OP_ADD OP_SPLIT OP_NIP // remove 0th input vaariable length elements\n <32> <4> OP_ADD OP_SPLIT OP_NIP // remove length of 1th outpoint\n <grandparent_input_1_bytecode_length> OP_NUM2VARINT OP_SIZE OP_NIP <grandparent_input_1_bytecode_length> OP_ADD // length of 1th variable length elements\n OP_SPLIT OP_NIP\n <1> OP_SPLIT OP_NIP // output count\n <8> OP_SPLIT OP_NIP // output 0 value\n <1> OP_SPLIT OP_NIP // output 0 bytecode length\n <23> OP_SPLIT OP_NIP // grandparent output 0 bytecode\n <8> OP_SPLIT OP_NIP // output 1 value\n <1> OP_SPLIT OP_NIP // output 1 bytecode length\n <23> OP_SPLIT OP_NIP // grandparent output 1 bytecode\n)> // remaining grandparent TX serialization\n\n/**\n * Grandparent public key pushes (for all n keys)\n * TODO: make this a script/variable rather than hardcoded\n */\n<first_holder_key.public_key>\n<<grandparent_key_count>> // Push grandparent public key count opcode (n)\n<<parent_key_count>> // Push parent public key count opcode (n in m-of-n)\n<<grandparent_threshold>> // Push grandparent threshold opcode (m in m-of-n)\n\n<$(\n <example_tx1> <5> <32> <4> OP_ADD OP_ADD OP_SPLIT OP_NIP // remove length of fixed input elements\n <grandparent_input_0_bytecode_length> OP_NUM2VARINT OP_SIZE OP_NIP <grandparent_input_0_bytecode_length> OP_ADD OP_SPLIT OP_NIP // remove 0th input vaariable length elements\n <32> <4> OP_ADD OP_SPLIT OP_NIP // remove length of 1th outpoint\n <grandparent_input_1_bytecode_length> OP_NUM2VARINT OP_SIZE OP_NIP <grandparent_input_1_bytecode_length> OP_ADD // length of 1th variable length elements\n OP_SPLIT OP_NIP\n <1> OP_SPLIT OP_NIP // output count\n <8> OP_SPLIT OP_NIP // output 0 value\n <1> OP_SPLIT OP_NIP // output 0 bytecode length\n <23> OP_SPLIT OP_NIP // grandparent output 0 bytecode\n <8> OP_SPLIT OP_DROP // output 1 value\n)> // Grandparent's CashToken covenant value\n\n/**\n * Mint TX elements\n */\n<$(\n <example_tx1> <5> <32> <4> OP_ADD OP_ADD OP_SPLIT OP_NIP // remove length of fixed input elements\n <grandparent_input_0_bytecode_length> OP_NUM2VARINT OP_SIZE OP_NIP <grandparent_input_0_bytecode_length> OP_ADD OP_SPLIT OP_NIP // remove 0th input vaariable length elements\n <32> <4> OP_ADD OP_SPLIT OP_NIP // remove length of 1th outpoint\n <grandparent_input_1_bytecode_length> OP_NUM2VARINT OP_SIZE OP_NIP <grandparent_input_1_bytecode_length> OP_ADD // length of 1th variable length elements\n OP_SPLIT OP_NIP\n <1> OP_SPLIT OP_NIP // output count\n <8> OP_SPLIT OP_NIP // output 0 value\n <1> OP_SPLIT OP_NIP // output 0 bytecode length\n <23> OP_SPLIT OP_DROP\n)> // grandparent output 0 bytecode\n<$(\n <example_tx1> <5> <32> <4> OP_ADD OP_ADD OP_SPLIT OP_NIP // remove length of fixed input elements\n <grandparent_input_0_bytecode_length> OP_NUM2VARINT OP_SIZE OP_NIP <grandparent_input_0_bytecode_length> OP_ADD OP_SPLIT OP_NIP // remove 0th input vaariable length elements\n <32> <4> OP_ADD OP_SPLIT OP_NIP // remove length of 1th outpoint\n <grandparent_input_1_bytecode_length> OP_NUM2VARINT OP_SIZE OP_NIP <grandparent_input_1_bytecode_length> OP_ADD // length of 1th variable length elements\n OP_SPLIT OP_NIP\n <1> OP_SPLIT OP_NIP // output count\n <8> OP_SPLIT OP_DROP\n)> // grandparent output 0 value\n\n<0> // grandparent is_notarized\n\n/**\n * Input 1 elements\n */\n<grandparent_input_1_bytecode_length>\n<$(\n <example_tx1> <5> <32> <4> OP_ADD OP_ADD OP_SPLIT OP_NIP // remove length of fixed input elements\n <grandparent_input_0_bytecode_length> OP_NUM2VARINT OP_SIZE OP_NIP OP_SPLIT OP_NIP <grandparent_input_0_bytecode_length> OP_NUM2VARINT OP_SIZE OP_NIP OP_SPLIT OP_NIP <grandparent_input_0_bytecode_length> OP_SPLIT OP_NIP // remove 0th input vaariable length elements\n <32> <4> OP_ADD OP_SPLIT OP_NIP // remove length of 1th outpoint\n <grandparent_input_1_bytecode_length> OP_NUM2VARINT OP_SIZE OP_NIP OP_SPLIT OP_NIP <grandparent_input_1_bytecode_length>\n OP_SPLIT OP_DROP \n)> // grandparent input 1 bytecode\n<$(\n <example_tx1> <5> <32> <4> OP_ADD OP_ADD OP_SPLIT OP_NIP // remove length of fixed input elements\n <grandparent_input_0_bytecode_length> OP_NUM2VARINT OP_SIZE OP_NIP OP_SPLIT OP_NIP <grandparent_input_0_bytecode_length> OP_NUM2VARINT OP_SIZE OP_NIP OP_SPLIT OP_NIP <grandparent_input_0_bytecode_length> OP_SPLIT OP_NIP // remove 0th input vaariable length elements\n <32> <4> OP_ADD OP_SPLIT OP_DROP\n)> // grandparent input 1 outpoint\n\n/**\n * Input 0 elements\n */\n<grandparent_input_0_bytecode_length>\n<$(\n <example_tx1> <5> OP_SPLIT // tx version (4) + input count (1) \n OP_NIP <$( <32> <4> OP_ADD )> OP_SPLIT // outpoint tx hash (32) + input index (4)\n OP_NIP \n <grandparent_input_0_bytecode_length> OP_NUM2VARINT OP_SIZE OP_NIP OP_SPLIT OP_NIP\n <grandparent_input_0_bytecode_length> OP_SPLIT\n OP_DROP\n )> // grandparent input 0 bytecode\n<$( \n <example_tx1> <5> OP_SPLIT // tx version (4) + input count (1) \n OP_NIP <$( <32> <4> OP_ADD )> OP_SPLIT // outpoint tx hash (32) + input index (4)\n OP_DROP )\n> // grandparent input 0 outpoint\n\n<example_tx2> // TODO: switch to `<parent_tx>`, fix scenario generation error (using `example_tx2` for `bytecode.parent_successful_tx`)\n\n/**\n * Required signatures\n */\n<0> // OP_CHECKMULTISIG \"dummy\" or \"checkbits\" value\n<first_holder_key.signature.all_outputs>",
"unlocks": "cashtoken_covenant"
},
"token_transfer_cont_tx3": {
"name": "Token Transfer (...tx3)",
"script": "/**\n * TODO: demonstrate a transfer in which the grandparent\n * transaction is a transfer (not the mint transaction)\n */",
"unlocks": "cashtoken_covenant"
},
"redeem_token": {
"name": "Redeem Token",
"script": "/**\n * TODO: demonstrate a redeem, emulate support for reading sibling UTXOs in the VM.\n * \n * The Libauth VM implementation is designed to maximize internal inspectability \n * from operation to operation. Though all transaction input and UTXO information \n * is technically required by the VM during signature validation, the Libauth VM \n * currently avoids complex state types by pre-processing these parts of the \n * serialized transaction prior to VM evaluation. As a result, the Libauth VM and \n * Bitauth IDE require deeper refactoring to expose this data to other operations. \n * Other existing VM implementations (like the Satoshi `EvalScript` implementation)\n * should be able to add support for most TxInt operations with relatively \n * fewer changes.\n*/\n\n/**\n * To redeem a token we must prove:\n * - TX input 1 spends a valid CashToken covenant for expected \"mint parent hash\":\n * - validate UTXO bytecode to confirm the precise expected redeem bytecode\n * - if the transaction is valid, we know the proof-of-lineage was provided\n * - (For heterogenous tokens: TX input 1 has the expected satoshi value)\n * - TX output 0 is the next covenant state where:\n * - the new tree replaces a leaf containing \"mint parent hash\" with an an empty leaf (<0>)\n * - the new covenant UTXO has a value of 10,000 satoshis less than before\n * \n * rt\n * / \\\n * b0 b1\n * / \\ / \\\n * a0 a1 a2 a3\n * \n * This example verifies that a0 is the mint parent hash, then replaces it with\n * the hash of `0`.\n */",
"unlocks": "cashtoken_corporation_covenant"
},
"cashtoken_corporation_covenant": {
"lockingType": "p2sh",
"name": "CashToken Corporation Covenant",
"script": "<empty_root_hash> OP_TOALTSTACK \n\nOP_IF\n/**\n * branch: Mint Token\n */\n\n/**\n * we need 2 copies of the \"validation path\": \n * 1. confirm the leaf was empty\n * 2. confirm the new root only changes that leaf\n */\nOP_2DUP\nOP_FROMALTSTACK OP_ROT OP_ROT // current_root_hash\nOP_TOALTSTACK OP_TOALTSTACK\n\n// First we confirm the leaf being updated was empty:\n<4> OP_PICK // sibling_tier_2_hash\n<4> OP_PICK // sibling_leaf_hash\n<0> OP_HASH160 // constant: hash160 of <0>\n\nOP_FROMALTSTACK // check if leaf requires swap\nOP_IF OP_SWAP OP_ENDIF\nOP_CAT OP_HASH160\n\nOP_FROMALTSTACK // check if tier 2 requires swap\nOP_IF OP_SWAP OP_ENDIF\nOP_CAT OP_HASH160 // should be current root\n\nOP_EQUALVERIFY // verified that replaced leaf was <0>\n\n// Now we confirm that the new leaf uses outpoint TX hash\n\nOP_TOALTSTACK OP_TOALTSTACK\n\nOP_OUTPOINTTXHASH\nOP_HASH160\n\nOP_FROMALTSTACK // check if leaf requires swap\nOP_IF OP_SWAP OP_ENDIF\nOP_CAT OP_HASH160\n\nOP_FROMALTSTACK // check if tier 2 requires swap\nOP_IF OP_SWAP OP_ENDIF\nOP_CAT OP_HASH160 // verified new root\n\nOP_DUP OP_TOALTSTACK // save for later use\nOP_EQUALVERIFY // new root is correct\n\n\n// require that output 0 is 10_000 satoshis larger than last value\nOP_UTXOVALUE\n<10_000> OP_ADD\n<0> OP_OUTPUTVALUE\nOP_EQUALVERIFY // covenant has been paid properly\n\n// require that the correct covenant script is used\n<OP_PUSHBYTES_20>\nOP_FROMALTSTACK // root_hash_after_mint\nOP_UTXOBYTECODE // get this covenant's bytecode\n/**\n * OP_SPLIT is often unsafe for user input, but this input\n * comes from the VM/previous contract.\n */\n<21> OP_SPLIT OP_NIP // trim off previous root hash push\nOP_CAT OP_CAT\nOP_HASH160\n<OP_EQUAL> <OP_HASH160> <OP_PUSHBYTES_20> OP_2SWAP\nOP_CAT OP_CAT OP_CAT\n// require that output 0 is the new covenant bytecode\n<0> OP_OUTPUTBYTECODE\nOP_EQUALVERIFY // TX pays to the updated covenant\n\n// require that output 1 is a cashtoken UTXO\n<1> OP_OUTPUTBYTECODE\n<0xaabbccddee> // TODO: push expected cashtoken bytecode\n/**\n * We get the Token ID from the last covenant TX ID. This: \n * 1) prevents tokens from being created to impersonate existing tokens\n * 2) saves bytecode space (vs. pushing an ID)\n * 3) avoid bugs in wallet implementations (validating at contract-level)\n */\n// TODO: use <0> OP_OUTPOINTTXHASH to get the Token ID, concat into CashToken covenant\nOP_EQUAL\nOP_ELSE\n/**\n * branch: Redeem Token\n * TODO: leaving out of first draft, see notes in unlocking script\n */\nOP_ENDIF\n"
},
"cashtoken_covenant": {
"lockingType": "p2sh",
"name": "CashToken Covenant",
"script": "/**\n * This script is the key to the inductive proof. It's \n * designed to be unspendable unless:\n * 1. Its parent was the mint transaction funded by the transaction\n * hash claimed (requires full parent transaction), or\n * 2. It was previously spent successfully – the parent's 0th input's\n * UTXO used this covenant, i.e. the parent's 0th-input-parent's\n * 0th output uses this covenant. (Valiated by checking the full\n * parent and grandparent transactions.)\n * \n * With these limitations, if a user can move the CashToken,\n * we know the CashToken has the lineage it claims.\n */\n\n/**\n * By \"baking-in\" the code branch selection, we prevent the\n * CashToken from being unecessarily notarized multiple times.\n */\n<is_notarized>\n\n// mint parent hash: the outpoint TX hash in the 0th output of the mint transaction\n<tx0_hash> \n\nOP_TOALTSTACK // tx0_hash\nOP_TOALTSTACK // is_notarized\n\n<1> // threshold (m)\n\n// push n public keys (up to 3)\n// TODO: convert to variable\n<first_holder_key.public_key>\n\n<1> // count (n)\n\n/**\n * Though this is only checked by the transfer branch,\n * it drops all ownership-related items from the stack\n * more efficiently than can be done with manual dropping\n * operations.\n * \n * Note, notarizations could be restricted to token owners by\n * replacing this with OP_CHECKMULTISIGVERIFY.\n */\nOP_CHECKMULTISIG\n\n// in both branches, verify the full parent transaction is provided\nOP_DUP\nOP_HASH256\nOP_OUTPOINTTXHASH\n\nOP_DROP OP_DROP // debug: switch comment to skip check\n// OP_EQUALVERIFY // verfied provided transaction is parent\n\nOP_FROMALTSTACK // is_notarized\nOP_NOTIF\n /**\n * Notarization branch:\n * Prove that this transaction's parent is the claimed token mint transaction,\n * i.e. its 0th input spends from the claimed token ID (outpoint TX hash).\n * \n * Note, this branch can be executed by any interested observer (doesn't \n * require access to any private keys), so it must be carefully validated\n * to avoid griefing.\n */\n\n OP_DROP // drop failed multisig check\n\n <4> OP_SPLIT OP_NIP // remove and discard tx version\n <1> OP_SPLIT OP_SWAP // get first byte of tx input count\n /**\n * Between 0 and 127, Script Numbers and VarInts are compatible.\n * \n * 127 (`0xfc`) is the largest integer which can be represented\n * by the Script Number format in a single byte (0x80 begins \n * the range negative 0 through 127).\n */\n <2> OP_EQUALVERIFY // require exactly 2 inputs (covenant + fee funding)\n\n <32> OP_SPLIT OP_DROP // get 0th outpoint tx hash, drop everything else\n OP_FROMALTSTACK // tx0_hash\n OP_EQUALVERIFY // Token ID verified: parent transaction spends from claimed outpoint tx hash\n\n /**\n * Verify the transaction's 0th output is re-assigned\n * the updated covenant.\n */\n OP_UTXOVALUE\n <0> OP_OUTPUTVALUE\n OP_EQUALVERIFY // require output value to be the same\n <OP_HASH160 OP_PUSHBYTES_20>\n <OP_1> OP_UTXOBYTECODE\n <1> OP_SPLIT OP_NIP OP_CAT // remove is_notarized of OP_0, replace with OP_1\n OP_HASH160 <OP_EQUAL>\n OP_CAT OP_CAT // expected P2SH bytecode\n <0> OP_OUTPUTBYTECODE\n OP_EQUAL // 0th output bytecode is correct\n /**\n * TODO: further optimization: after notarization, can we prune the\n * notarization branch?\n * Requires other changes to validation in both convenants.\n */\n\nOP_ELSE\n /**\n * Transfer branch:\n * Prove that the outpoint tx spends from this same covenant. Then\n * prove that this transaction is signed by the required private key(s).\n * \n * Note: to save space, this branch doesn't validate the locking bytecode\n * to which the transaction pays, making it possible for wallets to burn\n * the CashToken (intentionally or due to a bug). This is unlikely to\n * be a problem in practice because CashTokens can only be moved by\n * wallets which support the CashToken covenant template (no\n * backwards-compatibility with wallet which might unintentionally\n * burn tokens).\n * \n * By not forward-validating outputs, we elliminate the need for each\n * CashToken covenant to be capable of correctly \"identifying\" its \n * parent covenant during redeem transactions. (Instead, we verify the\n * lineage of the token by checking its parent and grandparent.)\n */\n\n OP_VERIFY // Signature validation must have been successful\n\n // Owner has authorized transfer, now prove lineage:\n\n // get parent tx's outpoint tx hash, then verify we have the grandparent\n <4> OP_SPLIT OP_NIP // remove and discard tx version (no validation)\n <1> OP_SPLIT OP_SWAP // get first byte of tx input count\n <0x02> OP_EQUALVERIFY // require exactly 2 inputs\n <32> OP_SPLIT // get parent outpoint tx hash\n <4> OP_SPLIT OP_DROP // get parent outpoint index, drop everything else\n OP_BIN2NUM\n <0> OP_EQUALVERIFY // must be grandparent's 0th output (grandparent may be 1th for mint)\n OP_TOALTSTACK // parent outpoint tx hash\n\n // validate and concat grandparent back together, confirming it used this covenant\n <\n 0x02000000 // always require version 2\n 0x02 // always require exactly 2 inputs\n >\n\n OP_SWAP // top is grandparent input 0 outpoint (hash + index)\n OP_SIZE <36> OP_EQUALVERIFY // require grandparent input 0 outpoint to be 36 bytes\n OP_CAT\n\n OP_SWAP // top is grandparent input 0 bytecode\n OP_SIZE \n OP_DUP\n <4> OP_ROLL OP_EQUALVERIFY // provided bytecode is expected size\n OP_NUM2VARINT // serialize length\n OP_SWAP\n OP_CAT\n OP_CAT // grandparent TX up to input 1\n\n \n OP_SWAP // top is grandparent input 1 outpoint (hash + index)\n OP_SIZE <36> OP_EQUALVERIFY // require grandparent input 1 outpoint to be 36 bytes\n OP_CAT\n\n OP_SWAP // top is grandparent input 1 bytecode\n OP_SIZE\n OP_DUP\n <4> OP_ROLL OP_EQUALVERIFY // provided bytecode is expected size\n OP_NUM2VARINT // serialize length\n OP_SWAP\n OP_CAT\n OP_CAT // grandparent TX through inputs\n \n // start building grandparent outputs\n\n OP_SWAP // top is grandparent is_notarized\n <0> OP_EQUAL\n OP_IF\n /**\n * Grandparent is not notarized (mint transaction), the 1th output is the covenant.\n */\n <3> OP_CAT // require exatly 3 outputs in mint transaction\n OP_SWAP // top is 0th output satoshi value\n OP_SIZE <8> OP_EQUALVERIFY // satoshi value must be 8 bytes\n OP_CAT\n <$(<23> OP_NUM2VARINT)> // length of corporate covenant bytecode (P2SH)\n OP_CAT\n OP_SWAP\n OP_SIZE <23> OP_EQUALVERIFY // corporate covenant bytecode must be P2SH length\n OP_CAT\n // end of 0th output\n OP_ELSE\n /**\n * Grandparent is notarized, the 0th output is the covenant.\n */\n <0x02> OP_CAT // require exactly 2 outputs in transfer transactions\n OP_ENDIF\n\n OP_SWAP // top is grandparent's cashtoken covenant value\n OP_DUP\n /**\n * To support heterogenous tokens, this covenant prevents the\n * satoshi value of the token from changing during transfers.\n * \n * However, we can't simply verify the \"next\" satoshi value is the\n * same as the \"current\" value - eventually, the token will be\n * redeemed with the corporation covenant, and the covenant's \n * expected balance at that time can't necesssarily be predicted.\n * \n * Instead, we only verify backwards by comparing the token output\n * values of the grandparent and current transaction. With this\n * limitation, we can know the token's value can't be modified\n * until the redeem transaction, where the parent covenant can\n * read the value before it is destroyed.\n */\n OP_UTXOVALUE <8> OP_NUM2BIN\n OP_EQUALVERIFY // disallow token value from being modified\n OP_CAT\n\n < <23> OP_HASH160 > OP_CAT // length of cashtoken covenant bytecode (P2SH)\n\n // begin transforming parent bytecode into grandparent bytecode for validation\n\n <OP_0> // is_notarized for mint transactions\n OP_UTXOBYTECODE // get parent bytecode\n <1> OP_SPLIT OP_NIP OP_CAT // replace is_notarized with OP_0\n \n <36> OP_SPLIT // preserve token ID (OP_0 + OP_PUSHBYTES_32 + 32 bytes + OP_TOALTSTACK + OP_TOALTSTACK)\n \n <1> OP_SPLIT OP_NIP // remove parent threshold pushing opcode (<<m>>)\n <3> OP_ROLL // add grandparent_threshold pushing opcode\n OP_SWAP // top is parent bytecode after removed m push\n\n <4> OP_ROLL // get parent public key count opcode\n\n // TODO: PRs welcome – are there more efficient ways to implement this \"case\" statement?\n // check for parent n of 1, 2, or 3 (saving the opcode)\n OP_TUCK \n <<1>> OP_EQUAL OP_IF\n <34> OP_SPLIT\n OP_ELSE\n OP_OVER <<2>> OP_EQUAL OP_IF\n <$(<34> <34> OP_ADD)> OP_SPLIT\n OP_ELSE\n OP_OVER <<3>> OP_EQUAL OP_IF\n <$(<34> <34> <34> OP_ADD OP_ADD)> OP_SPLIT\n OP_ELSE\n OP_RETURN // fail, parent must match a planned n\n OP_ENDIF\n OP_ENDIF\n OP_ENDIF\n /**\n * TODO: SECURITY: do we need to validate these bytes? (E.g. in the\n * corporation covenant, they must be validated.) Can a malicious\n * grandparent transaction use different opcodes in these bytes to\n * defraud future token holders?\n */\n OP_NIP // drop the removed parent public key pushes\n OP_NIP // drop the parent public key count opcode\n <1> OP_SPLIT OP_NIP // drop parent n opcode from bytecode\n\n <4> OP_ROLL // pick grandparent public key count opcode\n // TODO: PRs welcome – there are definitely more efficient ways to implement this one\n OP_DUP\n <<1>> OP_EQUAL OP_IF\n <OP_PUSHBYTES_33> <6> OP_ROLL OP_SIZE <33> OP_EQUALVERIFY\n OP_CAT\n OP_ELSE\n OP_DUP <<2>> OP_EQUAL OP_IF\n <OP_PUSHBYTES_33> <6> OP_ROLL OP_SIZE <33> OP_EQUALVERIFY\n OP_CAT\n <OP_PUSHBYTES_33> <7> OP_ROLL OP_SIZE <33> OP_EQUALVERIFY \n OP_CAT OP_CAT\n OP_ELSE\n OP_DUP <<3>> OP_EQUAL OP_IF\n <OP_PUSHBYTES_33> <6> OP_ROLL OP_SIZE <33> OP_EQUALVERIFY\n OP_CAT\n <OP_PUSHBYTES_33> <7> OP_ROLL OP_SIZE <33> OP_EQUALVERIFY\n OP_CAT OP_CAT\n <OP_PUSHBYTES_33> <7> OP_ROLL OP_SIZE <33> OP_EQUALVERIFY\n OP_CAT OP_CAT\n OP_ELSE\n OP_RETURN // fail, grandparent must match a planned n\n OP_ENDIF\n OP_ENDIF\n OP_ENDIF\n OP_SWAP OP_CAT // concat grandparent n opcode after public key pushes\n\n OP_SWAP\n OP_CAT OP_CAT OP_CAT // reconstructed grandparent redeem bytecode\n OP_HASH160 // get redeem script hash\n OP_CAT\n <OP_EQUAL> OP_CAT\n\n OP_SWAP // top is remaining TX serialization\n\n // no need to verify remaining grandparent outputs\n\n OP_CAT // full grandparent transaction\n OP_HASH256 // grandparent transaction hash\n OP_FROMALTSTACK // outpoint tx hash from parent\n\n OP_DROP OP_DROP <1> // debug: switch comment to skip check\n // OP_EQUAL // verify grandparent is parent outpoint tx\n\n // (don't bother dropping the mint parent hash left on altstack)\n\nOP_ENDIF\n"
},
"compute_tree_proof": {
"name": "Compute Tree Proof",
"script": "/**\n * Return a proof of the current state tree. This template \n * allows for 4 active tokens:\n * \n * rt\n * / \\\n * b0 b1\n * / \\ / \\\n * a0 a1 a2 a3\n * \n */\n\nOP_HASH160 OP_TOALTSTACK\nOP_HASH160 OP_TOALTSTACK\nOP_HASH160 OP_TOALTSTACK\nOP_HASH160 OP_TOALTSTACK\n \nOP_FROMALTSTACK\nOP_FROMALTSTACK\nOP_FROMALTSTACK\nOP_FROMALTSTACK\n\n// Now we have the hash of each leaf, save the sibling leaf hash:\n<4> OP_ROLL\nOP_PICK OP_TOALTSTACK\n\n// Hash each pair\nOP_CAT OP_HASH160 OP_TOALTSTACK\nOP_CAT OP_HASH160 OP_TOALTSTACK \n\nOP_FROMALTSTACK\nOP_FROMALTSTACK\n\n// Now we have the hash of each tier 2 node, save the sibling hash:\n<2> OP_ROLL\nOP_PICK OP_TOALTSTACK\n\nOP_CAT OP_HASH160 // root hash\nOP_FROMALTSTACK // tier 2 sibling hash\nOP_FROMALTSTACK // leaf sibling hash\n",
"tests": [
{
"check": "// leaf sibling\n<0xb472a266d0bd89c13706a4132ccfb16f7c3b9fcb> OP_EQUALVERIFY\n// tier 2 sibling\n<0xdedc67ea808575d6b39666eb62dc949386e3176a> OP_EQUALVERIFY\n// root\n<0xbcd72713b594ea45d44512ca7912c625f7e69092> OP_EQUAL",
"name": "Empty Root, a0 Proof",
"setup": "selected_leaf_path\n\n// 4 leaves, initially set to 0:\n<0>\n<0>\n<0>\n<0>"
}
]
},
"selected_leaf_path": {
"name": "Selected Leaf Path",
"script": "// stack depth (total-index) of tier 1 sibling node\n<0> // b0:1, b1:0 \n// stack depth (total-index) of sibling leaf hash\n<3> // a0:3, a1:2, a2:3, a3:0"
},
"sibling_leaf_hash": {
"name": "Sibling Leaf Hash",
"script": "$(\n selected_leaf_path\n <0> <0> <0> <0>\n compute_tree_proof\n OP_NIP OP_NIP\n)"
},
"sibling_tier_2_hash": {
"name": "Sibling Tier 2 Hash",
"script": "$(\n selected_leaf_path\n <0> <0> <0> <0>\n compute_tree_proof\n OP_DROP OP_NIP\n)"
},
"root_hash_after_mint": {
"name": "Root Hash After Mint",
"pushed": true,
"script": "$(\n selected_leaf_path\n <tx0_hash> <0> <0> <0>\n compute_tree_proof\n OP_DROP OP_DROP\n)",
"tests": [
{
"passes": [
"tx1"
],
"check": "",
"name": "tx1"
}
]
},
"empty_root_hash": {
"name": "Empty Root Hash",
"script": "$(\n selected_leaf_path\n <0> <0> <0> <0>\n compute_tree_proof\n OP_DROP OP_DROP\n)"
},
"corporation_covenant_after_hash": {
"name": "Corporation Covenant After Hash",
"script": "/**\n * Bitauth templates don't currently support recursion,\n * so this push must be manually updated whenever the\n * Corporate Covenant is changed.\n */\n<0x14bcd72713b594ea45d44512ca7912c625f7e690926b636e6c7b7b6b6b5479547900a96c637c687ea96c637c687ea9886b6b20978306aa4e02fd06e251b38d2e961f78f4af2ea6524a3e4531126776276a6af1a96c637c687ea96c637c687ea9766b880210270210279300760087637502204e677500688800760087637502204e6775006868>\n// The stack should now contain two `corporate_covenant`s\n\n<21> OP_SPLIT // split after the root hash push\nOP_NIP",
"tests": [
{
"check": "<<empty_root_hash>> // bytecode to push root hash\nOP_SWAP OP_CAT\n<cashtoken_corporation_covenant> OP_EQUAL\n",
"name": "Matches Corporate Covenant"
}
]
},
"example_tx1": {
"name": "Example tx1",
"pushed": true,
"script": "/**\n * This script is used to mock a serialized mint transaction for use in\n * testing the notarization path of the CashToken covenant.\n */\n$(\n<0x02000000> // version\n<0x02> // number of inputs\nOP_CAT\n\n/**\n * Input 0 (the covenant UTXO preceeding mint transaction)\n */\n<tx0_hash> // outpoint tx hash\n<0x00000000> // previous output index\n<0x02> // unlocking bytecode length\n<OP_1 OP_2> // unlocking bytecode\n\nOP_CAT OP_CAT OP_CAT OP_CAT\n\n/**\n * Input 1 (any funding output)\n */\n<0x0999000000000000000000000000000000000000000000000000000000000000> // outpoint tx hash\n<0x00000000> // previous output index\n<$(<128> OP_NUM2VARINT)> // unlocking bytecode length\n<$(<0> <128> OP_NUM2BIN)> // unlocking bytecode\n\nOP_CAT OP_CAT OP_CAT OP_CAT\n\n<0x03> // number of outputs\n\nOP_CAT\n/**\n * Output 0 (the covenant UTXO after mint)\n */\n<$( <20000> <8> OP_NUM2BIN )> // value satoshis\n<23> // P2SH locking bytecode length\n<OP_HASH160 <$(<0> <20> OP_NUM2BIN)> OP_EQUAL> // locking bytecode\nOP_CAT OP_CAT OP_CAT\n\n/**\n * Output 1 (the CashToken UTXO after mint)\n */\n<$( <546> <8> OP_NUM2BIN )> // value satoshis\n<23> // P2SH locking bytecode length\n<OP_HASH160 <$(<1> OP_HASH160)> OP_EQUAL> // TODO: update example locking bytecode when covenant is done\nOP_CAT OP_CAT OP_CAT\n\n/**\n * Output 2 (change output)\n */\n<$( <10000> <8> OP_NUM2BIN )> // value satoshis\n<0x01> // locking bytecode length\n<OP_NOP> // locking bytecode\nOP_CAT OP_CAT \nOP_CAT\n\n<0x00000000> // locktime\nOP_CAT\n)\n",
"tests": [
{
"passes": [
"tx1"
],
"check": "\nOP_HASH256 // double-sha256\n<0x1f82d0cafbd8676e0cd414af090b8d48a9add84c042b243240338d29ca414dfa>\nOP_EQUAL",
"name": "Expected Hash"
}
]
},
"example_tx2": {
"name": "Example tx2",
"pushed": true,
"script": "/**\n * This script is used to mock a serialized mint transaction for use in\n * testing the transfer path of the CashToken covenant.\n */\n$(\n<0x02000000> // version\n<0x02> // number of inputs\nOP_CAT\n\n/**\n * Input 0 (the parent CashToken covenant UTXO)\n */\n<$( <example_tx1> OP_HASH256 )> // outpoint tx hash\n<0x00000000> // previous output index\n<0x01> // unlocking bytecode length\n<OP_1> // unlocking bytecode\n\nOP_CAT OP_CAT OP_CAT OP_CAT\n\n/**\n * Input 1 (any funding output)\n */\n<0x0999000000000000000000000000000000000000000000000000000000000000> // outpoint tx hash\n<0x00000000> // previous output index\n<0x01> // unlocking bytecode length\n<OP_1> // unlocking bytecode\n\nOP_CAT OP_CAT OP_CAT OP_CAT\n\n<0x02> // number of outputs\n\nOP_CAT\n/**\n * Output 0 (the next covenant UTXO)\n */\n<$( <20000> <8> OP_NUM2BIN )> // value satoshis\n<0x01> // locking bytecode length\n<OP_NOP> // locking bytecode\nOP_CAT OP_CAT OP_CAT\n\n/**\n * Output 1 (any change output)\n */\n<$( <546> <8> OP_NUM2BIN )> // value satoshis\n<0x01> // locking bytecode length\n<OP_NOP> // locking bytecode\nOP_CAT OP_CAT OP_CAT\n\n<0x00000000> // locktime\nOP_CAT\n)\n",
"tests": [
{
"passes": [
"tx2"
],
"check": "OP_HASH256 // double-sha256\n<0xc0f936e5c82f181e81f1f621e8e0fd60b6b30a6c6b1be26a1617a7e4f658339f>\nOP_EQUAL",
"name": "Expected Hash"
}
]
}
},
"supported": [
"BCH_2022_05_SPEC"
],
"version": 0
}
@gandrewstone
Copy link

gandrewstone commented Feb 4, 2021

I'm looking for a "theory of operation" type document. I've read your 5000 foot descriptions, and I see the code in the bitauth IDE, but I'm looking for something that describes the transaction structure and how it maps to tokens during essential lifecycle functions: mint, transfer, split, join, atomic exchange of multiple tokens, melt. Its kind of hard to read the BCH script without understand the theory of operation.

EDIT: the home page (that I just noticed) in the bitauth IDE is a pretty good start. But I'd like to understand the high level operation of the Corporation and CashToken Covenants, and I'd like to understand the purpose of the various steps (notarization, etc) -- that is why won't it work if a step is left out?

@bitjson
Copy link
Author

bitjson commented Feb 4, 2021

@gandrewstone thanks for taking a look!

On OP_UTXOBYTECODE: Yes, that could definitely be clearer. It just pushes the full bytecode provided by the UTXO. So when it's evaluated during redeem script evaluation, it would be the exact contents of the script parameter (in derivatives of the Satoshi C++ implementation). In Libauth, it's the full contents of instructions when the OP_UTXOBYTECODE operation is executed.

It looks like you found the template description – I've tried to provide a full technical summary there. You'll also find specific notes about each script in the comment block at the top of that script. E.g. the Notarization unlocking script begins with:

/**
 * This is the initial "Notarization" spend. It can only 
 * be successful if the parent (mint) transaction includes 
 * the outpoint TX hash referenced by the CashToken
 * covenant (in its 0th output). 
 * 
 * To prove that this notarization step has been
 * completed successfully, future spends need only 
 * prove that the covenant has previously been
 * successfully spent.
 * 
 * This implementation does not offer a way to transfer
 * the token to a new set of holders during the notarization
 * step. Rather, the notarization transaction must lock funds
 * in a covenant with the same signing requirements as those
 * specified in the mint transaction. This is a feature:
 * notarization can be safely performed by anyone, making
 * CashTokens slightly easier to implement in some types of
 * wallets (and reducing the size of the covenant by saving
 * on validation bytecode).
 */

Which is paired with the below CashToken Covenant locking script (a P2SH script). I think its top comment may help to answer your question about why "notarization" is necessary. I've tried to add comments in a lot of other places too (about design, optimization, and security decisions):

/**
 * This script is the key to the inductive proof. It's 
 * designed to be unspendable unless:
 * 1. Its parent was the mint transaction funded by the transaction
 *    hash claimed (requires full parent transaction), or
 * 2. It was previously spent successfully – the parent's 0th input's
 *    UTXO used this covenant, i.e. the parent's 0th-input-parent's
 *    0th output uses this covenant. (Valiated by checking the full
 *    parent and grandparent transactions.)
 * 
 * With these limitations, if a user can move the CashToken,
 * we know the CashToken has the lineage it claims.
 */

/**
 * By "baking-in" the code branch selection, we prevent the
 * CashToken from being unecessarily notarized multiple times.
 */
<is_notarized>

// mint parent hash: the outpoint TX hash in the 0th output of the mint transaction
<tx0_hash> 

OP_TOALTSTACK // tx0_hash
OP_TOALTSTACK // is_notarized

<1> // threshold (m)

// push n public keys (up to 3)
// TODO: convert to variable
<first_holder_key.public_key>

<1> // count (n)

/**
 * Though this is only checked by the transfer branch,
 * it drops all ownership-related items from the stack
 * more efficiently than can be done with manual dropping
 * operations.
 * 
 * Note, notarizations could be restricted to token owners by
 * replacing this with OP_CHECKMULTISIGVERIFY.
 */
OP_CHECKMULTISIG

// in both branches, verify the full parent transaction is provided
OP_DUP
OP_HASH256
OP_OUTPOINTTXHASH

// OP_DROP OP_DROP // debug: switch comment to skip check
OP_EQUALVERIFY // verfied provided transaction is parent

OP_FROMALTSTACK // is_notarized
OP_NOTIF
    /**
     * Notarization branch:
     * Prove that this transaction's parent is the claimed token mint transaction,
     * i.e. its 0th input spends from the claimed token ID (outpoint TX hash).
     * 
     * Note, this branch can be executed by any interested observer (doesn't 
     * require access to any private keys), so it must be carefully validated
     * to avoid griefing.
     */

    OP_DROP // drop failed multisig check

    <4> OP_SPLIT OP_NIP // remove and discard tx version
    <1> OP_SPLIT OP_SWAP // get first byte of tx input count
    /**
     * Between 0 and 127, Script Numbers and VarInts are compatible.
     * 
     * 127 (`0xfc`) is the largest integer which can be represented
     * by the Script Number format in a single byte (0x80 begins 
     * the range negative 0 through 127).
     */
    <2> OP_EQUALVERIFY // require exactly 2 inputs (covenant + fee funding)

    <32> OP_SPLIT OP_DROP // get 0th outpoint tx hash, drop everything else
    OP_FROMALTSTACK // tx0_hash
    OP_EQUALVERIFY // Token ID verified: parent transaction spends from claimed outpoint tx hash

    /**
     * Verify the transaction's 0th output is re-assigned
     * the updated covenant.
     */
    OP_UTXOVALUE
    <0> OP_OUTPUTVALUE
    OP_EQUALVERIFY // require output value to be the same
    <OP_HASH160 OP_PUSHBYTES_20>
    <OP_1> OP_UTXOBYTECODE
    <1> OP_SPLIT OP_NIP OP_CAT // remove is_notarized of OP_0, replace with OP_1
    OP_HASH160 <OP_EQUAL>
    OP_CAT OP_CAT // expected P2SH bytecode
    <0> OP_OUTPUTBYTECODE
    OP_EQUAL // 0th output bytecode is correct
    /**
     * TODO: further optimization: after notarization, can we prune the
     * notarization branch?
     * Requires other changes to validation in both convenants.
     */

OP_ELSE
    /**
     * Transfer branch:
     * Prove that the outpoint tx spends from this same covenant. Then
     * prove that this transaction is signed by the required private key(s).
     * 
     * Note: to save space, this branch doesn't validate the locking bytecode
     * to which the transaction pays, making it possible for wallets to burn
     * the CashToken (intentionally or due to a bug). This is unlikely to
     * be a problem in practice because CashTokens can only be moved by
     * wallets which support the CashToken covenant template (no
     * backwards-compatibility with wallet which might unintentionally
     * burn tokens).
     * 
     * By not forward-validating outputs, we elliminate the need for each
     * CashToken covenant to be capable of correctly "identifying" its 
     * parent covenant during redeem transactions. (Instead, we verify the
     * lineage of the token by checking its parent and grandparent.)
     */

    OP_VERIFY // Signature validation must have been successful

    // Owner has authorized transfer, now prove lineage:

    // get parent tx's outpoint tx hash, then verify we have the grandparent
    <4> OP_SPLIT OP_NIP // remove and discard tx version (no validation)
    <1> OP_SPLIT OP_SWAP // get first byte of tx input count
    <0x02> OP_EQUALVERIFY // require exactly 2 inputs
    <32> OP_SPLIT // get parent outpoint tx hash
    <4> OP_SPLIT OP_DROP // get parent outpoint index, drop everything else
    OP_BIN2NUM
    <0> OP_EQUALVERIFY // must be grandparent's 0th output (grandparent may be 1th for mint)
    OP_TOALTSTACK // parent outpoint tx hash

    // validate and concat grandparent back together, confirming it used this covenant
    <
      0x02000000 // always require version 2
      0x02 // always require exactly 2 inputs
    >

    OP_SWAP // top is grandparent input 0 outpoint (hash + index)
    OP_SIZE <36> OP_EQUALVERIFY // require grandparent input 0 outpoint to be 36 bytes
    OP_CAT

    OP_SWAP // top is grandparent input 0 bytecode
    OP_SIZE 
    OP_DUP
    <4> OP_ROLL OP_EQUALVERIFY // provided bytecode is expected size
    OP_NUM2VARINT // serialize length
    OP_SWAP
    OP_CAT
    OP_CAT // grandparent TX up to input 1

    
    OP_SWAP // top is grandparent input 1 outpoint (hash + index)
    OP_SIZE <36> OP_EQUALVERIFY // require grandparent input 1 outpoint to be 36 bytes
    OP_CAT

    OP_SWAP // top is grandparent input 1 bytecode
    OP_SIZE
    OP_DUP
    <4> OP_ROLL OP_EQUALVERIFY // provided bytecode is expected size
    OP_NUM2VARINT // serialize length
    OP_SWAP
    OP_CAT
    OP_CAT // grandparent TX through inputs
    
    // start building grandparent outputs

    OP_SWAP // top is grandparent is_notarized
    <0> OP_EQUAL
    OP_IF
      /**
       * Grandparent is not notarized (mint transaction), the 1th output is the covenant.
       */
      <3> OP_CAT // require exatly 3 outputs in mint transaction
      OP_SWAP // top is 0th output satoshi value
      OP_SIZE <8> OP_EQUALVERIFY // satoshi value must be 8 bytes
      OP_CAT
      <$(<23> OP_NUM2VARINT)> // length of corporate covenant bytecode (P2SH)
      OP_CAT
      OP_SWAP
      OP_SIZE <23> OP_EQUALVERIFY // corporate covenant bytecode must be P2SH length
      OP_CAT
      // end of 0th output
    OP_ELSE
      /**
       * Grandparent is notarized, the 0th output is the covenant.
       */
      <0x02> OP_CAT // require exactly 2 outputs in transfer transactions
    OP_ENDIF

    OP_SWAP // top is grandparent's cashtoken covenant value
    OP_DUP
    /**
     * To support heterogenous tokens, this covenant prevents the
     * satoshi value of the token from changing during transfers.
     * 
     * However, we can't simply verify the "next" satoshi value is the
     * same as the "current" value - eventually, the token will be
     * redeemed with the corporation covenant, and the covenant's 
     * expected balance at that time can't necesssarily be predicted.
     * 
     * Instead, we only verify backwards by comparing the token output
     * values of the grandparent and current transaction. With this
     * limitation, we can know the token's value can't be modified
     * until the redeem transaction, where the parent covenant can
     * read the value before it is destroyed.
     */
    OP_UTXOVALUE <8> OP_NUM2BIN
    OP_EQUALVERIFY // disallow token value from being modified
    OP_CAT

    < <23> OP_HASH160 > OP_CAT // length of cashtoken covenant bytecode (P2SH)

    // begin transforming parent bytecode into grandparent bytecode for validation

    <OP_0> // is_notarized for mint transactions
    OP_UTXOBYTECODE // get parent bytecode
    <1> OP_SPLIT OP_NIP OP_CAT // replace is_notarized with OP_0
    
    <36> OP_SPLIT // preserve token ID (OP_0 + OP_PUSHBYTES_32 + 32 bytes + OP_TOALTSTACK + OP_TOALTSTACK)
    
    <1> OP_SPLIT OP_NIP // remove parent threshold pushing opcode (<<m>>)
    <3> OP_ROLL // add grandparent_threshold pushing opcode
    OP_SWAP // top is parent bytecode after removed m push

    <4> OP_ROLL // get parent public key count opcode

    // TODO: PRs welcome – are there more efficient ways to implement this "case" statement?
    // check for parent n of 1, 2, or 3 (saving the opcode)
    OP_TUCK 
    <<1>> OP_EQUAL OP_IF
      <34> OP_SPLIT
      OP_ELSE
      OP_OVER <<2>> OP_EQUAL OP_IF
        <$(<34> <34> OP_ADD)> OP_SPLIT
      OP_ELSE
        OP_OVER <<3>> OP_EQUAL OP_IF
          <$(<34> <34> <34> OP_ADD OP_ADD)> OP_SPLIT
        OP_ELSE
          OP_RETURN // fail, parent must match a planned n
        OP_ENDIF
      OP_ENDIF
    OP_ENDIF
    /**
     * TODO: SECURITY: do we need to validate these bytes? (E.g. in the
     * corporation covenant, they must be validated.) Can a malicious
     * grandparent transaction use different opcodes in these bytes to
     * defraud future token holders?
     */
    OP_NIP // drop the removed parent public key pushes
    OP_NIP // drop the parent public key count opcode
    <1> OP_SPLIT OP_NIP // drop parent n opcode from bytecode

    <4> OP_ROLL // pick grandparent public key count opcode
    // TODO: PRs welcome – there are definitely more efficient ways to implement this one
    OP_DUP
    <<1>> OP_EQUAL OP_IF
      <OP_PUSHBYTES_33> <6> OP_ROLL OP_SIZE <33> OP_EQUALVERIFY
      OP_CAT
      OP_ELSE
      OP_DUP <<2>> OP_EQUAL OP_IF
        <OP_PUSHBYTES_33> <6> OP_ROLL OP_SIZE <33> OP_EQUALVERIFY
        OP_CAT
        <OP_PUSHBYTES_33> <7> OP_ROLL OP_SIZE <33> OP_EQUALVERIFY 
        OP_CAT OP_CAT
      OP_ELSE
        OP_DUP <<3>> OP_EQUAL OP_IF
          <OP_PUSHBYTES_33> <6> OP_ROLL OP_SIZE <33> OP_EQUALVERIFY
          OP_CAT
          <OP_PUSHBYTES_33> <7> OP_ROLL OP_SIZE <33> OP_EQUALVERIFY
          OP_CAT OP_CAT
          <OP_PUSHBYTES_33> <7> OP_ROLL OP_SIZE <33> OP_EQUALVERIFY
          OP_CAT OP_CAT
        OP_ELSE
          OP_RETURN // fail, grandparent must match a planned n
        OP_ENDIF
      OP_ENDIF
    OP_ENDIF
    OP_SWAP OP_CAT // concat grandparent n opcode after public key pushes

    OP_SWAP
    OP_CAT OP_CAT OP_CAT // reconstructed grandparent redeem bytecode
    OP_HASH160 // get redeem script hash
    OP_CAT
    <OP_EQUAL> OP_CAT

    OP_SWAP // top is remaining TX serialization

    // no need to verify remaining grandparent outputs

    OP_CAT // full grandparent transaction
    OP_HASH256 // grandparent transaction hash
    OP_FROMALTSTACK // outpoint tx hash from parent

    OP_DROP OP_DROP <1> // debug: switch comment to skip check
    // OP_EQUAL // verify grandparent is parent outpoint tx

    // (don't bother dropping the mint parent hash left on altstack)

OP_ENDIF

You will notice I also use a OP_NUM2VARINT operation in this version – with PMv3, transaction integers are made compatible with Script Numbers, so those "VarInt conversion opcodes" can be simplified away.

@bitjson
Copy link
Author

bitjson commented Feb 4, 2021

@gandrewstone
Copy link

WRT OP_UTXOBYTECODE, I think you should call it OP_PUSHUTXOSCRIPT or something similar. I personally don't read the word bytecode as the full serialized script, so that was very confusing to me.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment