diff --git a/core/.changelog.d/2166.added b/core/.changelog.d/2166.added new file mode 100644 index 000000000..a6d73e923 --- /dev/null +++ b/core/.changelog.d/2166.added @@ -0,0 +1 @@ +Support Zcash version 5 transaction format diff --git a/core/SConscript.firmware b/core/SConscript.firmware index e81ae4de2..a849a8e98 100644 --- a/core/SConscript.firmware +++ b/core/SConscript.firmware @@ -622,7 +622,7 @@ if FROZEN: exclude=[ SOURCE_PY_DIR + 'apps/bitcoin/sign_tx/decred.py', SOURCE_PY_DIR + 'apps/bitcoin/sign_tx/bitcoinlike.py', - SOURCE_PY_DIR + 'apps/bitcoin/sign_tx/zcash.py', + SOURCE_PY_DIR + 'apps/bitcoin/sign_tx/zcash_v4.py', ]) ) @@ -661,11 +661,13 @@ if FROZEN: SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/tezos/*.py')) SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/enums/Tezos*.py')) + SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/zcash/*.py')) + SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/webauthn/*.py')) SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/bitcoin/sign_tx/decred.py')) SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/bitcoin/sign_tx/bitcoinlike.py')) - SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/bitcoin/sign_tx/zcash.py')) + SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/bitcoin/sign_tx/zcash_v4.py')) source_mpy = env.FrozenModule(source=SOURCE_PY, source_dir=SOURCE_PY_DIR, bitcoin_only=BITCOIN_ONLY) diff --git a/core/SConscript.unix b/core/SConscript.unix index 9249c12b7..dab42df27 100644 --- a/core/SConscript.unix +++ b/core/SConscript.unix @@ -586,7 +586,7 @@ if FROZEN: exclude=[ SOURCE_PY_DIR + 'apps/bitcoin/sign_tx/decred.py', SOURCE_PY_DIR + 'apps/bitcoin/sign_tx/bitcoinlike.py', - SOURCE_PY_DIR + 'apps/bitcoin/sign_tx/zcash.py', + SOURCE_PY_DIR + 'apps/bitcoin/sign_tx/zcash_v4.py', ]) ) @@ -625,11 +625,13 @@ if FROZEN: SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/tezos/*.py')) SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'trezor/enums/Tezos*.py')) + SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/zcash/*.py')) + SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/webauthn/*.py')) SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/bitcoin/sign_tx/decred.py')) SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/bitcoin/sign_tx/bitcoinlike.py')) - SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/bitcoin/sign_tx/zcash.py')) + SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/bitcoin/sign_tx/zcash_v4.py')) source_mpy = env.FrozenModule(source=SOURCE_PY, source_dir=SOURCE_PY_DIR, bitcoin_only=BITCOIN_ONLY) diff --git a/core/src/all_modules.py b/core/src/all_modules.py index 30ef62100..f2ee4a4f0 100644 --- a/core/src/all_modules.py +++ b/core/src/all_modules.py @@ -466,8 +466,8 @@ if not utils.BITCOIN_ONLY: import apps.binance.layout apps.binance.sign_tx import apps.binance.sign_tx - apps.bitcoin.sign_tx.zcash - import apps.bitcoin.sign_tx.zcash + apps.bitcoin.sign_tx.zcash_v4 + import apps.bitcoin.sign_tx.zcash_v4 apps.cardano import apps.cardano apps.cardano.address @@ -750,6 +750,12 @@ if not utils.BITCOIN_ONLY: import apps.webauthn.remove_resident_credential apps.webauthn.resident_credentials import apps.webauthn.resident_credentials + apps.zcash + import apps.zcash + apps.zcash.hasher + import apps.zcash.hasher + apps.zcash.signer + import apps.zcash.signer # generate full alphabet a diff --git a/core/src/apps/bitcoin/sign_tx/__init__.py b/core/src/apps/bitcoin/sign_tx/__init__.py index 5a429f137..3c9481492 100644 --- a/core/src/apps/bitcoin/sign_tx/__init__.py +++ b/core/src/apps/bitcoin/sign_tx/__init__.py @@ -9,7 +9,8 @@ from ..keychain import with_keychain from . import approvers, bitcoin, helpers, progress if not utils.BITCOIN_ONLY: - from . import bitcoinlike, decred, zcash + from . import bitcoinlike, decred, zcash_v4 + from apps.zcash.signer import Zcash if TYPE_CHECKING: from typing import Protocol @@ -70,7 +71,10 @@ async def sign_tx( if coin.decred: signer_class = decred.Decred elif coin.overwintered: - signer_class = zcash.Zcashlike + if msg.version == 5: + signer_class = Zcash + else: + signer_class = zcash_v4.ZcashV4 else: signer_class = bitcoinlike.Bitcoinlike diff --git a/core/src/apps/bitcoin/sign_tx/decred.py b/core/src/apps/bitcoin/sign_tx/decred.py index d819e8abd..e9db36b2b 100644 --- a/core/src/apps/bitcoin/sign_tx/decred.py +++ b/core/src/apps/bitcoin/sign_tx/decred.py @@ -79,6 +79,13 @@ class DecredSigHasher: ) -> bytes: raise NotImplementedError + def hash_zip244( + self, + txi: TxInput | None, + script_pubkey: bytes | None, + ) -> bytes: + raise NotImplementedError + class Decred(Bitcoin): def __init__( diff --git a/core/src/apps/bitcoin/sign_tx/sig_hasher.py b/core/src/apps/bitcoin/sign_tx/sig_hasher.py index 8d6fe8724..bdb8d3daa 100644 --- a/core/src/apps/bitcoin/sign_tx/sig_hasher.py +++ b/core/src/apps/bitcoin/sign_tx/sig_hasher.py @@ -39,6 +39,13 @@ if TYPE_CHECKING: ) -> bytes: ... + def hash_zip244( + self, + txi: TxInput | None, + script_pubkey: bytes | None, + ) -> bytes: + ... + # BIP-0143 hash class BitcoinSigHasher: @@ -166,3 +173,10 @@ class BitcoinSigHasher: writers.write_uint32(h_sigmsg, i) return h_sigmsg.get_digest() + + def hash_zip244( + self, + txi: TxInput | None, + script_pubkey: bytes | None, + ) -> bytes: + raise NotImplementedError diff --git a/core/src/apps/bitcoin/sign_tx/zcash.py b/core/src/apps/bitcoin/sign_tx/zcash_v4.py similarity index 96% rename from core/src/apps/bitcoin/sign_tx/zcash.py rename to core/src/apps/bitcoin/sign_tx/zcash_v4.py index 0de428ad2..d92cd7aa8 100644 --- a/core/src/apps/bitcoin/sign_tx/zcash.py +++ b/core/src/apps/bitcoin/sign_tx/zcash_v4.py @@ -35,7 +35,7 @@ if TYPE_CHECKING: OVERWINTERED = const(0x8000_0000) -class ZcashSigHasher: +class Zip243SigHasher: def __init__(self) -> None: self.h_prevouts = HashWriter(blake2b(outlen=32, personal=b"ZcashPrevoutHash")) self.h_sequence = HashWriter(blake2b(outlen=32, personal=b"ZcashSequencHash")) @@ -113,8 +113,15 @@ class ZcashSigHasher: ) -> bytes: raise NotImplementedError + def hash_zip244( + self, + txi: TxInput | None, + script_pubkey: bytes | None, + ) -> bytes: + raise NotImplementedError + -class Zcashlike(Bitcoinlike): +class ZcashV4(Bitcoinlike): def __init__( self, tx: SignTx, @@ -129,7 +136,7 @@ class Zcashlike(Bitcoinlike): raise wire.DataError("Unsupported transaction version.") def create_sig_hasher(self, tx: SignTx | PrevTx) -> SigHasher: - return ZcashSigHasher() + return Zip243SigHasher() async def step7_finish(self) -> None: self.write_tx_footer(self.serialized_tx, self.tx_info.tx) diff --git a/core/src/apps/zcash/__init__.py b/core/src/apps/zcash/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/core/src/apps/zcash/hasher.py b/core/src/apps/zcash/hasher.py new file mode 100644 index 000000000..fcfd47f51 --- /dev/null +++ b/core/src/apps/zcash/hasher.py @@ -0,0 +1,279 @@ +""" +Implementation of Zcash txid and sighash algorithms +according to the ZIP-0244. + +specification: https://zips.z.cash/zip-0244 +""" + +from typing import TYPE_CHECKING + +from trezor.crypto.hashlib import blake2b +from trezor.utils import HashWriter, empty_bytearray + +from apps.bitcoin.common import SigHashType +from apps.bitcoin.writers import ( + TX_HASH_SIZE, + write_bytes_fixed, + write_bytes_prefixed, + write_bytes_reversed, + write_tx_output, + write_uint8, + write_uint32, + write_uint64, +) + +if TYPE_CHECKING: + from trezor.messages import TxInput, TxOutput, SignTx, PrevTx + from trezor.utils import Writer + from apps.common.coininfo import CoinInfo + from typing import Sequence + + +def write_hash(w: Writer, hash: bytes) -> None: + write_bytes_fixed(w, hash, TX_HASH_SIZE) + + +def write_prevout(w: Writer, txi: TxInput) -> None: + write_bytes_reversed(w, txi.prev_hash, TX_HASH_SIZE) + write_uint32(w, txi.prev_index) + + +class ZcashHasher: + def __init__(self, tx: SignTx | PrevTx): + self.header = HeaderHasher(tx) + self.transparent = TransparentHasher() + self.sapling = SaplingHasher() + self.orchard = OrchardHasher() + + assert tx.branch_id is not None # checked in sanitize_sign_tx + tx_hash_person = empty_bytearray(16) + write_bytes_fixed(tx_hash_person, b"ZcashTxHash_", 12) + write_uint32(tx_hash_person, tx.branch_id) + self.tx_hash_person = bytes(tx_hash_person) + + # The `txid_digest` method is currently a dead code, + # but we keep it for future use cases. + def txid_digest(self) -> bytes: + """ + Returns the transaction identifier. + + see: https://zips.z.cash/zip-0244#id4 + """ + h = HashWriter(blake2b(outlen=32, personal=self.tx_hash_person)) + + write_hash(h, self.header.digest()) # T.1 + write_hash(h, self.transparent.digest()) # T.2 + write_hash(h, self.sapling.digest()) # T.3 + write_hash(h, self.orchard.digest()) # T.4 + + return h.get_digest() + + def signature_digest( + self, txi: TxInput | None, script_pubkey: bytes | None + ) -> bytes: + """ + Returns the transaction signature digest. + + see: https://zips.z.cash/zip-0244#id13 + """ + h = HashWriter(blake2b(outlen=32, personal=self.tx_hash_person)) + + write_hash(h, self.header.digest()) # S.1 + write_hash(h, self.transparent.sig_digest(txi, script_pubkey)) # S.2 + write_hash(h, self.sapling.digest()) # S.3 + write_hash(h, self.orchard.digest()) # S.4 + + return h.get_digest() + + # implement `SigHasher` interface: + + def add_input(self, txi: TxInput, script_pubkey: bytes) -> None: + self.transparent.add_input(txi, script_pubkey) + + def add_output(self, txo: TxOutput, script_pubkey: bytes) -> None: + self.transparent.add_output(txo, script_pubkey) + + def hash143( + self, + txi: TxInput, + public_keys: Sequence[bytes | memoryview], + threshold: int, + tx: SignTx | PrevTx, + coin: CoinInfo, + hash_type: int, + ) -> bytes: + raise NotImplementedError + + def hash341( + self, + i: int, + tx: SignTx | PrevTx, + sighash_type: SigHashType, + ) -> bytes: + raise NotImplementedError + + def hash_zip244( + self, + txi: TxInput | None, + script_pubkey: bytes | None, + ) -> bytes: + return self.signature_digest(txi, script_pubkey) + + +class HeaderHasher: + def __init__(self, tx: SignTx | PrevTx): + h = HashWriter(blake2b(outlen=32, personal=b"ZTxIdHeadersHash")) + + assert tx.version_group_id is not None + assert tx.branch_id is not None # checked in sanitize_* + assert tx.expiry is not None + + write_uint32(h, tx.version | (1 << 31)) # T.1a + write_uint32(h, tx.version_group_id) # T.1b + write_uint32(h, tx.branch_id) # T.1c + write_uint32(h, tx.lock_time) # T.1d + write_uint32(h, tx.expiry) # T.1e + + self._digest = h.get_digest() + + def digest(self) -> bytes: + """ + Returns `T.1: header_digest` field. + + see: https://zips.z.cash/zip-0244#t-1-header-digest + """ + return self._digest + + +class TransparentHasher: + def __init__(self) -> None: + self.prevouts = HashWriter( + blake2b(outlen=32, personal=b"ZTxIdPrevoutHash") + ) # a hasher for fields T.2a & S.2b + + self.amounts = HashWriter( + blake2b(outlen=32, personal=b"ZTxTrAmountsHash") + ) # a hasher for field S.2c + + self.scriptpubkeys = HashWriter( + blake2b(outlen=32, personal=b"ZTxTrScriptsHash") + ) # a hasher for field S.2d + + self.sequence = HashWriter( + blake2b(outlen=32, personal=b"ZTxIdSequencHash") + ) # a hasher for fields T.2b & S.2e + + self.outputs = HashWriter( + blake2b(outlen=32, personal=b"ZTxIdOutputsHash") + ) # a hasher for fields T.2c & S.2f + + self.empty = True # inputs_amount + outputs_amount == 0 + + def add_input(self, txi: TxInput, script_pubkey: bytes) -> None: + self.empty = False + + write_prevout(self.prevouts, txi) + write_uint64(self.amounts, txi.amount) + write_bytes_prefixed(self.scriptpubkeys, script_pubkey) + write_uint32(self.sequence, txi.sequence) + + def add_output(self, txo: TxOutput, script_pubkey: bytes) -> None: + self.empty = False + + write_tx_output(self.outputs, txo, script_pubkey) + + def digest(self) -> bytes: + """ + Returns `T.2: transparent_digest` field for txid computation. + + see: https://zips.z.cash/zip-0244#t-2-transparent-digest + """ + h = HashWriter(blake2b(outlen=32, personal=b"ZTxIdTranspaHash")) + + if not self.empty: + write_hash(h, self.prevouts.get_digest()) # T.2a + write_hash(h, self.sequence.get_digest()) # T.2b + write_hash(h, self.outputs.get_digest()) # T.2c + + return h.get_digest() + + def sig_digest( + self, + txi: TxInput | None, + script_pubkey: bytes | None, + ) -> bytes: + """ + Returns `S.2: transparent_sig_digest` field for signature + digest computation. + + see: https://zips.z.cash/zip-0244#s-2-transparent-sig-digest + """ + + if self.empty: + assert txi is None + assert script_pubkey is None + return self.digest() + + h = HashWriter(blake2b(outlen=32, personal=b"ZTxIdTranspaHash")) + + # only SIGHASH_ALL is supported in Trezor + write_uint8(h, SigHashType.SIGHASH_ALL) # S.2a + write_hash(h, self.prevouts.get_digest()) # S.2b + write_hash(h, self.amounts.get_digest()) # S.2c + write_hash(h, self.scriptpubkeys.get_digest()) # S.2d + write_hash(h, self.sequence.get_digest()) # S.2e + write_hash(h, self.outputs.get_digest()) # S.2f + write_hash(h, _txin_sig_digest(txi, script_pubkey)) # S.2g + + return h.get_digest() + + +def _txin_sig_digest( + txi: TxInput | None, + script_pubkey: bytes | None, +) -> bytes: + """ + Returns `S.2g: txin_sig_digest` field for signature digest computation. + + see: https://zips.z.cash/zip-0244#s-2g-txin-sig-digest + """ + + h = HashWriter(blake2b(outlen=32, personal=b"Zcash___TxInHash")) + + if txi is not None: + assert script_pubkey is not None + + write_prevout(h, txi) # 2.Sg.i + write_uint64(h, txi.amount) # 2.Sg.ii + write_bytes_prefixed(h, script_pubkey) # 2.Sg.iii + write_uint32(h, txi.sequence) # 2.Sg.iv + + return h.get_digest() + + +class SaplingHasher: + """ + Empty Sapling bundle hasher. + """ + + def digest(self) -> bytes: + """ + Returns `T.3: sapling_digest` field. + + see: https://zips.z.cash/zip-0244#t-3-sapling-digest + """ + return blake2b(outlen=32, personal=b"ZTxIdSaplingHash").digest() + + +class OrchardHasher: + """ + Empty Orchard bundle hasher. + """ + + def digest(self) -> bytes: + """ + Returns `T.4: orchard_digest` field. + + see: https://zips.z.cash/zip-0244#t-4-orchard-digest + """ + return blake2b(outlen=32, personal=b"ZTxIdOrchardHash").digest() diff --git a/core/src/apps/zcash/signer.py b/core/src/apps/zcash/signer.py new file mode 100644 index 000000000..c25660a93 --- /dev/null +++ b/core/src/apps/zcash/signer.py @@ -0,0 +1,112 @@ +from micropython import const +from typing import TYPE_CHECKING + +from trezor.messages import SignTx +from trezor.utils import ensure +from trezor.wire import DataError, ProcessError + +from apps.bitcoin.common import ecdsa_sign +from apps.bitcoin.sign_tx.bitcoinlike import Bitcoinlike +from apps.common.writers import write_compact_size, write_uint32_le + +from .hasher import ZcashHasher + +if TYPE_CHECKING: + from typing import Sequence + from apps.common.coininfo import CoinInfo + from apps.bitcoin.sign_tx.tx_info import OriginalTxInfo, TxInfo + from apps.bitcoin.writers import Writer + from apps.bitcoin.sign_tx.approvers import Approver + from trezor.utils import HashWriter + from trezor.messages import ( + PrevTx, + TxInput, + ) + from apps.bitcoin.keychain import Keychain + +OVERWINTERED = const(0x8000_0000) + + +class Zcash(Bitcoinlike): + def __init__( + self, + tx: SignTx, + keychain: Keychain, + coin: CoinInfo, + approver: Approver | None, + ) -> None: + ensure(coin.overwintered) + if tx.version != 5: + raise DataError("Expected transaction version 5.") + + super().__init__(tx, keychain, coin, approver) + + def create_sig_hasher(self, tx: SignTx | PrevTx) -> ZcashHasher: + return ZcashHasher(tx) + + def create_hash_writer(self) -> HashWriter: + # Replacement transactions are not supported + # so this should never be called. + raise NotImplementedError + + async def step3_verify_inputs(self) -> None: + # Replacement transactions are not supported. + + # We don't check prevouts, because BIP-341 techniques + # were adapted in ZIP-244 sighash algorithm. + # see: https://github.com/zcash/zips/issues/574 + self.taproot_only = True # turn on taproot behavior + await super().step3_verify_inputs() + self.taproot_only = False # turn off taproot behavior + + async def step5_serialize_outputs(self) -> None: + await super().step5_serialize_outputs() + + async def sign_nonsegwit_input(self, i_sign: int) -> None: + await self.sign_nonsegwit_bip143_input(i_sign) + + def sign_bip143_input(self, i: int, txi: TxInput) -> tuple[bytes, bytes]: + signature_digest = self.tx_info.sig_hasher.hash_zip244( + txi, self.input_derive_script(txi) + ) + node = self.keychain.derive(txi.address_n) + signature = ecdsa_sign(node, signature_digest) + return node.public_key(), signature + + async def process_original_input(self, txi: TxInput, script_pubkey: bytes) -> None: + raise ProcessError("Replacement transactions are not supported.") + # Zcash transaction fees are very low + # so there is no need to bump the fee. + + async def get_tx_digest( + self, + i: int, + txi: TxInput, + tx_info: TxInfo | OriginalTxInfo, + public_keys: Sequence[bytes | memoryview], + threshold: int, + script_pubkey: bytes, + tx_hash: bytes | None = None, + ) -> bytes: + return self.tx_info.sig_hasher.hash_zip244(txi, script_pubkey) + + def write_tx_header( + self, w: Writer, tx: SignTx | PrevTx, witness_marker: bool + ) -> None: + # defined in ZIP-225 (see https://zips.z.cash/zip-0225) + assert tx.version_group_id is not None + assert tx.branch_id is not None # checked in sanitize_* + assert tx.expiry is not None + + write_uint32_le(w, tx.version | OVERWINTERED) # nVersion | fOverwintered + write_uint32_le(w, tx.version_group_id) # nVersionGroupId + write_uint32_le(w, tx.branch_id) # nConsensusBranchId + write_uint32_le(w, tx.lock_time) # lock_time + write_uint32_le(w, tx.expiry) # expiryHeight + + def write_tx_footer(self, w: Writer, tx: SignTx | PrevTx) -> None: + # serialize Sapling bundle + write_compact_size(w, 0) # nSpendsSapling + write_compact_size(w, 0) # nOutputsSapling + # serialize Orchard bundle + write_compact_size(w, 0) # nActionsOrchard diff --git a/core/tests/test_apps.bitcoin.zcash.zip243.py b/core/tests/test_apps.bitcoin.zcash.zip243.py index ab8b70d7f..c1f419b0d 100644 --- a/core/tests/test_apps.bitcoin.zcash.zip243.py +++ b/core/tests/test_apps.bitcoin.zcash.zip243.py @@ -9,7 +9,7 @@ from apps.bitcoin.common import SigHashType from apps.bitcoin.writers import get_tx_hash if not utils.BITCOIN_ONLY: - from apps.bitcoin.sign_tx.zcash import ZcashSigHasher + from apps.bitcoin.sign_tx.zcash_v4 import Zip243SigHasher # test vectors inspired from https://github.com/zcash-hackworks/zcash-test-vectors/blob/master/zip_0243.py @@ -191,7 +191,7 @@ class TestZcashZip243(unittest.TestCase): branch_id=v["branch_id"], ) - zip243 = ZcashSigHasher() + zip243 = Zip243SigHasher() for i in v["inputs"]: txi = TxInput(