Merge pull request #20 from zcash-hackworks/tx-v5
Test vectors for ZIP 225 and ZIP 244
This commit is contained in:
commit
cd50b88703
221
transaction.py
221
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('<Q', self.nValue) + bytes(self.scriptPubKey)
|
||||
|
||||
|
||||
class Transaction(object):
|
||||
class LegacyTransaction(object):
|
||||
def __init__(self, rand, version):
|
||||
if version == OVERWINTER_TX_VERSION:
|
||||
self.fOverwintered = True
|
||||
|
@ -243,12 +311,12 @@ class Transaction(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('<I', self.header())
|
||||
ret += struct.pack('<I', self.version_bytes())
|
||||
if self.fOverwintered:
|
||||
ret += struct.pack('<I', self.nVersionGroupId)
|
||||
|
||||
|
@ -295,3 +363,138 @@ class Transaction(object):
|
|||
ret += self.bindingSig
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
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
|
||||
|
||||
# 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('<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)
|
||||
|
||||
# Transparent Transaction Fields
|
||||
ret += write_compact_size(len(self.vin))
|
||||
for x in self.vin:
|
||||
ret += bytes(x)
|
||||
ret += write_compact_size(len(self.vout))
|
||||
for x in self.vout:
|
||||
ret += bytes(x)
|
||||
|
||||
# Sapling Transaction Fields
|
||||
hasSapling = len(self.vSpendsSapling) + len(self.vOutputsSapling) > 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('<Q', self.valueBalanceSapling)
|
||||
if len(self.vSpendsSapling) > 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('<Q', self.valueBalanceOrchard)
|
||||
ret += bytes(self.anchorOrchard)
|
||||
ret += write_compact_size(len(self.proofsOrchard))
|
||||
ret += self.proofsOrchard
|
||||
for desc in self.vActionsOrchard:
|
||||
ret += bytes(desc.spendAuthSig)
|
||||
ret += bytes(self.bindingSigOrchard)
|
||||
|
||||
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)
|
||||
else:
|
||||
self.inner = LegacyTransaction(rand, version)
|
||||
|
||||
def __getattr__(self, item):
|
||||
return getattr(self.inner, item)
|
||||
|
||||
def __bytes__(self):
|
||||
return bytes(self.inner)
|
||||
|
|
18
zip_0143.py
18
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
|
||||
|
@ -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('<I', x.nSequence))
|
||||
return digest.digest()
|
||||
|
||||
def getHashOutputs(tx):
|
||||
digest = blake2b(digest_size=32, person=b'ZcashOutputsHash')
|
||||
def getHashOutputs(tx, person=b'ZcashOutputsHash'):
|
||||
digest = blake2b(digest_size=32, person=person)
|
||||
for x in tx.vout:
|
||||
digest.update(bytes(x))
|
||||
return digest.digest()
|
||||
|
@ -76,7 +76,7 @@ def signature_hash(scriptCode, tx, nIn, nHashType, amount, consensusBranchId):
|
|||
person=b'ZcashSigHash' + struct.pack('<I', consensusBranchId),
|
||||
)
|
||||
|
||||
digest.update(struct.pack('<I', tx.header()))
|
||||
digest.update(struct.pack('<I', tx.version_bytes()))
|
||||
digest.update(struct.pack('<I', tx.nVersionGroupId))
|
||||
digest.update(hashPrevouts)
|
||||
digest.update(hashSequence)
|
||||
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
@ -80,7 +80,7 @@ def signature_hash(scriptCode, tx, nIn, nHashType, amount, consensusBranchId):
|
|||
person=b'ZcashSigHash' + struct.pack('<I', consensusBranchId),
|
||||
)
|
||||
|
||||
digest.update(struct.pack('<I', tx.header()))
|
||||
digest.update(struct.pack('<I', tx.version_bytes()))
|
||||
digest.update(struct.pack('<I', tx.nVersionGroupId))
|
||||
digest.update(hashPrevouts)
|
||||
digest.update(hashSequence)
|
||||
|
@ -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):
|
||||
|
|
|
@ -0,0 +1,349 @@
|
|||
#!/usr/bin/env python3
|
||||
from pyblake2 import blake2b
|
||||
import struct
|
||||
|
||||
from transaction import (
|
||||
MAX_MONEY,
|
||||
NU5_TX_VERSION,
|
||||
Script,
|
||||
TransactionV5,
|
||||
)
|
||||
from tv_output import render_args, render_tv, Some
|
||||
from tv_rand import Rand
|
||||
from zip_0143 import (
|
||||
getHashOutputs,
|
||||
getHashPrevouts,
|
||||
getHashSequence,
|
||||
SIGHASH_ALL,
|
||||
SIGHASH_ANYONECANPAY,
|
||||
SIGHASH_NONE,
|
||||
SIGHASH_SINGLE,
|
||||
)
|
||||
|
||||
|
||||
# Transparent
|
||||
|
||||
def transparent_digest(tx):
|
||||
digest = blake2b(digest_size=32, person=b'ZTxIdTranspaHash')
|
||||
|
||||
if len(tx.vin) + len(tx.vout) > 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('<Q', tx.valueBalanceSapling))
|
||||
|
||||
return digest.digest()
|
||||
|
||||
# - Spends
|
||||
|
||||
def sapling_spends_digest(tx):
|
||||
digest = blake2b(digest_size=32, person=b'ZTxIdSSpendsHash')
|
||||
|
||||
if len(tx.vSpendsSapling) > 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('<B', tx.flagsOrchard))
|
||||
digest.update(struct.pack('<Q', tx.valueBalanceOrchard))
|
||||
digest.update(bytes(tx.anchorOrchard))
|
||||
|
||||
return digest.digest()
|
||||
|
||||
# - Actions
|
||||
|
||||
def orchard_actions_compact_digest(tx):
|
||||
digest = blake2b(digest_size=32, person=b'ZTxIdOrcActCHash')
|
||||
for desc in tx.vActionsOrchard:
|
||||
digest.update(bytes(desc.nullifier))
|
||||
digest.update(bytes(desc.cmx))
|
||||
digest.update(bytes(desc.ephemeralKey))
|
||||
digest.update(desc.encCiphertext[:52])
|
||||
return digest.digest()
|
||||
|
||||
def orchard_actions_memos_digest(tx):
|
||||
digest = blake2b(digest_size=32, person=b'ZTxIdOrcActMHash')
|
||||
for desc in tx.vActionsOrchard:
|
||||
digest.update(desc.encCiphertext[52:564])
|
||||
return digest.digest()
|
||||
|
||||
def orchard_actions_noncompact_digest(tx):
|
||||
digest = blake2b(digest_size=32, person=b'ZTxIdOrcActNHash')
|
||||
for desc in tx.vActionsOrchard:
|
||||
digest.update(bytes(desc.cv))
|
||||
digest.update(bytes(desc.rk))
|
||||
digest.update(desc.encCiphertext[564:])
|
||||
digest.update(desc.outCiphertext)
|
||||
return digest.digest()
|
||||
|
||||
# Transaction
|
||||
|
||||
def header_digest(tx):
|
||||
digest = blake2b(digest_size=32, person=b'ZTxIdHeadersHash')
|
||||
|
||||
digest.update(struct.pack('<I', tx.version_bytes()))
|
||||
digest.update(struct.pack('<I', tx.nVersionGroupId))
|
||||
digest.update(struct.pack('<I', tx.nConsensusBranchId))
|
||||
digest.update(struct.pack('<I', tx.nLockTime))
|
||||
digest.update(struct.pack('<I', tx.nExpiryHeight))
|
||||
|
||||
return digest.digest()
|
||||
|
||||
def txid_digest(tx):
|
||||
digest = blake2b(
|
||||
digest_size=32,
|
||||
person=b'ZcashTxHash_' + struct.pack('<I', tx.nConsensusBranchId),
|
||||
)
|
||||
|
||||
digest.update(header_digest(tx))
|
||||
digest.update(transparent_digest(tx))
|
||||
digest.update(sapling_digest(tx))
|
||||
digest.update(orchard_digest(tx))
|
||||
|
||||
return digest.digest()
|
||||
|
||||
# Signatures
|
||||
|
||||
class TransparentInput(object):
|
||||
def __init__(self, tx, rand):
|
||||
self.scriptCode = Script(rand)
|
||||
self.nIn = rand.u8() % len(tx.vin)
|
||||
self.amount = rand.u64() % (MAX_MONEY + 1)
|
||||
|
||||
def signature_digest(tx, nHashType, txin):
|
||||
digest = blake2b(
|
||||
digest_size=32,
|
||||
person=b'ZcashTxHash_' + struct.pack('<I', tx.nConsensusBranchId),
|
||||
)
|
||||
|
||||
digest.update(header_digest(tx))
|
||||
digest.update(transparent_sig_digest(tx, nHashType, txin))
|
||||
digest.update(sapling_digest(tx))
|
||||
digest.update(orchard_digest(tx))
|
||||
|
||||
return digest.digest()
|
||||
|
||||
def transparent_sig_digest(tx, nHashType, txin):
|
||||
# Sapling Spend or Orchard Action
|
||||
if txin is None:
|
||||
return transparent_digest(tx)
|
||||
|
||||
digest = blake2b(digest_size=32, person=b'ZTxIdTranspaHash')
|
||||
|
||||
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):
|
||||
# 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):
|
||||
# if the SIGHASH_ANYONECANPAY flag is not set, and the sighash type is neither
|
||||
# SIGHASH_SINGLE nor SIGHASH_NONE:
|
||||
if (
|
||||
(not (nHashType & SIGHASH_ANYONECANPAY)) and \
|
||||
(nHashType & 0x1f) != SIGHASH_SINGLE and \
|
||||
(nHashType & 0x1f) != SIGHASH_NONE
|
||||
):
|
||||
return getHashSequence(tx, b'ZTxIdSequencHash')
|
||||
else:
|
||||
return blake2b(digest_size=32, person=b'ZTxIdSequencHash').digest()
|
||||
|
||||
def outputs_sig_digest(tx, nHashType, txin):
|
||||
# If the sighash type is neither SIGHASH_SINGLE nor SIGHASH_NONE:
|
||||
if (nHashType & 0x1f) != SIGHASH_SINGLE and (nHashType & 0x1f) != SIGHASH_NONE:
|
||||
return getHashOutputs(tx, b'ZTxIdOutputsHash')
|
||||
|
||||
# If the sighash type is SIGHASH_SINGLE and the signature hash is being computed for
|
||||
# the transparent input at a particular index, and a transparent output appears in the
|
||||
# transaction at that index:
|
||||
elif (nHashType & 0x1f) == SIGHASH_SINGLE and 0 <= txin.nIn and txin.nIn < len(tx.vout):
|
||||
digest = blake2b(digest_size=32, person=b'ZTxIdOutputsHash')
|
||||
digest.update(bytes(tx.vout[txin.nIn]))
|
||||
return digest.digest()
|
||||
|
||||
else:
|
||||
return blake2b(digest_size=32, person=b'ZTxIdOutputsHash').digest()
|
||||
|
||||
def txin_sig_digest(tx, txin):
|
||||
digest = blake2b(digest_size=32, person=b'Zcash___TxInHash')
|
||||
digest.update(bytes(tx.vin[txin.nIn].prevout))
|
||||
digest.update(bytes(txin.scriptCode))
|
||||
digest.update(struct.pack('<Q', txin.amount))
|
||||
digest.update(struct.pack('<I', tx.vin[txin.nIn].nSequence))
|
||||
return digest.digest()
|
||||
|
||||
|
||||
def main():
|
||||
args = render_args()
|
||||
|
||||
from random import Random
|
||||
rng = Random(0xabad533d)
|
||||
def randbytes(l):
|
||||
ret = []
|
||||
while len(ret) < l:
|
||||
ret.append(rng.randrange(0, 256))
|
||||
return bytes(ret)
|
||||
rand = Rand(randbytes)
|
||||
|
||||
consensusBranchId = 0xF919A198 # NU5
|
||||
|
||||
test_vectors = []
|
||||
for _ in range(10):
|
||||
tx = TransactionV5(rand, consensusBranchId)
|
||||
txid = txid_digest(tx)
|
||||
|
||||
# If there are any transparent inputs, derive a corresponding transparent sighash.
|
||||
if len(tx.vin) > 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<u8>', 'bitcoin_flavoured': False}),
|
||||
('txid', '[u8; 32]'),
|
||||
('transparent_input', {
|
||||
'rust_type': 'Option<u32>',
|
||||
'rust_fmt': lambda x: None if x is None else Some(x),
|
||||
}),
|
||||
('script_code', {
|
||||
'rust_type': 'Option<Vec<u8>>',
|
||||
'rust_fmt': lambda x: None if x is None else Some(x),
|
||||
}),
|
||||
('amount', {
|
||||
'rust_type': 'Option<i64>',
|
||||
'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()
|
Loading…
Reference in New Issue