Implement ZIP 243 test vectors.
Co-authored-by: Jack Grigg <jack@z.cash>
This commit is contained in:
parent
4e8e7425a2
commit
60db123206
111
transaction.py
111
transaction.py
|
@ -1,6 +1,9 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
import struct
|
import struct
|
||||||
|
|
||||||
|
from sapling_generators import find_group_hash, SPENDING_KEY_BASE
|
||||||
|
from sapling_jubjub import Fq
|
||||||
|
from sapling_utils import leos2ip
|
||||||
from zc_utils import write_compact_size
|
from zc_utils import write_compact_size
|
||||||
|
|
||||||
MAX_MONEY = 21000000 * 100000000
|
MAX_MONEY = 21000000 * 100000000
|
||||||
|
@ -9,6 +12,24 @@ TX_EXPIRY_HEIGHT_THRESHOLD = 500000000
|
||||||
OVERWINTER_VERSION_GROUP_ID = 0x03C48270
|
OVERWINTER_VERSION_GROUP_ID = 0x03C48270
|
||||||
OVERWINTER_TX_VERSION = 3
|
OVERWINTER_TX_VERSION = 3
|
||||||
|
|
||||||
|
SAPLING_VERSION_GROUP_ID = 0x892F2085
|
||||||
|
SAPLING_TX_VERSION = 4
|
||||||
|
|
||||||
|
# Sapling note magic values, copied from src/zcash/Zcash.h
|
||||||
|
NOTEENCRYPTION_AUTH_BYTES = 16
|
||||||
|
ZC_NOTEPLAINTEXT_LEADING = 1
|
||||||
|
ZC_V_SIZE = 8
|
||||||
|
ZC_RHO_SIZE = 32
|
||||||
|
ZC_R_SIZE = 32
|
||||||
|
ZC_MEMO_SIZE = 512
|
||||||
|
ZC_DIVERSIFIER_SIZE = 11
|
||||||
|
ZC_JUBJUB_POINT_SIZE = 32
|
||||||
|
ZC_JUBJUB_SCALAR_SIZE = 32
|
||||||
|
ZC_NOTEPLAINTEXT_SIZE = ZC_NOTEPLAINTEXT_LEADING + ZC_V_SIZE + ZC_RHO_SIZE + ZC_R_SIZE + ZC_MEMO_SIZE
|
||||||
|
ZC_SAPLING_ENCPLAINTEXT_SIZE = ZC_NOTEPLAINTEXT_LEADING + ZC_DIVERSIFIER_SIZE + ZC_V_SIZE + ZC_R_SIZE + ZC_MEMO_SIZE
|
||||||
|
ZC_SAPLING_OUTPLAINTEXT_SIZE = ZC_JUBJUB_POINT_SIZE + ZC_JUBJUB_SCALAR_SIZE
|
||||||
|
ZC_SAPLING_ENCCIPHERTEXT_SIZE = ZC_SAPLING_ENCPLAINTEXT_SIZE + NOTEENCRYPTION_AUTH_BYTES
|
||||||
|
ZC_SAPLING_OUTCIPHERTEXT_SIZE = ZC_SAPLING_OUTPLAINTEXT_SIZE + NOTEENCRYPTION_AUTH_BYTES
|
||||||
|
|
||||||
# BN254 encoding of G1 elements. p[1] is big-endian.
|
# BN254 encoding of G1 elements. p[1] is big-endian.
|
||||||
def pack_g1(p):
|
def pack_g1(p):
|
||||||
|
@ -41,9 +62,59 @@ class PHGRProof(object):
|
||||||
pack_g1(self.g_H)
|
pack_g1(self.g_H)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
class GrothProof(object):
|
||||||
|
def __init__(self, rand):
|
||||||
|
self.g_A = rand.b(48)
|
||||||
|
self.g_B = rand.b(96)
|
||||||
|
self.g_C = rand.b(48)
|
||||||
|
|
||||||
|
def __bytes__(self):
|
||||||
|
return (
|
||||||
|
self.g_A +
|
||||||
|
self.g_B +
|
||||||
|
self.g_C
|
||||||
|
)
|
||||||
|
|
||||||
|
class SpendDescription(object):
|
||||||
|
def __init__(self, rand):
|
||||||
|
self.cv = find_group_hash(b'TVRandPt', rand.b(32))
|
||||||
|
self.anchor = Fq(leos2ip(rand.b(32)))
|
||||||
|
self.nullifier = rand.b(32)
|
||||||
|
self.rk = rand.b(32)
|
||||||
|
self.proof = GrothProof(rand)
|
||||||
|
self.spendAuthSig = rand.b(64) # Invalid
|
||||||
|
|
||||||
|
def __bytes__(self):
|
||||||
|
return (
|
||||||
|
bytes(self.cv) +
|
||||||
|
bytes(self.anchor) +
|
||||||
|
self.nullifier +
|
||||||
|
self.rk +
|
||||||
|
bytes(self.proof) +
|
||||||
|
self.spendAuthSig
|
||||||
|
)
|
||||||
|
|
||||||
|
class OutputDescription(object):
|
||||||
|
def __init__(self, rand):
|
||||||
|
self.cv = find_group_hash(b'TVRandPt', rand.b(32))
|
||||||
|
self.cmu = Fq(leos2ip(rand.b(32)))
|
||||||
|
self.ephemeralKey = find_group_hash(b'TVRandPt', rand.b(32))
|
||||||
|
self.encCiphertext = rand.b(ZC_SAPLING_ENCCIPHERTEXT_SIZE)
|
||||||
|
self.outCipherText = rand.b(ZC_SAPLING_OUTCIPHERTEXT_SIZE)
|
||||||
|
self.proof = GrothProof(rand)
|
||||||
|
|
||||||
|
def __bytes__(self):
|
||||||
|
return (
|
||||||
|
bytes(self.cv) +
|
||||||
|
bytes(self.cmu) +
|
||||||
|
bytes(self.ephemeralKey) +
|
||||||
|
self.encCiphertext +
|
||||||
|
self.outCipherText +
|
||||||
|
bytes(self.proof)
|
||||||
|
)
|
||||||
|
|
||||||
class JoinSplit(object):
|
class JoinSplit(object):
|
||||||
def __init__(self, rand):
|
def __init__(self, rand, fUseGroth = False):
|
||||||
self.vpub_old = 0
|
self.vpub_old = 0
|
||||||
self.vpub_new = 0
|
self.vpub_new = 0
|
||||||
self.anchor = rand.b(32)
|
self.anchor = rand.b(32)
|
||||||
|
@ -52,7 +123,7 @@ class JoinSplit(object):
|
||||||
self.ephemeralKey = rand.b(32)
|
self.ephemeralKey = rand.b(32)
|
||||||
self.randomSeed = rand.b(32)
|
self.randomSeed = rand.b(32)
|
||||||
self.macs = (rand.b(32), rand.b(32))
|
self.macs = (rand.b(32), rand.b(32))
|
||||||
self.proof = PHGRProof(rand)
|
self.proof = GrothProof(rand) if fUseGroth else PHGRProof(rand)
|
||||||
self.ciphertexts = (rand.b(601), rand.b(601))
|
self.ciphertexts = (rand.b(601), rand.b(601))
|
||||||
|
|
||||||
def __bytes__(self):
|
def __bytes__(self):
|
||||||
|
@ -132,6 +203,10 @@ class Transaction(object):
|
||||||
self.fOverwintered = True
|
self.fOverwintered = True
|
||||||
self.nVersionGroupId = OVERWINTER_VERSION_GROUP_ID
|
self.nVersionGroupId = OVERWINTER_VERSION_GROUP_ID
|
||||||
self.nVersion = OVERWINTER_TX_VERSION
|
self.nVersion = OVERWINTER_TX_VERSION
|
||||||
|
elif version == SAPLING_TX_VERSION:
|
||||||
|
self.fOverwintered = True
|
||||||
|
self.nVersionGroupId = SAPLING_VERSION_GROUP_ID
|
||||||
|
self.nVersion = SAPLING_TX_VERSION
|
||||||
else:
|
else:
|
||||||
self.fOverwintered = False
|
self.fOverwintered = False
|
||||||
self.nVersion = rand.u32() & ((1 << 31) - 1)
|
self.nVersion = rand.u32() & ((1 << 31) - 1)
|
||||||
|
@ -146,15 +221,26 @@ class Transaction(object):
|
||||||
|
|
||||||
self.nLockTime = rand.u32()
|
self.nLockTime = rand.u32()
|
||||||
self.nExpiryHeight = rand.u32() % TX_EXPIRY_HEIGHT_THRESHOLD
|
self.nExpiryHeight = rand.u32() % TX_EXPIRY_HEIGHT_THRESHOLD
|
||||||
|
self.valueBalance = rand.u64() % (MAX_MONEY + 1)
|
||||||
|
|
||||||
|
self.vShieldedSpends = []
|
||||||
|
self.vShieldedOutputs = []
|
||||||
|
if self.nVersion >= SAPLING_TX_VERSION:
|
||||||
|
for _ in range(rand.u8() % 5):
|
||||||
|
self.vShieldedSpends.append(SpendDescription(rand))
|
||||||
|
for _ in range(rand.u8() % 5):
|
||||||
|
self.vShieldedOutputs.append(OutputDescription(rand))
|
||||||
|
|
||||||
self.vJoinSplit = []
|
self.vJoinSplit = []
|
||||||
if self.nVersion >= 2:
|
if self.nVersion >= 2:
|
||||||
for i in range(rand.u8() % 3):
|
for i in range(rand.u8() % 3):
|
||||||
self.vJoinSplit.append(JoinSplit(rand))
|
self.vJoinSplit.append(JoinSplit(rand, self.fOverwintered and self.nVersion >= SAPLING_TX_VERSION))
|
||||||
if len(self.vJoinSplit) > 0:
|
if len(self.vJoinSplit) > 0:
|
||||||
self.joinSplitPubKey = rand.b(32) # Potentially invalid
|
self.joinSplitPubKey = rand.b(32) # Potentially invalid
|
||||||
self.joinSplitSig = rand.b(64) # Invalid
|
self.joinSplitSig = rand.b(64) # Invalid
|
||||||
|
|
||||||
|
self.bindingSig = rand.b(64) # Invalid
|
||||||
|
|
||||||
def header(self):
|
def header(self):
|
||||||
return self.nVersion | (1 << 31 if self.fOverwintered else 0)
|
return self.nVersion | (1 << 31 if self.fOverwintered else 0)
|
||||||
|
|
||||||
|
@ -169,6 +255,11 @@ class Transaction(object):
|
||||||
self.nVersionGroupId == OVERWINTER_VERSION_GROUP_ID and \
|
self.nVersionGroupId == OVERWINTER_VERSION_GROUP_ID and \
|
||||||
self.nVersion == OVERWINTER_TX_VERSION
|
self.nVersion == OVERWINTER_TX_VERSION
|
||||||
|
|
||||||
|
isSaplingV4 = \
|
||||||
|
self.fOverwintered and \
|
||||||
|
self.nVersionGroupId == SAPLING_VERSION_GROUP_ID and \
|
||||||
|
self.nVersion == SAPLING_TX_VERSION
|
||||||
|
|
||||||
ret += write_compact_size(len(self.vin))
|
ret += write_compact_size(len(self.vin))
|
||||||
for x in self.vin:
|
for x in self.vin:
|
||||||
ret += bytes(x)
|
ret += bytes(x)
|
||||||
|
@ -178,9 +269,18 @@ class Transaction(object):
|
||||||
ret += bytes(x)
|
ret += bytes(x)
|
||||||
|
|
||||||
ret += struct.pack('<I', self.nLockTime)
|
ret += struct.pack('<I', self.nLockTime)
|
||||||
if isOverwinterV3:
|
if isOverwinterV3 or isSaplingV4:
|
||||||
ret += struct.pack('<I', self.nExpiryHeight)
|
ret += struct.pack('<I', self.nExpiryHeight)
|
||||||
|
|
||||||
|
if isSaplingV4:
|
||||||
|
ret += struct.pack('<Q', self.valueBalance)
|
||||||
|
ret += write_compact_size(len(self.vShieldedSpends))
|
||||||
|
for desc in self.vShieldedSpends:
|
||||||
|
ret += bytes(desc)
|
||||||
|
ret += write_compact_size(len(self.vShieldedOutputs))
|
||||||
|
for desc in self.vShieldedOutputs:
|
||||||
|
ret += bytes(desc)
|
||||||
|
|
||||||
if self.nVersion >= 2:
|
if self.nVersion >= 2:
|
||||||
ret += write_compact_size(len(self.vJoinSplit))
|
ret += write_compact_size(len(self.vJoinSplit))
|
||||||
for jsdesc in self.vJoinSplit:
|
for jsdesc in self.vJoinSplit:
|
||||||
|
@ -189,4 +289,7 @@ class Transaction(object):
|
||||||
ret += self.joinSplitPubKey
|
ret += self.joinSplitPubKey
|
||||||
ret += self.joinSplitSig
|
ret += self.joinSplitSig
|
||||||
|
|
||||||
|
if isSaplingV4 and not (len(self.vShieldedSpends) == 0 and len(self.vShieldedOutputs) == 0):
|
||||||
|
ret += self.bindingSig
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
|
|
|
@ -0,0 +1,175 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
from pyblake2 import blake2b
|
||||||
|
import struct
|
||||||
|
|
||||||
|
from transaction import (
|
||||||
|
MAX_MONEY,
|
||||||
|
SAPLING_TX_VERSION,
|
||||||
|
Script,
|
||||||
|
Transaction,
|
||||||
|
)
|
||||||
|
from tv_output import render_args, render_tv, Some
|
||||||
|
from tv_rand import Rand
|
||||||
|
|
||||||
|
from zip_0143 import (
|
||||||
|
getHashJoinSplits,
|
||||||
|
getHashOutputs,
|
||||||
|
getHashPrevouts,
|
||||||
|
getHashSequence,
|
||||||
|
NOT_AN_INPUT,
|
||||||
|
SIGHASH_ALL,
|
||||||
|
SIGHASH_ANYONECANPAY,
|
||||||
|
SIGHASH_NONE,
|
||||||
|
SIGHASH_SINGLE,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def getHashShieldedSpends(tx):
|
||||||
|
digest = blake2b(digest_size=32, person=b'ZcashSSpendsHash')
|
||||||
|
for desc in tx.vShieldedSpends:
|
||||||
|
# We don't pass in serialized form of desc as spendAuthSig is not part of the hash
|
||||||
|
digest.update(bytes(desc.cv))
|
||||||
|
digest.update(bytes(desc.anchor))
|
||||||
|
digest.update(desc.nullifier)
|
||||||
|
digest.update(desc.rk)
|
||||||
|
digest.update(bytes(desc.proof))
|
||||||
|
return digest.digest()
|
||||||
|
|
||||||
|
def getHashShieldedOutputs(tx):
|
||||||
|
digest = blake2b(digest_size=32, person=b'ZcashSOutputHash')
|
||||||
|
for desc in tx.vShieldedOutputs:
|
||||||
|
digest.update(bytes(desc))
|
||||||
|
return digest.digest()
|
||||||
|
|
||||||
|
def signature_hash(scriptCode, tx, nIn, nHashType, amount, consensusBranchId):
|
||||||
|
hashPrevouts = b'\x00'*32
|
||||||
|
hashSequence = b'\x00'*32
|
||||||
|
hashOutputs = b'\x00'*32
|
||||||
|
hashJoinSplits = b'\x00'*32
|
||||||
|
hashShieldedSpends = b'\x00'*32
|
||||||
|
hashShieldedOutputs = b'\x00'*32
|
||||||
|
|
||||||
|
if not (nHashType & SIGHASH_ANYONECANPAY):
|
||||||
|
hashPrevouts = getHashPrevouts(tx)
|
||||||
|
|
||||||
|
if (not (nHashType & SIGHASH_ANYONECANPAY)) and \
|
||||||
|
(nHashType & 0x1f) != SIGHASH_SINGLE and \
|
||||||
|
(nHashType & 0x1f) != SIGHASH_NONE:
|
||||||
|
hashSequence = getHashSequence(tx)
|
||||||
|
|
||||||
|
if (nHashType & 0x1f) != SIGHASH_SINGLE and \
|
||||||
|
(nHashType & 0x1f) != SIGHASH_NONE:
|
||||||
|
hashOutputs = getHashOutputs(tx)
|
||||||
|
elif (nHashType & 0x1f) == SIGHASH_SINGLE and \
|
||||||
|
0 <= nIn and nIn < len(tx.vout):
|
||||||
|
digest = blake2b(digest_size=32, person=b'ZcashOutputsHash')
|
||||||
|
digest.update(bytes(tx.vout[nIn]))
|
||||||
|
hashOutputs = digest.digest()
|
||||||
|
|
||||||
|
if len(tx.vJoinSplit) > 0:
|
||||||
|
hashJoinSplits = getHashJoinSplits(tx)
|
||||||
|
|
||||||
|
if len(tx.vShieldedSpends) > 0:
|
||||||
|
hashShieldedSpends = getHashShieldedSpends(tx)
|
||||||
|
|
||||||
|
if len(tx.vShieldedOutputs) > 0:
|
||||||
|
hashShieldedOutputs = getHashShieldedOutputs(tx)
|
||||||
|
|
||||||
|
digest = blake2b(
|
||||||
|
digest_size=32,
|
||||||
|
person=b'ZcashSigHash' + struct.pack('<I', consensusBranchId),
|
||||||
|
)
|
||||||
|
|
||||||
|
digest.update(struct.pack('<I', tx.header()))
|
||||||
|
digest.update(struct.pack('<I', tx.nVersionGroupId))
|
||||||
|
digest.update(hashPrevouts)
|
||||||
|
digest.update(hashSequence)
|
||||||
|
digest.update(hashOutputs)
|
||||||
|
digest.update(hashJoinSplits)
|
||||||
|
digest.update(hashShieldedSpends)
|
||||||
|
digest.update(hashShieldedOutputs)
|
||||||
|
digest.update(struct.pack('<I', tx.nLockTime))
|
||||||
|
digest.update(struct.pack('<I', tx.nExpiryHeight))
|
||||||
|
digest.update(struct.pack('<Q', tx.valueBalance))
|
||||||
|
digest.update(struct.pack('<I', nHashType))
|
||||||
|
|
||||||
|
if nIn != NOT_AN_INPUT:
|
||||||
|
digest.update(bytes(tx.vin[nIn].prevout))
|
||||||
|
digest.update(bytes(scriptCode))
|
||||||
|
digest.update(struct.pack('<Q', amount))
|
||||||
|
digest.update(struct.pack('<I', tx.vin[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 = 0x76b809bb # Sapling
|
||||||
|
|
||||||
|
test_vectors = []
|
||||||
|
for _ in range(10):
|
||||||
|
tx = Transaction(rand, SAPLING_TX_VERSION)
|
||||||
|
scriptCode = Script(rand)
|
||||||
|
nIn = rand.u8() % (len(tx.vin) + 1)
|
||||||
|
if nIn == len(tx.vin):
|
||||||
|
nIn = NOT_AN_INPUT
|
||||||
|
nHashType = SIGHASH_ALL if nIn == NOT_AN_INPUT else rand.a([
|
||||||
|
SIGHASH_ALL,
|
||||||
|
SIGHASH_NONE,
|
||||||
|
SIGHASH_SINGLE,
|
||||||
|
SIGHASH_ALL | SIGHASH_ANYONECANPAY,
|
||||||
|
SIGHASH_NONE | SIGHASH_ANYONECANPAY,
|
||||||
|
SIGHASH_SINGLE | SIGHASH_ANYONECANPAY,
|
||||||
|
])
|
||||||
|
amount = rand.u64() % (MAX_MONEY + 1)
|
||||||
|
|
||||||
|
sighash = signature_hash(
|
||||||
|
scriptCode,
|
||||||
|
tx,
|
||||||
|
nIn,
|
||||||
|
nHashType,
|
||||||
|
amount,
|
||||||
|
consensusBranchId,
|
||||||
|
)
|
||||||
|
|
||||||
|
test_vectors.append({
|
||||||
|
'tx': bytes(tx),
|
||||||
|
'script_code': scriptCode.raw(),
|
||||||
|
'transparent_input': nIn,
|
||||||
|
'hash_type': nHashType,
|
||||||
|
'amount': amount,
|
||||||
|
'consensus_branch_id': consensusBranchId,
|
||||||
|
'sighash': sighash,
|
||||||
|
})
|
||||||
|
|
||||||
|
render_tv(
|
||||||
|
args,
|
||||||
|
'zip_0243',
|
||||||
|
(
|
||||||
|
('tx', {'rust_type': 'Vec<u8>', 'bitcoin_flavoured': False}),
|
||||||
|
('script_code', 'Vec<u8>'),
|
||||||
|
('transparent_input', {
|
||||||
|
'rust_type': 'Option<u32>',
|
||||||
|
'rust_fmt': lambda x: None if x == -1 else Some(x),
|
||||||
|
}),
|
||||||
|
('hash_type', 'u32'),
|
||||||
|
('amount', 'u64'),
|
||||||
|
('consensus_branch_id', 'u32'),
|
||||||
|
('sighash', '[u8; 32]'),
|
||||||
|
),
|
||||||
|
test_vectors,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
Loading…
Reference in New Issue