-
-
Notifications
You must be signed in to change notification settings - Fork 655
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(core): add support for Zcash unified addresses
- Loading branch information
Showing
14 changed files
with
426 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
Add support for Zcash unified addresses |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
Oops, something went wrong.