Skip to content

Commit

Permalink
feat(core): add support for Zcash unified addresses
Browse files Browse the repository at this point in the history
  • Loading branch information
krnak committed Jul 23, 2022
1 parent 421eec1 commit cc876ce
Show file tree
Hide file tree
Showing 14 changed files with 426 additions and 0 deletions.
17 changes: 17 additions & 0 deletions common/protob/messages-zcash.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
syntax = "proto2";
package hw.trezor.messages.zcash;

// Sugar for easier handling in Java
option java_package = "com.satoshilabs.trezor.lib.protobuf";
option java_outer_classname = "TrezorMessageZcash";

/**
* Receiver typecodes for unified addresses.
* see: https://zips.z.cash/zip-0316#encoding-of-unified-addresses
*/
enum ZcashReceiverTypecode {
P2PKH = 0;
P2SH = 1;
SAPLING = 2;
ORCHARD = 3;
}
1 change: 1 addition & 0 deletions core/.changelog.d/2398.added
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add support for Zcash unified addresses
6 changes: 6 additions & 0 deletions core/src/all_modules.py
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,8 @@
import trezor.enums.TezosBallotType
trezor.enums.TezosContractType
import trezor.enums.TezosContractType
trezor.enums.ZcashReceiverTypecode
import trezor.enums.ZcashReceiverTypecode
trezor.ui.components.common.webauthn
import trezor.ui.components.common.webauthn
trezor.ui.components.tt.webauthn
Expand Down Expand Up @@ -775,10 +777,14 @@
import apps.webauthn.resident_credentials
apps.zcash
import apps.zcash
apps.zcash.f4jumble
import apps.zcash.f4jumble
apps.zcash.hasher
import apps.zcash.hasher
apps.zcash.signer
import apps.zcash.signer
apps.zcash.unified_addresses
import apps.zcash.unified_addresses

# generate full alphabet
a
Expand Down
58 changes: 58 additions & 0 deletions core/src/apps/zcash/f4jumble.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"""
Memory-optimized implementation of F4jumble permutation specified in ZIP-316.
specification: https://zips.z.cash/zip-0316#jumbling
reference implementation: https://github.com/daira/jumble/blob/main/f4jumble.py
"""

from micropython import const

from trezor.crypto.hashlib import blake2b

HASH_LENGTH = const(64)


def xor(target: memoryview, mask: bytes) -> None:
for i in range(len(target)):
target[i] ^= mask[i]


