Skip to content

Commit

Permalink
add zcash v5 (nu5) transactions support (draft)
Browse files Browse the repository at this point in the history
Issue: spesmilo#181

Todo: clean-up mininode and other unused code in electrumx/lib/zcash,
rewrite zcash_txid_v5 using only bytes object operations and hashes
computation, probably, without full tx (re)construct.
  • Loading branch information
DeckerSU committed Oct 2, 2022
1 parent fb037fb commit 25d2aee
Show file tree
Hide file tree
Showing 9 changed files with 4,720 additions and 30 deletions.
138 changes: 108 additions & 30 deletions electrumx/lib/tx.py
Original file line number Diff line number Diff line change
Expand Up @@ -370,52 +370,130 @@ class DeserializerEquihashSegWit(DeserializerSegWit, DeserializerEquihash):
pass


# https://zips.z.cash/zip-0202
# https://zips.z.cash/zip-0225

class DeserializerZcash(DeserializerEquihash):

OVERWINTER_VERSION_GROUP_ID = 0x03C48270
SAPLING_VERSION_GROUP_ID = 0x892F2085
ZIP225_VERSION_GROUP_ID = 0x26A7270A
OVERWINTER_TX_VERSION = 3
SAPLING_TX_VERSION = 4
ZIP225_TX_VERSION = 5

ZFUTURE_VERSION_GROUP_ID = 0xFFFFFFFF
ZFUTURE_TX_VERSION = 0x0000FFFF

def read_tx(self):
header = self._read_le_uint32()
overwintered = ((header >> 31) == 1)
if overwintered:
version = header & 0x7fffffff
self.cursor += 4 # versionGroupId
nVersionGroupId = self._read_le_uint32()
else:
version = header

is_overwinter_v3 = version == 3
is_sapling_v4 = version == 4
is_zip225_v5 = (overwintered and nVersionGroupId == self.ZIP225_VERSION_GROUP_ID and version == self.ZIP225_TX_VERSION)

base_tx = Tx(
version,
self._read_inputs(), # inputs
self._read_outputs(), # outputs
self._read_le_uint32() # locktime
)
if not(is_zip225_v5):
base_tx = Tx(
version,
self._read_inputs(), # inputs
self._read_outputs(), # outputs
self._read_le_uint32() # locktime
)

if is_overwinter_v3 or is_sapling_v4:
self.cursor += 4 # expiryHeight

has_shielded = False
if is_sapling_v4:
self.cursor += 8 # valueBalance
shielded_spend_size = self._read_varint()
self.cursor += shielded_spend_size * 384 # vShieldedSpend
shielded_output_size = self._read_varint()
self.cursor += shielded_output_size * 948 # vShieldedOutput
has_shielded = shielded_spend_size > 0 or shielded_output_size > 0

if base_tx.version >= 2:
joinsplit_size = self._read_varint()
if joinsplit_size > 0:
joinsplit_desc_len = 1506 + (192 if is_sapling_v4 else 296)
# JSDescription
self.cursor += joinsplit_size * joinsplit_desc_len
self.cursor += 32 # joinSplitPubKey
self.cursor += 64 # joinSplitSig

