diff --git a/transaction.py b/transaction.py index 19aba90..ac5f803 100644 --- a/transaction.py +++ b/transaction.py @@ -1,8 +1,17 @@ #!/usr/bin/env python3 import struct +from orchard_pallas import ( + Fp as PallasBase, + Scalar as PallasScalar, +) +from orchard_sinsemilla import group_hash as pallas_group_hash from sapling_generators import find_group_hash, SPENDING_KEY_BASE -from sapling_jubjub import Fq, Point +from sapling_jubjub import ( + Fq, + Point, + Fr as JubjubScalar, +) from utils import leos2ip from zc_utils import write_compact_size @@ -15,6 +24,9 @@ OVERWINTER_TX_VERSION = 3 SAPLING_VERSION_GROUP_ID = 0x892F2085 SAPLING_TX_VERSION = 4 +NU5_VERSION_GROUP_ID = 0x26A7270A +NU5_TX_VERSION = 5 + # Sapling note magic values, copied from src/zcash/Zcash.h NOTEENCRYPTION_AUTH_BYTES = 16 ZC_NOTEPLAINTEXT_LEADING = 1 @@ -75,14 +87,43 @@ class GrothProof(object): self.g_C ) -class SpendDescription(object): +class RedJubjubSignature(object): def __init__(self, rand): + self.R = find_group_hash(b'TVRandPt', rand.b(32)) + self.S = JubjubScalar(leos2ip(rand.b(32))) + + def __bytes__(self): + return ( + bytes(self.R) + + bytes(self.S) + ) + +class RedPallasSignature(object): + def __init__(self, rand): + self.R = pallas_group_hash(b'TVRandPt', rand.b(32)) + self.S = PallasScalar(leos2ip(rand.b(32))) + + def __bytes__(self): + return ( + bytes(self.R) + + bytes(self.S) + ) + +class SpendDescription(object): + def __init__(self, rand, anchor=None): self.cv = find_group_hash(b'TVRandPt', rand.b(32)) - self.anchor = Fq(leos2ip(rand.b(32))) + self.anchor = Fq(leos2ip(rand.b(32))) if anchor is None else anchor self.nullifier = rand.b(32) self.rk = Point.rand(rand) self.proof = GrothProof(rand) - self.spendAuthSig = rand.b(64) # Invalid + self.spendAuthSig = rand.b(64) if anchor is None else RedJubjubSignature(rand) # Invalid + + def bytes_v5(self): + return ( + bytes(self.cv) + + self.nullifier + + bytes(self.rk) + ) def __bytes__(self): return ( @@ -103,16 +144,43 @@ class OutputDescription(object): self.outCipherText = rand.b(ZC_SAPLING_OUTCIPHERTEXT_SIZE) self.proof = GrothProof(rand) - def __bytes__(self): + def bytes_v5(self): return ( bytes(self.cv) + bytes(self.cmu) + bytes(self.ephemeralKey) + self.encCiphertext + - self.outCipherText + + self.outCipherText + ) + + def __bytes__(self): + return ( + self.bytes_v5() + bytes(self.proof) ) +class OrchardActionDescription(object): + def __init__(self, rand): + self.cv = pallas_group_hash(b'TVRandPt', rand.b(32)) + self.nullifier = PallasBase(leos2ip(rand.b(32))) + self.rk = pallas_group_hash(b'TVRandPt', rand.b(32)) + self.cmx = PallasBase(leos2ip(rand.b(32))) + self.ephemeralKey = pallas_group_hash(b'TVRandPt', rand.b(32)) + self.encCiphertext = rand.b(ZC_SAPLING_ENCCIPHERTEXT_SIZE) + self.outCiphertext = rand.b(ZC_SAPLING_OUTCIPHERTEXT_SIZE) + self.spendAuthSig = RedPallasSignature(rand) + + def __bytes__(self): + return ( + bytes(self.cv) + + bytes(self.nullifier) + + bytes(self.rk) + + bytes(self.cmx) + + bytes(self.ephemeralKey) + + self.encCiphertext + + self.outCiphertext + ) + class JoinSplit(object): def __init__(self, rand, fUseGroth = False): self.vpub_old = 0 @@ -197,7 +265,7 @@ class TxOut(object): return struct.pack('= SAPLING_TX_VERSION: self.bindingSig = rand.b(64) # Invalid - def header(self): + def version_bytes(self): return self.nVersion | (1 << 31 if self.fOverwintered else 0) def __bytes__(self): ret = b'' - ret += struct.pack('> 0) % 2 + have_transparent_out = (flip_coins >> 1) % 2 + have_sapling = (flip_coins >> 2) % 2 + have_orchard = (flip_coins >> 3) % 2 + + # 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 + + # Transparent Transaction Fields + self.vin = [] + self.vout = [] + if have_transparent_in: + for _ in range((rand.u8() % 3) + 1): + self.vin.append(TxIn(rand)) + if have_transparent_out: + for _ in range((rand.u8() % 3) + 1): + self.vout.append(TxOut(rand)) + + # Sapling Transaction Fields + self.vSpendsSapling = [] + self.vOutputsSapling = [] + if have_sapling: + self.anchorSapling = Fq(leos2ip(rand.b(32))) + for _ in range(rand.u8() % 3): + self.vSpendsSapling.append(SpendDescription(rand, self.anchorSapling)) + for _ in range(rand.u8() % 3): + self.vOutputsSapling.append(OutputDescription(rand)) + self.valueBalanceSapling = rand.u64() % (MAX_MONEY + 1) + 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 + + # Orchard Transaction Fields + self.vActionsOrchard = [] + if have_orchard: + for _ in range(rand.u8() % 5): + self.vActionsOrchard.append(OrchardActionDescription(rand)) + self.flagsOrchard = rand.u8() & 3 # Only two flag bits are currently defined. + self.valueBalanceOrchard = rand.u64() % (MAX_MONEY + 1) + self.anchorOrchard = PallasBase(leos2ip(rand.b(32))) + self.proofsOrchard = rand.b(rand.u8() + 32) # Proof will always contain at least one element + self.bindingSigOrchard = RedPallasSignature(rand) + else: + # If valueBalanceOrchard is not present in the serialized transaction, then + # v^balanceOrchard is defined to be 0. + self.valueBalanceOrchard = 0 + + def version_bytes(self): + return NU5_TX_VERSION | (1 << 31) + + # TODO: Update ZIP 225 to document endianness + def __bytes__(self): + ret = b'' + + # Common Transaction Fields + ret += struct.pack(' 0 + ret += write_compact_size(len(self.vSpendsSapling)) + for desc in self.vSpendsSapling: + ret += desc.bytes_v5() + ret += write_compact_size(len(self.vOutputsSapling)) + for desc in self.vOutputsSapling: + ret += desc.bytes_v5() + if hasSapling: + ret += struct.pack(' 0: + ret += bytes(self.anchorSapling) + # Not explicitly gated in the protocol spec, but if the gate + # were inactive then these loops would be empty by definition. + for desc in self.vSpendsSapling: # vSpendProofsSapling + ret += bytes(desc.proof) + for desc in self.vSpendsSapling: # vSpendAuthSigsSapling + ret += bytes(desc.spendAuthSig) + for desc in self.vOutputsSapling: # vOutputProofsSapling + ret += bytes(desc.proof) + if hasSapling: + ret += bytes(self.bindingSigSapling) + + # Orchard Transaction Fields + ret += write_compact_size(len(self.vActionsOrchard)) + if len(self.vActionsOrchard) > 0: + # Not explicitly gated in the protocol spec, but if the gate + # were inactive then these loops would be empty by definition. + for desc in self.vActionsOrchard: + ret += bytes(desc) # Excludes spendAuthSig + ret += struct.pack('B', self.flagsOrchard) + ret += struct.pack(' 0: + digest.update(getHashPrevouts(tx, b'ZTxIdPrevoutHash')) + digest.update(getHashSequence(tx, b'ZTxIdSequencHash')) + digest.update(getHashOutputs(tx, b'ZTxIdOutputsHash')) + + return digest.digest() + +# Sapling + +def sapling_digest(tx): + digest = blake2b(digest_size=32, person=b'ZTxIdSaplingHash') + + if len(tx.vSpendsSapling) + len(tx.vOutputsSapling) > 0: + digest.update(sapling_spends_digest(tx)) + digest.update(sapling_outputs_digest(tx)) + digest.update(struct.pack(' 0: + digest.update(sapling_spends_compact_digest(tx)) + digest.update(sapling_spends_noncompact_digest(tx)) + + return digest.digest() + +def sapling_spends_compact_digest(tx): + digest = blake2b(digest_size=32, person=b'ZTxIdSSpendCHash') + for desc in tx.vSpendsSapling: + digest.update(desc.nullifier) + return digest.digest() + +def sapling_spends_noncompact_digest(tx): + digest = blake2b(digest_size=32, person=b'ZTxIdSSpendNHash') + for desc in tx.vSpendsSapling: + digest.update(bytes(desc.cv)) + digest.update(bytes(desc.anchor)) + digest.update(bytes(desc.rk)) + return digest.digest() + +# - Outputs + +def sapling_outputs_digest(tx): + digest = blake2b(digest_size=32, person=b'ZTxIdSOutputHash') + + if len(tx.vOutputsSapling) > 0: + digest.update(sapling_outputs_compact_digest(tx)) + digest.update(sapling_outputs_memos_digest(tx)) + digest.update(sapling_outputs_noncompact_digest(tx)) + + return digest.digest() + +def sapling_outputs_compact_digest(tx): + digest = blake2b(digest_size=32, person=b'ZTxIdSOutC__Hash') + for desc in tx.vOutputsSapling: + digest.update(bytes(desc.cmu)) + digest.update(bytes(desc.ephemeralKey)) + digest.update(desc.encCiphertext[:52]) + return digest.digest() + +def sapling_outputs_memos_digest(tx): + digest = blake2b(digest_size=32, person=b'ZTxIdSOutM__Hash') + for desc in tx.vOutputsSapling: + digest.update(desc.encCiphertext[52:564]) + return digest.digest() + +def sapling_outputs_noncompact_digest(tx): + digest = blake2b(digest_size=32, person=b'ZTxIdSOutN__Hash') + for desc in tx.vOutputsSapling: + digest.update(bytes(desc.cv)) + digest.update(desc.encCiphertext[564:]) + digest.update(desc.outCipherText) + return digest.digest() + +# Orchard + +def orchard_digest(tx): + digest = blake2b(digest_size=32, person=b'ZTxIdOrchardHash') + + if len(tx.vActionsOrchard) > 0: + digest.update(orchard_actions_compact_digest(tx)) + digest.update(orchard_actions_memos_digest(tx)) + digest.update(orchard_actions_noncompact_digest(tx)) + digest.update(struct.pack(' 0: + txin = TransparentInput(tx, rand) + else: + txin = None + + sighash_all = signature_digest(tx, SIGHASH_ALL, txin) + other_sighashes = None if txin is None else [ + signature_digest(tx, nHashType, txin) + for nHashType in [ + SIGHASH_NONE, + SIGHASH_SINGLE, + SIGHASH_ALL | SIGHASH_ANYONECANPAY, + SIGHASH_NONE | SIGHASH_ANYONECANPAY, + SIGHASH_SINGLE | SIGHASH_ANYONECANPAY, + ] + ] + + test_vectors.append({ + 'tx': bytes(tx), + 'txid': txid, + 'transparent_input': None if txin is None else txin.nIn, + 'script_code': None if txin is None else txin.scriptCode.raw(), + 'amount': None if txin is None else txin.amount, + 'sighash_all': sighash_all, + 'sighash_none': None if txin is None else other_sighashes[0], + 'sighash_single': None if txin is None else other_sighashes[1], + 'sighash_all_anyone': None if txin is None else other_sighashes[2], + 'sighash_none_anyone': None if txin is None else other_sighashes[3], + 'sighash_single_anyone': None if txin is None else other_sighashes[4], + }) + + render_tv( + args, + 'zip_0244', + ( + ('tx', {'rust_type': 'Vec', 'bitcoin_flavoured': False}), + ('txid', '[u8; 32]'), + ('transparent_input', { + 'rust_type': 'Option', + 'rust_fmt': lambda x: None if x is None else Some(x), + }), + ('script_code', { + 'rust_type': 'Option>', + 'rust_fmt': lambda x: None if x is None else Some(x), + }), + ('amount', { + 'rust_type': 'Option', + 'rust_fmt': lambda x: None if x is None else Some(x), + }), + ('sighash_all', '[u8; 32]'), + ('sighash_none', { + 'rust_type': 'Option<[u8; 32]>', + 'rust_fmt': lambda x: None if x is None else Some(x), + }), + ('sighash_single', { + 'rust_type': 'Option<[u8; 32]>', + 'rust_fmt': lambda x: None if x is None else Some(x), + }), + ('sighash_all_anyone', { + 'rust_type': 'Option<[u8; 32]>', + 'rust_fmt': lambda x: None if x is None else Some(x), + }), + ('sighash_none_anyone', { + 'rust_type': 'Option<[u8; 32]>', + 'rust_fmt': lambda x: None if x is None else Some(x), + }), + ('sighash_single_anyone', { + 'rust_type': 'Option<[u8; 32]>', + 'rust_fmt': lambda x: None if x is None else Some(x), + }), + ), + test_vectors, + ) + + +if __name__ == '__main__': + main()