From b9ce8224b65881690e541582c45619617871018e Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Fri, 23 Apr 2021 08:47:47 +1200 Subject: [PATCH 1/5] Implement structural generator for v5 transaction format (ZIP 225) --- transaction.py | 217 +++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 210 insertions(+), 7 deletions(-) diff --git a/transaction.py b/transaction.py index 19aba90..24567ce 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('> 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 header(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(' Date: Thu, 6 May 2021 08:13:03 +1200 Subject: [PATCH 2/5] Test vectors for ZIP 244 --- zip_0143.py | 12 +- zip_0244.py | 349 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 355 insertions(+), 6 deletions(-) create mode 100644 zip_0244.py diff --git a/zip_0143.py b/zip_0143.py index 0403d84..72d6eb5 100644 --- a/zip_0143.py +++ b/zip_0143.py @@ -19,20 +19,20 @@ SIGHASH_ANYONECANPAY = 0x80 NOT_AN_INPUT = -1 # For portability of the test vectors; replaced with None for Rust -def getHashPrevouts(tx): - digest = blake2b(digest_size=32, person=b'ZcashPrevoutHash') +def getHashPrevouts(tx, person=b'ZcashPrevoutHash'): + digest = blake2b(digest_size=32, person=person) for x in tx.vin: digest.update(bytes(x.prevout)) return digest.digest() -def getHashSequence(tx): - digest = blake2b(digest_size=32, person=b'ZcashSequencHash') +def getHashSequence(tx, person=b'ZcashSequencHash'): + digest = blake2b(digest_size=32, person=person) for x in tx.vin: digest.update(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() From a5a73402909462ae8b0aa0bd73a81dd6eab45d71 Mon Sep 17 00:00:00 2001 From: str4d Date: Wed, 26 May 2021 17:50:50 +0100 Subject: [PATCH 3/5] Remove unnecessary txin parameters These were leftover from an earlier version of the PR. Co-authored-by: ying tong --- zip_0244.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/zip_0244.py b/zip_0244.py index 6f96b59..a9d6f68 100644 --- a/zip_0244.py +++ b/zip_0244.py @@ -199,21 +199,21 @@ def transparent_sig_digest(tx, nHashType, txin): digest = blake2b(digest_size=32, person=b'ZTxIdTranspaHash') - digest.update(prevouts_sig_digest(tx, nHashType, txin)) - digest.update(sequence_sig_digest(tx, nHashType, txin)) + digest.update(prevouts_sig_digest(tx, nHashType)) + digest.update(sequence_sig_digest(tx, nHashType)) digest.update(outputs_sig_digest(tx, nHashType, txin)) digest.update(txin_sig_digest(tx, txin)) return digest.digest() -def prevouts_sig_digest(tx, nHashType, txin): +def prevouts_sig_digest(tx, nHashType): # If the SIGHASH_ANYONECANPAY flag is not set: if not (nHashType & SIGHASH_ANYONECANPAY): return getHashPrevouts(tx, b'ZTxIdPrevoutHash') else: return blake2b(digest_size=32, person=b'ZTxIdPrevoutHash').digest() -def sequence_sig_digest(tx, nHashType, txin): +def sequence_sig_digest(tx, nHashType): # if the SIGHASH_ANYONECANPAY flag is not set, and the sighash type is neither # SIGHASH_SINGLE nor SIGHASH_NONE: if ( From bc75d044a1954a5ea792cb8635cfa926015b75c6 Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Wed, 26 May 2021 18:01:00 +0100 Subject: [PATCH 4/5] Use specific tx formats compatible with sighash algorithms ZIP 244 is only defined for v5 transactions, so use the TransactionV5 type directly; likewise use LegacyTransaction with the ZIP 143 and ZIP 243 APIs. --- zip_0143.py | 4 ++-- zip_0243.py | 4 ++-- zip_0244.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/zip_0143.py b/zip_0143.py index 72d6eb5..12218ca 100644 --- a/zip_0143.py +++ b/zip_0143.py @@ -3,10 +3,10 @@ from pyblake2 import blake2b import struct from transaction import ( + LegacyTransaction, MAX_MONEY, OVERWINTER_TX_VERSION, Script, - Transaction, ) from tv_output import render_args, render_tv, Some from tv_rand import Rand @@ -111,7 +111,7 @@ def main(): test_vectors = [] for i in range(10): - tx = Transaction(rand, OVERWINTER_TX_VERSION) + tx = LegacyTransaction(rand, OVERWINTER_TX_VERSION) scriptCode = Script(rand) nIn = rand.i8() % (len(tx.vin) + 1) if nIn == len(tx.vin): diff --git a/zip_0243.py b/zip_0243.py index 3edef09..fdb42bd 100644 --- a/zip_0243.py +++ b/zip_0243.py @@ -3,10 +3,10 @@ from pyblake2 import blake2b import struct from transaction import ( + LegacyTransaction, MAX_MONEY, SAPLING_TX_VERSION, Script, - Transaction, ) from tv_output import render_args, render_tv, Some from tv_rand import Rand @@ -118,7 +118,7 @@ def main(): test_vectors = [] for _ in range(10): - tx = Transaction(rand, SAPLING_TX_VERSION) + tx = LegacyTransaction(rand, SAPLING_TX_VERSION) scriptCode = Script(rand) nIn = rand.i8() % (len(tx.vin) + 1) if nIn == len(tx.vin): diff --git a/zip_0244.py b/zip_0244.py index a9d6f68..3dec433 100644 --- a/zip_0244.py +++ b/zip_0244.py @@ -6,7 +6,7 @@ from transaction import ( MAX_MONEY, NU5_TX_VERSION, Script, - Transaction, + TransactionV5, ) from tv_output import render_args, render_tv, Some from tv_rand import Rand @@ -266,7 +266,7 @@ def main(): test_vectors = [] for _ in range(10): - tx = Transaction(rand, NU5_TX_VERSION, consensusBranchId) + tx = TransactionV5(rand, consensusBranchId) txid = txid_digest(tx) # If there are any transparent inputs, derive a corresponding transparent sighash. From 445c2602f40a5fbe5f92c739e59551423f8d2bde Mon Sep 17 00:00:00 2001 From: Jack Grigg Date: Wed, 26 May 2021 18:12:16 +0100 Subject: [PATCH 5/5] Rename header() to version_bytes() Now that the v5 transaction format has an actual header region, this old naming makes less sense. --- transaction.py | 8 ++++---- zip_0143.py | 2 +- zip_0243.py | 2 +- zip_0244.py | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/transaction.py b/transaction.py index 24567ce..ac5f803 100644 --- a/transaction.py +++ b/transaction.py @@ -311,12 +311,12 @@ class LegacyTransaction(object): if self.nVersion >= 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('