From 3e77a9a158b731961c333cce71e2bd30ee06b1df Mon Sep 17 00:00:00 2001 From: Kris Nuttycombe Date: Mon, 6 Dec 2021 21:08:36 -0700 Subject: [PATCH] Add test vectors for unified viewing keys. Fixes #47 Co-authored-by: str4d --- poetry.lock | 119 +++++++++++++++++++++++++++++- pyproject.toml | 1 + requirements.txt | 6 -- sapling_zip32.py | 11 ++- tv_rand.py | 8 +++ unified_address.py | 96 ++++--------------------- unified_encoding.py | 82 +++++++++++++++++++++ unified_full_viewing_keys.py | 136 +++++++++++++++++++++++++++++++++++ zc_utils.py | 6 +- 9 files changed, 369 insertions(+), 96 deletions(-) delete mode 100644 requirements.txt create mode 100644 unified_encoding.py create mode 100755 unified_full_viewing_keys.py diff --git a/poetry.lock b/poetry.lock index e4c16d5..7c83bc2 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,3 +1,14 @@ +[[package]] +name = "cffi" +version = "1.15.0" +description = "Foreign Function Interface for Python calling C code." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +pycparser = "*" + [[package]] name = "chacha20poly1305" version = "0.0.3" @@ -10,6 +21,25 @@ python-versions = "*" dev = ["pep8", "tox", "pypandoc"] docs = ["Sphinx (>=1.0)", "sphinx-rtd-theme", "sphinxcontrib-programoutput"] +[[package]] +name = "cryptography" +version = "36.0.0" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +cffi = ">=1.12" + +[package.extras] +docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"] +docstest = ["pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"] +pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"] +sdist = ["setuptools_rust (>=0.11.4)"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["pytest (>=6.2.0)", "pytest-cov", "pytest-subtests", "pytest-xdist", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"] + [[package]] name = "numpy" version = "1.21.0" @@ -26,15 +56,98 @@ category = "main" optional = false python-versions = "*" +[[package]] +name = "pycparser" +version = "2.21" +description = "C parser in Python" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "d8e6774b362633dc3d6f4b311004cd15462c394c93c9dae64afd27c6d777a4f0" +content-hash = "75e4faa5164449065d8912033077d9670d9b8e4e72b2962358701870528e0e74" [metadata.files] +cffi = [ + {file = "cffi-1.15.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:c2502a1a03b6312837279c8c1bd3ebedf6c12c4228ddbad40912d671ccc8a962"}, + {file = "cffi-1.15.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:23cfe892bd5dd8941608f93348c0737e369e51c100d03718f108bf1add7bd6d0"}, + {file = "cffi-1.15.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:41d45de54cd277a7878919867c0f08b0cf817605e4eb94093e7516505d3c8d14"}, + {file = "cffi-1.15.0-cp27-cp27m-win32.whl", hash = "sha256:4a306fa632e8f0928956a41fa8e1d6243c71e7eb59ffbd165fc0b41e316b2474"}, + {file = "cffi-1.15.0-cp27-cp27m-win_amd64.whl", hash = "sha256:e7022a66d9b55e93e1a845d8c9eba2a1bebd4966cd8bfc25d9cd07d515b33fa6"}, + {file = "cffi-1.15.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:14cd121ea63ecdae71efa69c15c5543a4b5fbcd0bbe2aad864baca0063cecf27"}, + {file = "cffi-1.15.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:d4d692a89c5cf08a8557fdeb329b82e7bf609aadfaed6c0d79f5a449a3c7c023"}, + {file = "cffi-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0104fb5ae2391d46a4cb082abdd5c69ea4eab79d8d44eaaf79f1b1fd806ee4c2"}, + {file = "cffi-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:91ec59c33514b7c7559a6acda53bbfe1b283949c34fe7440bcf917f96ac0723e"}, + {file = "cffi-1.15.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f5c7150ad32ba43a07c4479f40241756145a1f03b43480e058cfd862bf5041c7"}, + {file = "cffi-1.15.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:00c878c90cb53ccfaae6b8bc18ad05d2036553e6d9d1d9dbcf323bbe83854ca3"}, + {file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abb9a20a72ac4e0fdb50dae135ba5e77880518e742077ced47eb1499e29a443c"}, + {file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a5263e363c27b653a90078143adb3d076c1a748ec9ecc78ea2fb916f9b861962"}, + {file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f54a64f8b0c8ff0b64d18aa76675262e1700f3995182267998c31ae974fbc382"}, + {file = "cffi-1.15.0-cp310-cp310-win32.whl", hash = "sha256:c21c9e3896c23007803a875460fb786118f0cdd4434359577ea25eb556e34c55"}, + {file = "cffi-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:5e069f72d497312b24fcc02073d70cb989045d1c91cbd53979366077959933e0"}, + {file = "cffi-1.15.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:64d4ec9f448dfe041705426000cc13e34e6e5bb13736e9fd62e34a0b0c41566e"}, + {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2756c88cbb94231c7a147402476be2c4df2f6078099a6f4a480d239a8817ae39"}, + {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b96a311ac60a3f6be21d2572e46ce67f09abcf4d09344c49274eb9e0bf345fc"}, + {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75e4024375654472cc27e91cbe9eaa08567f7fbdf822638be2814ce059f58032"}, + {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:59888172256cac5629e60e72e86598027aca6bf01fa2465bdb676d37636573e8"}, + {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:27c219baf94952ae9d50ec19651a687b826792055353d07648a5695413e0c605"}, + {file = "cffi-1.15.0-cp36-cp36m-win32.whl", hash = "sha256:4958391dbd6249d7ad855b9ca88fae690783a6be9e86df65865058ed81fc860e"}, + {file = "cffi-1.15.0-cp36-cp36m-win_amd64.whl", hash = "sha256:f6f824dc3bce0edab5f427efcfb1d63ee75b6fcb7282900ccaf925be84efb0fc"}, + {file = "cffi-1.15.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:06c48159c1abed75c2e721b1715c379fa3200c7784271b3c46df01383b593636"}, + {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c2051981a968d7de9dd2d7b87bcb9c939c74a34626a6e2f8181455dd49ed69e4"}, + {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:fd8a250edc26254fe5b33be00402e6d287f562b6a5b2152dec302fa15bb3e997"}, + {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91d77d2a782be4274da750752bb1650a97bfd8f291022b379bb8e01c66b4e96b"}, + {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:45db3a33139e9c8f7c09234b5784a5e33d31fd6907800b316decad50af323ff2"}, + {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:263cc3d821c4ab2213cbe8cd8b355a7f72a8324577dc865ef98487c1aeee2bc7"}, + {file = "cffi-1.15.0-cp37-cp37m-win32.whl", hash = "sha256:17771976e82e9f94976180f76468546834d22a7cc404b17c22df2a2c81db0c66"}, + {file = "cffi-1.15.0-cp37-cp37m-win_amd64.whl", hash = "sha256:3415c89f9204ee60cd09b235810be700e993e343a408693e80ce7f6a40108029"}, + {file = "cffi-1.15.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4238e6dab5d6a8ba812de994bbb0a79bddbdf80994e4ce802b6f6f3142fcc880"}, + {file = "cffi-1.15.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0808014eb713677ec1292301ea4c81ad277b6cdf2fdd90fd540af98c0b101d20"}, + {file = "cffi-1.15.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:57e9ac9ccc3101fac9d6014fba037473e4358ef4e89f8e181f8951a2c0162024"}, + {file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b6c2ea03845c9f501ed1313e78de148cd3f6cad741a75d43a29b43da27f2e1e"}, + {file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:10dffb601ccfb65262a27233ac273d552ddc4d8ae1bf93b21c94b8511bffe728"}, + {file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:786902fb9ba7433aae840e0ed609f45c7bcd4e225ebb9c753aa39725bb3e6ad6"}, + {file = "cffi-1.15.0-cp38-cp38-win32.whl", hash = "sha256:da5db4e883f1ce37f55c667e5c0de439df76ac4cb55964655906306918e7363c"}, + {file = "cffi-1.15.0-cp38-cp38-win_amd64.whl", hash = "sha256:181dee03b1170ff1969489acf1c26533710231c58f95534e3edac87fff06c443"}, + {file = "cffi-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:45e8636704eacc432a206ac7345a5d3d2c62d95a507ec70d62f23cd91770482a"}, + {file = "cffi-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:31fb708d9d7c3f49a60f04cf5b119aeefe5644daba1cd2a0fe389b674fd1de37"}, + {file = "cffi-1.15.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6dc2737a3674b3e344847c8686cf29e500584ccad76204efea14f451d4cc669a"}, + {file = "cffi-1.15.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:74fdfdbfdc48d3f47148976f49fab3251e550a8720bebc99bf1483f5bfb5db3e"}, + {file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffaa5c925128e29efbde7301d8ecaf35c8c60ffbcd6a1ffd3a552177c8e5e796"}, + {file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f7d084648d77af029acb79a0ff49a0ad7e9d09057a9bf46596dac9514dc07df"}, + {file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ef1f279350da2c586a69d32fc8733092fd32cc8ac95139a00377841f59a3f8d8"}, + {file = "cffi-1.15.0-cp39-cp39-win32.whl", hash = "sha256:2a23af14f408d53d5e6cd4e3d9a24ff9e05906ad574822a10563efcef137979a"}, + {file = "cffi-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:3773c4d81e6e818df2efbc7dd77325ca0dcb688116050fb2b3011218eda36139"}, + {file = "cffi-1.15.0.tar.gz", hash = "sha256:920f0d66a896c2d99f0adbb391f990a84091179542c205fa53ce5787aff87954"}, +] chacha20poly1305 = [ {file = "chacha20poly1305-0.0.3.tar.gz", hash = "sha256:f2f005c7cf4638ffa4ff06c02c78748068b642916795c6d16c7cc5e355e70edf"}, ] +cryptography = [ + {file = "cryptography-36.0.0-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:9511416e85e449fe1de73f7f99b21b3aa04fba4c4d335d30c486ba3756e3a2a6"}, + {file = "cryptography-36.0.0-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:97199a13b772e74cdcdb03760c32109c808aff7cd49c29e9cf4b7754bb725d1d"}, + {file = "cryptography-36.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:494106e9cd945c2cadfce5374fa44c94cfadf01d4566a3b13bb487d2e6c7959e"}, + {file = "cryptography-36.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6fbbbb8aab4053fa018984bb0e95a16faeb051dd8cca15add2a27e267ba02b58"}, + {file = "cryptography-36.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:684993ff6f67000a56454b41bdc7e015429732d65a52d06385b6e9de6181c71e"}, + {file = "cryptography-36.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c702855cd3174666ef0d2d13dcc879090aa9c6c38f5578896407a7028f75b9f"}, + {file = "cryptography-36.0.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d91bc9f535599bed58f6d2e21a2724cb0c3895bf41c6403fe881391d29096f1d"}, + {file = "cryptography-36.0.0-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:b17d83b3d1610e571fedac21b2eb36b816654d6f7496004d6a0d32f99d1d8120"}, + {file = "cryptography-36.0.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:8982c19bb90a4fa2aad3d635c6d71814e38b643649b4000a8419f8691f20ac44"}, + {file = "cryptography-36.0.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:24469d9d33217ffd0ce4582dfcf2a76671af115663a95328f63c99ec7ece61a4"}, + {file = "cryptography-36.0.0-cp36-abi3-win32.whl", hash = "sha256:f6a5a85beb33e57998dc605b9dbe7deaa806385fdf5c4810fb849fcd04640c81"}, + {file = "cryptography-36.0.0-cp36-abi3-win_amd64.whl", hash = "sha256:2deab5ec05d83ddcf9b0916319674d3dae88b0e7ee18f8962642d3cde0496568"}, + {file = "cryptography-36.0.0-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2049f8b87f449fc6190350de443ee0c1dd631f2ce4fa99efad2984de81031681"}, + {file = "cryptography-36.0.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a776bae1629c8d7198396fd93ec0265f8dd2341c553dc32b976168aaf0e6a636"}, + {file = "cryptography-36.0.0-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:aa94d617a4cd4cdf4af9b5af65100c036bce22280ebb15d8b5262e8273ebc6ba"}, + {file = "cryptography-36.0.0-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:5c49c9e8fb26a567a2b3fa0343c89f5d325447956cc2fc7231c943b29a973712"}, + {file = "cryptography-36.0.0-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ef216d13ac8d24d9cd851776662f75f8d29c9f2d05cdcc2d34a18d32463a9b0b"}, + {file = "cryptography-36.0.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:231c4a69b11f6af79c1495a0e5a85909686ea8db946935224b7825cfb53827ed"}, + {file = "cryptography-36.0.0-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:f92556f94e476c1b616e6daec5f7ddded2c082efa7cee7f31c7aeda615906ed8"}, + {file = "cryptography-36.0.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:d73e3a96c38173e0aa5646c31bf8473bc3564837977dd480f5cbeacf1d7ef3a3"}, + {file = "cryptography-36.0.0.tar.gz", hash = "sha256:52f769ecb4ef39865719aedc67b4b7eae167bafa48dbc2a26dd36fa56460507f"}, +] numpy = [ {file = "numpy-1.21.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d5caa946a9f55511e76446e170bdad1d12d6b54e17a2afe7b189112ed4412bb8"}, {file = "numpy-1.21.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ac4fd578322842dbda8d968e3962e9f22e862b6ec6e3378e7415625915e2da4d"}, @@ -76,3 +189,7 @@ pyblake2 = [ {file = "pyblake2-1.1.2-cp36-cp36m-win_amd64.whl", hash = "sha256:c53417ee0bbe77db852d5fd1036749f03696ebc2265de359fe17418d800196c4"}, {file = "pyblake2-1.1.2.tar.gz", hash = "sha256:5ccc7eb02edb82fafb8adbb90746af71460fbc29aa0f822526fc976dff83e93f"}, ] +pycparser = [ + {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, + {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, +] diff --git a/pyproject.toml b/pyproject.toml index c16c08a..7701516 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,4 +25,5 @@ classifiers = [ python = "^3.7" numpy = "1.21.0" chacha20poly1305 = "0.0.3" +cryptography = "36.0.0" pyblake2 = "1.1.2" diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 4ac2a7d..0000000 --- a/requirements.txt +++ /dev/null @@ -1,6 +0,0 @@ -cffi==1.14.5 -chacha20poly1305==0.0.3 -cryptography==3.4.7 -numpy==1.21.0 -pyblake2==1.1.2 -pycparser==2.20 diff --git a/sapling_zip32.py b/sapling_zip32.py index 63beb94..6ffc9f9 100755 --- a/sapling_zip32.py +++ b/sapling_zip32.py @@ -133,6 +133,12 @@ class ExtendedFullViewingKey(DerivedIvk, ExtendedBase): def nk(self): return self._nk + def ovk(self): + return self._ovk + + def dk(self): + return self._dk + def is_xsk(self): return False @@ -164,12 +170,13 @@ class ExtendedFullViewingKey(DerivedIvk, ExtendedBase): c_i = I_R return self.__class__(ak_i, nk_i, ovk_i, dk_i, c_i, self.depth()+1, self.tag(), i) +def hardened(i): + assert(i < (1<<31)) + return i + (1<<31) def main(): args = render_args() - def hardened(i): return i + (1<<31) - seed = bytes(range(32)) m = ExtendedSpendingKey.master(seed) m_1 = m.child(1) diff --git a/tv_rand.py b/tv_rand.py index e064332..8647b6a 100644 --- a/tv_rand.py +++ b/tv_rand.py @@ -1,6 +1,14 @@ import os import struct +def randbytes_inner(rng, l): + ret = [] + while len(ret) < l: + ret.append(rng.randrange(0, 256)) + return bytes(ret) + +def randbytes(rng): + return lambda l: randbytes_inner(rng, l) class Rand(object): def __init__(self, random=os.urandom): diff --git a/unified_address.py b/unified_address.py index f1cfcf4..80514aa 100755 --- a/unified_address.py +++ b/unified_address.py @@ -2,99 +2,25 @@ import sys; assert sys.version_info[0] >= 3, "Python 3 required." import math +from random import Random import struct from bech32m import bech32_encode, bech32_decode, convertbits, Encoding from tv_output import render_args, render_tv, Some -from tv_rand import Rand +from tv_rand import Rand, randbytes from zc_utils import write_compact_size, parse_compact_size from f4jumble import f4jumble, f4jumble_inv import sapling_key_components import orchard_key_components - - -def tlv(typecode, value): - return b"".join([write_compact_size(typecode), write_compact_size(len(value)), value]) - -def padding(hrp): - assert(len(hrp) <= 16) - return bytes(hrp, "utf8") + bytes(16 - len(hrp)) - -def encode_unified(receivers): - orchard_receiver = b"" - if receivers[0]: - orchard_receiver = tlv(0x03, receivers[0]) - - sapling_receiver = b"" - if receivers[1]: - sapling_receiver = tlv(0x02, receivers[1]) - - t_receiver = b"" - if receivers[2][1]: - if receivers[2][0]: - typecode = 0x00 - else: - typecode = 0x01 - t_receiver = tlv(typecode, receivers[2][1]) - - hrp = "u" - - r_bytes = b"".join([orchard_receiver, sapling_receiver, t_receiver, padding(hrp)]) - converted = convertbits(f4jumble(r_bytes), 8, 5) - return bech32_encode(hrp, converted, Encoding.BECH32M) - -def decode_unified(addr_str): - (hrp, data, encoding) = bech32_decode(addr_str) - assert hrp == "u" and encoding == Encoding.BECH32M - - decoded = f4jumble_inv(bytes(convertbits(data, 5, 8, False))) - suffix = decoded[-16:] - # check trailing padding bytes - assert suffix == padding(hrp) - rest = decoded[:-16] - - result = {} - while len(rest) > 0: - (receiver_type, rest) = parse_compact_size(rest) - (receiver_len, rest) = parse_compact_size(rest) - - expected_len = {0: 20, 1: 20, 2: 43, 3: 43}.get(receiver_type) - if expected_len is not None: - assert receiver_len == expected_len, "incorrect receiver length" - - assert len(rest) >= receiver_len - (receiver, rest) = (rest[:receiver_len], rest[receiver_len:]) - - if receiver_type == 0 or receiver_type == 1: - assert not ('transparent' in result), "duplicate transparent receiver detected" - assert len(receiver) == 20 - result['transparent'] = receiver - - elif receiver_type == 2: - assert not ('sapling' in result), "duplicate sapling receiver detected" - assert len(receiver) == 43 - result['sapling'] = receiver - - elif receiver_type == 3: - assert not ('orchard' in result), "duplicate orchard receiver detected" - assert len(receiver) == 43 - result['orchard'] = receiver - - return result - +from unified_encoding import encode_unified, decode_unified +from unified_encoding import P2PKH_ITEM, P2SH_ITEM, SAPLING_ITEM, ORCHARD_ITEM def main(): args = render_args() - from random import Random rng = Random(0xabad533d) - def randbytes(l): - ret = [] - while len(ret) < l: - ret.append(rng.randrange(0, 256)) - return bytes(ret) - rand = Rand(randbytes) + rand = Rand(randbytes(rng)) test_vectors = [] for _ in range(0, 10): @@ -125,13 +51,15 @@ def main(): is_p2pkh = rand.bool() receivers = [ - orchard_raw_addr, - sapling_raw_addr, - (is_p2pkh, t_addr) + (ORCHARD_ITEM, orchard_raw_addr), + (SAPLING_ITEM, sapling_raw_addr), + (P2PKH_ITEM, t_addr if is_p2pkh else None), + (P2SH_ITEM, None if is_p2pkh else t_addr), ] - ua = encode_unified(receivers) + ua = encode_unified(rng, receivers, "u") - decoded = decode_unified(ua) + expected_lengths = {P2PKH_ITEM: 20, P2SH_ITEM: 20, SAPLING_ITEM: 43, ORCHARD_ITEM: 43} + decoded = decode_unified(ua, "u", expected_lengths) assert decoded.get('orchard') == orchard_raw_addr assert decoded.get('sapling') == sapling_raw_addr assert decoded.get('transparent') == t_addr diff --git a/unified_encoding.py b/unified_encoding.py new file mode 100644 index 0000000..5c73e5b --- /dev/null +++ b/unified_encoding.py @@ -0,0 +1,82 @@ +import sys; assert sys.version_info[0] >= 3, "Python 3 required." + +from random import Random + +from zc_utils import write_compact_size, parse_compact_size +from bech32m import bech32_encode, bech32_decode, convertbits, Encoding +from f4jumble import f4jumble, f4jumble_inv + +P2PKH_ITEM = 0x00 +P2SH_ITEM = 0x01 +SAPLING_ITEM = 0x02 +ORCHARD_ITEM = 0x03 + +def tlv(typecode, value): + return b"".join([write_compact_size(typecode), write_compact_size(len(value)), value]) + +def padding(hrp): + assert(len(hrp) <= 16) + return bytes(hrp, "utf8") + bytes(16 - len(hrp)) + +def encode_unified(rng, items, hrp): + encoded_items = [] + + has_p2pkh = False + has_p2sh = False + for item in items: + if item[1]: + if item[0] == P2PKH_ITEM: + has_p2pkh = True + if item[0] == P2SH_ITEM: + has_p2sh = True + assert (not (has_p2pkh and has_p2sh)) + encoded_items.append(tlv(item[0], item[1])) + + items_bytes = rng.sample(encoded_items, k=len(encoded_items)) + items_bytes.append(padding(hrp)) + + r_bytes = b"".join(items_bytes) + converted = convertbits(f4jumble(r_bytes), 8, 5) + return bech32_encode(hrp, converted, Encoding.BECH32M) + +def decode_unified(encoded, expected_hrp, expected_lengths): + (hrp, data, encoding) = bech32_decode(encoded) + assert hrp == expected_hrp and encoding == Encoding.BECH32M + assert(len(data) >= 48) + + decoded = f4jumble_inv(bytes(convertbits(data, 5, 8, False))) + suffix = decoded[-16:] + # check trailing padding bytes + assert suffix == padding(hrp) + rest = decoded[:-16] + + result = {} + while len(rest) > 0: + (item_type, rest) = parse_compact_size(rest) + (item_len, rest) = parse_compact_size(rest) + + expected_len = expected_lengths.get(item_type) + if expected_len is not None: + assert item_len == expected_len, "incorrect item length" + + assert len(rest) >= item_len + (item, rest) = (rest[:item_len], rest[item_len:]) + + if item_type == P2PKH_ITEM or item_type == P2SH_ITEM: + assert not ('transparent' in result), "duplicate transparent item detected" + result['transparent'] = item + + elif item_type == SAPLING_ITEM: + assert not ('sapling' in result), "duplicate sapling item detected" + result['sapling'] = item + + elif item_type == ORCHARD_ITEM: + assert not ('orchard' in result), "duplicate orchard item detected" + result['orchard'] = item + + else: + assert not ('unknown' in result), "duplicate unknown item detected" + result['unknown'] = (item_type, item) + + return result + diff --git a/unified_full_viewing_keys.py b/unified_full_viewing_keys.py new file mode 100755 index 0000000..2eca5e0 --- /dev/null +++ b/unified_full_viewing_keys.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python3 +import sys; assert sys.version_info[0] >= 3, "Python 3 required." + +import math +from random import Random +import struct + +from cryptography.hazmat.primitives.asymmetric import ec +from cryptography.hazmat.primitives.serialization import PublicFormat, Encoding + +from tv_output import render_args, render_tv, Some +from tv_rand import Rand, randbytes +from zc_utils import write_compact_size, parse_compact_size +from f4jumble import f4jumble, f4jumble_inv +import orchard_key_components +import sapling_key_components +import sapling_zip32 +from unified_encoding import encode_unified, decode_unified +from unified_encoding import P2PKH_ITEM, P2SH_ITEM, SAPLING_ITEM, ORCHARD_ITEM + +def main(): + args = render_args() + + rng = Random(0xabad533d) + rand = Rand(randbytes(rng)) + seed = rand.b(32) + + test_vectors = [] + for i in range(0, 10): + has_t_key = rand.bool() + if has_t_key: + c = rand.b(32) + privkey = ec.derive_private_key(int.from_bytes(rand.b(32), 'little'), ec.SECP256K1()) + pubkey = privkey.public_key() + pubkey_bytes = pubkey.public_bytes(Encoding.X962, PublicFormat.CompressedPoint) + t_key_bytes = c + pubkey_bytes + else: + t_key_bytes = None + + has_s_key = rand.bool() + if has_s_key: + root_key = sapling_zip32.ExtendedSpendingKey.master(seed) + purpose_key = root_key.child(sapling_zip32.hardened(32)) + coin_key = purpose_key.child(sapling_zip32.hardened(133)) + account_key = coin_key.child(sapling_zip32.hardened(i)) + sapling_fvk = account_key.to_extended_fvk() + + sapling_fvk_bytes = b"".join([ + bytes(sapling_fvk.ak()), + bytes(sapling_fvk.nk()), + sapling_fvk.ovk(), + sapling_fvk.dk() + ]) + else: + sapling_fvk_bytes = None + + has_o_key = (not has_s_key) or rand.bool() + if has_o_key: + orchard_sk = orchard_key_components.SpendingKey(rand.b(32)) + orchard_fvk = orchard_key_components.FullViewingKey(orchard_sk) + orchard_fvk_bytes = b"".join([ + bytes(orchard_fvk.ak), + bytes(orchard_fvk.nk), + bytes(orchard_fvk.rivk) + ]) + else: + orchard_fvk_bytes = None + + # include an unknown item 1/4 of the time + has_unknown_item = rand.bool() and rand.bool() + # use the range reserved for experimental typecodes for unknowns + unknown_tc = rng.randrange(0xFFFA, 0xFFFF+1) + unknown_len = rng.randrange(32, 256) + if has_unknown_item: + unknown_bytes = b"".join([rand.b(unknown_len)]) + else: + unknown_bytes = None + + receivers = [ + (ORCHARD_ITEM, orchard_fvk_bytes), + (SAPLING_ITEM, sapling_fvk_bytes), + (P2PKH_ITEM, t_key_bytes), + (unknown_tc, unknown_bytes), + ] + ufvk = encode_unified(rng, receivers, "uview") + + expected_lengths = { + P2PKH_ITEM: 65, + SAPLING_ITEM: 128, + ORCHARD_ITEM: 96, + unknown_tc: unknown_len + } + decoded = decode_unified(ufvk, "uview", expected_lengths) + assert decoded.get('orchard') == orchard_fvk_bytes + assert decoded.get('sapling') == sapling_fvk_bytes + assert decoded.get('transparent') == t_key_bytes + assert decoded.get('unknown') == ((unknown_tc, unknown_bytes) if unknown_bytes else None) + + test_vectors.append({ + 't_key_bytes': t_key_bytes, + 'sapling_fvk_bytes': sapling_fvk_bytes, + 'orchard_fvk_bytes': orchard_fvk_bytes, + 'unknown_fvk_typecode': unknown_tc, + 'unknown_fvk_bytes': unknown_bytes, + 'unified_fvk': ufvk.encode(), + }) + + render_tv( + args, + 'unified_full_viewing_keys', + ( + ('t_key_bytes', { + 'rust_type': 'Option<[u8; 65]>', + 'rust_fmt': lambda x: None if x is None else Some(x), + }), + ('sapling_fvk_bytes', { + 'rust_type': 'Option<[u8; 128]>', + 'rust_fmt': lambda x: None if x is None else Some(x), + }), + ('orchard_fvk_bytes', { + 'rust_type': 'Option<[u8; 96]>', + 'rust_fmt': lambda x: None if x is None else Some(x), + }), + ('unknown_fvk_typecode', 'u32'), + ('unknown_fvk_bytes', { + 'rust_type': 'Option>', + 'rust_fmt': lambda x: None if x is None else Some(x), + }), + ('unified_fvk', 'Vec') + ), + test_vectors, + ) + + +if __name__ == "__main__": + main() diff --git a/zc_utils.py b/zc_utils.py index 1fa2c58..e430a85 100755 --- a/zc_utils.py +++ b/zc_utils.py @@ -3,10 +3,10 @@ import sys; assert sys.version_info[0] >= 3, "Python 3 required." import struct -MAX_SIZE = 0x2000000 +MAX_COMPACT_SIZE = 0x2000000 def write_compact_size(n, allow_u64=False): - assert allow_u64 or n <= MAX_SIZE + assert allow_u64 or n <= MAX_COMPACT_SIZE if n < 253: return struct.pack('B', n) elif n <= 0xFFFF: @@ -18,7 +18,7 @@ def write_compact_size(n, allow_u64=False): def parse_compact_size(rest, allow_u64=False): (n, rest) = parse_compact_u64(rest) - assert allow_u64 or n <= MAX_SIZE + assert allow_u64 or n <= MAX_COMPACT_SIZE return (n, rest) def parse_compact_u64(rest):