if is_sapling_v4 and has_shielded:
self.cursor += 64 # bindingSig
else:
nConsensusBranchId = self._read_le_uint32()
nLockTime = self._read_le_uint32()
self.cursor += 4 # nExpiryHeight
base_tx = Tx(
version,
# Transparent Transaction Fields
self._read_inputs(), # inputs
self._read_outputs(), # outputs
nLockTime # locktime
)
# Sapling Transaction Fields (SaplingBundle)
nSpendsSapling = self._read_varint()
self.cursor += 96 * nSpendsSapling # vSpendsSapling
nOutputsSapling = self._read_varint()
self.cursor += 756 * nOutputsSapling # vOutputsSapling
hasSapling = not(nSpendsSapling == 0 and nOutputsSapling == 0)
if (hasSapling):
self.cursor += 8 # valueBalanceSapling
if not(nSpendsSapling == 0):
self.cursor += 32 # anchorSapling
self.cursor += 192 * nSpendsSapling # vSpendProofsSapling
self.cursor += 64 * nSpendsSapling # vSpendAuthSigsSapling
self.cursor += 192 * nOutputsSapling # vOutputProofsSapling
if (hasSapling):
self.cursor += 64 # bindingSigSapling
# Orchard Transaction Fields (OrchardBundle)
# orchard_bundle_serialize (rust)
nActionsOrchard = self._read_varint()
self.cursor += 820 * nActionsOrchard # vActionsOrchard
if (nActionsOrchard > 0):
self.cursor += 1 # flagsOrchard
self.cursor += 8 # valueBalanceOrchard
self.cursor += 32 # anchorOrchard
sizeProofsOrchard = self._read_varint()
self.cursor += sizeProofsOrchard # proofsOrchard
self.cursor += 64 * nActionsOrchard # vSpendAuthSigsOrchard
self.cursor += 64 # bindingSigOrchard
return base_tx

if is_overwinter_v3 or is_sapling_v4:
self.cursor += 4 # expiryHeight
@staticmethod
def zcash_txid_v5(txin):

has_shielded = False
if is_sapling_v4:
self.cursor += 8 # valueBalance
shielded_spend_size = self._read_varint()
self.cursor += shielded_spend_size * 384 # vShieldedSpend
shielded_output_size = self._read_varint()
self.cursor += shielded_output_size * 948 # vShieldedOutput
has_shielded = shielded_spend_size > 0 or shielded_output_size > 0

if base_tx.version >= 2:
joinsplit_size = self._read_varint()
if joinsplit_size > 0:
joinsplit_desc_len = 1506 + (192 if is_sapling_v4 else 296)
# JSDescription
self.cursor += joinsplit_size * joinsplit_desc_len
self.cursor += 32 # joinSplitPubKey
self.cursor += 64 # joinSplitSig

if is_sapling_v4 and has_shielded:
self.cursor += 64 # bindingSig
from electrumx.lib.zcash.mininode import CTransaction
from io import BytesIO
from electrumx.lib.zcash.util import hex_str_to_bytes
tx = CTransaction()
tx.deserialize(BytesIO(txin))
tx.rehash()
# print(repr(tx))
return bytes(reversed(hex_str_to_bytes(tx.hash)))

return base_tx
def read_tx_and_hash(self):
'''Return a (deserialized TX, tx_hash) pair.
The hash needs to be reversed for human display; for efficiency
we process it in the natural serialized order.
'''
start = self.cursor
_tx = self.read_tx()
if (_tx.version < 5):
_txhash = double_sha256(self.binary[start:self.cursor])
else:
_txhash = self.zcash_txid_v5(self.binary[start:self.cursor])
return _tx, _txhash

@dataclass
class TxPIVX:
Expand Down
Empty file added electrumx/lib/zcash/__init__.py
Empty file.
100 changes: 100 additions & 0 deletions electrumx/lib/zcash/bignum.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
#!/usr/bin/env python3
#
# bignum.py
#
# This file is copied from python-bitcoinlib.
#
# Distributed under the MIT software license, see the accompanying
# file COPYING or https://www.opensource.org/licenses/mit-license.php .
#

"""Bignum routines"""

import struct


# generic big endian MPI format

