Skip to content
Merged
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
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ zip_0032_arbitrary = "zcash_test_vectors.zip_0032:arbitrary_key_derivation_tvs"
zip_0143 = "zcash_test_vectors.zip_0143:main"
zip_0243 = "zcash_test_vectors.zip_0243:main"
zip_0244 = "zcash_test_vectors.zip_0244:main"
zip_0233 = "zcash_test_vectors.zip_0233:main"

# Transparent test vectors
bip_0032 = "zcash_test_vectors.transparent.bip_0032:main"
Expand Down
14 changes: 14 additions & 0 deletions test-vectors/json/zip_0233.json

Large diffs are not rendered by default.

18 changes: 9 additions & 9 deletions test-vectors/json/zip_0244.json

Large diffs are not rendered by default.

416 changes: 416 additions & 0 deletions test-vectors/rust/zip_0233.rs

Large diffs are not rendered by default.

264 changes: 128 additions & 136 deletions test-vectors/rust/zip_0244.rs

Large diffs are not rendered by default.

18 changes: 9 additions & 9 deletions test-vectors/zcash/zip_0244.json

Large diffs are not rendered by default.

98 changes: 88 additions & 10 deletions zcash_test_vectors/transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Copy link
Copy Markdown
Contributor

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.

Copy link
Copy Markdown
Contributor

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.


# Sapling note magic values, copied from src/zcash/Zcash.h
NOTEENCRYPTION_AUTH_BYTES = 16
ZC_NOTEPLAINTEXT_LEADING = 1
Expand Down Expand Up @@ -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 = []
Expand All @@ -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 = []
Comment thread
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):
Expand All @@ -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:
Comment thread
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:
Expand All @@ -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)

Expand All @@ -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:
Expand All @@ -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))
Expand All @@ -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:
Expand All @@ -549,12 +605,34 @@ def __bytes__(self):

return ret

class TransactionV6(TransactionV5):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI: In #108, the TransactionV5 is renamed to TransactionBase:

class TransactionBase(object):

Then TransactionV5(TransactionBase) and TransactionV6(TransactionBase) are defined. I think that makes sense.

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)

Expand Down
91 changes: 91 additions & 0 deletions zcash_test_vectors/zip_0233.py
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()
Loading