-
-
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
1 parent
054b545
commit 031bac4
Showing
11 changed files
with
394 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 @@ | ||
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
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/zcash/librustzcash/blob/main/components/f4jumble/src/lib.rs | ||
""" | ||
|
||
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 48 <= len(message) <= 4194368 | ||
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 48 <= len(message) <= 4194368 | ||
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,137 @@ | ||
""" | ||
Implementation of encoding and decoding of Zcash | ||
unified addresses according to the ZIP-316. | ||
see: https://zips.z.cash/zip-0316 | ||
""" | ||
|
||
from typing import TYPE_CHECKING | ||
|
||
from trezor.crypto.bech32 import Encoding, bech32_decode, bech32_encode, convertbits | ||
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 | ||
|
||
if TYPE_CHECKING: | ||
from enum import IntEnum | ||
else: | ||
IntEnum = object | ||
|
||
|
||
class Typecode(IntEnum): | ||
P2PKH = 0x00 | ||
P2SH = 0x01 | ||
SAPLING = 0x02 | ||
ORCHARD = 0x03 | ||
|
||
|
||
def receiver_length(typecode: int) -> int | None: | ||
"""Byte length of a receiver.""" | ||
if typecode in (Typecode.P2PKH, Typecode.P2SH): | ||
return 20 | ||
if typecode in (Typecode.SAPLING, Typecode.ORCHARD): | ||
return 43 | ||
return None | ||
|
||
|
||
def prefix(coin: CoinInfo) -> str: | ||
"""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 hrp.encode() + 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) | ||
# at least one shielded address must be present | ||
assert Typecode.SAPLING in receivers or Typecode.ORCHARD in receivers | ||
|
||
length = 16 # 16 bytes for padding | ||
for receiver_bytes in receivers.values(): | ||
length += 2 # typecode (1 byte) + length (1 byte) | ||
length += len(receiver_bytes) | ||
|
||
w = empty_bytearray(length) | ||
|
||
# receivers in ascending order | ||
receivers_list = list(receivers.items()) | ||
receivers_list.sort() | ||
|
||
for (typecode, raw_bytes) in receivers_list: | ||
length = receiver_length(typecode) or len(raw_bytes) | ||
write_compact_size(w, typecode) | ||
write_compact_size(w, length) | ||
write_bytes_fixed(w, raw_bytes, length) | ||
|
||
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: dict[int, bytes] = dict() | ||
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 | ||
expected_length = receiver_length(typecode) | ||
if expected_length is not None and length != expected_length: | ||
raise DataError("Unexpected receiver length") | ||
|
||
if r.remaining_count() < length: | ||
raise DataError("Invalid receiver length") | ||
|
||
receivers[typecode] = r.read(length) | ||
|
||
if Typecode.P2PKH in receivers and Typecode.P2SH in receivers: | ||
raise DataError("Multiple transparent receivers") | ||
|
||
if len(receivers) == 1: | ||
the_receiver = list(receivers.keys())[0] | ||
if the_receiver in (Typecode.P2PKH, Typecode.P2SH): | ||
raise DataError("Only transparent receiver") | ||
|
||
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,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.