def bn_bytes(v, have_ext=False):
ext = 0
if have_ext:
ext = 1
return ((v.bit_length()+7)//8) + ext

def bn2bin(v):
s = bytearray()
i = bn_bytes(v)
while i > 0:
s.append((v >> ((i-1) * 8)) & 0xff)
i -= 1
return s

def bin2bn(s):
l = 0
for ch in s:
l = (l << 8) | ch
return l

def bn2mpi(v):
have_ext = False
if v.bit_length() > 0:
have_ext = (v.bit_length() & 0x07) == 0

neg = False
if v < 0:
neg = True
v = -v

s = struct.pack(b">I", bn_bytes(v, have_ext))
ext = bytearray()
if have_ext:
ext.append(0)
v_bin = bn2bin(v)
if neg:
if have_ext:
ext[0] |= 0x80
else:
v_bin[0] |= 0x80
return s + ext + v_bin

def mpi2bn(s):
if len(s) < 4:
return None
s_size = bytes(s[:4])
v_len = struct.unpack(b">I", s_size)[0]
if len(s) != (v_len + 4):
return None
if v_len == 0:
return 0

v_str = bytearray(s[4:])
neg = False
i = v_str[0]
if i & 0x80:
neg = True
i &= ~0x80
v_str[0] = i

v = bin2bn(v_str)

if neg:
return -v
return v

# bitcoin-specific little endian format, with implicit size
def mpi2vch(s):
r = s[4:] # strip size
r = r[::-1] # reverse string, converting BE->LE
return r

def bn2vch(v):
return bytes(mpi2vch(bn2mpi(v)))

def vch2mpi(s):
r = struct.pack(b">I", len(s)) # size
r += s[::-1] # reverse string, converting LE->BE
return r

def vch2bn(s):
return mpi2bn(vch2mpi(s))

107 changes: 107 additions & 0 deletions electrumx/lib/zcash/coverage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
#!/usr/bin/env python3
# Copyright (c) 2015-2016 The Bitcoin Core developers
# Copyright (c) 2020-2022 The Zcash developers
# Distributed under the MIT software license, see the accompanying
# file COPYING or https://www.opensource.org/licenses/mit-license.php .

"""
This module contains utilities for doing coverage analysis on the RPC
interface.
It provides a way to track which RPC commands are exercised during
testing.
"""
import os


REFERENCE_FILENAME = 'rpc_interface.txt'


class AuthServiceProxyWrapper(object):
"""
An object that wraps AuthServiceProxy to record specific RPC calls.
"""
def __init__(self, auth_service_proxy_instance, coverage_logfile=None):
"""
Kwargs:
auth_service_proxy_instance (AuthServiceProxy): the instance
being wrapped.
coverage_logfile (str): if specified, write each service_name
out to a file when called.
"""
self.auth_service_proxy_instance = auth_service_proxy_instance
self.coverage_logfile = coverage_logfile

def __getattr__(self, *args, **kwargs):
return_val = self.auth_service_proxy_instance.__getattr__(
*args, **kwargs)

return AuthServiceProxyWrapper(return_val, self.coverage_logfile)

def __call__(self, *args, **kwargs):
"""
Delegates to AuthServiceProxy, then writes the particular RPC method
called to a file.
"""
return_val = self.auth_service_proxy_instance.__call__(*args, **kwargs)
rpc_method = self.auth_service_proxy_instance._service_name

if self.coverage_logfile:
with open(self.coverage_logfile, 'a+', encoding='utf8') as f:
f.write("%s\n" % rpc_method)

return return_val

@property
def url(self):
return self.auth_service_proxy_instance.url


def get_filename(dirname, n_node):
"""
Get a filename unique to the test process ID and node.
This file will contain a list of RPC commands covered.
"""
pid = str(os.getpid())
return os.path.join(
dirname, "coverage.pid%s.node%s.txt" % (pid, str(n_node)))


def write_all_rpc_commands(dirname, node):
"""
Write out a list of all RPC functions available in `bitcoin-cli` for
coverage comparison. This will only happen once per coverage
directory.
Args:
dirname (str): temporary test dir
node (AuthServiceProxy): client
Returns:
bool. if the RPC interface file was written.
"""
filename = os.path.join(dirname, REFERENCE_FILENAME)

if os.path.isfile(filename):
return False

help_output = node.help().split('\n')
commands = set()

for line in help_output:
line = line.strip()

# Ignore blanks and headers
if line and not line.startswith('='):
commands.add("%s\n" % line.split()[0])

with open(filename, 'w', encoding='utf8') as f:
f.writelines(list(commands))

return True
Loading

0 comments on commit 25d2aee

Please sign in to comment.