diff --git a/INSTALL.md b/INSTALL.md new file mode 100644 index 000000000..d6c579887 --- /dev/null +++ b/INSTALL.md @@ -0,0 +1,93 @@ +# How to Set up Electrumx Server with Docker Method + +Installation with docker method in linux server is the recommended method. This installation method requires your linux server either on x64 or arm64 hardware to have +docker enabled. Below installation guide is based off runing electrumx server using Nengcoin and Cheetahcoin as example. The docker setup is quite general and would +work for other coins supported by electrumx. + +Below electrumx server setup for 2 coins (nengcoin and cheetahcoin) are tested for ubuntu20.04 or ubuntu22.04 on x86_64 or arm64 hardware. + + +## Run NENG or CHTA Full Node + +In the same linux server, you will need to run nengcoin and/or cheetahcoin full node. Download latest version of NENG or CHTA core wallet, set up proper +rpc username/password and rpc port in the "nengcoin.conf" and/or "cheetahcoin.conf" in their proper wallet folder, make sure the conf file has one line below to insure your full node contain all transactions: + +``` +txindex=1 +``` + +Run full node and sync the full node to latest block height. + +Copy down the rpcuser/rpcpassword/rpcport information, they will be used in below electrumx server configuration. + +## Prepare electrumx folders + +We run electrumx folder at "/opt/electrumx" in host for mouting db and ssl folder from host to container. You can pick another path as your choice and +change your container launch scripts volume mount point accordingly. + +Ran below: +``` +sudo mkdir -p /opt/electrumx/db-NENG +sudo mkdir -p /opt/electrumx/db-CHTA +sudo mkdir -p /opt/electrumx/ssl +``` +The above commands created proper folders that will be used in docker run jobs + +## Obtain SSL certificate through Certbot/NGINX + +SSL or WSS URL connection method is recommended for current Komodo Wallet/Cheetahdex Wallet version and will be required for future versions while self created certificate files do not work for Komodo Wallet/Cheetahdex Wallet. Here we obtain free letsencrypt certificate through certbot/NGINX. +The below setup steps are largely based off komodo guide on this issue: https://komodoplatform.com/en/docs/komodo/setup-electrumx-server/ + +- Dependency - firewall/ port / certbot / nginx + +First of all, make sure to open up port 80 if your server is behind firewall because certbot ssl validation requires port 80 through nginx. You may also open up more ports +on 10001, 10002, 10003 used by Nengcoin TCP/SSL/WSS and ports 10007/10008/10009 used by Cheetahcoin TCP/SSL/WSS electrumx services. + +Perform installation of certbot uses snap and installation of nginx uses apt-get in ubuntu 20.04 or ubuntu 22.04: +``` + sudo apt install snapd + sudo apt install nginx-core +``` + + +Ran below to obtain SSL certificate needed for electrum ssl/wss connection. +``` +sudo snap install core; sudo snap refresh core +sudo apt-get remove certbot +sudo snap install --classic certbot +sudo ln -s /snap/bin/certbot /usr/bin/certbot +sudo certbot --nginx +``` + +certbot/nginx above command will prompt you for desired subdomain or domain for certificates and will install live copy when process succeed. +```commandline +cp /etc/letsencrypt/live/electrum2.mooo.com/fullchain.pem /opt/electrumx/ssl/ +cp /etc/letsencrypt/live/electrum2.mooo.com/privkey.pem /opt/electrumx/ssl/ +``` + +The free letsencrypt certificate is valid for 3 months and you can renew once it expires. + + +## Install docker + +In ubuntu or debian, you can run below to install docker in your linux machine: + +``` + sudo apt-get update + sudo apt-get install -y docker.io +``` + +Check out online guide if your linux is openSUSE, fedora or arch based. + + +## Install coin docker images, run docker job + +For Nengcoin, checkout 'contrib/nengcoin' of this repos README guide, follow the guide step by step to install docker image, run a docker-run job, and trouble shoot +job log information or other maintanence tasks if needed. + +For Cheetahcoin, checkout 'contrib/cheetahcoin' of this repos README guide, follow the guide step by step to install docker image, run a docker-run job, and trouble shoot +job log information or other maintanence tasks if needed. + +Your can run 2 coins with docker jobs together in the same linux server. + +The electrumx server should be running now allow Komodo Wallet/Cheetahdex Wallet, electrum-NENG or electrum-CHTA to connect to your server. diff --git a/contrib/cheetahcoin/Dockerfile b/contrib/cheetahcoin/Dockerfile new file mode 100644 index 000000000..243759075 --- /dev/null +++ b/contrib/cheetahcoin/Dockerfile @@ -0,0 +1,39 @@ +FROM ubuntu:22.04 + +WORKDIR / + +RUN apt-get update && \ + apt-get -y install python3-setuptools python3-multidict python3.10 python3.10-dev libleveldb-dev python3-setuptools python3-multidict gcc g++ libsnappy-dev zlib1g-dev libbz2-dev libgflags-dev build-essential python3-pip git + +RUN python3.10 -m pip install --upgrade pip +RUN python3.10 -m pip install aiohttp pylru Cython uvloop quark_hash +RUN python3.10 -m pip install plyvel +RUN git clone https://github.com/ShorelineCrypto/electrumx-1 /opt/electrumx + +ENV COIN=Cheetahcoin +ENV DB_DIRECTORY=/db +ENV DAEMON_URL="http://RPCUSER:RPCPASSWORD@IP:RPCPORT/" +ENV SERVICES="tcp://:10007,rpc://0.0.0.0:8007,ssl://:10008,wss://:10009" +ENV EVENT_LOOP_POLICY=uvloop +ENV PEER_DISCOVERY=self +ENV INITIAL_CONCURRENT=50 +ENV COST_SOFT_LIMIT=10000 +ENV COST_HARD_LIMIT=100000 +ENV BANDWIDTH_UNIT_COST=10000 +ENV SSL_CERTFILE=/ssl/fullchain.pem +ENV SSL_KEYFILE=/ssl/privkey.pem +ENV ALLOW_ROOT=true +ENV MAX_SEND=10000000 +ENV CACHE_MB=2000 + +VOLUME /db +VOLUME /ssl + +RUN mkdir -p "$DB_DIRECTORY" && ulimit -n 1048576 +RUN mkdir -p /ssl + +WORKDIR /opt/electrumx + +RUN python3.10 setup.py install +CMD ["/usr/bin/python3.10", "/opt/electrumx/electrumx_server"] + diff --git a/contrib/cheetahcoin/README.md b/contrib/cheetahcoin/README.md new file mode 100644 index 000000000..9e6622f11 --- /dev/null +++ b/contrib/cheetahcoin/README.md @@ -0,0 +1,104 @@ +## Install electrumx docker image for Cheetahcoin on x86_64 (amd64) or arm64 (aarch64) GNU/linux: + +Pull down a working docker image from docker hub: + +x64 +``` + docker pull shorelinecrypto/electrumx-chta:amd64 + docker tag shorelinecrypto/electrumx-chta:amd64 electrumx-chta:latest +``` +arm64 +``` + docker pull shorelinecrypto/electrumx-chta:arm64 + docker tag shorelinecrypto/electrumx-chta:arm64 electrumx-chta:latest +``` +Alternatively, build docker image from source: + +``` + docker build -t electrumx-chta . +``` + +### Run electrumx Cheetahcoin server with docker + +Replace with your CHTA full node rpcuser/rpcpassword and your server hostname with below command, assuming the CHTA full node runs at rpcport=8546 : + +``` + docker run -d --net=host -v /opt/electrumx/db-CHTA/:/db -v /opt/electrumx/ssl:/ssl -e DAEMON_URL="http://youruser:yourpass@127.0.0.1:8546" -e REPORT_SERVICES=tcp://yourhost:10007,ssl://yourhost:10008,wss://yourhost:10009 electrumx-chta +``` + +### Trouble shoot or check docker container status + +Your docker run electrumx server job should be running, run below to obtain image / container ID + +``` + docker ps + docker container ls -la + docker images -a +``` + +In order to trouble shoot issues or check log information of electrumx job, run blow to get real time log information + +``` + docker logs CONTAINER_ID +``` + +## Shut down electrum Cheetahcoin docker server + for a proper clean shutdown, send TERM signal to the running container eg.: + +``` + docker kill --signal="TERM" CONTAINER_ID + +``` + +## clean up and remove residual containers + +Stopped containers take up disk and memory resources, you may want to clean up and remove those dead containers to free up resources + +``` + docker rm CONTAINER_ID +``` + +## electrumx server crash maintenance + +Electrumx server tend to crash from time to time after running smoothly for several weeks. Using docker logs CONTAINER_ID method can find +crash error like this assuming your CONTAINER_ID is 'c39a7d4a07d7': +``` + docker ps -a + docker logs c39a7d4a07d7 + +--- skipped logs informations --- + struct.error: 'H' format requires 0 <= number <= 65535 +``` + +The ROOT CAUSE of the crash is due to database overflow and can be fixed with below steps : + +#### (1) delete the exited docker container +``` + docker ps -a + docker rm c39a7d4a07d7 +``` + +#### (2) setup "test" container to do maintenance job +``` + docker run -it --name test -v /opt/electrumx/db-CHTA/:/db electrumx-chta /bin/bash +``` + +This above command will enter docker container with root account. +#### (3) do maintenance in "test" container + +Run below commands inside container root account in electrumx folder /opt/electrumx + +``` + python3.10 electrumx_compact_history + exit +``` + +The above python3.10 command should take a few minutes to complete and then exit container + + +#### (4) Delete the containers and re-start electrumx-chta container job +``` + docker rm test +``` + go back to command step above under "Run electrumx Cheetahcoin server with docker" + diff --git a/contrib/cheetahcoin/electrumx_maintain.sh b/contrib/cheetahcoin/electrumx_maintain.sh new file mode 100755 index 000000000..c9a4c1014 --- /dev/null +++ b/contrib/cheetahcoin/electrumx_maintain.sh @@ -0,0 +1,33 @@ +#! /bin/bash + +if (test $# != 1) +then + echo "electrum_maintain.sh " + exit +fi + + +image_name=$1 + + +##obtain exited images + +container_id=`docker ps --filter "status=exited" | grep ${image_name} | cut -f1 -d' '` + +if ( test -z ${container_id} ) +then + echo "electrumx server running" > /dev/null +elif [[ $image_name == "electrumx-neng" ]] +then + docker rm $container_id + docker run --rm -v /opt/electrumx/db-NENG/:/db ${image_name} python3.10 electrumx_compact_history + bash /root/docker_run_${image_name}.sh +elif [[ $image_name == "electrumx-chta" ]] +then + docker rm $container_id + docker run --rm -v /opt/electrumx/db-CHTA/:/db ${image_name} python3.10 electrumx_compact_history + bash /root/docker_run_${image_name}.sh +else + echo "wrong argument" +fi + diff --git a/contrib/nengcoin/Dockerfile b/contrib/nengcoin/Dockerfile new file mode 100644 index 000000000..d9e1610bc --- /dev/null +++ b/contrib/nengcoin/Dockerfile @@ -0,0 +1,39 @@ +FROM ubuntu:22.04 + +WORKDIR / + +RUN apt-get update && \ + apt-get -y install python3-setuptools python3-multidict python3.10 python3.10-dev libleveldb-dev python3-setuptools python3-multidict gcc g++ libsnappy-dev zlib1g-dev libbz2-dev libgflags-dev build-essential python3-pip git + +RUN python3.10 -m pip install --upgrade pip +RUN python3.10 -m pip install aiohttp pylru Cython uvloop quark_hash +RUN python3.10 -m pip install plyvel +RUN git clone https://github.com/ShorelineCrypto/electrumx-1 /opt/electrumx + +ENV COIN=Nengcoin +ENV DB_DIRECTORY=/db +ENV DAEMON_URL="http://RPCUSER:RPCPASSWORD@IP:RPCPORT/" +ENV SERVICES="tcp://:10001,rpc://0.0.0.0:8001,ssl://:10002,wss://:10003" +ENV EVENT_LOOP_POLICY=uvloop +ENV PEER_DISCOVERY=self +ENV INITIAL_CONCURRENT=50 +ENV COST_SOFT_LIMIT=10000 +ENV COST_HARD_LIMIT=100000 +ENV BANDWIDTH_UNIT_COST=10000 +ENV SSL_CERTFILE=/ssl/fullchain.pem +ENV SSL_KEYFILE=/ssl/privkey.pem +ENV ALLOW_ROOT=true +ENV MAX_SEND=10000000 +ENV CACHE_MB=2000 + +VOLUME /db +VOLUME /ssl + +RUN mkdir -p "$DB_DIRECTORY" && ulimit -n 1048576 +RUN mkdir -p /ssl + +WORKDIR /opt/electrumx + +RUN python3.10 setup.py install +CMD ["/usr/bin/python3.10", "/opt/electrumx/electrumx_server"] + diff --git a/contrib/nengcoin/README.md b/contrib/nengcoin/README.md new file mode 100644 index 000000000..f5becfc90 --- /dev/null +++ b/contrib/nengcoin/README.md @@ -0,0 +1,104 @@ +## Install electrumx docker image for Nengcoin on x86_64 (amd64) or arm64 (aarch64) GNU/linux: + +Pull down a working docker image from docker hub: + +x64 +``` + docker pull shorelinecrypto/electrumx-neng:amd64 + docker tag shorelinecrypto/electrumx-neng:amd64 electrumx-neng:latest +``` +arm64 +``` + docker pull shorelinecrypto/electrumx-neng:arm64 + docker tag shorelinecrypto/electrumx-neng:arm64 electrumx-neng:latest +``` +Alternatively, build docker image from source: + +``` + docker build -t electrumx-neng . +``` + +### Run electrumx Nengcoin server with docker + +Replace with your NENG full node rpcuser/rpcpassword and your server hostname with below command, assuming the NENG full node runs at rpcport=8388 : + +``` + docker run -d --net=host -v /opt/electrumx/db-NENG/:/db -v /opt/electrumx/ssl:/ssl -e DAEMON_URL="http://youruser:yourpass@127.0.0.1:8388" -e REPORT_SERVICES=tcp://yourhost:10001,ssl://yourhost:10002,wss://yourhost:10003 electrumx-neng +``` + +### Trouble shoot or check docker container status + +Your docker run electrumx server job should be running, run below to obtain image / container ID + +``` + docker ps + docker container ls -la + docker images -a +``` + +In order to trouble shoot issues or check log information of electrumx job, run blow to get real time log information + +``` + docker logs CONTAINER_ID +``` + +## Shut down electrum Nengcoin docker server + for a proper clean shutdown, send TERM signal to the running container eg.: + +``` + docker kill --signal="TERM" CONTAINER_ID + +``` + +## clean up and remove residual containers + +Stopped containers take up disk and memory resources, you may want to clean up and remove those dead containers to free up resources + +``` + docker rm CONTAINER_ID +``` + +## electrumx server crash maintenance + +Electrumx server tend to crash from time to time after running smoothly for several weeks. Using docker logs CONTAINER_ID method can find +crash error like this assuming your CONTAINER_ID is 'c39a7d4a07d7': +``` + docker ps -a + docker logs c39a7d4a07d7 + +--- skipped logs informations --- + struct.error: 'H' format requires 0 <= number <= 65535 +``` + +The ROOT CAUSE of the crash is due to database overflow and can be fixed with below steps : + +#### (1) delete the exited docker container +``` + docker ps -a + docker rm c39a7d4a07d7 +``` + +#### (2) setup "test" container to do maintenance job +``` + docker run -it --name test -v /opt/electrumx/db-NENG/:/db electrumx-neng /bin/bash +``` + +This above command will enter docker container with root account. +#### (3) do maintenance in "test" container + +Run below commands inside container root account in electrumx folder /opt/electrumx + +``` + python3.10 electrumx_compact_history + exit +``` + +The above python3.10 command should take a few minutes to complete and then exit container + + +#### (4) Delete the containers and re-start electrumx-neng container job +``` + docker rm test +``` + go back to command step above under "Run electrumx Nengcoin server with docker" + diff --git a/electrumx/lib/coins.py b/electrumx/lib/coins.py index ef25c74c9..c34c309b0 100644 --- a/electrumx/lib/coins.py +++ b/electrumx/lib/coins.py @@ -2293,6 +2293,23 @@ class Bitcore(BitcoinMixin, Coin): 'ele3.bitcore.cc s t', 'ele4.bitcore.cc s t' ] + +class GleecBTC(Coin): + NAME = "GleecBTC" + SHORTNAME = "GLEEC" + NET = "mainnet" + XPUB_VERBYTES = bytes.fromhex("0488B21E") + XPRV_VERBYTES = bytes.fromhex("0488ADE4") + P2PKH_VERBYTE = bytes.fromhex("23") + P2SH_VERBYTES = (bytes.fromhex("26"),) + WIF_BYTE = bytes.fromhex("41") + DESERIALIZER = lib_tx.DeserializerSegWit + GENESIS_HASH = ('000000000019d6689c085ae165831e93' + '4ff763ae46a2a6c172b3f1b60a8ce26f') + TX_COUNT = 1759864 + TX_COUNT_HEIGHT = 1614311 + TX_PER_BLOCK = 1.08 + RPC_PORT = 8332 class GameCredits(Coin): @@ -4078,6 +4095,43 @@ class Quebecoin(AuxPowMixin, Coin): RPC_PORT = 10890 +class SmartUSD(NameIndexMixin, AuxPowMixin, Coin): + NAME = "SmartUSD" + SHORTNAME = "SFUSD" + NET = "mainnet" + XPUB_VERBYTES = bytes.fromhex("0488b21e") + XPRV_VERBYTES = bytes.fromhex("0488ade4") + P2PKH_VERBYTE = bytes.fromhex("3f") + P2SH_VERBYTES = (bytes.fromhex("55"),) + WIF_BYTE = bytes.fromhex("bc") + GENESIS_HASH = ('00000000c36f0406d516605e0a2d2702' + '085d565ec0c1283883002127dfcd52b7') + DESERIALIZER = lib_tx.DeserializerAuxPowSegWit + TX_COUNT = 11000 + TX_COUNT_HEIGHT = 11000 + TX_PER_BLOCK = 1 + RPC_PORT = 47776 + + BLOCK_PROCESSOR = block_proc.NameIndexBlockProcessor + + # Name opcodes + OP_NAME_NEW = OpCodes.OP_1 + OP_NAME_FIRSTUPDATE = OpCodes.OP_2 + OP_NAME_UPDATE = OpCodes.OP_3 + + # Valid name prefixes. + NAME_NEW_OPS = [OP_NAME_NEW, -1, OpCodes.OP_2DROP] + NAME_FIRSTUPDATE_OPS = [OP_NAME_FIRSTUPDATE, "name", -1, -1, + OpCodes.OP_2DROP, OpCodes.OP_2DROP] + NAME_UPDATE_OPS = [OP_NAME_UPDATE, "name", -1, OpCodes.OP_2DROP, + OpCodes.OP_DROP] + NAME_OPERATIONS = ( + NAME_NEW_OPS, + NAME_FIRSTUPDATE_OPS, + NAME_UPDATE_OPS, + ) + + class Beyondcoin(Coin): NAME = "Beyondcoin" SHORTNAME = "BYND" @@ -4135,6 +4189,24 @@ class Lbry(Coin): REORG_LIMIT = 5000 +class Milevium(Coin): + NAME = "Milevium" + SHORTNAME = "MIL" + NET = "mainnet" + P2PKH_VERBYTE = bytes.fromhex("32") + P2SH_VERBYTES = (bytes.fromhex("c4"),) + WIF_BYTE = bytes.fromhex("ef") + GENESIS_HASH = ('b5e4d2c5b166103c2bdb563fd2c804a4' + 'ff16d098d20f604af2ee23b21e918b67') + DESERIALIZER = lib_tx.DeserializerSegWit + TX_COUNT = 78861 + TX_COUNT_HEIGHT = 78789 + TX_PER_BLOCK = 2 + RPC_PORT = 41889 + REORG_LIMIT = 800 + BASIC_HEADER_SIZE = 120 + + class Bitweb(Coin): NAME = "Bitweb" SHORTNAME = "BTE" @@ -4191,6 +4263,96 @@ class Garlicoin(Coin): ] +class Clam(ScryptMixin, Coin): + NAME = "Clam" + SHORTNAME = "CLAM" + NET = "mainnet" + XPUB_VERBYTES = bytes.fromhex("0488b21e") + XPRV_VERBYTES = bytes.fromhex("0488ade4") + P2PKH_VERBYTE = bytes.fromhex("89") + P2SH_VERBYTES = (bytes.fromhex("0d"),) + WIF_BYTE = bytes.fromhex("85") + GENESIS_HASH = ('00000c3ce6b3d823a35224a39798eca9' + 'ad889966aeb5a9da7b960ffb9869db35') + DESERIALIZER = lib_tx.DeserializerTrezarcoin + DAEMON = daemon.FakeEstimateFeeDaemon + TX_COUNT = 10553627 + TX_COUNT_HEIGHT = 4691882 + TX_PER_BLOCK = 2 + RPC_PORT = 30174 + REORG_LIMIT = 2000 + + +class Nengcoin(Coin): + NAME = "Nengcoin" + SHORTNAME = "NENG" + NET = "mainnet" + XPUB_VERBYTES = bytes.fromhex("0488B21E") + XPRV_VERBYTES = bytes.fromhex("0488ADE4") + P2PKH_VERBYTE = bytes.fromhex("35") + P2SH_VERBYTES = (bytes.fromhex("05"),) + WIF_BYTE = bytes.fromhex("b0") + GENESIS_HASH = ('14683bb988bcb69c74276df315c8de10' + '8d990fcff07483d5f2a044a3b4a592d8') + TX_COUNT = 4512019 + TX_COUNT_HEIGHT = 3539846 + TX_PER_BLOCK = 2 + REORG_LIMIT = 2000 + + +class Cheetahcoin(Coin): + NAME = "Cheetahcoin" + SHORTNAME = "CHTA" + NET = "mainnet" + XPUB_VERBYTES = bytes.fromhex("0488B21E") + XPRV_VERBYTES = bytes.fromhex("0488ADE4") + P2PKH_VERBYTE = bytes.fromhex("1c") + P2SH_VERBYTES = (bytes.fromhex("05"),) + WIF_BYTE = bytes.fromhex("80") + GENESIS_HASH = ('0000000090ae6bab6c2abd99179a7632' + 'b84f286f876def641dd35c3221eee7be') + TX_COUNT = 952551 + TX_COUNT_HEIGHT = 802661 + TX_PER_BLOCK = 2 + REORG_LIMIT = 2000 + + +class Diabase(Dash): + NAME = "Diabase" + SHORTNAME = "DIAC" + NET = "mainnet" + GENESIS_HASH = ('0000057be3e5420fcefa43eda26de60a' + '3802bfc55a967443b07a41c133e0008f') + TX_COUNT = 25640 + TX_COUNT_HEIGHT = 181237 + TX_PER_BLOCK = 2 + RPC_PORT = 7675 + + +class Riecoin(Coin): + NAME = "Riecoin" + SHORTNAME = "RIC" + NET = "mainnet" + P2PKH_VERBYTE = bytes.fromhex("3c") + P2SH_VERBYTES = (bytes.fromhex("41"),) + WIF_BYTE = bytes.fromhex("bc") + GENESIS_HASH = ('e1ea18d0676ef9899fbc78ef428d1d26' + 'a2416d0f0441d46668d33bcb41275740') + DESERIALIZER = lib_tx.DeserializerSegWit + BASIC_HEADER_SIZE = 112 + TX_COUNT = 920177 + TX_COUNT_HEIGHT = 2108109 + TX_PER_BLOCK = 2 + RPC_PORT = 28332 + REORG_LIMIT = 5000 + + @classmethod + def header_hash(cls, header): + '''Given a header return the hash.''' + import riecoin_module as riecoin + return hex_str_to_hash(riecoin.riecoin_hash(hash_to_hex_str(header[::-1]))) + + class Ferrite(Coin): NAME = "Ferrite" SHORTNAME = "FEC" @@ -4239,3 +4401,4 @@ class FerriteTestnet(Ferrite): 'enode2.ferritecoin.org s t', 'enode3.ferritecoin.org s t', ] + diff --git a/electrumx/lib/tx.py b/electrumx/lib/tx.py index 95d69b20f..4edf1dce6 100644 --- a/electrumx/lib/tx.py +++ b/electrumx/lib/tx.py @@ -479,52 +479,130 @@ class DeserializerEquihashSegWit(DeserializerSegWit, DeserializerEquihash): pass +# https://zips.z.cash/zip-0202 +# https://zips.z.cash/zip-0225 + class DeserializerZcash(DeserializerEquihash): + + OVERWINTER_VERSION_GROUP_ID = 0x03C48270 + SAPLING_VERSION_GROUP_ID = 0x892F2085 + ZIP225_VERSION_GROUP_ID = 0x26A7270A + OVERWINTER_TX_VERSION = 3 + SAPLING_TX_VERSION = 4 + ZIP225_TX_VERSION = 5 + + ZFUTURE_VERSION_GROUP_ID = 0xFFFFFFFF + ZFUTURE_TX_VERSION = 0x0000FFFF + def read_tx(self): header = self._read_le_uint32() overwintered = ((header >> 31) == 1) if overwintered: version = header & 0x7fffffff - self.cursor += 4 # versionGroupId + nVersionGroupId = self._read_le_uint32() else: version = header is_overwinter_v3 = version == 3 is_sapling_v4 = version == 4 + is_zip225_v5 = (overwintered and nVersionGroupId == self.ZIP225_VERSION_GROUP_ID and version == self.ZIP225_TX_VERSION) - base_tx = Tx( - version, - self._read_inputs(), # inputs - self._read_outputs(), # outputs - self._read_le_uint32() # locktime - ) + if not(is_zip225_v5): + base_tx = Tx( + version, + self._read_inputs(), # inputs + self._read_outputs(), # outputs + self._read_le_uint32() # locktime + ) + + if is_overwinter_v3 or is_sapling_v4: + self.cursor += 4 # expiryHeight + + has_shielded = False + if is_sapling_v4: + self.cursor += 8 # valueBalance + shielded_spend_size = self._read_varint() + self.cursor += shielded_spend_size * 384 # vShieldedSpend + shielded_output_size = self._read_varint() + self.cursor += shielded_output_size * 948 # vShieldedOutput + has_shielded = shielded_spend_size > 0 or shielded_output_size > 0 + + if base_tx.version >= 2: + joinsplit_size = self._read_varint() + if joinsplit_size > 0: + joinsplit_desc_len = 1506 + (192 if is_sapling_v4 else 296) + # JSDescription + self.cursor += joinsplit_size * joinsplit_desc_len + self.cursor += 32 # joinSplitPubKey + self.cursor += 64 # joinSplitSig + + if is_sapling_v4 and has_shielded: + self.cursor += 64 # bindingSig + else: + nConsensusBranchId = self._read_le_uint32() + nLockTime = self._read_le_uint32() + self.cursor += 4 # nExpiryHeight + base_tx = Tx( + version, + # Transparent Transaction Fields + self._read_inputs(), # inputs + self._read_outputs(), # outputs + nLockTime # locktime + ) + # Sapling Transaction Fields (SaplingBundle) + nSpendsSapling = self._read_varint() + self.cursor += 96 * nSpendsSapling # vSpendsSapling + nOutputsSapling = self._read_varint() + self.cursor += 756 * nOutputsSapling # vOutputsSapling + hasSapling = not(nSpendsSapling == 0 and nOutputsSapling == 0) + if (hasSapling): + self.cursor += 8 # valueBalanceSapling + if not(nSpendsSapling == 0): + self.cursor += 32 # anchorSapling + self.cursor += 192 * nSpendsSapling # vSpendProofsSapling + self.cursor += 64 * nSpendsSapling # vSpendAuthSigsSapling + self.cursor += 192 * nOutputsSapling # vOutputProofsSapling + if (hasSapling): + self.cursor += 64 # bindingSigSapling + # Orchard Transaction Fields (OrchardBundle) + # orchard_bundle_serialize (rust) + nActionsOrchard = self._read_varint() + self.cursor += 820 * nActionsOrchard # vActionsOrchard + if (nActionsOrchard > 0): + self.cursor += 1 # flagsOrchard + self.cursor += 8 # valueBalanceOrchard + self.cursor += 32 # anchorOrchard + sizeProofsOrchard = self._read_varint() + self.cursor += sizeProofsOrchard # proofsOrchard + self.cursor += 64 * nActionsOrchard # vSpendAuthSigsOrchard + self.cursor += 64 # bindingSigOrchard + return base_tx - if is_overwinter_v3 or is_sapling_v4: - self.cursor += 4 # expiryHeight + @staticmethod + def zcash_txid_v5(txin): - has_shielded = False - if is_sapling_v4: - self.cursor += 8 # valueBalance - shielded_spend_size = self._read_varint() - self.cursor += shielded_spend_size * 384 # vShieldedSpend - shielded_output_size = self._read_varint() - self.cursor += shielded_output_size * 948 # vShieldedOutput - has_shielded = shielded_spend_size > 0 or shielded_output_size > 0 - - if base_tx.version >= 2: - joinsplit_size = self._read_varint() - if joinsplit_size > 0: - joinsplit_desc_len = 1506 + (192 if is_sapling_v4 else 296) - # JSDescription - self.cursor += joinsplit_size * joinsplit_desc_len - self.cursor += 32 # joinSplitPubKey - self.cursor += 64 # joinSplitSig - - if is_sapling_v4 and has_shielded: - self.cursor += 64 # bindingSig + from electrumx.lib.zcash.mininode import CTransaction + from io import BytesIO + from electrumx.lib.zcash.util import hex_str_to_bytes + tx = CTransaction() + tx.deserialize(BytesIO(txin)) + tx.rehash() + # print(repr(tx)) + return bytes(reversed(hex_str_to_bytes(tx.hash))) - return base_tx + def read_tx_and_hash(self): + '''Return a (deserialized TX, tx_hash) pair. + The hash needs to be reversed for human display; for efficiency + we process it in the natural serialized order. + ''' + start = self.cursor + _tx = self.read_tx() + if (_tx.version < 5): + _txhash = double_sha256(self.binary[start:self.cursor]) + else: + _txhash = self.zcash_txid_v5(self.binary[start:self.cursor]) + return _tx, _txhash @dataclass class TxPIVX: diff --git a/electrumx/lib/zcash/__init__.py b/electrumx/lib/zcash/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/electrumx/lib/zcash/bignum.py b/electrumx/lib/zcash/bignum.py new file mode 100644 index 000000000..f56cea98e --- /dev/null +++ b/electrumx/lib/zcash/bignum.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 +# +# bignum.py +# +# This file is copied from python-bitcoinlib. +# +# Distributed under the MIT software license, see the accompanying +# file COPYING or https://www.opensource.org/licenses/mit-license.php . +# + +"""Bignum routines""" + +import struct + + +# generic big endian MPI format + +def bn_bytes(v, have_ext=False): + ext = 0 + if have_ext: + ext = 1 + return ((v.bit_length()+7)//8) + ext + +def bn2bin(v): + s = bytearray() + i = bn_bytes(v) + while i > 0: + s.append((v >> ((i-1) * 8)) & 0xff) + i -= 1 + return s + +def bin2bn(s): + l = 0 + for ch in s: + l = (l << 8) | ch + return l + +def bn2mpi(v): + have_ext = False + if v.bit_length() > 0: + have_ext = (v.bit_length() & 0x07) == 0 + + neg = False + if v < 0: + neg = True + v = -v + + s = struct.pack(b">I", bn_bytes(v, have_ext)) + ext = bytearray() + if have_ext: + ext.append(0) + v_bin = bn2bin(v) + if neg: + if have_ext: + ext[0] |= 0x80 + else: + v_bin[0] |= 0x80 + return s + ext + v_bin + +def mpi2bn(s): + if len(s) < 4: + return None + s_size = bytes(s[:4]) + v_len = struct.unpack(b">I", s_size)[0] + if len(s) != (v_len + 4): + return None + if v_len == 0: + return 0 + + v_str = bytearray(s[4:]) + neg = False + i = v_str[0] + if i & 0x80: + neg = True + i &= ~0x80 + v_str[0] = i + + v = bin2bn(v_str) + + if neg: + return -v + return v + +# bitcoin-specific little endian format, with implicit size +def mpi2vch(s): + r = s[4:] # strip size + r = r[::-1] # reverse string, converting BE->LE + return r + +def bn2vch(v): + return bytes(mpi2vch(bn2mpi(v))) + +def vch2mpi(s): + r = struct.pack(b">I", len(s)) # size + r += s[::-1] # reverse string, converting LE->BE + return r + +def vch2bn(s): + return mpi2bn(vch2mpi(s)) + diff --git a/electrumx/lib/zcash/coverage.py b/electrumx/lib/zcash/coverage.py new file mode 100644 index 000000000..02e1b7b4d --- /dev/null +++ b/electrumx/lib/zcash/coverage.py @@ -0,0 +1,107 @@ +#!/usr/bin/env python3 +# Copyright (c) 2015-2016 The Bitcoin Core developers +# Copyright (c) 2020-2022 The Zcash developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or https://www.opensource.org/licenses/mit-license.php . + +""" +This module contains utilities for doing coverage analysis on the RPC +interface. + +It provides a way to track which RPC commands are exercised during +testing. + +""" +import os + + +REFERENCE_FILENAME = 'rpc_interface.txt' + + +class AuthServiceProxyWrapper(object): + """ + An object that wraps AuthServiceProxy to record specific RPC calls. + + """ + def __init__(self, auth_service_proxy_instance, coverage_logfile=None): + """ + Kwargs: + auth_service_proxy_instance (AuthServiceProxy): the instance + being wrapped. + coverage_logfile (str): if specified, write each service_name + out to a file when called. + + """ + self.auth_service_proxy_instance = auth_service_proxy_instance + self.coverage_logfile = coverage_logfile + + def __getattr__(self, *args, **kwargs): + return_val = self.auth_service_proxy_instance.__getattr__( + *args, **kwargs) + + return AuthServiceProxyWrapper(return_val, self.coverage_logfile) + + def __call__(self, *args, **kwargs): + """ + Delegates to AuthServiceProxy, then writes the particular RPC method + called to a file. + + """ + return_val = self.auth_service_proxy_instance.__call__(*args, **kwargs) + rpc_method = self.auth_service_proxy_instance._service_name + + if self.coverage_logfile: + with open(self.coverage_logfile, 'a+', encoding='utf8') as f: + f.write("%s\n" % rpc_method) + + return return_val + + @property + def url(self): + return self.auth_service_proxy_instance.url + + +def get_filename(dirname, n_node): + """ + Get a filename unique to the test process ID and node. + + This file will contain a list of RPC commands covered. + """ + pid = str(os.getpid()) + return os.path.join( + dirname, "coverage.pid%s.node%s.txt" % (pid, str(n_node))) + + +def write_all_rpc_commands(dirname, node): + """ + Write out a list of all RPC functions available in `bitcoin-cli` for + coverage comparison. This will only happen once per coverage + directory. + + Args: + dirname (str): temporary test dir + node (AuthServiceProxy): client + + Returns: + bool. if the RPC interface file was written. + + """ + filename = os.path.join(dirname, REFERENCE_FILENAME) + + if os.path.isfile(filename): + return False + + help_output = node.help().split('\n') + commands = set() + + for line in help_output: + line = line.strip() + + # Ignore blanks and headers + if line and not line.startswith('='): + commands.add("%s\n" % line.split()[0]) + + with open(filename, 'w', encoding='utf8') as f: + f.writelines(list(commands)) + + return True diff --git a/electrumx/lib/zcash/equihash.py b/electrumx/lib/zcash/equihash.py new file mode 100755 index 000000000..e05544fb4 --- /dev/null +++ b/electrumx/lib/zcash/equihash.py @@ -0,0 +1,294 @@ +from operator import itemgetter +import struct +from functools import reduce + +DEBUG = False +VERBOSE = False + + +word_size = 32 +word_mask = (1<= 8 and word_size >= 7+bit_len + bit_len_mask = (1<= bit_len: + acc_bits -= bit_len + for x in range(byte_pad, out_width): + out[j+x] = ( + # Big-endian + acc_value >> (acc_bits+(8*(out_width-x-1))) + ) & ( + # Apply bit_len_mask across byte boundaries + (bit_len_mask >> (8*(out_width-x-1))) & 0xFF + ) + j += out_width + + return out + +def compress_array(inp, out_len, bit_len, byte_pad=0): + assert bit_len >= 8 and word_size >= 7+bit_len + + in_width = (bit_len+7)//8 + byte_pad + assert out_len == bit_len*len(inp)//(8*in_width) + out = bytearray(out_len) + + bit_len_mask = (1 << bit_len) - 1 + + # The acc_bits least-significant bits of acc_value represent a bit sequence + # in big-endian order. + acc_bits = 0; + acc_value = 0; + + j = 0 + for i in range(out_len): + # When we have fewer than 8 bits left in the accumulator, read the next + # input element. + if acc_bits < 8: + acc_value = ((acc_value << bit_len) & word_mask) | inp[j] + for x in range(byte_pad, in_width): + acc_value = acc_value | ( + ( + # Apply bit_len_mask across byte boundaries + inp[j+x] & ((bit_len_mask >> (8*(in_width-x-1))) & 0xFF) + ) << (8*(in_width-x-1))); # Big-endian + j += in_width + acc_bits += bit_len + + acc_bits -= 8 + out[i] = (acc_value >> acc_bits) & 0xFF + + return out + +def get_indices_from_minimal(minimal, bit_len): + eh_index_size = 4 + assert (bit_len+7)//8 <= eh_index_size + len_indices = 8*eh_index_size*len(minimal)//bit_len + byte_pad = eh_index_size - (bit_len+7)//8 + expanded = expand_array(minimal, len_indices, bit_len, byte_pad) + return [struct.unpack('>I', expanded[i:i+4])[0] for i in range(0, len_indices, eh_index_size)] + +def get_minimal_from_indices(indices, bit_len): + eh_index_size = 4 + assert (bit_len+7)//8 <= eh_index_size + len_indices = len(indices)*eh_index_size + min_len = bit_len*len_indices//(8*eh_index_size) + byte_pad = eh_index_size - (bit_len+7)//8 + byte_indices = bytearray(b''.join([struct.pack('>I', i) for i in indices])) + return compress_array(byte_indices, min_len, bit_len, byte_pad) + + +def hash_nonce(digest, nonce): + for i in range(8): + digest.update(struct.pack('> (32*i))) + +def hash_xi(digest, xi): + digest.update(struct.pack(' 0: + # 2b) Find next set of unordered pairs with collisions on first n/(k+1) bits + j = 1 + while j < len(X): + if not has_collision(X[-1][0], X[-1-j][0], i, collision_length): + break + j += 1 + + # 2c) Store tuples (X_i ^ X_j, (i, j)) on the table + for l in range(0, j-1): + for m in range(l+1, j): + # Check that there are no duplicate indices in tuples i and j + if distinct_indices(X[-1-l][1], X[-1-m][1]): + if X[-1-l][1][0] < X[-1-m][1][0]: + concat = X[-1-l][1] + X[-1-m][1] + else: + concat = X[-1-m][1] + X[-1-l][1] + Xc.append((xor(X[-1-l][0], X[-1-m][0]), concat)) + + # 2d) Drop this set + while j > 0: + X.pop(-1) + j -= 1 + # 2e) Replace previous list with new list + X = Xc + + # k+1) Find a collision on last 2n(k+1) bits + if DEBUG: + print('Final round:') + print('- Sorting list') + X.sort(key=itemgetter(0)) + if DEBUG and VERBOSE: + for Xi in X[-32:]: + print('%s %s' % (print_hash(Xi[0]), Xi[1])) + if DEBUG: print('- Finding collisions') + solns = [] + while len(X) > 0: + j = 1 + while j < len(X): + if not (has_collision(X[-1][0], X[-1-j][0], k, collision_length) and + has_collision(X[-1][0], X[-1-j][0], k+1, collision_length)): + break + j += 1 + + for l in range(0, j-1): + for m in range(l+1, j): + res = xor(X[-1-l][0], X[-1-m][0]) + if count_zeroes(res) == 8*hash_length and distinct_indices(X[-1-l][1], X[-1-m][1]): + if DEBUG and VERBOSE: + print('Found solution:') + print('- %s %s' % (print_hash(X[-1-l][0]), X[-1-l][1])) + print('- %s %s' % (print_hash(X[-1-m][0]), X[-1-m][1])) + if X[-1-l][1][0] < X[-1-m][1][0]: + solns.append(list(X[-1-l][1] + X[-1-m][1])) + else: + solns.append(list(X[-1-m][1] + X[-1-l][1])) + + # 2d) Drop this set + while j > 0: + X.pop(-1) + j -= 1 + return [get_minimal_from_indices(soln, collision_length+1) for soln in solns] + +def gbp_validate(digest, minimal, n, k): + validate_params(n, k) + collision_length = n//(k+1) + hash_length = (k+1)*((collision_length+7)//8) + indices_per_hash_output = 512//n + solution_width = (1 << k)*(collision_length+1)//8 + + if len(minimal) != solution_width: + print('Invalid solution length: %d (expected %d)' % \ + (len(minimal), solution_width)) + return False + + X = [] + for i in get_indices_from_minimal(minimal, collision_length+1): + r = i % indices_per_hash_output + # X_i = H(I||V||x_i) + curr_digest = digest.copy() + hash_xi(curr_digest, i//indices_per_hash_output) + tmp_hash = curr_digest.digest() + X.append(( + expand_array(bytearray(tmp_hash[r*n//8:(r+1)*n//8]), + hash_length, collision_length), + (i,) + )) + + for r in range(1, k+1): + Xc = [] + for i in range(0, len(X), 2): + if not has_collision(X[i][0], X[i+1][0], r, collision_length): + print('Invalid solution: invalid collision length between StepRows') + return False + if X[i+1][1][0] < X[i][1][0]: + print('Invalid solution: Index tree incorrectly ordered') + return False + if not distinct_indices(X[i][1], X[i+1][1]): + print('Invalid solution: duplicate indices') + return False + Xc.append((xor(X[i][0], X[i+1][0]), X[i][1] + X[i+1][1])) + X = Xc + + if len(X) != 1: + print('Invalid solution: incorrect length after end of rounds: %d' % len(X)) + return False + + if count_zeroes(X[0][0]) != 8*hash_length: + print('Invalid solution: incorrect number of zeroes: %d' % count_zeroes(X[0][0])) + return False + + return True + +def zcash_person(n, k): + return b'ZcashPoW' + struct.pack('= n): + raise ValueError('n must be larger than k') + if (((n//(k+1))+1) >= 32): + raise ValueError('Parameters must satisfy n/(k+1)+1 < 32') diff --git a/electrumx/lib/zcash/mininode.py b/electrumx/lib/zcash/mininode.py new file mode 100755 index 000000000..66c07fdbc --- /dev/null +++ b/electrumx/lib/zcash/mininode.py @@ -0,0 +1,2116 @@ +#!/usr/bin/env python3 +# Copyright (c) 2010 ArtForz -- public domain half-a-node +# Copyright (c) 2012 Jeff Garzik +# Copyright (c) 2010-2016 The Bitcoin Core developers +# Copyright (c) 2017-2022 The Zcash developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or https://www.opensource.org/licenses/mit-license.php . + +# +# mininode.py - Bitcoin P2P network half-a-node +# +# This python code was modified from ArtForz' public domain half-a-node, as +# found in the mini-node branch of https://github.com/jgarzik/pynode. +# +# NodeConn: an object which manages p2p connectivity to a bitcoin node +# NodeConnCB: a base class that describes the interface for receiving +# callbacks with network messages from a NodeConn +# CBlock, CTransaction, CBlockHeader, CTxIn, CTxOut, etc....: +# data structures that should map to corresponding structures in +# bitcoin/primitives +# msg_block, msg_tx, msg_headers, etc.: +# data structures that represent network messages +# ser_*, deser_*: functions that handle serialization/deserialization + + +import struct +import socket +import asyncore +import time +import sys +import random +from binascii import hexlify +from io import BytesIO +from codecs import encode +import hashlib +from threading import RLock +from threading import Thread +import logging +import copy +from hashlib import blake2b + +from .equihash import ( + gbp_basic, + gbp_validate, + hash_nonce, + zcash_person, +) +from .util import bytes_to_hex_str + + +BIP0031_VERSION = 60000 +SPROUT_PROTO_VERSION = 170002 # past bip-31 for ping/pong +OVERWINTER_PROTO_VERSION = 170003 +SAPLING_PROTO_VERSION = 170006 +BLOSSOM_PROTO_VERSION = 170008 +NU5_PROTO_VERSION = 170050 + +MY_SUBVERSION = b"/python-mininode-tester:0.0.3/" + +SPROUT_VERSION_GROUP_ID = 0x00000000 +OVERWINTER_VERSION_GROUP_ID = 0x03C48270 +SAPLING_VERSION_GROUP_ID = 0x892F2085 +ZIP225_VERSION_GROUP_ID = 0x26A7270A +# No transaction format change in Blossom. + +MAX_INV_SZ = 50000 + +COIN = 100000000 # 1 zec in zatoshis + +# The placeholder value used for the auth digest of pre-v5 transactions. +LEGACY_TX_AUTH_DIGEST = (1 << 256) - 1 + +# Keep our own socket map for asyncore, so that we can track disconnects +# ourselves (to workaround an issue with closing an asyncore socket when +# using select) +mininode_socket_map = dict() + +# One lock for synchronizing all data access between the networking thread (see +# NetworkThread below) and the thread running the test logic. For simplicity, +# NodeConn acquires this lock whenever delivering a message to to a NodeConnCB, +# and whenever adding anything to the send buffer (in send_message()). This +# lock should be acquired in the thread running the test logic to synchronize +# access to any data shared with the NodeConnCB or NodeConn. +mininode_lock = RLock() + +# Serialization/deserialization tools +def sha256(s): + return hashlib.new('sha256', s).digest() + +def hash256(s): + return sha256(sha256(s)) + +def nuparams(branch_id, height): + return '-nuparams=%x:%d' % (branch_id, height) + +def fundingstream(idx, start_height, end_height, addrs): + return '-fundingstream=%d:%d:%d:%s' % (idx, start_height, end_height, ",".join(addrs)) + +def ser_compactsize(n): + if n < 253: + return struct.pack("B", n) + elif n < 0x10000: + return struct.pack(">= 32 + return rs + + +def uint256_from_str(s): + r = 0 + t = struct.unpack("> 24) & 0xFF + v = (c & 0xFFFFFF) << (8 * (nbytes - 3)) + return v + + +def block_work_from_compact(c): + target = uint256_from_compact(c) + return 2**256 // (target + 1) + + +def deser_vector(f, c): + nit = struct.unpack("H", f.read(2))[0] + + def serialize(self): + r = b"" + r += struct.pack("H", self.port) + return r + + def __repr__(self): + return "CAddress(nServices=%i ip=%s port=%i)" % (self.nServices, + self.ip, self.port) + + +class CInv(object): + typemap = { + 0: b"Error", + 1: b"TX", + 2: b"Block", + 5: b"WTX", + } + + def __init__(self, t=0, h=0, h_aux=0): + self.type = t + self.hash = h + self.hash_aux = h_aux + if self.type == 1: + self.hash_aux = LEGACY_TX_AUTH_DIGEST + + def deserialize(self, f): + self.type = struct.unpack(" 0: + flags = struct.unpack("B", f.read(1))[0] + self.enableSpends = (flags & ORCHARD_FLAGS_ENABLE_SPENDS) != 0 + self.enableOutputs = (flags & ORCHARD_FLAGS_ENABLE_OUTPUTS) != 0 + self.valueBalance = struct.unpack(" 0: + r += struct.pack("B", self.flags()) + r += struct.pack(" 0 + if has_sapling: + self.valueBalance = struct.unpack(" 0: + self.anchor = deser_uint256(f) + for i in range(len(self.spends)): + self.spends[i].zkproof = Groth16Proof() + self.spends[i].zkproof.deserialize(f) + for i in range(len(self.spends)): + self.spends[i].spendAuthSig = RedJubjubSignature() + self.spends[i].spendAuthSig.deserialize(f) + for i in range(len(self.outputs)): + self.outputs[i].zkproof = Groth16Proof() + self.outputs[i].zkproof.deserialize(f) + if has_sapling: + self.bindingSig = RedJubjubSignature() + self.bindingSig.deserialize(f) + + def serialize(self): + r = b"" + r += ser_vector(self.spends) + r += ser_vector(self.outputs) + has_sapling = (len(self.spends) + len(self.outputs)) > 0 + if has_sapling: + r += struct.pack(" 0: + r += ser_uint256(self.anchor) + for spend in self.spends: + r += spend.zkproof.serialize() + for spend in self.spends: + r += spend.spendAuthSig.serialize() + for output in self.outputs: + r += output.zkproof.serialize() + if has_sapling: + r += self.bindingSig.serialize() + return r + + def __repr__(self): + return "SaplingBundle(spends=%r, outputs=%r, valueBalance=%i, bindingSig=%064x)" \ + % ( + self.spends, + self.outputs, + self.valueBalance, + self.bindingSig, + ) + + +G1_PREFIX_MASK = 0x02 +G2_PREFIX_MASK = 0x0a + +class ZCProof(object): + def __init__(self): + self.g_A = None + self.g_A_prime = None + self.g_B = None + self.g_B_prime = None + self.g_C = None + self.g_C_prime = None + self.g_K = None + self.g_H = None + + def deserialize(self, f): + def deser_g1(self, f): + leadingByte = struct.unpack("> 31) + self.nVersion = header & 0x7FFFFFFF + self.nVersionGroupId = (struct.unpack("= 2: + self.vJoinSplit = deser_vector(f, JSDescription) + if len(self.vJoinSplit) > 0: + self.joinSplitPubKey = deser_uint256(f) + self.joinSplitSig = f.read(64) + + if isSaplingV4 and not (len(self.shieldedSpends) == 0 and len(self.shieldedOutputs) == 0): + self.bindingSig = RedJubjubSignature() + self.bindingSig.deserialize(f) + + self.sha256 = None + self.hash = None + + def serialize(self): + header = (int(self.fOverwintered)<<31) | self.nVersion + isOverwinterV3 = (self.fOverwintered and + self.nVersionGroupId == OVERWINTER_VERSION_GROUP_ID and + self.nVersion == 3) + isSaplingV4 = (self.fOverwintered and + self.nVersionGroupId == SAPLING_VERSION_GROUP_ID and + self.nVersion == 4) + isNu5V5 = (self.fOverwintered and + self.nVersionGroupId == ZIP225_VERSION_GROUP_ID and + self.nVersion == 5) + + if isNu5V5: + r = b"" + + # Common transaction fields + r += struct.pack("= 2: + r += ser_vector(self.vJoinSplit) + if len(self.vJoinSplit) > 0: + r += ser_uint256(self.joinSplitPubKey) + r += self.joinSplitSig + if isSaplingV4 and not (len(self.shieldedSpends) == 0 and len(self.shieldedOutputs) == 0): + r += self.bindingSig.serialize() + return r + + def rehash(self): + self.sha256 = None + self.calc_sha256() + + def calc_sha256(self): + if self.nVersion >= 5: + from . import zip244 + txid = zip244.txid_digest(self) + self.auth_digest = zip244.auth_digest(self) + else: + txid = hash256(self.serialize()) + self.auth_digest = b'\xFF'*32 + if self.sha256 is None: + self.sha256 = uint256_from_str(txid) + self.hash = encode(txid[::-1], 'hex_codec').decode('ascii') + self.auth_digest_hex = encode(self.auth_digest[::-1], 'hex_codec').decode('ascii') + + def is_valid(self): + self.calc_sha256() + for tout in self.vout: + if tout.nValue < 0 or tout.nValue > 21000000 * 100000000: + return False + return True + + def __repr__(self): + r = ("CTransaction(fOverwintered=%r nVersion=%i nVersionGroupId=0x%08x " + "vin=%r vout=%r nLockTime=%i nExpiryHeight=%i " + "valueBalance=%i shieldedSpends=%r shieldedOutputs=%r" + % (self.fOverwintered, self.nVersion, self.nVersionGroupId, + self.vin, self.vout, self.nLockTime, self.nExpiryHeight, + self.valueBalance, self.shieldedSpends, self.shieldedOutputs)) + if self.nVersion >= 2: + r += " vJoinSplit=%r" % (self.vJoinSplit,) + if len(self.vJoinSplit) > 0: + r += " joinSplitPubKey=%064x joinSplitSig=%s" \ + % (self.joinSplitPubKey, bytes_to_hex_str(self.joinSplitSig)) + if len(self.shieldedSpends) > 0 or len(self.shieldedOutputs) > 0: + r += " bindingSig=%r" % self.bindingSig + r += ")" + return r + + +class CBlockHeader(object): + def __init__(self, header=None): + if header is None: + self.set_null() + else: + self.nVersion = header.nVersion + self.hashPrevBlock = header.hashPrevBlock + self.hashMerkleRoot = header.hashMerkleRoot + self.hashFinalSaplingRoot = header.hashFinalSaplingRoot + self.nTime = header.nTime + self.nBits = header.nBits + self.nNonce = header.nNonce + self.nSolution = header.nSolution + self.sha256 = header.sha256 + self.hash = header.hash + self.calc_sha256() + + def set_null(self): + self.nVersion = 4 + self.hashPrevBlock = 0 + self.hashMerkleRoot = 0 + self.hashFinalSaplingRoot = 0 + self.nTime = 0 + self.nBits = 0 + self.nNonce = 0 + self.nSolution = [] + self.sha256 = None + self.hash = None + + def deserialize(self, f): + self.nVersion = struct.unpack(" 1: + newhashes = [] + for i in range(0, len(hashes), 2): + i2 = min(i+1, len(hashes)-1) + newhashes.append(hash256(hashes[i] + hashes[i2])) + hashes = newhashes + return uint256_from_str(hashes[0]) + + def calc_auth_data_root(self): + hashes = [] + nleaves = 0 + for tx in self.vtx: + tx.calc_sha256() + hashes.append(tx.auth_digest) + nleaves += 1 + # Continue adding leaves (of zeros) until reaching a power of 2 + while nleaves & (nleaves-1) > 0: + hashes.append(b'\x00'*32) + nleaves += 1 + while len(hashes) > 1: + newhashes = [] + for i in range(0, len(hashes), 2): + digest = blake2b(digest_size=32, person=b'ZcashAuthDatHash') + digest.update(hashes[i]) + digest.update(hashes[i+1]) + newhashes.append(digest.digest()) + hashes = newhashes + return uint256_from_str(hashes[0]) + + def is_valid(self, n=48, k=5): + # H(I||... + digest = blake2b(digest_size=(512//n)*n//8, person=zcash_person(n, k)) + digest.update(super(CBlock, self).serialize()[:108]) + hash_nonce(digest, self.nNonce) + if not gbp_validate(self.nSolution, digest, n, k): + return False + self.calc_sha256() + target = uint256_from_compact(self.nBits) + if self.sha256 > target: + return False + for tx in self.vtx: + if not tx.is_valid(): + return False + if self.calc_merkle_root() != self.hashMerkleRoot: + return False + return True + + def solve(self, n=48, k=5): + target = uint256_from_compact(self.nBits) + # H(I||... + digest = blake2b(digest_size=(512//n)*n//8, person=zcash_person(n, k)) + digest.update(super(CBlock, self).serialize()[:108]) + self.nNonce = 0 + while True: + # H(I||V||... + curr_digest = digest.copy() + hash_nonce(curr_digest, self.nNonce) + # (x_1, x_2, ...) = A(I, V, n, k) + solns = gbp_basic(curr_digest, n, k) + for soln in solns: + assert(gbp_validate(curr_digest, soln, n, k)) + self.nSolution = soln + self.rehash() + if self.sha256 <= target: + return + self.nNonce += 1 + + def __repr__(self): + return "CBlock(nVersion=%i hashPrevBlock=%064x hashMerkleRoot=%064x hashFinalSaplingRoot=%064x nTime=%s nBits=%08x nNonce=%064x nSolution=%r vtx=%r)" \ + % (self.nVersion, self.hashPrevBlock, self.hashMerkleRoot, + self.hashFinalSaplingRoot, time.ctime(self.nTime), self.nBits, + self.nNonce, self.nSolution, self.vtx) + + +class CUnsignedAlert(object): + def __init__(self): + self.nVersion = 1 + self.nRelayUntil = 0 + self.nExpiration = 0 + self.nID = 0 + self.nCancel = 0 + self.setCancel = [] + self.nMinVer = 0 + self.nMaxVer = 0 + self.setSubVer = [] + self.nPriority = 0 + self.strComment = b"" + self.strStatusBar = b"" + self.strReserved = b"" + + def deserialize(self, f): + self.nVersion = struct.unpack("= 106: + self.addrFrom = CAddress() + self.addrFrom.deserialize(f) + self.nNonce = struct.unpack("= 209: + self.nStartingHeight = struct.unpack(" +class msg_headers(object): + command = b"headers" + + def __init__(self): + self.headers = [] + + def deserialize(self, f): + # comment in bitcoind indicates these should be deserialized as blocks + blocks = deser_vector(f, CBlock) + for x in blocks: + self.headers.append(CBlockHeader(x)) + + def serialize(self): + blocks = [CBlock(x) for x in self.headers] + return ser_vector(blocks) + + def __repr__(self): + return "msg_headers(headers=%s)" % repr(self.headers) + + +class msg_reject(object): + command = b"reject" + REJECT_MALFORMED = 1 + + def __init__(self): + self.message = b"" + self.code = 0 + self.reason = b"" + self.data = 0 + + def deserialize(self, f): + self.message = deser_string(f) + self.code = struct.unpack("= 209: + conn.send_message(msg_verack()) + conn.ver_send = min(SPROUT_PROTO_VERSION, message.nVersion) + if message.nVersion < 209: + conn.ver_recv = conn.ver_send + + def on_verack(self, conn, message): + conn.ver_recv = conn.ver_send + self.verack_received = True + + def on_inv(self, conn, message): + want = msg_getdata() + for i in message.inv: + if i.type != 0: + want.inv.append(i) + if len(want.inv): + conn.send_message(want) + + def on_addr(self, conn, message): pass + def on_alert(self, conn, message): pass + def on_getdata(self, conn, message): pass + def on_notfound(self, conn, message): pass + def on_getblocks(self, conn, message): pass + def on_tx(self, conn, message): pass + def on_block(self, conn, message): pass + def on_getaddr(self, conn, message): pass + def on_headers(self, conn, message): pass + def on_getheaders(self, conn, message): pass + def on_ping(self, conn, message): + if conn.ver_send > BIP0031_VERSION: + conn.send_message(msg_pong(message.nonce)) + def on_reject(self, conn, message): pass + def on_close(self, conn): pass + def on_mempool(self, conn): pass + def on_pong(self, conn, message): pass + + +# The actual NodeConn class +# This class provides an interface for a p2p connection to a specified node +class NodeConn(asyncore.dispatcher): + messagemap = { + b"version": msg_version, + b"verack": msg_verack, + b"addr": msg_addr, + b"alert": msg_alert, + b"inv": msg_inv, + b"getdata": msg_getdata, + b"notfound": msg_notfound, + b"getblocks": msg_getblocks, + b"tx": msg_tx, + b"block": msg_block, + b"getaddr": msg_getaddr, + b"ping": msg_ping, + b"pong": msg_pong, + b"headers": msg_headers, + b"getheaders": msg_getheaders, + b"reject": msg_reject, + b"mempool": msg_mempool + } + MAGIC_BYTES = { + "mainnet": b"\x24\xe9\x27\x64", # mainnet + "testnet3": b"\xfa\x1a\xf9\xbf", # testnet3 + "regtest": b"\xaa\xe8\x3f\x5f" # regtest + } + + def __init__(self, dstaddr, dstport, rpc, callback, net="regtest", protocol_version=SAPLING_PROTO_VERSION): + asyncore.dispatcher.__init__(self, map=mininode_socket_map) + self.log = logging.getLogger("NodeConn(%s:%d)" % (dstaddr, dstport)) + self.dstaddr = dstaddr + self.dstport = dstport + self.create_socket(socket.AF_INET, socket.SOCK_STREAM) + self.sendbuf = b"" + self.recvbuf = b"" + self.ver_send = 209 + self.ver_recv = 209 + self.last_sent = 0 + self.state = "connecting" + self.network = net + self.cb = callback + self.disconnect = False + + # stuff version msg into sendbuf + vt = msg_version(protocol_version) + vt.addrTo.ip = self.dstaddr + vt.addrTo.port = self.dstport + vt.addrFrom.ip = "0.0.0.0" + vt.addrFrom.port = 0 + self.send_message(vt, True) + print('MiniNode: Connecting to Bitcoin Node IP # ' + dstaddr + ':' \ + + str(dstport) + ' using version ' + str(protocol_version)) + + try: + self.connect((dstaddr, dstport)) + except: + self.handle_close() + self.rpc = rpc + + def show_debug_msg(self, msg): + self.log.debug(msg) + + def handle_connect(self): + self.show_debug_msg("MiniNode: Connected & Listening: \n") + self.state = b"connected" + + def handle_close(self): + self.show_debug_msg("MiniNode: Closing Connection to %s:%d... " + % (self.dstaddr, self.dstport)) + self.state = b"closed" + self.recvbuf = b"" + self.sendbuf = b"" + try: + self.close() + except: + pass + self.cb.on_close(self) + + def handle_read(self): + try: + t = self.recv(8192) + if len(t) > 0: + self.recvbuf += t + self.got_data() + except: + pass + + def readable(self): + return True + + def writable(self): + with mininode_lock: + length = len(self.sendbuf) + return (length > 0) + + def handle_write(self): + with mininode_lock: + try: + sent = self.send(self.sendbuf) + except: + self.handle_close() + return + self.sendbuf = self.sendbuf[sent:] + + def got_data(self): + try: + while True: + if len(self.recvbuf) < 4: + return + if self.recvbuf[:4] != self.MAGIC_BYTES[self.network]: + raise ValueError("got garbage %r" % (self.recvbuf,)) + if self.ver_recv < 209: + if len(self.recvbuf) < 4 + 12 + 4: + return + command = self.recvbuf[4:4+12].split(b"\x00", 1)[0] + msglen = struct.unpack("= 209: + th = sha256(data) + h = sha256(th) + tmsg += h[:4] + tmsg += data + with mininode_lock: + self.sendbuf += tmsg + self.last_sent = time.time() + + def got_message(self, message): + if message.command == b"version": + if message.nVersion <= BIP0031_VERSION: + self.messagemap[b'ping'] = msg_ping_prebip31 + if self.last_sent + 30 * 60 < time.time(): + self.send_message(self.messagemap[b'ping']()) + self.show_debug_msg("Recv %s" % repr(message)) + self.cb.deliver(self, message) + + def disconnect_node(self): + self.disconnect = True + + +class NetworkThread(Thread): + def run(self): + while mininode_socket_map: + # We check for whether to disconnect outside of the asyncore + # loop to workaround the behavior of asyncore when using + # select + disconnected = [] + for fd, obj in mininode_socket_map.items(): + if obj.disconnect: + disconnected.append(obj) + [ obj.handle_close() for obj in disconnected ] + asyncore.loop(0.1, use_poll=True, map=mininode_socket_map, count=1) + + +# An exception we can raise if we detect a potential disconnect +# (p2p or rpc) before the test is complete +class EarlyDisconnectError(Exception): + def __init__(self, value): + self.value = value + + def __str__(self): + return repr(self.value) diff --git a/electrumx/lib/zcash/script.py b/electrumx/lib/zcash/script.py new file mode 100644 index 000000000..644d46fcb --- /dev/null +++ b/electrumx/lib/zcash/script.py @@ -0,0 +1,979 @@ +#!/usr/bin/env python3 +# Copyright (c) 2015-2016 The Bitcoin Core developers +# Copyright (c) 2017-2022 The Zcash developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or https://www.opensource.org/licenses/mit-license.php . + +# +# script.py +# +# This file is modified from python-bitcoinlib. +# + +"""Scripts + +Functionality to build scripts, as well as SignatureHash(). +""" + +import sys +bchr = chr +bord = ord +if sys.version > '3': + long = int + bchr = lambda x: bytes([x]) + bord = lambda x: x + +from hashlib import blake2b + +from binascii import hexlify +import struct + +from .bignum import bn2vch +from .mininode import (CTransaction, CTxOut, hash256, ser_string, ser_uint256) + +MAX_SCRIPT_SIZE = 10000 +MAX_SCRIPT_ELEMENT_SIZE = 520 +MAX_SCRIPT_OPCODES = 201 + +OPCODE_NAMES = {} + +_opcode_instances = [] +class CScriptOp(int): + """A single script opcode""" + __slots__ = [] + + @staticmethod + def encode_op_pushdata(d): + """Encode a PUSHDATA op, returning bytes""" + if len(d) < 0x4c: + return b'' + struct.pack('B', len(d)) + d # OP_PUSHDATA + elif len(d) <= 0xff: + return b'\x4c' + struct.pack('B', len(d)) + d # OP_PUSHDATA1 + elif len(d) <= 0xffff: + return b'\x4d' + struct.pack(b'>= 8 + if r[-1] & 0x80: + r.append(0x80 if neg else 0) + elif neg: + r[-1] |= 0x80 + return struct.pack("B", len(r)) + r + + +class CScript(bytes): + """Serialized script + + A bytes subclass, so you can use this directly whenever bytes are accepted. + Note that this means that indexing does *not* work - you'll get an index by + byte rather than opcode. This format was chosen for efficiency so that the + general case would not require creating a lot of little CScriptOP objects. + + iter(script) however does iterate by opcode. + """ + @classmethod + def __coerce_instance(cls, other): + # Coerce other into bytes + if isinstance(other, CScriptOp): + other = bytes([other]) + elif isinstance(other, CScriptNum): + if (other.value == 0): + other = bytes([CScriptOp(OP_0)]) + else: + other = CScriptNum.encode(other) + elif isinstance(other, int): + if 0 <= other <= 16: + other = bytes([CScriptOp.encode_op_n(other)]) + elif other == -1: + other = bytes([OP_1NEGATE]) + else: + other = CScriptOp.encode_op_pushdata(bn2vch(other)) + elif isinstance(other, (bytes, bytearray)): + other = bytes(CScriptOp.encode_op_pushdata(other)) + return other + + def __add__(self, other): + # Do the coercion outside of the try block so that errors in it are + # noticed. + other = self.__coerce_instance(other) + + try: + # bytes.__add__ always returns bytes instances unfortunately + return CScript(super(CScript, self).__add__(other)) + except TypeError: + raise TypeError('Can not add a %r instance to a CScript' % other.__class__) + + def join(self, iterable): + # join makes no sense for a CScript() + raise NotImplementedError + + def __new__(cls, value=b''): + if isinstance(value, bytes) or isinstance(value, bytearray): + return super(CScript, cls).__new__(cls, value) + else: + def coerce_iterable(iterable): + for instance in iterable: + yield cls.__coerce_instance(instance) + # Annoyingly on both python2 and python3 bytes.join() always + # returns a bytes instance even when subclassed. + return super(CScript, cls).__new__(cls, b''.join(coerce_iterable(value))) + + def raw_iter(self): + """Raw iteration + + Yields tuples of (opcode, data, sop_idx) so that the different possible + PUSHDATA encodings can be accurately distinguished, as well as + determining the exact opcode byte indexes. (sop_idx) + """ + i = 0 + while i < len(self): + sop_idx = i + opcode = bord(self[i]) + i += 1 + + if opcode > OP_PUSHDATA4: + yield (opcode, None, sop_idx) + else: + datasize = None + pushdata_type = None + if opcode < OP_PUSHDATA1: + pushdata_type = 'PUSHDATA(%d)' % opcode + datasize = opcode + + elif opcode == OP_PUSHDATA1: + pushdata_type = 'PUSHDATA1' + if i >= len(self): + raise CScriptInvalidError('PUSHDATA1: missing data length') + datasize = bord(self[i]) + i += 1 + + elif opcode == OP_PUSHDATA2: + pushdata_type = 'PUSHDATA2' + if i + 1 >= len(self): + raise CScriptInvalidError('PUSHDATA2: missing data length') + datasize = bord(self[i]) + (bord(self[i+1]) << 8) + i += 2 + + elif opcode == OP_PUSHDATA4: + pushdata_type = 'PUSHDATA4' + if i + 3 >= len(self): + raise CScriptInvalidError('PUSHDATA4: missing data length') + datasize = bord(self[i]) + (bord(self[i+1]) << 8) + (bord(self[i+2]) << 16) + (bord(self[i+3]) << 24) + i += 4 + + else: + assert False # shouldn't happen + + + data = bytes(self[i:i+datasize]) + + # Check for truncation + if len(data) < datasize: + raise CScriptTruncatedPushDataError('%s: truncated data' % pushdata_type, data) + + i += datasize + + yield (opcode, data, sop_idx) + + def __iter__(self): + """'Cooked' iteration + + Returns either a CScriptOP instance, an integer, or bytes, as + appropriate. + + See raw_iter() if you need to distinguish the different possible + PUSHDATA encodings. + """ + for (opcode, data, sop_idx) in self.raw_iter(): + if data is not None: + yield data + else: + opcode = CScriptOp(opcode) + + if opcode.is_small_int(): + yield opcode.decode_op_n() + else: + yield CScriptOp(opcode) + + def __repr__(self): + # For Python3 compatibility add b before strings so testcases don't + # need to change + def _repr(o): + if isinstance(o, bytes): + return b"x('%s')" % hexlify(o).decode('ascii') + else: + return repr(o) + + ops = [] + i = iter(self) + while True: + op = None + try: + op = _repr(next(i)) + except CScriptTruncatedPushDataError as err: + op = '%s...' % (_repr(err.data), err) + break + except CScriptInvalidError as err: + op = '' % err + break + except StopIteration: + break + finally: + if op is not None: + ops.append(op) + + return "CScript([%s])" % ', '.join(ops) + + def GetSigOpCount(self, fAccurate): + """Get the SigOp count. + + fAccurate - Accurately count CHECKMULTISIG, see BIP16 for details. + + Note that this is consensus-critical. + """ + n = 0 + lastOpcode = OP_INVALIDOPCODE + for (opcode, data, sop_idx) in self.raw_iter(): + if opcode in (OP_CHECKSIG, OP_CHECKSIGVERIFY): + n += 1 + elif opcode in (OP_CHECKMULTISIG, OP_CHECKMULTISIGVERIFY): + if fAccurate and (OP_1 <= lastOpcode <= OP_16): + n += opcode.decode_op_n() + else: + n += 20 + lastOpcode = opcode + return n + + +SIGHASH_ALL = 1 +SIGHASH_NONE = 2 +SIGHASH_SINGLE = 3 +SIGHASH_ANYONECANPAY = 0x80 + +def getHashPrevouts(tx, person=b'ZcashPrevoutHash'): + digest = blake2b(digest_size=32, person=person) + for x in tx.vin: + digest.update(x.prevout.serialize()) + return digest.digest() + +def getHashSequence(tx, person=b'ZcashSequencHash'): + digest = blake2b(digest_size=32, person=person) + for x in tx.vin: + digest.update(struct.pack('= len(txTo.vin): + raise ValueError("inIdx %d out of range (%d)" % (inIdx, len(txTo.vin))) + + if consensusBranchId != 0: + # ZIP 243 + hashPrevouts = b'\x00'*32 + hashSequence = b'\x00'*32 + hashOutputs = b'\x00'*32 + hashJoinSplits = b'\x00'*32 + hashShieldedSpends = b'\x00'*32 + hashShieldedOutputs = b'\x00'*32 + + if not (hashtype & SIGHASH_ANYONECANPAY): + hashPrevouts = getHashPrevouts(txTo) + + if (not (hashtype & SIGHASH_ANYONECANPAY)) and \ + (hashtype & 0x1f) != SIGHASH_SINGLE and \ + (hashtype & 0x1f) != SIGHASH_NONE: + hashSequence = getHashSequence(txTo) + + if (hashtype & 0x1f) != SIGHASH_SINGLE and \ + (hashtype & 0x1f) != SIGHASH_NONE: + hashOutputs = getHashOutputs(txTo) + elif (hashtype & 0x1f) == SIGHASH_SINGLE and \ + 0 <= inIdx and inIdx < len(txTo.vout): + digest = blake2b(digest_size=32, person=b'ZcashOutputsHash') + digest.update(txTo.vout[inIdx].serialize()) + hashOutputs = digest.digest() + + if len(txTo.vJoinSplit) > 0: + hashJoinSplits = getHashJoinSplits(txTo) + + if len(txTo.shieldedSpends) > 0: + hashShieldedSpends = getHashShieldedSpends(txTo) + + if len(txTo.shieldedOutputs) > 0: + hashShieldedOutputs = getHashShieldedOutputs(txTo) + + digest = blake2b( + digest_size=32, + person=b'ZcashSigHash' + struct.pack('= len(txtmp.vout): + raise ValueError("outIdx %d out of range (%d)" % (outIdx, len(txtmp.vout))) + + tmp = txtmp.vout[outIdx] + txtmp.vout = [] + for i in range(outIdx): + txtmp.vout.append(CTxOut()) + txtmp.vout.append(tmp) + + for i in range(len(txtmp.vin)): + if i != inIdx: + txtmp.vin[i].nSequence = 0 + + if hashtype & SIGHASH_ANYONECANPAY: + tmp = txtmp.vin[inIdx] + txtmp.vin = [] + txtmp.vin.append(tmp) + + s = txtmp.serialize() + s += struct.pack(b" 0: + if allow_different_tips: + tips = [ x.getblockcount() for x in rpc_connections ] + else: + tips = [ x.getbestblockhash() for x in rpc_connections ] + if tips == [ tips[0] ]*len(tips): + break + time.sleep(wait) + timeout -= wait + + # Now that the block counts are in sync, wait for the internal + # notifications to finish + while timeout > 0: + notified = [ x.getblockchaininfo()['fullyNotified'] for x in rpc_connections ] + if notified == [ True ] * len(notified): + return True + time.sleep(wait) + timeout -= wait + + raise AssertionError("Block sync failed") + +def sync_mempools(rpc_connections, wait=0.5, timeout=60): + """ + Wait until everybody has the same transactions in their memory + pools, and has notified all internal listeners of them + """ + while timeout > 0: + pool = set(rpc_connections[0].getrawmempool()) + num_match = 1 + for i in range(1, len(rpc_connections)): + if set(rpc_connections[i].getrawmempool()) == pool: + num_match = num_match+1 + if num_match == len(rpc_connections): + break + time.sleep(wait) + timeout -= wait + + # Now that the mempools are in sync, wait for the internal + # notifications to finish + while timeout > 0: + notified = [ x.getmempoolinfo()['fullyNotified'] for x in rpc_connections ] + if notified == [ True ] * len(notified): + return True + time.sleep(wait) + timeout -= wait + + raise AssertionError("Mempool sync failed") + +bitcoind_processes = {} + +def initialize_datadir(dirname, n, clock_offset=0): + datadir = os.path.join(dirname, "node"+str(n)) + if not os.path.isdir(datadir): + os.makedirs(datadir) + rpc_u, rpc_p = rpc_auth_pair(n) + with open(os.path.join(datadir, "zcash.conf"), 'w', encoding='utf8') as f: + f.write("regtest=1\n") + f.write("showmetrics=0\n") + f.write("rpcuser=" + rpc_u + "\n") + f.write("rpcpassword=" + rpc_p + "\n") + f.write("port="+str(p2p_port(n))+"\n") + f.write("rpcport="+str(rpc_port(n))+"\n") + f.write("listenonion=0\n") + if clock_offset != 0: + f.write('clockoffset='+str(clock_offset)+'\n') + + return datadir + +def rpc_auth_pair(n): + return 'rpcuser💻' + str(n), 'rpcpass🔑' + str(n) + +def rpc_url(i, rpchost=None): + rpc_u, rpc_p = rpc_auth_pair(i) + host = '127.0.0.1' + port = rpc_port(i) + if rpchost: + parts = rpchost.split(':') + if len(parts) == 2: + host, port = parts + else: + host = rpchost + return "http://%s:%s@%s:%d" % (rpc_u, rpc_p, host, int(port)) + +def wait_for_bitcoind_start(process, url, i): + ''' + Wait for bitcoind to start. This means that RPC is accessible and fully initialized. + Raise an exception if bitcoind exits during initialization. + ''' + while True: + if process.poll() is not None: + raise Exception('bitcoind exited with status %i during initialization' % process.returncode) + try: + rpc = get_rpc_proxy(url, i) + rpc.getblockcount() + break # break out of loop on success + except IOError as e: + if e.errno != errno.ECONNREFUSED: # Port not yet open? + raise # unknown IO error + except JSONRPCException as e: # Initialization phase + if e.error['code'] != -28: # RPC in warmup? + raise # unknown JSON RPC exception + time.sleep(0.25) + +def initialize_chain(test_dir, num_nodes, cachedir, cache_behavior='current'): + """ + Create a set of node datadirs in `test_dir`, based upon the specified + `cache_behavior` value. The following values are recognized for + `cache_behavior`: + + * 'current': create a 200-block-long chain (with wallet) for MAX_NODES + in `cachedir` if necessary. Afterward, create num_nodes copies in + `test_dir` from the cache. The resulting nodes will be configured to + use the -clockoffset config argument when starting to ensure that + the cached chain is not treated as being excessively out-of-date. + * 'sprout': use persisted chain data containing known amounts of Sprout + funds from the files in `qa/rpc-tests/cache/sprout`. This allows + testing of Sprout spends even though Sprout outputs can no longer + be created by zcashd software. The resulting nodes will be configured to + use the -clockoffset config argument when starting to ensure that + the cached chain is not treated as being excessively out-of-date. + * 'fresh': force re-creation of the cache, and then start as for `current`. + * 'clean': start the nodes without cached chain data, allowing the test + to take full control of chain setup. + """ + assert num_nodes <= MAX_NODES + + def rebuild_cache(): + #find and delete old cache directories if any exist + for i in range(MAX_NODES): + if os.path.isdir(os.path.join(cachedir,"node"+str(i))): + shutil.rmtree(os.path.join(cachedir,"node"+str(i))) + + # Create cache directories, run bitcoinds: + block_time = int(time.time()) - (200 * PRE_BLOSSOM_BLOCK_TARGET_SPACING) + for i in range(MAX_NODES): + datadir = initialize_datadir(cachedir, i) + args = [ os.getenv("ZCASHD", ZCASHD_BINARY), "-keypool=1", "-datadir="+datadir, "-discover=0" ] + args.extend([ + '-nuparams=5ba81b19:1', # Overwinter + '-nuparams=76b809bb:1', # Sapling + '-mocktime=%d' % block_time + ]) + if i > 0: + args.append("-connect=127.0.0.1:"+str(p2p_port(0))) + bitcoind_processes[i] = subprocess.Popen(args) + if os.getenv("PYTHON_DEBUG", ""): + print("initialize_chain: bitcoind started, waiting for RPC to come up") + wait_for_bitcoind_start(bitcoind_processes[i], rpc_url(i), i) + if os.getenv("PYTHON_DEBUG", ""): + print("initialize_chain: RPC successfully started") + + rpcs = [] + for i in range(MAX_NODES): + try: + rpcs.append(get_rpc_proxy(rpc_url(i), i)) + except: + sys.stderr.write("Error connecting to "+rpc_url(i)+"\n") + sys.exit(1) + + # Create a 200-block-long chain; each of the 4 first nodes + # gets 25 mature blocks and 25 immature. + # Note: To preserve compatibility with older versions of + # initialize_chain, only 4 nodes will generate coins. + # + # Blocks are created with timestamps 2.5 minutes apart (matching the + # chain defaulting above to Sapling active), starting 200 * 2.5 minutes + # before the current time. + for i in range(2): + for peer in range(4): + for j in range(25): + set_node_times(rpcs, block_time) + rpcs[peer].generate(1) + block_time += PRE_BLOSSOM_BLOCK_TARGET_SPACING + # Must sync before next peer starts generating blocks + sync_blocks(rpcs) + # Check that local time isn't going backwards + assert_greater_than(time.time() + 1, block_time) + + # Shut them down, and clean up cache directories: + stop_nodes(rpcs) + wait_bitcoinds() + for i in range(MAX_NODES): + # record the system time at which the cache was regenerated + with open(log_filename(cachedir, i, 'cache_config.json'), "w", encoding="utf8") as cache_conf_file: + cache_config = { "cache_time": time.time() } + cache_conf_json = json.dumps(cache_config, indent=4) + cache_conf_file.write(cache_conf_json) + + os.remove(log_filename(cachedir, i, "debug.log")) + os.remove(log_filename(cachedir, i, "db.log")) + os.remove(log_filename(cachedir, i, "peers.dat")) + os.remove(log_filename(cachedir, i, "fee_estimates.dat")) + + def init_from_cache(): + for i in range(num_nodes): + from_dir = os.path.join(cachedir, "node"+str(i)) + to_dir = os.path.join(test_dir, "node"+str(i)) + shutil.copytree(from_dir, to_dir) + with open(os.path.join(to_dir, 'regtest', 'cache_config.json'), "r", encoding="utf8") as cache_conf_file: + cache_conf = json.load(cache_conf_file) + # obtain the clock offset as a negative number of seconds + offset = round(cache_conf['cache_time']) - round(time.time()) + # overwrite port/rpcport and clock offset in zcash.conf + initialize_datadir(test_dir, i, clock_offset=offset) + + def init_sprout(): + assert num_nodes <= 4 # only 4 nodes with Sprout funds are supported + sprout_cache_path = os.path.join(os.path.dirname(os.path.dirname(os.path.realpath(__file__))), 'cache', 'sprout') + for i in range(num_nodes): + to_dir = os.path.join(test_dir, "node"+str(i), "regtest") + os.makedirs(to_dir) + # Unzip the persisted Sprout config file + with tarfile.open(os.path.join(sprout_cache_path, "chain_cache.tar.gz"), "r:gz") as tgz: + tgz.extractall(path = to_dir) + with tarfile.open(os.path.join(sprout_cache_path, "node"+str(i)+"_wallet.tar.gz"), "r:gz") as tgz: + tgz.extractall(path = os.path.join(to_dir, "wallet.dat")) + with open(os.path.join(to_dir, 'cache_config.json'), "r", encoding="utf8") as cache_conf_file: + cache_conf = json.load(cache_conf_file) + # obtain the clock offset as a negative number of seconds + offset = round(cache_conf['cache_time']) - round(time.time()) + # overwrite port/rpcport and clock offset in zcash.conf + initialize_datadir(test_dir, i, clock_offset=offset) + + def cache_rebuild_required(): + for i in range(MAX_NODES): + node_path = os.path.join(cachedir, 'node'+str(i)) + if os.path.isdir(node_path): + if not os.path.isfile(log_filename(cachedir, i, 'cache_config.json')): + return True + else: + return True + return False + + if cache_behavior == 'current': + if cache_rebuild_required(): rebuild_cache() + init_from_cache() + elif cache_behavior == 'sprout': + init_sprout() + elif cache_behavior == 'fresh': + rebuild_cache() + init_from_cache() + elif cache_behavior == 'clean': + initialize_chain_clean(test_dir, num_nodes) + else: + raise Exception('Cache behavior %s not recognized' % cache_behavior) + + +def initialize_chain_clean(test_dir, num_nodes): + """ + Create an empty blockchain and num_nodes wallets. + Useful if a test case wants complete control over initialization. + """ + for i in range(num_nodes): + initialize_datadir(test_dir, i) + + +def _rpchost_to_args(rpchost): + '''Convert optional IP:port spec to rpcconnect/rpcport args''' + if rpchost is None: + return [] + + match = re.match('(\[[0-9a-fA-f:]+\]|[^:]+)(?::([0-9]+))?$', rpchost) + if not match: + raise ValueError('Invalid RPC host spec ' + rpchost) + + rpcconnect = match.group(1) + rpcport = match.group(2) + + if rpcconnect.startswith('['): # remove IPv6 [...] wrapping + rpcconnect = rpcconnect[1:-1] + + rv = ['-rpcconnect=' + rpcconnect] + if rpcport: + rv += ['-rpcport=' + rpcport] + return rv + +def start_node(i, dirname, extra_args=None, rpchost=None, timewait=None, binary=None, stderr=None): + """ + Start a bitcoind and return RPC connection to it + """ + datadir = os.path.join(dirname, "node"+str(i)) + if binary is None: + binary = os.getenv("ZCASHD", ZCASHD_BINARY) + args = [ binary, "-datadir="+datadir, "-keypool=1", "-discover=0", "-rest" ] + args.extend([ + '-nuparams=5ba81b19:1', # Overwinter + '-nuparams=76b809bb:1', # Sapling + ]) + if extra_args is not None: args.extend(extra_args) + bitcoind_processes[i] = subprocess.Popen(args, stderr=stderr) + if os.getenv("PYTHON_DEBUG", ""): + print("start_node: bitcoind started, waiting for RPC to come up") + url = rpc_url(i, rpchost) + wait_for_bitcoind_start(bitcoind_processes[i], url, i) + if os.getenv("PYTHON_DEBUG", ""): + print("start_node: RPC successfully started") + proxy = get_rpc_proxy(url, i, timeout=timewait) + + if COVERAGE_DIR: + coverage.write_all_rpc_commands(COVERAGE_DIR, proxy) + + return proxy + +def assert_start_raises_init_error(i, dirname, extra_args=None, expected_msg=None): + with tempfile.SpooledTemporaryFile(max_size=2**16) as log_stderr: + try: + node = start_node(i, dirname, extra_args, stderr=log_stderr) + stop_node(node, i) + except Exception as e: + assert 'bitcoind exited' in str(e) #node must have shutdown + if expected_msg is not None: + log_stderr.seek(0) + stderr = log_stderr.read().decode('utf-8') + if expected_msg not in stderr: + raise AssertionError("Expected error \"" + expected_msg + "\" not found in:\n" + stderr) + else: + if expected_msg is None: + assert_msg = "bitcoind should have exited with an error" + else: + assert_msg = "bitcoind should have exited with expected error " + expected_msg + raise AssertionError(assert_msg) + +def start_nodes(num_nodes, dirname, extra_args=None, rpchost=None, binary=None): + """ + Start multiple bitcoinds, return RPC connections to them + """ + if extra_args is None: extra_args = [ None for _ in range(num_nodes) ] + if binary is None: binary = [ None for _ in range(num_nodes) ] + rpcs = [] + try: + for i in range(num_nodes): + rpcs.append(start_node(i, dirname, extra_args[i], rpchost, binary=binary[i])) + except: # If one node failed to start, stop the others + stop_nodes(rpcs) + raise + return rpcs + +def log_filename(dirname, n_node, logname): + return os.path.join(dirname, "node"+str(n_node), "regtest", logname) + +def check_node(i): + bitcoind_processes[i].poll() + return bitcoind_processes[i].returncode + +def stop_node(node, i): + try: + node.stop() + except http.client.CannotSendRequest as e: + print("WARN: Unable to stop node: " + repr(e)) + bitcoind_processes[i].wait() + del bitcoind_processes[i] + +def stop_nodes(nodes): + for node in nodes: + try: + node.stop() + except http.client.CannotSendRequest as e: + print("WARN: Unable to stop node: " + repr(e)) + del nodes[:] # Emptying array closes connections as a side effect + +def set_node_times(nodes, t): + for node in nodes: + node.setmocktime(t) + +def wait_bitcoinds(): + # Wait for all bitcoinds to cleanly exit + for bitcoind in list(bitcoind_processes.values()): + bitcoind.wait() + bitcoind_processes.clear() + +def connect_nodes(from_connection, node_num): + ip_port = "127.0.0.1:"+str(p2p_port(node_num)) + from_connection.addnode(ip_port, "onetry") + # poll until version handshake complete to avoid race conditions + # with transaction relaying + while any(peer['version'] == 0 for peer in from_connection.getpeerinfo()): + time.sleep(0.1) + +def connect_nodes_bi(nodes, a, b): + connect_nodes(nodes[a], b) + connect_nodes(nodes[b], a) + +def find_output(node, txid, amount): + """ + Return index to output of txid with value amount + Raises exception if there is none. + """ + txdata = node.getrawtransaction(txid, 1) + for i in range(len(txdata["vout"])): + if txdata["vout"][i]["value"] == amount: + return i + raise RuntimeError("find_output txid %s : %s not found"%(txid,str(amount))) + + +def gather_inputs(from_node, amount_needed, confirmations_required=1): + """ + Return a random set of unspent txouts that are enough to pay amount_needed + """ + assert(confirmations_required >=0) + utxo = from_node.listunspent(confirmations_required) + random.shuffle(utxo) + inputs = [] + total_in = Decimal("0.00000000") + while total_in < amount_needed and len(utxo) > 0: + t = utxo.pop() + total_in += t["amount"] + inputs.append({ "txid" : t["txid"], "vout" : t["vout"], "address" : t["address"] } ) + if total_in < amount_needed: + raise RuntimeError("Insufficient funds: need %d, have %d"%(amount_needed, total_in)) + return (total_in, inputs) + +def make_change(from_node, amount_in, amount_out, fee): + """ + Create change output(s), return them + """ + outputs = {} + amount = amount_out+fee + change = amount_in - amount + if change > amount*2: + # Create an extra change output to break up big inputs + change_address = from_node.getnewaddress() + # Split change in two, being careful of rounding: + outputs[change_address] = Decimal(change/2).quantize(Decimal('0.00000001'), rounding=ROUND_DOWN) + change = amount_in - amount - outputs[change_address] + if change > 0: + outputs[from_node.getnewaddress()] = change + return outputs + +def send_zeropri_transaction(from_node, to_node, amount, fee): + """ + Create&broadcast a zero-priority transaction. + Returns (txid, hex-encoded-txdata) + Ensures transaction is zero-priority by first creating a send-to-self, + then using its output + """ + + # Create a send-to-self with confirmed inputs: + self_address = from_node.getnewaddress() + (total_in, inputs) = gather_inputs(from_node, amount+fee*2) + outputs = make_change(from_node, total_in, amount+fee, fee) + outputs[self_address] = float(amount+fee) + + self_rawtx = from_node.createrawtransaction(inputs, outputs) + self_signresult = from_node.signrawtransaction(self_rawtx) + self_txid = from_node.sendrawtransaction(self_signresult["hex"], True) + + vout = find_output(from_node, self_txid, amount+fee) + # Now immediately spend the output to create a 1-input, 1-output + # zero-priority transaction: + inputs = [ { "txid" : self_txid, "vout" : vout } ] + outputs = { to_node.getnewaddress() : float(amount) } + + rawtx = from_node.createrawtransaction(inputs, outputs) + signresult = from_node.signrawtransaction(rawtx) + txid = from_node.sendrawtransaction(signresult["hex"], True) + + return (txid, signresult["hex"]) + +def random_zeropri_transaction(nodes, amount, min_fee, fee_increment, fee_variants): + """ + Create a random zero-priority transaction. + Returns (txid, hex-encoded-transaction-data, fee) + """ + from_node = random.choice(nodes) + to_node = random.choice(nodes) + fee = min_fee + fee_increment*random.randint(0,fee_variants) + (txid, txhex) = send_zeropri_transaction(from_node, to_node, amount, fee) + return (txid, txhex, fee) + +def random_transaction(nodes, amount, min_fee, fee_increment, fee_variants): + """ + Create a random transaction. + Returns (txid, hex-encoded-transaction-data, fee) + """ + from_node = random.choice(nodes) + to_node = random.choice(nodes) + fee = min_fee + fee_increment*random.randint(0,fee_variants) + + (total_in, inputs) = gather_inputs(from_node, amount+fee) + outputs = make_change(from_node, total_in, amount, fee) + outputs[to_node.getnewaddress()] = float(amount) + + rawtx = from_node.createrawtransaction(inputs, outputs) + signresult = from_node.signrawtransaction(rawtx) + txid = from_node.sendrawtransaction(signresult["hex"], True) + + return (txid, signresult["hex"], fee) + +def assert_equal(expected, actual, message=""): + if expected != actual: + if message: + message = "; %s" % message + raise AssertionError("(left == right)%s\n left: <%s>\n right: <%s>" % (message, str(expected), str(actual))) + +def assert_true(condition, message = ""): + if not condition: + raise AssertionError(message) + +def assert_false(condition, message = ""): + assert_true(not condition, message) + +def assert_greater_than(thing1, thing2): + if thing1 <= thing2: + raise AssertionError("%s <= %s"%(str(thing1),str(thing2))) + +def assert_raises(exc, fun, *args, **kwds): + assert_raises_message(exc, None, fun, *args, **kwds) + +def assert_raises_message(ExceptionType, errstr, func, *args, **kwargs): + """ + Asserts that func throws and that the exception contains 'errstr' + in its message. + """ + try: + func(*args, **kwargs) + except ExceptionType as e: + if errstr is not None and errstr not in str(e): + raise AssertionError("Invalid exception string: Couldn't find %r in %r" % ( + errstr, str(e))) + except Exception as e: + raise AssertionError("Unexpected exception raised: " + type(e).__name__) + else: + raise AssertionError("No exception raised") + +def fail(message=""): + raise AssertionError(message) + + +# Returns an async operation result +def wait_and_assert_operationid_status_result(node, myopid, in_status='success', in_errormsg=None, timeout=300): + print('waiting for async operation {}'.format(myopid)) + result = None + for _ in range(1, timeout): + results = node.z_getoperationresult([myopid]) + if len(results) > 0: + result = results[0] + break + time.sleep(1) + + assert_true(result is not None, "timeout occurred") + status = result['status'] + + debug = os.getenv("PYTHON_DEBUG", "") + if debug: + print('...returned status: {}'.format(status)) + + errormsg = None + if status == "failed": + errormsg = result['error']['message'] + if debug: + print('...returned error: {}'.format(errormsg)) + assert_equal(in_errormsg, errormsg) + + assert_equal(in_status, status, "Operation returned mismatched status. Error Message: {}".format(errormsg)) + + return result + + +# Returns txid if operation was a success or None +def wait_and_assert_operationid_status(node, myopid, in_status='success', in_errormsg=None, timeout=300): + result = wait_and_assert_operationid_status_result(node, myopid, in_status, in_errormsg, timeout) + if result['status'] == "success": + return result['result']['txid'] + else: + return None + +# Find a coinbase address on the node, filtering by the number of UTXOs it has. +# If no filter is provided, returns the coinbase address on the node containing +# the greatest number of spendable UTXOs. +# The default cached chain has one address per coinbase output. +def get_coinbase_address(node, expected_utxos=None): + addrs = [utxo['address'] for utxo in node.listunspent() if utxo['generated']] + assert(len(set(addrs)) > 0) + + if expected_utxos is None: + addrs = [(addrs.count(a), a) for a in set(addrs)] + return sorted(addrs, reverse=True)[0][1] + + addrs = [a for a in set(addrs) if addrs.count(a) == expected_utxos] + assert(len(addrs) > 0) + return addrs[0] + +def check_node_log(self, node_number, line_to_check, stop_node = True): + print("Checking node " + str(node_number) + " logs") + if stop_node: + self.nodes[node_number].stop() + bitcoind_processes[node_number].wait() + logpath = self.options.tmpdir + "/node" + str(node_number) + "/regtest/debug.log" + with open(logpath, "r", encoding="utf8") as myfile: + logdata = myfile.readlines() + for (n, logline) in enumerate(logdata): + if line_to_check in logline: + return n + raise AssertionError(repr(line_to_check) + " not found") + +def nustr(branch_id): + return '%08x' % branch_id + +def nuparams(branch_id, height): + return '-nuparams=%s:%d' % (nustr(branch_id), height) diff --git a/electrumx/lib/zcash/zip244.py b/electrumx/lib/zcash/zip244.py new file mode 100644 index 000000000..14aa22a7b --- /dev/null +++ b/electrumx/lib/zcash/zip244.py @@ -0,0 +1,293 @@ +#!/usr/bin/env python3 +# Copyright (c) 2021 The Zcash developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or https://www.opensource.org/licenses/mit-license.php . + +# +# zip244.py +# +# Functionality to create txids, auth digests, and signature digests. +# +# This file is modified from zcash/zcash-test-vectors. +# + +import struct + +from hashlib import blake2b + +from .mininode import ser_string, ser_uint256 +from .script import ( + SIGHASH_ANYONECANPAY, + SIGHASH_NONE, + SIGHASH_SINGLE, + getHashOutputs, + getHashPrevouts, + getHashSequence, +) + + +# Transparent + +def transparent_digest(tx): + digest = blake2b(digest_size=32, person=b'ZTxIdTranspaHash') + + if len(tx.vin) + len(tx.vout) > 0: + digest.update(getHashPrevouts(tx, b'ZTxIdPrevoutHash')) + digest.update(getHashSequence(tx, b'ZTxIdSequencHash')) + digest.update(getHashOutputs(tx, b'ZTxIdOutputsHash')) + + return digest.digest() + +def transparent_scripts_digest(tx): + digest = blake2b(digest_size=32, person=b'ZTxAuthTransHash') + for x in tx.vin: + digest.update(ser_string(x.scriptSig)) + return digest.digest() + +# Sapling + +def sapling_digest(saplingBundle): + digest = blake2b(digest_size=32, person=b'ZTxIdSaplingHash') + + if len(saplingBundle.spends) + len(saplingBundle.outputs) > 0: + digest.update(sapling_spends_digest(saplingBundle)) + digest.update(sapling_outputs_digest(saplingBundle)) + digest.update(struct.pack(' 0: + for desc in saplingBundle.spends: + digest.update(desc.zkproof.serialize()) + for desc in saplingBundle.spends: + digest.update(desc.spendAuthSig.serialize()) + for desc in saplingBundle.outputs: + digest.update(desc.zkproof.serialize()) + digest.update(saplingBundle.bindingSig.serialize()) + + return digest.digest() + +# - Spends + +def sapling_spends_digest(saplingBundle): + digest = blake2b(digest_size=32, person=b'ZTxIdSSpendsHash') + if len(saplingBundle.spends) > 0: + digest.update(sapling_spends_compact_digest(saplingBundle)) + digest.update(sapling_spends_noncompact_digest(saplingBundle)) + + return digest.digest() + +def sapling_spends_compact_digest(saplingBundle): + digest = blake2b(digest_size=32, person=b'ZTxIdSSpendCHash') + for desc in saplingBundle.spends: + digest.update(ser_uint256(desc.nullifier)) + return digest.digest() + +def sapling_spends_noncompact_digest(saplingBundle): + digest = blake2b(digest_size=32, person=b'ZTxIdSSpendNHash') + for desc in saplingBundle.spends: + digest.update(ser_uint256(desc.cv)) + digest.update(ser_uint256(saplingBundle.anchor)) # Decker + digest.update(ser_uint256(desc.rk)) + return digest.digest() + +# - Outputs + +def sapling_outputs_digest(saplingBundle): + digest = blake2b(digest_size=32, person=b'ZTxIdSOutputHash') + + if len(saplingBundle.outputs) > 0: + digest.update(sapling_outputs_compact_digest(saplingBundle)) + digest.update(sapling_outputs_memos_digest(saplingBundle)) + digest.update(sapling_outputs_noncompact_digest(saplingBundle)) + + return digest.digest() + +def sapling_outputs_compact_digest(saplingBundle): + digest = blake2b(digest_size=32, person=b'ZTxIdSOutC__Hash') + for desc in saplingBundle.outputs: + digest.update(ser_uint256(desc.cmu)) + digest.update(ser_uint256(desc.ephemeralKey)) + digest.update(desc.encCiphertext[:52]) + return digest.digest() + +def sapling_outputs_memos_digest(saplingBundle): + digest = blake2b(digest_size=32, person=b'ZTxIdSOutM__Hash') + for desc in saplingBundle.outputs: + digest.update(desc.encCiphertext[52:564]) + return digest.digest() + +def sapling_outputs_noncompact_digest(saplingBundle): + digest = blake2b(digest_size=32, person=b'ZTxIdSOutN__Hash') + for desc in saplingBundle.outputs: + digest.update(ser_uint256(desc.cv)) + digest.update(desc.encCiphertext[564:]) + digest.update(desc.outCiphertext) + return digest.digest() + +# Orchard + +def orchard_digest(orchardBundle): + digest = blake2b(digest_size=32, person=b'ZTxIdOrchardHash') + + if len(orchardBundle.actions) > 0: + digest.update(orchard_actions_compact_digest(orchardBundle)) + digest.update(orchard_actions_memos_digest(orchardBundle)) + digest.update(orchard_actions_noncompact_digest(orchardBundle)) + digest.update(struct.pack('B', orchardBundle.flags())) + digest.update(struct.pack(' 0: + digest.update(bytes(orchardBundle.proofs)) + for desc in orchardBundle.actions: + digest.update(desc.spendAuthSig.serialize()) + digest.update(orchardBundle.bindingSig.serialize()) + + return digest.digest() + +# - Actions + +def orchard_actions_compact_digest(orchardBundle): + digest = blake2b(digest_size=32, person=b'ZTxIdOrcActCHash') + for desc in orchardBundle.actions: + digest.update(ser_uint256(desc.nullifier)) + digest.update(ser_uint256(desc.cmx)) + digest.update(ser_uint256(desc.ephemeralKey)) + digest.update(desc.encCiphertext[:52]) + return digest.digest() + +def orchard_actions_memos_digest(orchardBundle): + digest = blake2b(digest_size=32, person=b'ZTxIdOrcActMHash') + for desc in orchardBundle.actions: + digest.update(desc.encCiphertext[52:564]) + return digest.digest() + +def orchard_actions_noncompact_digest(orchardBundle): + digest = blake2b(digest_size=32, person=b'ZTxIdOrcActNHash') + for desc in orchardBundle.actions: + digest.update(ser_uint256(desc.cv)) + digest.update(ser_uint256(desc.rk)) + digest.update(desc.encCiphertext[564:]) + digest.update(desc.outCiphertext) + return digest.digest() + +# Transaction + +def header_digest(tx): + digest = blake2b(digest_size=32, person=b'ZTxIdHeadersHash') + + digest.update(struct.pack(' 0: + # for output in _tx.outputs: + # print(hash_to_hex_str(_tx_hash) + ": " + output.pk_script.hex() + " - " + str(output.value)) + assert hash_to_hex_str(_tx_hash) == txid + +# def main(): +# test_tx_txids(); + +# if __name__ == "__main__": +# main() \ No newline at end of file