-
Notifications
You must be signed in to change notification settings - Fork 25
Add NSM transaction test vectors #101
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
Changes from all commits
1a26651
03902cc
3f9b6f8
53e1d21
ead1eb4
0e5581a
1f0676d
96512b4
28bdf05
615492c
42b168d
99c7be5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
|
|
@@ -26,6 +26,10 @@ | |||
| NU5_VERSION_GROUP_ID = 0x26A7270A | ||||
| NU5_TX_VERSION = 5 | ||||
|
|
||||
| V6_TX_VERSION = 6 | ||||
| # TODO: change this | ||||
| V6_VERSION_GROUP_ID = 0xFFFFFFFF | ||||
|
|
||||
| # Sapling note magic values, copied from src/zcash/Zcash.h | ||||
| NOTEENCRYPTION_AUTH_BYTES = 16 | ||||
| ZC_NOTEPLAINTEXT_LEADING = 1 | ||||
|
|
@@ -415,19 +419,35 @@ def __bytes__(self): | |||
| class TransactionV5(object): | ||||
| def __init__(self, rand, consensus_branch_id): | ||||
| # Decide which transaction parts will be generated. | ||||
| flip_coins = rand.u8() | ||||
| have_transparent_in = (flip_coins >> 0) % 2 | ||||
| have_transparent_out = (flip_coins >> 1) % 2 | ||||
| have_sapling = (flip_coins >> 2) % 2 | ||||
| have_orchard = (flip_coins >> 3) % 2 | ||||
| is_coinbase = (not have_transparent_in) and (flip_coins >> 4) % 2 | ||||
| flip_coins_result = rand.u8() | ||||
|
|
||||
| have_transparent_in = (flip_coins_result >> 0) % 2 | ||||
| have_transparent_out = (flip_coins_result >> 1) % 2 | ||||
| have_sapling = (flip_coins_result >> 2) % 2 | ||||
| have_orchard = (flip_coins_result >> 3) % 2 | ||||
| is_coinbase = (not have_transparent_in) and (flip_coins_result >> 4) % 2 | ||||
|
|
||||
| self.init_header(consensus_branch_id, rand) | ||||
| self.init_transparent(rand, have_transparent_in, have_transparent_out, is_coinbase) | ||||
| self.init_sapling(rand, have_sapling, is_coinbase) | ||||
|
|
||||
| # Satisfy consensus rules that require at least one input and at least one output. | ||||
| if (not (is_coinbase or have_transparent_in or have_sapling) | ||||
| or not (have_transparent_out or have_sapling)): | ||||
| have_orchard = True | ||||
|
|
||||
| self.init_orchard(rand, have_orchard, is_coinbase) | ||||
|
|
||||
| assert is_coinbase == self.is_coinbase() | ||||
|
|
||||
| def init_header(self, consensus_branch_id, rand): | ||||
| # Common Transaction Fields | ||||
| self.nVersionGroupId = NU5_VERSION_GROUP_ID | ||||
| self.nConsensusBranchId = consensus_branch_id | ||||
| self.nLockTime = rand.u32() | ||||
| self.nExpiryHeight = rand.u32() % TX_EXPIRY_HEIGHT_THRESHOLD | ||||
|
|
||||
| def init_transparent(self, rand, have_transparent_in, have_transparent_out, is_coinbase): | ||||
| # Transparent Transaction Fields | ||||
| self.vin = [] | ||||
| self.vout = [] | ||||
|
|
@@ -443,10 +463,12 @@ def __init__(self, rand, consensus_branch_id): | |||
| for _ in range((rand.u8() % 3) + 1): | ||||
| self.vout.append(TxOut(rand)) | ||||
|
|
||||
| def init_sapling(self, rand, have_sapling, is_coinbase): | ||||
| # Sapling Transaction Fields | ||||
| self.vSpendsSapling = [] | ||||
| self.vOutputsSapling = [] | ||||
|
daira marked this conversation as resolved.
|
||||
| if have_sapling: | ||||
| # This anchor will be ignored if there are no Sapling spends. | ||||
| self.anchorSapling = Fq(leos2ip(rand.b(32))) | ||||
| # We use the randomness unconditionally here to avoid unnecessary test vector changes. | ||||
| for _ in range(rand.u8() % 3): | ||||
|
|
@@ -455,17 +477,31 @@ def __init__(self, rand, consensus_branch_id): | |||
| self.vSpendsSapling.append(spend) | ||||
| for _ in range(rand.u8() % 3): | ||||
| self.vOutputsSapling.append(OutputDescription(rand)) | ||||
| self.valueBalanceSapling = rand.u64() % (MAX_MONEY + 1) | ||||
|
|
||||
| # valueBalanceSapling is "The net value of Sapling spends minus outputs." | ||||
| # So it's invalid to have a positive valueBalanceSapling if there are no spends, | ||||
| # or a negative valueBalanceSapling if there are no outputs (this is not enforced | ||||
| # as a separate consensus rule but it holds under the assumption of soundness | ||||
| # of the spend and output circuits). | ||||
| valueBalanceSapling = rand.u64() % (MAX_MONEY + 1) | ||||
| if len(self.vSpendsSapling) == 0: | ||||
| valueBalanceSapling = min(0, valueBalanceSapling) | ||||
| if len(self.vOutputsSapling) == 0: | ||||
| valueBalanceSapling = max(0, valueBalanceSapling) | ||||
| self.valueBalanceSapling = valueBalanceSapling | ||||
|
|
||||
| # This binding sig will be ignored if there are neither Sapling spends nor outputs. | ||||
| self.bindingSigSapling = RedJubjubSignature(rand) | ||||
| else: | ||||
| # If valueBalanceSapling is not present in the serialized transaction, then | ||||
| # v^balanceSapling is defined to be 0. | ||||
| self.valueBalanceSapling = 0 | ||||
|
|
||||
| def init_orchard(self, rand, have_orchard, is_coinbase): | ||||
| # Orchard Transaction Fields | ||||
| self.vActionsOrchard = [] | ||||
| if have_orchard: | ||||
|
daira marked this conversation as resolved.
|
||||
| for _ in range(rand.u8() % 5): | ||||
| for _ in range(max(1, rand.u8() % 5)): | ||||
| self.vActionsOrchard.append(OrchardActionDescription(rand)) | ||||
| self.flagsOrchard = rand.u8() & 3 # Only two flag bits are currently defined. | ||||
| if is_coinbase: | ||||
|
|
@@ -480,8 +516,6 @@ def __init__(self, rand, consensus_branch_id): | |||
| # v^balanceOrchard is defined to be 0. | ||||
| self.valueBalanceOrchard = 0 | ||||
|
|
||||
| assert is_coinbase == self.is_coinbase() | ||||
|
|
||||
| def version_bytes(self): | ||||
| return NU5_TX_VERSION | (1 << 31) | ||||
|
|
||||
|
|
@@ -493,13 +527,27 @@ def is_coinbase(self): | |||
| def __bytes__(self): | ||||
| ret = b'' | ||||
|
|
||||
| ret += self.header_bytes() | ||||
| ret += self.transparent_bytes() | ||||
| ret += self.sapling_bytes() | ||||
| ret += self.orchard_bytes() | ||||
|
|
||||
| return ret | ||||
|
|
||||
| def header_bytes(self): | ||||
| ret = b'' | ||||
|
|
||||
| # Common Transaction Fields | ||||
| ret += struct.pack('<I', self.version_bytes()) | ||||
| ret += struct.pack('<I', self.nVersionGroupId) | ||||
| ret += struct.pack('<I', self.nConsensusBranchId) | ||||
| ret += struct.pack('<I', self.nLockTime) | ||||
| ret += struct.pack('<I', self.nExpiryHeight) | ||||
|
|
||||
| return ret | ||||
|
|
||||
| def transparent_bytes(self): | ||||
| ret = b'' | ||||
| # Transparent Transaction Fields | ||||
| ret += write_compact_size(len(self.vin)) | ||||
| for x in self.vin: | ||||
|
|
@@ -508,6 +556,10 @@ def __bytes__(self): | |||
| for x in self.vout: | ||||
| ret += bytes(x) | ||||
|
|
||||
| return ret | ||||
|
|
||||
| def sapling_bytes(self): | ||||
| ret = b'' | ||||
| # Sapling Transaction Fields | ||||
| hasSapling = len(self.vSpendsSapling) + len(self.vOutputsSapling) > 0 | ||||
| ret += write_compact_size(len(self.vSpendsSapling)) | ||||
|
|
@@ -531,6 +583,10 @@ def __bytes__(self): | |||
| if hasSapling: | ||||
| ret += bytes(self.bindingSigSapling) | ||||
|
|
||||
| return ret | ||||
|
|
||||
| def orchard_bytes(self): | ||||
| ret = b'' | ||||
| # Orchard Transaction Fields | ||||
| ret += write_compact_size(len(self.vActionsOrchard)) | ||||
| if len(self.vActionsOrchard) > 0: | ||||
|
|
@@ -549,12 +605,34 @@ def __bytes__(self): | |||
|
|
||||
| return ret | ||||
|
|
||||
| class TransactionV6(TransactionV5): | ||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. FYI: In #108, the
Then Given that this PR is probably going to be merged first, should we apply that change here ? |
||||
| def init_header(self, consensus_branch_id, rand): | ||||
| # Common Transaction Fields | ||||
| self.nVersionGroupId = V6_VERSION_GROUP_ID | ||||
| self.nConsensusBranchId = consensus_branch_id | ||||
| self.nLockTime = rand.u32() | ||||
| self.nExpiryHeight = rand.u32() % TX_EXPIRY_HEIGHT_THRESHOLD | ||||
| self.zip233Amount = 0 | ||||
|
|
||||
| def version_bytes(self): | ||||
| return V6_TX_VERSION | (1 << 31) | ||||
|
|
||||
| def header_bytes(self): | ||||
| ret = b'' | ||||
|
|
||||
| ret += super().header_bytes() | ||||
| ret += struct.pack('<Q', self.zip233Amount) | ||||
|
|
||||
| return ret | ||||
|
|
||||
| class Transaction(object): | ||||
| def __init__(self, rand, version, consensus_branch_id=None): | ||||
| if version == NU5_TX_VERSION: | ||||
| assert consensus_branch_id is not None | ||||
| self.inner = TransactionV5(rand, consensus_branch_id) | ||||
| elif version == V6_TX_VERSION: | ||||
| assert consensus_branch_id is not None | ||||
| self.inner = TransactionV6(rand, consensus_branch_id) | ||||
| else: | ||||
| self.inner = LegacyTransaction(rand, version) | ||||
|
|
||||
|
|
||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,91 @@ | ||
| #!/usr/bin/env python3 | ||
| import sys; assert sys.version_info[0] >= 3, "Python 3 required." | ||
|
|
||
| from .transaction import TransactionV6 | ||
| from .output import render_args, render_tv | ||
| from .rand import Rand | ||
| from .zip_0143 import ( | ||
| SIGHASH_ALL, | ||
| SIGHASH_ANYONECANPAY, | ||
| SIGHASH_NONE, | ||
| SIGHASH_SINGLE, | ||
| ) | ||
|
|
||
| from .zip_0244 import * | ||
|
|
||
| def main(): | ||
| args = render_args() | ||
|
|
||
| from random import Random | ||
| rng = Random(0xB7D6_0F44) | ||
| rand = randbytes(rng) | ||
|
|
||
| consensusBranchId = 0xFFFF_FFFF # ZFUTURE | ||
|
|
||
| test_vectors = [] | ||
| for _ in range(10): | ||
| tx = TransactionV6(rand, consensusBranchId) | ||
|
|
||
| # Generate amounts and scriptCodes for each non-dummy transparent input. | ||
| t_inputs = [] | ||
| sum_amount = 0 | ||
| in_count = len(tx.vin) | ||
| if not tx.is_coinbase() and in_count > 0: | ||
| t_inputs = [TransparentInput(i, rand, MAX_MONEY // (in_count-1)) for i in range(in_count-1)] | ||
| sum_amount = sum(x.amount for x in t_inputs) | ||
| # Ensure that at least one of the inputs can reach the full range. | ||
| t_inputs.append(TransparentInput(in_count-1, rand, MAX_MONEY - sum_amount)) | ||
| sum_amount += t_inputs[in_count-1].amount | ||
|
|
||
| tx.zip233Amount = rand.u64() % (MAX_MONEY - sum_amount + 1) | ||
| # Make half the zip233Amounts = 0 for a more realistic distribution. | ||
| if rand.u8() % 2 == 0: | ||
| tx.zip233Amount = 0 | ||
|
|
||
| txid = txid_digest(tx) | ||
| auth = auth_digest(tx) | ||
|
|
||
| [sighash_shielded, other_sighashes, txin] = generate_sighashes_and_txin(tx, t_inputs, rand) | ||
|
|
||
| test_vectors.append({ | ||
| 'tx': bytes(tx), | ||
| 'txid': txid, | ||
| 'auth_digest': auth, | ||
| 'amounts': [x.amount for x in t_inputs], | ||
| 'zip233_amount': tx.zip233Amount, | ||
| 'script_pubkeys': [x.scriptPubKey.raw() for x in t_inputs], | ||
| 'transparent_input': None if txin is None else txin.nIn, | ||
| 'sighash_shielded': sighash_shielded, | ||
| 'sighash_all': other_sighashes.get(SIGHASH_ALL), | ||
| 'sighash_none': other_sighashes.get(SIGHASH_NONE), | ||
| 'sighash_single': other_sighashes.get(SIGHASH_SINGLE), | ||
| 'sighash_all_anyone': other_sighashes.get(SIGHASH_ALL | SIGHASH_ANYONECANPAY), | ||
| 'sighash_none_anyone': other_sighashes.get(SIGHASH_NONE | SIGHASH_ANYONECANPAY), | ||
| 'sighash_single_anyone': other_sighashes.get(SIGHASH_SINGLE | SIGHASH_ANYONECANPAY), | ||
| }) | ||
|
|
||
| render_tv( | ||
| args, | ||
| 'zip_0233', | ||
| ( | ||
| ('tx', {'rust_type': 'Vec<u8>', 'bitcoin_flavoured': False}), | ||
| ('txid', '[u8; 32]'), | ||
| ('auth_digest', '[u8; 32]'), | ||
| ('amounts', 'Vec<i64>'), | ||
| ('zip233_amount', 'u64'), | ||
| ('script_pubkeys', {'rust_type': 'Vec<Vec<u8>>', 'bitcoin_flavoured': False}), | ||
| ('transparent_input', 'Option<u32>'), | ||
| ('sighash_shielded', '[u8; 32]'), | ||
| ('sighash_all', 'Option<[u8; 32]>'), | ||
| ('sighash_none', 'Option<[u8; 32]>'), | ||
| ('sighash_single', 'Option<[u8; 32]>'), | ||
| ('sighash_all_anyone', 'Option<[u8; 32]>'), | ||
| ('sighash_none_anyone', 'Option<[u8; 32]>'), | ||
| ('sighash_single_anyone', 'Option<[u8; 32]>'), | ||
| ), | ||
| test_vectors, | ||
| ) | ||
|
|
||
|
|
||
| if __name__ == '__main__': | ||
| main() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This intentionally isn't defined yet because ZIP 230 is not yet stable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've added this to match the versions in librustzcash, ensuring the generated vectors used in the librustzcash tests are correct.