def G_round(i: int, left: memoryview, right: memoryview) -> None:
for j in range((len(right) + HASH_LENGTH - 1) // HASH_LENGTH):
mask = blake2b(
personal=b"UA_F4Jumble_G" + bytes([i]) + j.to_bytes(2, "little"),
data=bytes(left),
).digest()
xor(right[j * HASH_LENGTH : (j + 1) * HASH_LENGTH], mask)


def H_round(i: int, left: memoryview, right: memoryview) -> None:
mask = blake2b(
personal=b"UA_F4Jumble_H" + bytes([i, 0, 0]),
outlen=len(left),
data=bytes(right),
).digest()
xor(left, mask)


def f4jumble(message: memoryview) -> None:
assert len(message) >= 22
left_length = min(HASH_LENGTH, len(message) // 2)

left = message[:left_length]
right = message[left_length:]
G_round(0, left, right)
H_round(0, left, right)
G_round(1, left, right)
H_round(1, left, right)


def f4unjumble(message: memoryview) -> None:
assert len(message) >= 22
left_length = min(HASH_LENGTH, len(message) // 2)

left = message[:left_length]
right = message[left_length:]
H_round(1, left, right)
G_round(1, left, right)
H_round(0, left, right)
G_round(0, left, right)
21 changes: 21 additions & 0 deletions core/src/apps/zcash/signer.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
from micropython import const
from typing import TYPE_CHECKING

from trezor.enums import OutputScriptType, ZcashReceiverTypecode as Receiver
from trezor.messages import SignTx
from trezor.utils import ensure
from trezor.wire import DataError, ProcessError

from apps.bitcoin import scripts
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 . import unified_addresses
from .hasher import ZcashHasher

if TYPE_CHECKING:
Expand All @@ -21,6 +24,7 @@
from trezor.messages import (
PrevTx,
TxInput,
TxOutput,
)
from apps.bitcoin.keychain import Keychain

Expand Down Expand Up @@ -110,3 +114,20 @@ def write_tx_footer(self, w: Writer, tx: SignTx | PrevTx) -> None:
write_compact_size(w, 0) # nOutputsSapling
# serialize Orchard bundle
write_compact_size(w, 0) # nActionsOrchard

def output_derive_script(self, txo: TxOutput) -> bytes:
# unified addresses
if txo.address is not None and txo.address[0] == "u":
assert txo.script_type is OutputScriptType.PAYTOADDRESS

receivers = unified_addresses.decode(txo.address, self.coin)
if Receiver.P2PKH in receivers:
pubkeyhash = receivers[Receiver.P2PKH]
return scripts.output_script_p2pkh(pubkeyhash)
if Receiver.P2SH in receivers:
scripthash = receivers[Receiver.P2SH]
return scripts.output_script_p2sh(scripthash)
raise DataError("Unified address does not include a transparent receiver.")

# transparent addresses
return super().output_derive_script(txo)
122 changes: 122 additions & 0 deletions core/src/apps/zcash/unified_addresses.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
"""
Implementation of encoding and decoding of Zcash
unified addresses according to the ZIP-316.
see: https://zips.z.cash/zip-0316
"""

from typing import Dict

from trezor.crypto.bech32 import Encoding, bech32_decode, bech32_encode, convertbits
from trezor.enums import ZcashReceiverTypecode as Typecode
from trezor.utils import BufferReader, empty_bytearray
from trezor.wire import DataError

from apps.common.coininfo import CoinInfo
from apps.common.readers import read_compact_size
from apps.common.writers import write_bytes_fixed, write_compact_size

from .f4jumble import f4jumble, f4unjumble


def receiver_length(typecode: int):
"""Byte length of a receiver."""
if typecode == Typecode.P2PKH:
return 20
if typecode == Typecode.P2SH:
return 20
if typecode == Typecode.SAPLING:
return 43
if typecode == Typecode.ORCHARD:
return 43
raise ValueError


def prefix(coin: CoinInfo):
"""Prefix for a unified address."""
if coin.coin_name == "Zcash":
return "u"
if coin.coin_name == "Zcash Testnet":
return "utest"
raise ValueError


def padding(hrp: str) -> bytes:
assert len(hrp) <= 16
return bytes(hrp, "utf8") + bytes(16 - len(hrp))


def encode(receivers: Dict[Typecode, bytes], coin: CoinInfo) -> str:
# multiple transparent receivers forbidden
assert not (Typecode.P2PKH in receivers and Typecode.P2SH in receivers)

length = 16 # 16 bytes for padding
for typecode in receivers.keys():
length += 2 # typecode (1 byte) + length (1 byte)
length += receiver_length(typecode)

w = empty_bytearray(length)

# receivers in decreasing order
receivers_list = list(receivers.items())
receivers_list.sort(reverse=True)

for (typecode, raw_bytes) in receivers_list:
write_compact_size(w, typecode)
write_compact_size(w, receiver_length(typecode))
write_bytes_fixed(w, raw_bytes, receiver_length(typecode))

hrp = prefix(coin)
write_bytes_fixed(w, padding(hrp), 16)
f4jumble(memoryview(w))
converted = convertbits(w, 8, 5)
return bech32_encode(hrp, converted, Encoding.BECH32M)


def decode(addr_str: str, coin: CoinInfo) -> Dict[int, bytes]:
(hrp, data, encoding) = bech32_decode(addr_str, max_bech_len=1000)
if (hrp, data, encoding) == (None, None, None):
raise DataError("Bech32m decoding failed.")
assert hrp is not None # to satisfy typecheckers
assert data is not None # to satisfy typecheckers
assert encoding is not None # to satisfy typecheckers
if hrp != prefix(coin):
raise DataError("Unexpected address prefix.")
if encoding != Encoding.BECH32M:
raise DataError("Bech32m encoding required.")

decoded = bytearray(convertbits(data, 5, 8, False))
f4unjumble(memoryview(decoded))

# check trailing padding bytes
if decoded[-16:] != padding(hrp):
raise DataError("Invalid padding bytes")

r = BufferReader(decoded[:-16])

last_typecode = None
receivers = {}
while r.remaining_count() > 0:
typecode = read_compact_size(r)
if typecode in receivers:
raise DataError("Duplicated typecode")
if typecode > 0x02000000:
raise DataError("Invalid typecode")
if last_typecode is not None and typecode > last_typecode:
raise DataError("Invalid receivers order")
last_typecode = typecode

length = read_compact_size(r)
# if the typecode of the receiver is known, then verify receiver length
try:
expected_length = receiver_length(typecode)
if length != expected_length:
raise DataError("Unexpected receiver length")
except ValueError:
pass

if r.remaining_count() < length:
raise DataError("Invalid receiver length")

receivers[typecode] = r.read(length)

return receivers
8 changes: 8 additions & 0 deletions core/src/trezor/enums/ZcashReceiverTypecode.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Automatically generated by pb2py
# fmt: off
# isort:skip_file

P2PKH = 0
P2SH = 1
SAPLING = 2
ORCHARD = 3
6 changes: 6 additions & 0 deletions core/src/trezor/enums/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -504,3 +504,9 @@ class TezosBallotType(IntEnum):
Yay = 0
Nay = 1
Pass = 2

class ZcashReceiverTypecode(IntEnum):
P2PKH = 0
P2SH = 1
SAPLING = 2
ORCHARD = 3
1 change: 1 addition & 0 deletions core/src/trezor/messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ def __getattr__(name: str) -> Any:
from trezor.enums import TezosBallotType # noqa: F401
from trezor.enums import TezosContractType # noqa: F401
from trezor.enums import WordRequestType # noqa: F401
from trezor.enums import ZcashReceiverTypecode # noqa: F401

class BinanceGetAddress(protobuf.MessageType):
address_n: "list[int]"
Expand Down
30 changes: 30 additions & 0 deletions core/tests/test_apps.zcash.f4jumble.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from common import *

from apps.zcash.f4jumble import f4jumble, f4unjumble

@unittest.skipUnless(not utils.BITCOIN_ONLY, "altcoin")
class TestZcashF4jumble(unittest.TestCase):
def test_f4jumble(self):
#source: https://github.com/zcash/librustzcash/blob/main/components/f4jumble/src/test_vectors.rs
TEST_VECTORS = [
{'jumbled': unhexlify('0304d029141b995da5387c125970673504d6c764d91ea6c082123770c7139ccd88ee27368cd0c0921a0444c8e5858d22'),
'normal': unhexlify('5d7a8f739a2d9e945b0ce152a8049e294c4d6e66b164939daffa2ef6ee6921481cdd86b3cc4318d9614fc820905d042b')},
{'jumbled': unhexlify('5271fa3321f3adbcfb075196883d542b438ec6339176537daf859841fe6a56222bff76d1662b5509a9e1079e446eeedd2e683c31aae3ee1851d7954328526be1'),
'normal': unhexlify('b1ef9ca3f24988c7b3534201cfb1cd8dbf69b8250c18ef41294ca97993db546c1fe01f7e9c8e36d6a5e29d4e30a73594bf5098421c69378af1e40f64e125946f')},
{'jumbled': unhexlify('498cf1b1ba6f4577effe64151d67469adc30acc325e326207e7d78487085b4162669f82f02f9774c0cc26ae6e1a76f1e266c6a9a8a2f4ffe8d2d676b1ed71cc47195a3f19208998f7d8cdfc0b74d2a96364d733a62b4273c77d9828aa1fa061588a7c4c88dd3d3dde02239557acfaad35c55854f4541e1a1b3bc8c17076e7316'),
'normal': unhexlify('62c2fa7b2fecbcb64b6968912a6381ce3dc166d56a1d62f5a8d7551db5fd9313e8c7203d996af7d477083756d59af80d06a745f44ab023752cb5b406ed8985e18130ab33362697b0e4e4c763ccb8f676495c222f7fba1e31defa3d5a57efc2e1e9b01a035587d5fb1a38e01d94903d3c3e0ad3360c1d3710acd20b183e31d49f')},
{'jumbled': unhexlify('7508a3a146714f229db91b543e240633ed57853f6451c9db6d64c6e86af1b88b28704f608582c53c51ce7d5b8548827a971d2b98d41b7f6258655902440cd66ee11e84dbfac7d2a43696fd0468810a3d9637c3fa58e7d2d341ef250fa09b9fb71a78a41d389370138a55ea58fcde779d714a04e0d30e61dc2d8be0da61cd684509'),
'normal': unhexlify('25c9a138f49b1a537edcf04be34a9851a7af9db6990ed83dd64af3597c04323ea51b0052ad8084a8b9da948d320dadd64f5431e61ddf658d24ae67c22c8d1309131fc00fe7f235734276d38d47f1e191e00c7a1d48af046827591e9733a97fa6b679f3dc601d008285edcbdae69ce8fc1be4aac00ff2711ebd931de518856878f7')},
{'jumbled': unhexlify('5139912fe8b95492c12731995a0f4478dbeb81ec36653a21bc80d673f3c6a0feef70b6c566f9d34bb726c098648382d105afb19b2b8486b73cbd47a17a0d2d1fd593b14bb9826c5d114b850c6f0cf3083a6f61e38e42713a37ef7997ebd2b376c8a410d797b3932e5a6e39e726b2894ce79604b4ae3c00acaea3be2c1dfe697fa644755102cf9ad78794d0594585494fe38ab56fa6ef3271a68a33481015adf3944c115311421a7dc3ce73ef2abf47e18a6aca7f9dd25a85ce8dbd6f1ad89c8d'),
'normal': unhexlify('3476f21a482ec9378365c8f7393c94e2885315eb4671098b79535e790fe53e29fef2b3766697ac32b4f473f468a008e72389fc03880d780cb07fcfaabe3f1a84b27db59a4a153d882d2b2103596555ed9494c6ac893c49723833ec8926c1039586a7afcf4a0d9c731e985d99589c8bb838e8aaf745533ed9e8ae3a1cd074a51a20da8aba18d1dbebbc862ded42435e92476930d069896cff30eb414f727b89e001afa2fb8dc3436d75a4a6f26572504b192232ecb9f0c02411e52596bc5e9045')}
]

for tv in TEST_VECTORS:
message = memoryview(bytearray(tv["normal"]))
f4jumble(message)
self.assertEqual(bytes(message), tv["jumbled"])
f4unjumble(message)
self.assertEqual(bytes(message), tv["normal"])

if __name__ == '__main__':
unittest.main()
Loading

0 comments on commit cc876ce

Please sign in to comment.