From 5c3776b706b1f0eca502a53d4716bd6b9ab55cc2 Mon Sep 17 00:00:00 2001 From: k-matsuzawa <49718559+ko-matsu@users.noreply.github.com> Date: Sat, 21 Nov 2020 15:42:37 +0900 Subject: [PATCH] update to v0.2.2 (#3) * update from cryptogarageinc v0.2.4 + CI * fix: docker image name Co-authored-by: k-matsuzawa --- .github/workflows/check_pre-merge_develop.yml | 49 +- .github/workflows/check_pre-merge_master.yml | 52 +- .github/workflows/check_pre-merge_sprint.yml | 45 +- .github/workflows/code_scanner.yml | 4 +- .../workflows/create_release-and-upload.yml | 8 +- .github/workflows/docker/test_entrypoint.sh | 62 ++ .gitignore | 3 + Dockerfile | 35 + README.md | 11 +- VERSION | 2 +- cfd/confidential_transaction.py | 216 ++++- cfd/descriptor.py | 27 +- cfd/key.py | 2 +- cfd/util.py | 1 + docker-compose.yml | 17 + external/CMakeLists.txt | 2 +- integration_test/Dockerfile | 12 + integration_test/Pipfile | 21 + integration_test/README.md | 65 ++ integration_test/bitcoin.conf | 185 +++++ integration_test/docker-compose.yml | 11 + integration_test/elements.conf | 442 +++++++++++ integration_test/test_entrypoint.sh | 62 ++ integration_test/tests/helper.py | 21 + integration_test/tests/test_bitcoin.py | 357 +++++++++ integration_test/tests/test_elements.py | 743 ++++++++++++++++++ tests/data/descriptor_test.json | 38 +- tests/data/elements_coin_test.json | 5 +- tests/test_confidential_transaction.py | 260 +++++- tests/test_descriptor.py | 23 +- tests/util.py | 6 +- tools/build_for_docker.sh | 67 ++ 32 files changed, 2736 insertions(+), 118 deletions(-) create mode 100755 .github/workflows/docker/test_entrypoint.sh create mode 100644 Dockerfile create mode 100644 docker-compose.yml create mode 100644 integration_test/Dockerfile create mode 100644 integration_test/Pipfile create mode 100644 integration_test/README.md create mode 100644 integration_test/bitcoin.conf create mode 100644 integration_test/docker-compose.yml create mode 100644 integration_test/elements.conf create mode 100755 integration_test/test_entrypoint.sh create mode 100644 integration_test/tests/helper.py create mode 100644 integration_test/tests/test_bitcoin.py create mode 100644 integration_test/tests/test_elements.py create mode 100755 tools/build_for_docker.sh diff --git a/.github/workflows/check_pre-merge_develop.yml b/.github/workflows/check_pre-merge_develop.yml index e7ab378..fe0902e 100644 --- a/.github/workflows/check_pre-merge_develop.yml +++ b/.github/workflows/check_pre-merge_develop.yml @@ -13,6 +13,12 @@ on: - develop - test_ci +env: + GITHUB_VERSION: v0.0.5 + DOCKER_PYTHON_VERSION: 3.7 + GITHUB_DOCKER_IMAGE: docker.pkg.github.com/cryptogarageinc/elements-testing-dockerfile/elements-testing + ENTRYPOINT_PATH: /github/workspace/.github/workflows/docker/test_entrypoint.sh + jobs: build-and-test: name: build & test @@ -21,10 +27,10 @@ jobs: strategy: fail-fast: false matrix: - os: [macos-latest, windows-latest, ubuntu-latest] - py-ver: [3.6, 3.7, 3.8, pypy3] + os: [macos-10.15, windows-2019, ubuntu-18.04] + py-ver: [3.6, 3.7, 3.8, 3.9, pypy3] exclude: - - os: windows-latest + - os: windows-2019 py-ver: pypy3 steps: @@ -53,14 +59,16 @@ jobs: strategy: fail-fast: false matrix: - os: [macos-latest, windows-latest, ubuntu-latest] - py-ver: [3.6, 3.7, 3.8, pypy3] + os: [macos-10.15, macos-11.0, windows-2019, ubuntu-18.04, ubuntu-20.04] + py-ver: [3.6, 3.7, 3.8, 3.9, pypy3] exclude: - - os: windows-latest + - os: windows-2019 py-ver: 3.6 - - os: windows-latest + - os: windows-2019 py-ver: 3.7 - - os: windows-latest + - os: windows-2019 + py-ver: pypy3 + - os: macos-11.0 py-ver: pypy3 steps: @@ -80,9 +88,32 @@ jobs: - name: call example run: python ./tools/example.py + e2e-test: + timeout-minutes: 20 + name: e2e-test + runs-on: ubuntu-18.04 + + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: ${{ env.DOCKER_PYTHON_VERSION }} + - name: Display Python version + run: python -c "import sys; print(sys.version)" + - name: install pip + run: python -m pip install -U pip + - name: create wheel + run: pip wheel . + - name: e2e-test + run: | + docker login docker.pkg.github.com -u owner -p ${{ secrets.GITHUB_TOKEN }} + docker pull ${{ env.GITHUB_DOCKER_IMAGE }}:${{ env.GITHUB_VERSION }} + docker run -v ${{ github.workspace }}:/github/workspace --entrypoint ${{ env.ENTRYPOINT_PATH }} ${{ env.GITHUB_DOCKER_IMAGE }}:${{ env.GITHUB_VERSION }} + doxygen-ubuntu: name: doxygen-ubuntu - runs-on: ubuntu-latest + runs-on: ubuntu-18.04 steps: - uses: actions/checkout@v2 - name: install_doxygen diff --git a/.github/workflows/check_pre-merge_master.yml b/.github/workflows/check_pre-merge_master.yml index 6d5e320..81c13bc 100644 --- a/.github/workflows/check_pre-merge_master.yml +++ b/.github/workflows/check_pre-merge_master.yml @@ -4,10 +4,19 @@ on: push: branches: - master + paths-ignore: + - '.github/workflows/create_release-and-upload.yml' + - 'README.md' pull_request: branches: - master +env: + GITHUB_VERSION: v0.0.5 + DOCKER_PYTHON_VERSION: 3.7 + GITHUB_DOCKER_IMAGE: docker.pkg.github.com/cryptogarageinc/elements-testing-dockerfile/elements-testing + ENTRYPOINT_PATH: /github/workspace/.github/workflows/docker/test_entrypoint.sh + jobs: build-and-test: name: build & test @@ -16,10 +25,10 @@ jobs: strategy: fail-fast: false matrix: - os: [macos-latest, windows-latest, ubuntu-latest] - py-ver: [3.6, 3.7, 3.8, pypy3] + os: [macos-10.15, windows-2019, ubuntu-18.04] + py-ver: [3.6, 3.7, 3.8, 3.9, pypy3] exclude: - - os: windows-latest + - os: windows-2019 py-ver: pypy3 steps: @@ -48,14 +57,16 @@ jobs: strategy: fail-fast: false matrix: - os: [macos-latest, windows-latest, ubuntu-latest] - py-ver: [3.6, 3.7, 3.8, pypy3] + os: [macos-10.15, macos-11.0, windows-2019, ubuntu-18.04, ubuntu-20.04] + py-ver: [3.6, 3.7, 3.8, 3.9, pypy3] exclude: - - os: windows-latest + - os: windows-2019 py-ver: 3.6 - - os: windows-latest + - os: windows-2019 py-ver: 3.7 - - os: windows-latest + - os: windows-2019 + py-ver: pypy3 + - os: macos-11.0 py-ver: pypy3 steps: @@ -75,9 +86,32 @@ jobs: - name: call example run: python ./tools/example.py + e2e-test: + timeout-minutes: 20 + name: e2e-test + runs-on: ubuntu-18.04 + + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: ${{ env.DOCKER_PYTHON_VERSION }} + - name: Display Python version + run: python -c "import sys; print(sys.version)" + - name: install pip + run: python -m pip install -U pip + - name: create wheel + run: pip wheel . + - name: e2e-test + run: | + docker login docker.pkg.github.com -u owner -p ${{ secrets.GITHUB_TOKEN }} + docker pull ${{ env.GITHUB_DOCKER_IMAGE }}:${{ env.GITHUB_VERSION }} + docker run -v ${{ github.workspace }}:/github/workspace --entrypoint ${{ env.ENTRYPOINT_PATH }} ${{ env.GITHUB_DOCKER_IMAGE }}:${{ env.GITHUB_VERSION }} + doxygen-ubuntu: name: doxygen-ubuntu - runs-on: ubuntu-latest + runs-on: ubuntu-18.04 steps: - uses: actions/checkout@v2 - name: install_doxygen diff --git a/.github/workflows/check_pre-merge_sprint.yml b/.github/workflows/check_pre-merge_sprint.yml index a0bcb3c..22d1aad 100644 --- a/.github/workflows/check_pre-merge_sprint.yml +++ b/.github/workflows/check_pre-merge_sprint.yml @@ -11,6 +11,12 @@ on: branches: - features/sprint* +env: + GITHUB_VERSION: v0.0.5 + DOCKER_PYTHON_VERSION: 3.7 + GITHUB_DOCKER_IMAGE: docker.pkg.github.com/cryptogarageinc/elements-testing-dockerfile/elements-testing + ENTRYPOINT_PATH: /github/workspace/.github/workflows/docker/test_entrypoint.sh + jobs: build-and-test: name: build & test @@ -19,16 +25,18 @@ jobs: strategy: fail-fast: false matrix: - os: [macos-latest, windows-latest, ubuntu-latest] - py-ver: [3.6, 3.7, 3.8, pypy3] + os: [macos-10.15, windows-2019, ubuntu-18.04] + py-ver: [3.6, 3.7, 3.8, 3.9, pypy3] exclude: - - os: macos-latest + - os: macos-10.15 py-ver: 3.6 - - os: macos-latest + - os: macos-10.15 py-ver: 3.7 - - os: windows-latest + - os: macos-10.15 + py-ver: 3.8 + - os: windows-2019 py-ver: 3.6 - - os: windows-latest + - os: windows-2019 py-ver: pypy3 steps: @@ -50,9 +58,32 @@ jobs: - name: test run: python -m unittest discover -v tests + e2e-test: + timeout-minutes: 20 + name: e2e-test + runs-on: ubuntu-18.04 + + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: ${{ env.DOCKER_PYTHON_VERSION }} + - name: Display Python version + run: python -c "import sys; print(sys.version)" + - name: install pip + run: python -m pip install -U pip + - name: create wheel + run: pip wheel . + - name: e2e-test + run: | + docker login docker.pkg.github.com -u owner -p ${{ secrets.GITHUB_TOKEN }} + docker pull ${{ env.GITHUB_DOCKER_IMAGE }}:${{ env.GITHUB_VERSION }} + docker run -v ${{ github.workspace }}:/github/workspace --entrypoint ${{ env.ENTRYPOINT_PATH }} ${{ env.GITHUB_DOCKER_IMAGE }}:${{ env.GITHUB_VERSION }} + doxygen-ubuntu: name: doxygen-ubuntu - runs-on: ubuntu-latest + runs-on: ubuntu-18.04 steps: - uses: actions/checkout@v2 - name: install_doxygen diff --git a/.github/workflows/code_scanner.yml b/.github/workflows/code_scanner.yml index 2412605..832f575 100644 --- a/.github/workflows/code_scanner.yml +++ b/.github/workflows/code_scanner.yml @@ -16,7 +16,7 @@ on: jobs: analyze-CodeQL: name: CodeQL - runs-on: ubuntu-latest + runs-on: ubuntu-18.04 strategy: fail-fast: false @@ -68,7 +68,7 @@ jobs: OSSAR-Scan: # OSSAR runs on windows-latest. # ubuntu-latest and macos-latest support coming soon - runs-on: windows-latest + runs-on: windows-2019 steps: # Checkout your code repository to scan diff --git a/.github/workflows/create_release-and-upload.yml b/.github/workflows/create_release-and-upload.yml index cacf7ec..3e1863e 100644 --- a/.github/workflows/create_release-and-upload.yml +++ b/.github/workflows/create_release-and-upload.yml @@ -8,7 +8,7 @@ on: jobs: create_releases: name: create-releases - runs-on: ubuntu-latest + runs-on: ubuntu-18.04 outputs: release_url: ${{ steps.output_url.outputs.upload_url }} @@ -53,7 +53,7 @@ jobs: name: upload-sdist needs: create_releases timeout-minutes: 20 - runs-on: ubuntu-latest + runs-on: ubuntu-18.04 steps: - name: checkout @@ -109,7 +109,7 @@ jobs: strategy: fail-fast: false matrix: - os: [macos-10.15, windows-latest, ubuntu-18.04] + os: [macos-10.15, windows-2019, ubuntu-18.04] include: - os: macos-10.15 py-ver: 3.6 @@ -117,7 +117,7 @@ jobs: - os: ubuntu-18.04 py-ver: 3.6 pl-name: linux_x86_64 - - os: windows-latest + - os: windows-2019 py-ver: 3.8 pl-name: win_amd64 diff --git a/.github/workflows/docker/test_entrypoint.sh b/.github/workflows/docker/test_entrypoint.sh new file mode 100755 index 0000000..ac2eceb --- /dev/null +++ b/.github/workflows/docker/test_entrypoint.sh @@ -0,0 +1,62 @@ +#!/bin/bash -u + +# while :; do sleep 10; done +export WORKDIR_ROOT=github +export WORK_DIR=workspace +export WORKDIR_PATH=/${WORKDIR_ROOT}/${WORK_DIR} + +cd /${WORKDIR_ROOT} +if [ ! -d ${WORK_DIR} ]; then + mkdir ${WORK_DIR} +fi + +cd ${WORKDIR_PATH} +rm -rf bitcoind_datadir +rm -rf elementsd_datadir + +mkdir bitcoind_datadir +chmod 777 bitcoind_datadir +# cp /root/.bitcoin/bitcoin.conf bitcoind_datadir/ +cp ./integration_test/bitcoin.conf bitcoind_datadir/ +mkdir elementsd_datadir +chmod 777 elementsd_datadir +# cp /root/.elements/elements.conf elementsd_datadir/ +cp ./integration_test/elements.conf elementsd_datadir/ + +# boot daemon +bitcoind --regtest -datadir=${WORKDIR_PATH}/bitcoind_datadir +bitcoin-cli --regtest -datadir=${WORKDIR_PATH}/bitcoind_datadir ping > /dev/null 2>&1 +while [ $? -ne 0 ] +do + bitcoin-cli --regtest -datadir=${WORKDIR_PATH}/bitcoind_datadir ping > /dev/null 2>&1 +done +echo "start bitcoin node" + +elementsd -chain=liquidregtest -datadir=${WORKDIR_PATH}/elementsd_datadir +elements-cli -chain=liquidregtest -datadir=${WORKDIR_PATH}/elementsd_datadir ping > /dev/null 2>&1 +while [ $? -ne 0 ] +do + elements-cli -chain=liquidregtest -datadir=${WORKDIR_PATH}/elementsd_datadir ping > /dev/null 2>&1 +done +echo "start elements node" + +set -e + +python3 --version + +pip3 install *.whl +pip3 install python-bitcoinrpc + +cd integration_test + +python3 tests/test_bitcoin.py -v +if [ $? -gt 0 ]; then + cd ../.. + exit 1 +fi + +python3 tests/test_elements.py -v +if [ $? -gt 0 ]; then + cd ../.. + exit 1 +fi diff --git a/.gitignore b/.gitignore index ed2df2b..686584c 100644 --- a/.gitignore +++ b/.gitignore @@ -142,3 +142,6 @@ cmake_build/ /doc/latex /**/*_localdebug.bat /*.whl +/integration_test/localdata +/integration_test/bitcoind_datadir +/integration_test/elementsd_datadir diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..bc068f2 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,35 @@ +FROM python:3.9.0-slim-buster as cfdPythonBuilder + +RUN apt-get update && apt-get install -y tzdata +ENV TZ=Asia/Tokyo + +RUN apt-get install -y \ + gpg \ + wget \ + build-essential \ + git \ + && rm -rf /var/lib/apt/lists/* + +ENV GPG_KEY_SERVER hkp://keyserver.ubuntu.com:80 + +ENV CMAKE_VERSION 3.17.2 +ENV CMAKE_TARBALL cmake-${CMAKE_VERSION}-Linux-x86_64.tar.gz +ENV CMAKE_URL_BASE https://github.com/Kitware/CMake/releases/download/v${CMAKE_VERSION} +ENV CMAKE_PGP_KEY C6C265324BBEBDC350B513D02D2CEF1034921684 +RUN wget -qO ${CMAKE_TARBALL} ${CMAKE_URL_BASE}/${CMAKE_TARBALL} \ + && gpg --keyserver ${GPG_KEY_SERVER} --recv-keys ${CMAKE_PGP_KEY} \ + && wget -qO cmake-SHA-256.txt ${CMAKE_URL_BASE}/cmake-${CMAKE_VERSION}-SHA-256.txt \ + && wget -qO cmake-SHA-256.txt.asc ${CMAKE_URL_BASE}/cmake-${CMAKE_VERSION}-SHA-256.txt.asc \ + && gpg --verify cmake-SHA-256.txt.asc \ + && sha256sum --ignore-missing --check cmake-SHA-256.txt \ + && tar -xzvf ${CMAKE_TARBALL} --directory=/opt/ \ + && ln -sfn /opt/cmake-${CMAKE_VERSION}-Linux-x86_64/bin/* /usr/bin \ + && rm -f ${CMAKE_TARBALL} cmake-SHA-256.txt cmake-SHA-256.txt.asc + +RUN pip3 install wheel + +WORKDIR /root + +RUN python3 --version && cmake --version && env + +ENTRYPOINT ["/bin/bash", "-l", "-c"] \ No newline at end of file diff --git a/README.md b/README.md index f57525e..f5a4658 100644 --- a/README.md +++ b/README.md @@ -83,10 +83,10 @@ pip install --user . ### install from wheel -1. get releases asset. (ex. https://github.com/p2pderivatives/cfd-python/releases/download/v0.0.1/cfd-0.0.1-py3-none-win_amd64.whl ) +1. get releases asset. (ex. https://github.com/p2pderivatives/cfd-python/releases/download/v0.2.2/cfd-0.2.2-py3-none-win_amd64.whl ) 2. install pip ``` - pip install --user cfd-0.0.1-py3-none-win_amd64.whl + pip install --user cfd-0.2.2-py3-none-win_amd64.whl ``` ### uninstall @@ -137,6 +137,13 @@ python ./setup.py sdist pip wheel . ``` +### Using docker (for Ubuntu) + +``` +docker-compose build +docker-compose up +``` + --- ## Test diff --git a/VERSION b/VERSION index 7dff5b8..f477849 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.2.1 \ No newline at end of file +0.2.2 \ No newline at end of file diff --git a/cfd/confidential_transaction.py b/cfd/confidential_transaction.py index 61de8ba..768d5df 100644 --- a/cfd/confidential_transaction.py +++ b/cfd/confidential_transaction.py @@ -6,7 +6,7 @@ from .util import ReverseByteData, CfdError, JobHandle,\ CfdErrorCode, to_hex_string, get_util, ByteData from .address import Address, AddressUtil -from .key import Network, SigHashType, Privkey +from .key import Network, SigHashType, Privkey, Pubkey from .script import HashType from .transaction import UtxoData, OutPoint, Txid, TxIn, TxOut, _FundTxOpt,\ _TransactionBase @@ -16,6 +16,14 @@ import ctypes +## +# empty blinder +EMPTY_BLINDER = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, +] + + ## # @class BlindFactor # @brief blind factor (blinder) class. @@ -53,6 +61,13 @@ def __init__(self, data=''): def __str__(self): return self.hex + ## + # @brief check empty. + # @retval True empty. + # @retval False value exist. + def is_empty(self): + return (len(self.hex) == 0) + ## # @class ConfidentialAsset @@ -66,6 +81,9 @@ class ConfidentialAsset: # @brief constructor. # @param[in] data asset or asset commitment def __init__(self, data): + if isinstance(data, ConfidentialAsset): + self.hex = data.hex + return self.hex = to_hex_string(data) if len(self.hex) == 64: self.hex = str(ReverseByteData(data)) @@ -126,7 +144,7 @@ def create(cls, value, amount): _value_hex = to_hex_string(value) if isinstance(value, ConfidentialValue): return value - elif len(_value_hex) != 0: + elif len(value) == 66: return ConfidentialValue(_value_hex) else: return ConfidentialValue(amount) @@ -265,7 +283,7 @@ def __init__( self.is_issuance = is_issuance self.is_blind_issuance = is_blind_issuance self.is_pegin = is_pegin - self.pegin_btc_tx_size = pegin_btc_tx_size + self.pegin_btc_tx_size = int(pegin_btc_tx_size) self.fedpeg_script = fedpeg_script self.asset_blinder = asset_blinder self.amount_blinder = amount_blinder @@ -355,6 +373,78 @@ def __str__(self): return '{},{}'.format(self.asset, self.value) +## +# @class BlindData +# @brief blind data class. +class BlindData(UnblindData): + ## + # @var vout + # txout array index + ## + # @var is_issuance + # issuance flag + + ## + # @brief constructor. + # @param[in] vout txout array index + # @param[in] asset asset + # @param[in] amount amount + # @param[in] asset_blinder asset blind factor + # @param[in] amount_blinder amount blind factor + def __init__(self, vout, asset, amount, asset_blinder, amount_blinder): + super().__init__(asset, amount, asset_blinder, amount_blinder) + self.vout = vout + self.is_issuance = False + + +## +# @class IssuanceAssetBlindData +# @brief issuance asset blind data class. +class IssuanceAssetBlindData(BlindData): + ## + # @var outpoint + # issuance outpoint + ## + # @var is_issuance + # issuance flag + + ## + # @brief constructor. + # @param[in] outpoint txin outpoint + # @param[in] vout txin array index + # @param[in] asset asset + # @param[in] amount amount + # @param[in] amount_blinder amount blind factor + def __init__(self, outpoint, vout, asset, amount, amount_blinder): + super().__init__(vout, asset, amount, EMPTY_BLINDER, amount_blinder) + self.outpoint = outpoint + self.is_issuance = True + + +## +# @class IssuanceTokenBlindData +# @brief issuance token blind data class. +class IssuanceTokenBlindData(BlindData): + ## + # @var outpoint + # issuance outpoint + ## + # @var is_issuance + # issuance flag + + ## + # @brief constructor. + # @param[in] outpoint txin outpoint + # @param[in] vout txin array index + # @param[in] asset asset + # @param[in] amount amount + # @param[in] amount_blinder amount blind factor + def __init__(self, outpoint, vout, asset, amount, amount_blinder): + super().__init__(vout, asset, amount, EMPTY_BLINDER, amount_blinder) + self.outpoint = outpoint + self.is_issuance = True + + ## # @class Issuance # @brief Issuance data class. @@ -497,6 +587,49 @@ def __init__( self.surjectionproof = [] self.rangeproof = [] + ## + # @brief check fee. + # @retval true fee txout. + # @retval false other. + def is_fee(self): + return str(self.locking_script) == '' + + ## + # @brief get blind state. + # @retval True blinded. + # @retval False unblind. + def has_blind(self): + return self.value.has_blind() + + ## + # @brief constructor. + # @param[in] network network + # @param[in] is_confidential Returns Confidential Address if possible. + # @return address. + def get_address(self, network=Network.LIQUID_V1, + is_confidential=False): + _network = Network.get(network) + if _network not in [Network.LIQUID_V1, Network.ELEMENTS_REGTEST]: + raise CfdError(error_code=1, + message='Error: Invalid network type.') + if isinstance(self.address, ConfidentialAddress): + return self.address if is_confidential else self.address.address + addr = self.address if isinstance(self.address, Address) else None + if (addr is None) and (self.address != ''): + if ConfidentialAddress.valid(self.address): + ca = ConfidentialAddress.parse(self.address) + return ca if is_confidential else ca.address + addr = AddressUtil.parse(self.address) + if addr is None: + addr = AddressUtil.from_locking_script( + self.locking_script, _network) + + if self.has_blind() or self.nonce.is_empty() or ( + not is_confidential): + return addr + else: + return ConfidentialAddress(addr, Pubkey(self.nonce)) + ## # @class TargetAmountData @@ -833,6 +966,12 @@ def get_txout_list(handle, tx_handle): self.txout_list = get_txout_list(handle, tx_handle) return self.txin_list, self.txout_list + ## + # @brief get transaction output fee index. + # @return index + def get_txout_fee_index(self): + return self.get_txout_index() + ## # @brief add transaction input. # @param[in] outpoint outpoint @@ -950,15 +1089,19 @@ def update_txout_fee_amount(self, amount): # @param[in] minimum_range_value minimum range value # @param[in] exponent exponent # @param[in] minimum_bits minimum bits - # @return void + # @param[in] collect_blinder blinder collect flag. + # @return blinder_list (if collect_blinder is true) def blind_txout(self, utxo_list, confidential_address_list=[], direct_confidential_key_map={}, - minimum_range_value=1, exponent=0, minimum_bits=-1): - self.blind(utxo_list=utxo_list, - confidential_address_list=confidential_address_list, - direct_confidential_key_map=direct_confidential_key_map, - minimum_range_value=minimum_range_value, - exponent=exponent, minimum_bits=minimum_bits) + minimum_range_value=1, exponent=0, minimum_bits=-1, + collect_blinder=False): + return self.blind( + utxo_list=utxo_list, + confidential_address_list=confidential_address_list, + direct_confidential_key_map=direct_confidential_key_map, + minimum_range_value=minimum_range_value, + exponent=exponent, minimum_bits=minimum_bits, + collect_blinder=collect_blinder) ## # @brief blind transaction output. @@ -969,19 +1112,24 @@ def blind_txout(self, utxo_list, confidential_address_list=[], # @param[in] minimum_range_value minimum range value # @param[in] exponent exponent # @param[in] minimum_bits minimum bits - # @return void + # @param[in] collect_blinder blinder collect flag. + # @return blinder_list (if collect_blinder is true) def blind(self, utxo_list, issuance_key_map={}, confidential_address_list=[], direct_confidential_key_map={}, - minimum_range_value=1, exponent=0, minimum_bits=-1): + minimum_range_value=1, exponent=0, minimum_bits=-1, + collect_blinder=False): if minimum_bits == -1: minimum_bits = self.DEFAULT_BLIND_MINIMUM_BITS def set_opt(handle, tx_handle, key, i_val=0): + val = i_val + if isinstance(val, bool): + val = 1 if val is True else 0 util.call_func( 'CfdSetBlindTxOption', handle.get_handle(), - tx_handle.get_handle(), key.value, i_val) + tx_handle.get_handle(), key.value, val) util = get_util() with util.create_handle() as handle: @@ -1023,10 +1171,42 @@ def set_opt(handle, tx_handle, key, i_val=0): set_opt(handle, tx_handle, _BlindOpt.EXPONENT, exponent) set_opt(handle, tx_handle, _BlindOpt.MINIMUM_BITS, minimum_bits) + set_opt(handle, tx_handle, _BlindOpt.COLLECT_BLINDER, + bool(collect_blinder)) self.hex = util.call_func( 'CfdFinalizeBlindTx', handle.get_handle(), tx_handle.get_handle(), self.hex) self._update_tx_all() + blinder_list = [] + if bool(collect_blinder): + try: + i = 0 + while True: + vout, asset, amount, asset_blinder,\ + value_blinder, issuance_txid,\ + issuance_vout, is_issue_asset,\ + is_issue_token = util.call_func( + 'CfdGetBlindTxBlindData', + handle.get_handle(), + tx_handle.get_handle(), i) + if is_issue_asset: + blinder_list.append(IssuanceAssetBlindData( + OutPoint(issuance_txid, issuance_vout), + vout, asset, amount, + value_blinder)) + elif is_issue_token: + blinder_list.append(IssuanceTokenBlindData( + OutPoint(issuance_txid, issuance_vout), + vout, asset, amount, value_blinder)) + else: + blinder_list.append(BlindData( + vout, asset, amount, + asset_blinder, value_blinder)) + i += 1 + except CfdError as err: + if err.error_code != CfdErrorCode.OUT_OF_RANGE.value: + raise err + return blinder_list ## # @brief unblind transaction output. @@ -1334,7 +1514,7 @@ def set_opt(handle, tx_handle, key, i_val=0, f_val=0, b_val=False): str(utxo.outpoint.txid), utxo.outpoint.vout, str(utxo.descriptor), str(utxo.asset), utxo.is_issuance, utxo.is_blind_issuance, - utxo.is_pegin, utxo.pegin_btc_tx_size, + utxo.is_pegin, int(utxo.pegin_btc_tx_size), to_hex_string(utxo.fedpeg_script), to_hex_string(utxo.scriptsig_template)) @@ -1413,7 +1593,7 @@ def set_opt(handle, tx_handle, key, i_val=0, f_val=0, b_val=False): utxo.amount, str(utxo.descriptor), str(utxo.asset), utxo.is_issuance, utxo.is_blind_issuance, - utxo.is_pegin, utxo.pegin_btc_tx_size, + utxo.is_pegin, int(utxo.pegin_btc_tx_size), to_hex_string(utxo.fedpeg_script), to_hex_string(utxo.scriptsig_template)) for utxo in utxo_list: @@ -1474,6 +1654,9 @@ class _BlindOpt(Enum): ## # blind minimum bits (for elements) MINIMUM_BITS = 3 + ## + # collect blinder (for elements) + COLLECT_BLINDER = 4 ## @@ -1511,6 +1694,9 @@ class _FeeOpt(Enum): 'Issuance', 'IssuanceKeyPair', 'UnblindData', + 'BlindData', + 'IssuanceAssetBlindData', + 'IssuanceTokenBlindData', 'TargetAmountData', 'ConfidentialTxIn', 'ConfidentialTxOut', diff --git a/cfd/descriptor.py b/cfd/descriptor.py index a846611..eea5be1 100644 --- a/cfd/descriptor.py +++ b/cfd/descriptor.py @@ -199,8 +199,11 @@ class DescriptorScriptData: # @var address # address ## + # @var locking_script + # locking script + ## # @var redeem_script - # redeem script + # redeem script for script hash ## # @var key_data # key data @@ -217,12 +220,14 @@ class DescriptorScriptData: # @param[in] depth depth # @param[in] hash_type hash type # @param[in] address address + # @param[in] locking_script locking script # @param[in] redeem_script redeem script # @param[in] key_data key data # @param[in] key_list key list # @param[in] multisig_require_num multisig require num def __init__( self, script_type, depth, hash_type, address, + locking_script, redeem_script='', key_data=None, key_list=[], @@ -231,6 +236,7 @@ def __init__( self.depth = depth self.hash_type = hash_type self.address = address + self.locking_script = locking_script self.redeem_script = redeem_script self.key_data = key_data self.key_list = key_list @@ -313,7 +319,8 @@ def get_key(index): if _script_type != DescriptorScriptType.RAW: _hash_type = HashType.get(hash_type) data = DescriptorScriptData( - _script_type, depth, _hash_type, address) + _script_type, depth, _hash_type, address, + locking_script) if _script_type in { DescriptorScriptType.COMBO, DescriptorScriptType.PK, @@ -328,9 +335,8 @@ def get_key(index): DescriptorScriptType.MULTI, DescriptorScriptType.SORTED_MULTI}: data.address = AddressUtil.parse(address, hash_type) - if is_multisig is False: - data.redeem_script = redeem_script - else: + data.redeem_script = redeem_script + if is_multisig: key_list = [] for i in range(max_key_num): key_info = DescriptorKeyData(*get_key(i)) @@ -352,8 +358,8 @@ def get_key(index): # @brief analyze descriptor. # @return reference data def _analyze(self): - if (self.script_list[0].hash_type in { - HashType.P2WSH, HashType.P2SH}) and ( + if (self.script_list[0].hash_type in [ + HashType.P2WSH, HashType.P2SH]) and ( len(self.script_list) > 1) and ( self.script_list[1].hash_type == HashType.P2PKH): data = DescriptorScriptData( @@ -361,7 +367,8 @@ def _analyze(self): self.script_list[0].depth, self.script_list[0].hash_type, self.script_list[0].address, - self.script_list[0].redeem_script, + self.script_list[0].locking_script, + self.script_list[1].locking_script, self.script_list[1].key_data) return data @@ -373,6 +380,7 @@ def _analyze(self): self.script_list[0].depth, self.script_list[0].hash_type, self.script_list[0].address, + self.script_list[0].locking_script, self.script_list[1].redeem_script, self.script_list[2].key_data) return data @@ -387,6 +395,7 @@ def _analyze(self): self.script_list[0].depth, self.script_list[0].hash_type, self.script_list[0].address, + self.script_list[0].locking_script, self.script_list[1].redeem_script, key_list=self.script_list[1].key_list, multisig_require_num=multisig_require_num) @@ -397,6 +406,7 @@ def _analyze(self): self.script_list[0].depth, self.script_list[0].hash_type, self.script_list[0].address, + self.script_list[0].locking_script, self.script_list[1].redeem_script) return data elif self.script_list[0].hash_type == HashType.P2SH_P2WPKH: @@ -405,6 +415,7 @@ def _analyze(self): self.script_list[0].depth, self.script_list[0].hash_type, self.script_list[0].address, + self.script_list[0].locking_script, key_data=self.script_list[1].key_data) return data return self.script_list[0] diff --git a/cfd/key.py b/cfd/key.py index a64cbf7..6705b5b 100644 --- a/cfd/key.py +++ b/cfd/key.py @@ -320,7 +320,7 @@ def calculate_ec_signature(self, sighash, grind_r=True): signature = util.call_func( 'CfdCalculateEcSignature', handle.get_handle(), _sighash, self.hex, '', self.network.value, grind_r) - sign = SignParameter(signature) + sign = SignParameter(signature, self.pubkey) sign.use_der_encode = True return sign diff --git a/cfd/util.py b/cfd/util.py index d096d7f..0fd7c03 100644 --- a/cfd/util.py +++ b/cfd/util.py @@ -468,6 +468,7 @@ class CfdUtil: ("CfdAddBlindTxOutData", c_int, [c_void_p, c_void_p, c_uint32, c_char_p]), # noqa: E501 ("CfdAddBlindTxOutByAddress", c_int, [c_void_p, c_void_p, c_char_p]), # noqa: E501 ("CfdFinalizeBlindTx", c_int, [c_void_p, c_void_p, c_char_p, c_char_p_p]), # noqa: E501 + ("CfdGetBlindTxBlindData", c_int, [c_void_p, c_void_p, c_uint32, c_uint32_p, c_char_p_p, c_int64_p, c_char_p_p, c_char_p_p, c_char_p_p, c_uint32_p, c_bool_p, c_bool_p]), # noqa: E501 ("CfdFreeBlindHandle", c_int, [c_void_p, c_void_p]), # noqa: E501 ("CfdAddConfidentialTxSign", c_int, [c_void_p, c_char_p, c_char_p, c_uint32, c_bool, c_char_p, c_bool, c_char_p_p]), # noqa: E501 ("CfdAddConfidentialTxDerSign", c_int, [c_void_p, c_char_p, c_char_p, c_uint32, c_bool, c_char_p, c_int, c_bool, c_bool, c_char_p_p]), # noqa: E501 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..7d79456 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,17 @@ +version: '3.5' + +services: + cfd-py-build: + container_name: 'cfd-python-builder' + build: + context: . + dockerfile: Dockerfile + target: cfdPythonBuilder + environment: + CFD_SRC: /private/cfd-python + CFD_WORK: /private/work/cfd-python + working_dir: /private/cfd-python + volumes: + - /private/work + - .:/private/cfd-python + entrypoint: /private/cfd-python/tools/build_for_docker.sh diff --git a/external/CMakeLists.txt b/external/CMakeLists.txt index 2375a8b..491f376 100644 --- a/external/CMakeLists.txt +++ b/external/CMakeLists.txt @@ -43,7 +43,7 @@ if(CFD_TARGET_VERSION) set(CFD_TARGET_TAG ${CFD_TARGET_VERSION}) message(STATUS "[external project local] cfd target=${CFD_TARGET_VERSION}") else() -set(CFD_TARGET_TAG v0.2.0) +set(CFD_TARGET_TAG v0.2.1) endif() if(CFD_TARGET_URL) set(CFD_TARGET_REP ${CFD_TARGET_URL}) diff --git a/integration_test/Dockerfile b/integration_test/Dockerfile new file mode 100644 index 0000000..917c15b --- /dev/null +++ b/integration_test/Dockerfile @@ -0,0 +1,12 @@ +FROM cryptogarageinc/elements-testing:v0.18.1.7 + +ENV PATH /usr/local/bin:$PATH +ENV LANG C.UTF-8 + +RUN apt update && apt install -y \ + python3 \ + python3-dev \ + python3-pip \ + && rm -rf /var/lib/apt/lists/* + +RUN bitcoin-cli --version && elements-cli --version diff --git a/integration_test/Pipfile b/integration_test/Pipfile new file mode 100644 index 0000000..0d6c613 --- /dev/null +++ b/integration_test/Pipfile @@ -0,0 +1,21 @@ +[dev-packages] +autopep8 = "*" +flake8 = "*" + +[packages] +python-bitcoinrpc = "*" + +[scripts] +test = "python -m unittest discover -v tests" +test3 = "python3 -m unittest discover -v tests" +test_bitcoin = "python tests/test_bitcoin.py -v" +test3_bitcoin = "python3 tests/test_bitcoin.py -v" +test_elements = "python tests/test_elements.py -v" +test3_elements = "python3 tests/test_elements.py -v" +format = "autopep8 --aggressive --aggressive -ir setup.py cfd tests" +format_detail = "autopep8 --aggressive --aggressive -ivr setup.py cfd tests" +format_check = "autopep8 -d -r setup.py src" +lint = "flake8 --show-source setup.py cfd tests" +pip_list = "pip list" +pip_install = "pip install " +pip_uninstall = "pip uninstall " diff --git a/integration_test/README.md b/integration_test/README.md new file mode 100644 index 0000000..368004c --- /dev/null +++ b/integration_test/README.md @@ -0,0 +1,65 @@ +# cfd-python integration test + +## use app + + python-bitcoinrpc: lightweight rpc tool + +## execute test + +### install pipenv + + ``` + pip install --user pipenv + ``` + +### setup pipenv + + use pipenv + ``` + pipenv install + + (for developer): + pipenv install -d + ``` + +### install cfd + + 1. create wheel file + 2. move to current directory + 3. `pipenv run pip_install (wheel file path)` + +### run bitcoind & elementsd + + ``` + rm -rf localdata + mkdir localdata + mkdir localdata/bitcoind_datadir + cp bitcoin.conf localdata/bitcoind_datadir/ + bitcoind --regtest -datadir=localdata/bitcoind_datadir + + mkdir localdata/elementsd_datadir + cp elements.conf localdata/elementsd_datadir/ + elementsd --regtest -datadir=localdata/elementsd_datadir + ``` + +### run test + + 1. `pipenv run test` or `pipenv run test3` + +### after test + + ``` + elements-cli -datadir=localdata/elementsd_datadir stop + bitcoin-cli -datadir=localdata/elementsd_datadir stop + ``` + +--- + +## Using docker (for Ubuntu) + + ``` + (need wheel file on this folder.) + docker-compose build + docker-compose up + ``` + diff --git a/integration_test/bitcoin.conf b/integration_test/bitcoin.conf new file mode 100644 index 0000000..e1b2a11 --- /dev/null +++ b/integration_test/bitcoin.conf @@ -0,0 +1,185 @@ +## +## bitcoin.conf configuration file. Lines beginning with # are comments. +## + +# Network-related settings: + +# Note that if you use testnet or regtest, particularly with the options +# addnode, connect, port, bind, rpcport, rpcbind or wallet, you will also +# want to read "[Sections]" further down. + +# Run on the test network instead of the real bitcoin network. +#testnet=0 + +# Run a regression test network +regtest=1 + +# Connect via a SOCKS5 proxy +#proxy=127.0.0.1:9050 + +# Bind to given address and always listen on it. Use [host]:port notation for IPv6 +#bind= + +# Bind to given address and whitelist peers connecting to it. Use [host]:port notation for IPv6 +#whitebind= + +############################################################## +## Quick Primer on addnode vs connect ## +## Let's say for instance you use addnode=4.2.2.4 ## +## addnode will connect you to and tell you about the ## +## nodes connected to 4.2.2.4. In addition it will tell ## +## the other nodes connected to it that you exist so ## +## they can connect to you. ## +## connect will not do the above when you 'connect' to it. ## +## It will *only* connect you to 4.2.2.4 and no one else.## +## ## +## So if you're behind a firewall, or have other problems ## +## finding nodes, add some using 'addnode'. ## +## ## +## If you want to stay private, use 'connect' to only ## +## connect to "trusted" nodes. ## +## ## +## If you run multiple nodes on a LAN, there's no need for ## +## all of them to open lots of connections. Instead ## +## 'connect' them all to one node that is port forwarded ## +## and has lots of connections. ## +## Thanks goes to [Noodle] on Freenode. ## +############################################################## + +# Use as many addnode= settings as you like to connect to specific peers +#addnode=69.164.218.197 +#addnode=10.0.0.2:8333 + +# Alternatively use as many connect= settings as you like to connect ONLY to specific peers +#connect=69.164.218.197 +#connect=10.0.0.1:8333 + +# Listening mode, enabled by default except when 'connect' is being used +#listen=1 + +# Port on which to listen for connections (default: 8333, testnet: 18333, regtest: 18444) +port=18444 + +# Maximum number of inboundꋫ客 connections. +#maxconnections= + +# +# JSON-RPC options (for controlling a running Bitcoin/bitcoind process) +# + +# server=1 tells Bitcoin-Qt and bitcoind to accept JSON-RPC commands +#server=0 +server=1 +daemon=1 + +# Bind to given address to listen for JSON-RPC connections. Use [host]:port notation for IPv6. +# This option can be specified multiple times (default: bind to all interfaces) +#rpcbind= + +# If no rpcpassword is set, rpc cookie auth is sought. The default `-rpccookiefile` name +# is .cookie and found in the `-datadir` being used for bitcoind. This option is typically used +# when the server and client are run as the same user. +# +# If not, you must set rpcuser and rpcpassword to secure the JSON-RPC API. +# +# The config option `rpcauth` can be added to server startup argument. It is set at initialization time +# using the output from the script in share/rpcauth/rpcauth.py after providing a username: +# +# ./share/rpcauth/rpcauth.py alice +# String to be appended to bitcoin.conf: +# rpcauth=alice:f7efda5c189b999524f151318c0c86$d5b51b3beffbc02b724e5d095828e0bc8b2456e9ac8757ae3211a5d9b16a22ae +# Your password: +# DONT_USE_THIS_YOU_WILL_GET_ROBBED_8ak1gI25KFTvjovL3gAM967mies3E= +# +# On client-side, you add the normal user/password pair to send commands: +#rpcuser=alice +#rpcpassword=DONT_USE_THIS_YOU_WILL_GET_ROBBED_8ak1gI25KFTvjovL3gAM967mies3E= +# +# You can even add multiple entries of these to the server conf file, and client can use any of them: +# rpcauth=bob:b2dd077cb54591a2f3139e69a897ac$4e71f08d48b4347cf8eff3815c0e25ae2e9a4340474079f55705f40574f4ec99 +rpcuser=bitcoinrpc +rpcpassword=password + +# How many seconds bitcoin will wait for a complete RPC HTTP request. +# after the HTTP connection is established. +#rpcclienttimeout=30 + +# By default, only RPC connections from localhost are allowed. +# Specify as many rpcallowip= settings as you like to allow connections from other hosts, +# either as a single IPv4/IPv6 or with a subnet specification. + +# NOTE: opening up the RPC port to hosts outside your local trusted network is NOT RECOMMENDED, +# because the rpcpassword is transmitted over the network unencrypted. + +# server=1 tells Bitcoin-Qt to accept JSON-RPC commands. +# it is also read by bitcoind to determine if RPC should be enabled +#rpcallowip=10.1.1.34/255.255.255.0 +#rpcallowip=1.2.3.4/24 +#rpcallowip=2001:db8:85a3:0:0:8a2e:370:7334/96 + +# Listen for RPC connections on this TCP port: +rpcport=18443 + +# You can use Bitcoin or bitcoind to send commands to Bitcoin/bitcoind +# running on another host using this option: +#rpcconnect=127.0.0.1 + +# Wallet options + +# Specify where to find wallet, lockfile and logs. If not present, those files will be +# created as new. +#wallet= + +# Create transactions that have enough fees so they are likely to begin confirmation within n blocks (default: 6). +# This setting is over-ridden by the -paytxfee option. +#txconfirmtarget=n + +# Pay a transaction fee every time you send bitcoins. +#paytxfee=0.000x + +# Miscellaneous options + +# Pre-generate this many public/private key pairs, so wallet backups will be valid for +# both prior transactions and several dozen future transactions. +#keypool=100 + +# Enable pruning to reduce storage requirements by deleting old blocks. +# This mode is incompatible with -txindex and -rescan. +# 0 = default (no pruning). +# 1 = allows manual pruning via RPC. +# >=550 = target to stay under in MiB. +#prune=550 +#prune=1024 + +# User interface options + +# Start Bitcoin minimized +#min=1 + +# Minimize to the system tray +#minimizetotray=1 + +# [Sections] +# Most options apply to mainnet, testnet and regtest. +# If you want to confine an option to just one network, you should add it in the +# relevant section below. +# EXCEPTIONS: The options addnode, connect, port, bind, rpcport, rpcbind and wallet +# only apply to mainnet unless they appear in the appropriate section below. + +# Options only for mainnet +[main] +port=8333 +rpcport=8334 +txindex=1 +prune=550 + +# Options only for testnet +[test] +port=18333 +rpcport=18334 + +# Options only for regtest +[regtest] +port=18442 +rpcport=18443 +txindex=1 diff --git a/integration_test/docker-compose.yml b/integration_test/docker-compose.yml new file mode 100644 index 0000000..d2b3143 --- /dev/null +++ b/integration_test/docker-compose.yml @@ -0,0 +1,11 @@ +version: '3.5' + +services: + cfd-python-test: + build: + context: . + dockerfile: Dockerfile + volumes: + - ./test_entrypoint.sh:/root/test_entrypoint.sh + - .:/root/integration_test + entrypoint: "sh -c \"/root/test_entrypoint.sh\"" \ No newline at end of file diff --git a/integration_test/elements.conf b/integration_test/elements.conf new file mode 100644 index 0000000..32daff4 --- /dev/null +++ b/integration_test/elements.conf @@ -0,0 +1,442 @@ +### Liquid Core Daemon configuration file example + +# Chain is Liquid +# chain=liquidv1 +chain=liquidregtest + +# Execute command when a relevant alert is received or we see a really +# long fork (%s in cmd is replaced by message) +# alertnotify= + +# Execute command when the best block changes (%s in cmd is replaced by +# block hash) +# blocknotify= + +# If this block is in the chain assume that it and its ancestors are valid +# and potentially skip their script verification (0 to verify all, +# default: 0000000000000000000000000000000000000000000000000000000000000000) +# assumevalid= + +# Specify configuration file (default: liquid.conf) +# conf= + +# Run in the background as a daemon and accept commands +daemon=1 + +# Specify data directory +# datadir= + +# Set database cache size in megabytes (4 to 16384, default: 450) +# dbcache=450 + +# Imports blocks from external blk000??.dat file on startup +# loadblock= + +# Keep at most unconnectable transactions in memory (default: 100) +# maxorphantx=100 + +# Keep the transaction memory pool below megabytes (default: 300) +# maxmempool=300 + +# Do not keep transactions in the mempool longer than hours (default: +# 336) +# mempoolexpiry= + +# Extra transactions to keep in memory for compact block reconstructions +# (default: 100) +# blockreconstructionextratxn=100 + +# Set the number of script verification threads (-2 to 16, 0 = auto, <0 = +# leave that many cores free, default: 0) +# par=0 + +# Specify pid file (default: liquidd.pid) +# pid= + +# Reduce storage requirements by enabling pruning (deleting) of old +# blocks. This allows the pruneblockchain RPC to be called to +# delete specific blocks, and enables automatic pruning of old +# blocks if a target size in MiB is provided. This mode is +# incompatible with -txindex and -rescan. Warning: Reverting this +# setting requires re-downloading the entire blockchain. (default: +# 0 = disable pruning blocks, 1 = allow manual pruning via RPC, +# >550 = automatically prune block files to stay under the +# specified target size in MiB) +# prune=0 +# prune=550 + +# Rebuild chain state from the currently indexed blocks +# reindex-chainstate + +# Rebuild chain state and block index from the blk*.dat files on disk +# reindex + +# Create new files with system default permissions, instead of umask 077 +# (only effective with disabled wallet functionality) +# sysperms + +# Maintain a full transaction index, used by the getrawtransaction rpc +# call (default: 0) +# txindex=0 +# txindex=1 +txindex=1 + +### Connection options: + +# Add a node to connect to and attempt to keep the connection open +# addnode= + +# Threshold for disconnecting misbehaving peers (default: 100) +# banscore= + +# Number of seconds to keep misbehaving peers from reconnecting (default: +# 86400) +# bantime= + +# Bind to given address and always listen on it. Use [host]:port notation +# for IPv6 +# bind= + +# Connect only to the specified node(s); -noconnect or -connect=0 alone to +# disable automatic connections +# connect= + +# Discover own IP addresses (default: 1 when listening and no -externalip +# or -proxy) +# discover=1 + +# Allow DNS lookups for -addnode, -seednode and -connect (default: 1) +# dns=1 + +# Query for peer addresses via DNS lookup, if low on addresses (default: 1 +# unless -connect/-noconnect) +# dnsseed=1 + +# Specify your own public address +# externalip= + +# Always query for peer addresses via DNS lookup (default: 0) +# forcednsseed=0 + +# Accept connections from outside (default: 1 if no -proxy or +# connect/-noconnect) +# listen=1 + +# Automatically create Tor hidden service (default: 1) +# listenonion=1 + +# Maintain at most connections to peers (default: 125) +# maxconnections=125 + +# Maximum per-connection receive buffer, *1000 bytes (default: 5000) +# maxreceivebuffer=5000 + +# Maximum per-connection send buffer, *1000 bytes (default: 1000) +# maxsendbuffer=1000 + +# Maximum allowed median peer time offset adjustment. Local perspective of +# time may be influenced by peers forward or backward by this +# amount. (default: 4200 seconds) +# maxtimeadjustment=4200 + +# Use separate SOCKS5 proxy to reach peers via Tor hidden services +# (default: -proxy) +# onion= + +# Only connect to nodes in network (ipv4, ipv6 or onion) +# onlynet= + +# Relay non-P2SH multisig (default: 1) +# permitbaremultisig=1 + +# Support filtering of blocks and transaction with bloom filters (default: +#0) +# peerbloomfilters=0 + +# Listen for connections on (default: 7042) +port=7042 + +# Connect through SOCKS5 proxy +# proxy= + +# Randomize credentials for every proxy connection. This enables Tor +# tream isolation (default: 1) +# proxyrandomize=1 + +# Sets the serialization of raw transaction or block hex returned in +# non-verbose mode, non-segwit(0) or segwit(1) (default: 1) +# rpcserialversion=1 + +# Connect to a node to retrieve peer addresses, and disconnect +# seednode= + +# Specify connection timeout in milliseconds (minimum: 1, default: 5000) +# timeout=5000 + +# Tor control port to use if onion listening enabled (default: +# 127.0.0.1:9051) +# torcontrol=127.0.0.1:9051 + +# Tor control port password (default: empty) +# torpassword= + +# Use UPnP to map the listening port (default: 0) +# upnp=0 + +# Bind to given address and whitelist peers connecting to it. Use +# [host]:port notation for IPv6 +# whitebind= + +# Whitelist peers connecting from the given IP address (e.g. 1.2.3.4) or +# CIDR notated network (e.g. 1.2.3.0/24). Can be specified multiple +# times. Whitelisted peers cannot be DoS banned and their +# transactions are always relayed, even if they are already in the +# mempool, useful e.g. for a gateway +# whitelist= + +# Accept relayed transactions received from whitelisted peers even when +# not relaying transactions (default: 1) +# whitelistrelay=1 + +# Force relay of transactions from whitelisted peers even if they violate +# local relay policy (default: 1) +# whitelistforcerelay=1 + +# Tries to keep outbound traffic under the given target (in MiB per 24h), +# 0 = no limit (default: 0) +# maxuploadtarget=0 + +### Wallet options: + +# Do not load the wallet and disable wallet RPC calls +# disablewallet=0 + +# Set key pool size to (default: 100) +# keypool=100 + +# A fee rate (in BTC/kB) that will be used when fee estimation has +# insufficient data (default: 0.00001) +# fallbackfee=0.00001 + +# Fees (in BTC/kB) smaller than this are considered zero fee for +# transaction creation (default: 0.00001) +# mintxfee=0.00001 + +# Fee (in BTC/kB) to add to transactions you send (default: 0.00) +# paytxfee=0.00 + +# Rescan the block chain for missing wallet transactions on startup +# rescan=0 + +# Attempt to recover private keys from a corrupt wallet on startup +# salvagewallet=0 + +# Spend unconfirmed change when sending transactions (default: 1) +# pendzeroconfchange=0 + +# If paytxfee is not set, include enough fee so transactions begin +# confirmation on average within n blocks (default: 6) +# txconfirmtarget=6 + +# Use hierarchical deterministic key generation (HD) after BIP32. Only has +# effect during wallet creation/first start (default: 1) +# usehd=1 + +# Upgrade wallet to latest format on startup +# upgradewallet=0 + +# Specify wallet file (within data directory) (default: wallet.dat) +# wallet= + +# Make the wallet broadcast transactions (default: 1) +# walletbroadcast=1 + +# Execute command when a wallet transaction changes (%s in cmd is replaced +# by TxID) +# walletnotify= + +# Delete all wallet transactions and only recover those parts of the +# blockchain through -rescan on startup (1 = keep tx meta data e.g. +# account owner and payment request information, 2 = drop tx meta +# data) +# zapwallettxes= + +### ZeroMQ notification options: + +# Enable publish hash block in
+# zmqpubhashblock=
+ +# Enable publish hash transaction in
+# zmqpubhashtx=
+ +# Enable publish raw block in
+# zmqpubrawblock=
+ +# Enable publish raw transaction in
+# zmqpubrawtx=
+ +### Debugging/Testing options: + +# Append comment to the user agent string +# uacomment= + +# Output debugging information (default: 0, supplying is +# optional). If is not supplied or if = 1, +# output all debugging information. can be: addrman, +# alert, bench, cmpctblock, coindb, db, http, libevent, lock, +# mempool, mempoolrej, net, proxy, prune, rand, reindex, rpc, +# electcoins, tor, zmq. +# debug=0 + +### Show all debugging options (usage: --help -help-debug) +# help-debug + +# Include IP addresses in debug output (default: 0) +# logips=0 + +# Prepend debug output with timestamp (default: 1) +# logtimestamps=1 + +# Asset ID (hex) for mempool/relay fees (default: +# f44259d4fe4b055254b512efe86a88f44d3473039fbee77f6b21060b80c91464) +# feeasset= + +# Fees (in BTC/kB) smaller than this are considered zero fee for relaying, +# mining and transaction creation (default: 0.00001) +# minrelaytxfee=0.00001 + +# Maximum total fees (in BTC) to use in a single wallet transaction or raw +# transaction; setting this too low may abort large transactions +# (default: 0.10) +# maxtxfee=0.10 + +# Send trace/debug info to console instead of debug.log file +# printtoconsole=0 + +# Shrink debug.log file on client startup (default: 1 when no -debug) +# hrinkdebugfile=1 + +### Chain selection options: + +# Use the chain (default: liquidregtest). Anything except main is +# allowed +# chain= + +### Node relay options: + +# Equivalent bytes per sigop in transactions for relay and mining +# (default: 20) +# bytespersigop=20 + +### Relay and mine data carrier transactions (default: 1) +# datacarrier=1 + +# Maximum size of data in data carrier transactions we relay and mine +# (default: 83) +# datacarriersize=83 + +### Block creation options: + +# Set maximum BIP141 block weight (default: 4000000) +# blockmaxweight=4000000 + +# Set maximum block size in bytes (default: 1000000) +# blockmaxsize=1000000 + +# Set maximum size of high-priority/low-fee transactions in bytes +# (default: 200000) +# blockprioritysize=200000 + +# Set lowest fee rate (in BTC/kB) for transactions to be included in block +# creation. (default: 0.00001) +# blockmintxfee=0.00001 + +### RPC server options: + +# Accept command line and JSON-RPC commands +server=1 + +# Accept public REST requests (default: 0) +# rest=0 + +# Bind to given address to listen for JSON-RPC connections. Use +# [host]:port notation for IPv6. This option can be specified +# multiple times (default: bind to all interfaces) +# rpcbind= + +# Location of the auth cookie (default: data dir) +# rpccookiefile= + +# Username for JSON-RPC connections +# rpcuser= +rpcuser=elementsrpc + +# Password for JSON-RPC connections +# rpcpassword= +rpcpassword=password + +# Username and hashed password for JSON-RPC connections. The field +# comes in the format: :$. A +# canonical python script is included in share/rpcuser. The client +# then connects normally using the +# rpcuser=/rpcpassword= pair of arguments. This +# option can be specified multiple times +# rpcauth= + +# Listen for JSON-RPC connections on (default: 8332) +rpcport=7041 + +# Allow JSON-RPC connections from specified source. Valid for are a +# single IP (e.g. 1.2.3.4), a network/netmask (e.g. +# 1.2.3.4/255.255.255.0) or a network/CIDR (e.g. 1.2.3.4/24). This +# option can be specified multiple times +# rpcallowip= + +# Set the number of threads to service RPC calls (default: 4) +# rpcthreads=4 + +### Federated peg options: + +# Validate pegin claims. All functionaries must run this. (default: 1) +# validatepegin=1 + +# The address which the daemon will try to connect to validate peg-ins, if +# enabled. (default: cookie auth) +# mainchainrpchost= + +# The port which the daemon will try to connect to validate peg-ins, if +# enabled. (default: cookie auth) +# mainchainrpcport= +mainchainrpcport=18443 + +# The rpc username that the daemon will use to connect to validate +# peg-ins, if enabled. (default: cookie auth) +# mainchainrpcuser= +mainchainrpcuser=bitcoinrpc + +# The rpc password which the daemon will use to connect to validate +# peg-ins, if enabled. (default: cookie auth) +# mainchainrpcpassword= +mainchainrpcpassword=password + +# The bitcoind cookie auth path which the daemon will use to connect to +# validate peg-ins, if enabled. (default: default bitcoind datadir) +# mainchainrpccookiefile= + +# The other settings +peginconfirmationdepth=1 +# fedpegscript=7455876354210226c5051397d83ac461c376cbe39644e62bd3091aee553fd678cddaedba14cfc72102343eba0bdc9f8f06b7fd04c4c59d5a5f9971abfefddbfcb5fd984b65f55c5274210385b8ec42f0e33833fe80bf53b16cd7b4bb45abd4205c8da5c9dbacc6f1eb37452103ada3498b65e1d5704c3d0c59eb7dec870c3f57f9137161c4d14620a537beb63b2103cd705b9d0083ba03f83171e6875cec640a116bf10cb92bc0b3a22e7d6cfdb28c556702e007b275522102aef2b8a39966d49183fdddaefdc75af6d81ea6d16f7aba745cc4855e88f830842102141d452c3deeb937efff9f3378cd50bbde0543b77bbc6df6fc0e0addbf5578c52103948d24a9622cb14b198aed0739783d7c03d74c32c05780a86b43429c65679def5368ae + +[liquidregtest] +enforce_pak=1 +port=18444 +rpcport=18445 +mainchainrpcport=18443 +mainchainrpcuser=bitcoinrpc +mainchainrpcpassword=password +parentpubkeyprefix=111 +parentscriptprefix=196 +parent_bech32_hrp=bcrt +peginconfirmationdepth=1 +con_connect_genesis_outputs=1 +fedpegscript=51 diff --git a/integration_test/test_entrypoint.sh b/integration_test/test_entrypoint.sh new file mode 100755 index 0000000..a90e8a9 --- /dev/null +++ b/integration_test/test_entrypoint.sh @@ -0,0 +1,62 @@ +#!/bin/bash -u + +# while :; do sleep 10; done +export WORKDIR_ROOT=root +export WORK_DIR=integration_test +export WORKDIR_PATH=/${WORKDIR_ROOT}/${WORK_DIR} + +cd /${WORKDIR_ROOT} +if [ ! -d ${WORK_DIR} ]; then + mkdir ${WORK_DIR} +fi + +cd ${WORKDIR_PATH} +rm -rf bitcoind_datadir +rm -rf elementsd_datadir + +mkdir bitcoind_datadir +chmod 777 bitcoind_datadir +# cp /root/.bitcoin/bitcoin.conf bitcoind_datadir/ +cp ./bitcoin.conf bitcoind_datadir/ +mkdir elementsd_datadir +chmod 777 elementsd_datadir +# cp /root/.elements/elements.conf elementsd_datadir/ +cp ./elements.conf elementsd_datadir/ + +# boot daemon +bitcoind --regtest -datadir=${WORKDIR_PATH}/bitcoind_datadir +bitcoin-cli --regtest -datadir=${WORKDIR_PATH}/bitcoind_datadir ping > /dev/null 2>&1 +while [ $? -ne 0 ] +do + bitcoin-cli --regtest -datadir=${WORKDIR_PATH}/bitcoind_datadir ping > /dev/null 2>&1 +done +echo "start bitcoin node" + +elementsd -chain=liquidregtest -datadir=${WORKDIR_PATH}/elementsd_datadir +elements-cli -chain=liquidregtest -datadir=${WORKDIR_PATH}/elementsd_datadir ping > /dev/null 2>&1 +while [ $? -ne 0 ] +do + elements-cli -chain=liquidregtest -datadir=${WORKDIR_PATH}/elementsd_datadir ping > /dev/null 2>&1 +done +echo "start elements node" + +set -e + +python3 --version + +pip3 install *.whl +pip3 install python-bitcoinrpc + +# cd integration_test + +python3 tests/test_bitcoin.py -v +if [ $? -gt 0 ]; then + cd ../.. + exit 1 +fi + +python3 tests/test_elements.py -v +if [ $? -gt 0 ]; then + cd ../.. + exit 1 +fi diff --git a/integration_test/tests/helper.py b/integration_test/tests/helper.py new file mode 100644 index 0000000..55f9cd7 --- /dev/null +++ b/integration_test/tests/helper.py @@ -0,0 +1,21 @@ +from bitcoinrpc.authproxy import AuthServiceProxy + + +LISTUNSPENT_MAX = 9999999 + + +class RpcWrapper: + def __init__(self, host='127.0.0.1', port=8432, + rpc_user='', rpc_password=''): + self.rpc_connection = AuthServiceProxy('http://{}:{}@{}:{}'.format( + rpc_user, rpc_password, host, port)) + + def command(self, command, *args): + return self.rpc_connection.command(args) + + def get_rpc(self): + return self.rpc_connection + + +def get_utxo(conn, address_list=[]): + return conn.listunspent(0, LISTUNSPENT_MAX, address_list) diff --git a/integration_test/tests/test_bitcoin.py b/integration_test/tests/test_bitcoin.py new file mode 100644 index 0000000..f07b9e6 --- /dev/null +++ b/integration_test/tests/test_bitcoin.py @@ -0,0 +1,357 @@ +import unittest +from helper import RpcWrapper, get_utxo +from cfd.address import AddressUtil +from cfd.key import SigHashType +from cfd.hdwallet import HDWallet +from cfd.script import HashType +from cfd.descriptor import parse_descriptor +from cfd.transaction import Transaction, TxIn, TxOut, UtxoData +from decimal import Decimal +import logging +import time + +MNEMONIC = [ + 'clerk', 'zoo', 'mercy', 'board', 'grab', 'service', 'impact', 'tortoise', + 'step', 'crash', 'load', 'aerobic', 'suggest', 'rack', 'refuse', 'can', + 'solve', 'become', 'upset', 'jump', 'token', 'anchor', 'apart', 'dog'] +PASSPHRASE = 'Unx3HmdQ' +NETWORK = 'regtest' +ROOT_PATH = 'm/44h/0h/0h' +FEE_PATH = ROOT_PATH + '/1/0' +BTC_AMOUNT = 100000000 +BTC_AMOUNT_BIT = 8 + + +def convert_bitcoin_utxos(test_obj, utxo_list): + # {"txid": "f3c8453e1bda1366bc859532e27a829c8ce623b766ae699a0377b168993c44b5", "vout": 0, "address": "bcrt1qyq7xhec45m75m5nvhzuh47vsj3as7tqf8t8vkr", "label": "test_fee", "scriptPubKey": "0014203c6be715a6fd4dd26cb8b97af990947b0f2c09", "amount": 50.0, "confirmations": 101, "spendable": false, "solvable": false, "safe": true} # noqa8 + utxos = [] + for utxo in utxo_list: + desc = test_obj.desc_dic[utxo['address']] + value = Decimal(str(utxo['amount'])) + value = value * BTC_AMOUNT + data = UtxoData(txid=utxo['txid'], vout=utxo['vout'], + amount=int(value), descriptor=desc) + utxos.append(data) + return utxos + + +def search_utxos(test_obj, utxo_list, outpoint): + for utxo in utxo_list: + if utxo.outpoint == outpoint: + return utxo + test_obj.assertTrue(False, 'UTXO is empty. outpoint={}'.format(outpoint)) + + +def create_bitcoin_address(test_obj): + # fee address + pk = str(test_obj.hdwallet.get_pubkey(path=FEE_PATH).pubkey) + addr = AddressUtil.p2wpkh(pk, network=NETWORK) + test_obj.path_dic[str(addr)] = FEE_PATH + test_obj.addr_dic['fee'] = addr + test_obj.desc_dic[str(addr)] = parse_descriptor( + 'wpkh({})'.format(str(pk)), network=NETWORK) + print('set fee addr: ' + str(addr)) + + # wpkh main address + path = '{}/0/0'.format(ROOT_PATH) + pk = str(test_obj.hdwallet.get_pubkey(path=path).pubkey) + addr = AddressUtil.p2wpkh(pk, network=NETWORK) + test_obj.path_dic[str(addr)] = path + test_obj.addr_dic['main'] = addr + test_obj.desc_dic[str(addr)] = parse_descriptor( + 'wpkh({})'.format(str(pk)), network=NETWORK) + print('set main addr: ' + str(addr)) + # pkh address + path = '{}/0/1'.format(ROOT_PATH) + pk = str(test_obj.hdwallet.get_pubkey(path=path).pubkey) + addr = AddressUtil.p2pkh(pk, network=NETWORK) + test_obj.path_dic[str(addr)] = path + test_obj.addr_dic['p2pkh'] = addr + test_obj.desc_dic[str(addr)] = parse_descriptor( + 'pkh({})'.format(str(pk)), network=NETWORK) + print('set p2pkh addr: ' + str(addr)) + # wpkh address + path = '{}/0/2'.format(ROOT_PATH) + pk = str(test_obj.hdwallet.get_pubkey(path=path).pubkey) + addr = AddressUtil.p2wpkh(pk, network=NETWORK) + test_obj.path_dic[str(addr)] = path + test_obj.addr_dic['p2wpkh'] = addr + test_obj.desc_dic[str(addr)] = parse_descriptor( + 'wpkh({})'.format(str(pk)), network=NETWORK) + print('set p2wpkh addr: ' + str(addr)) + # p2sh-p2wpkh address + path = '{}/0/3'.format(ROOT_PATH) + pk = str(test_obj.hdwallet.get_pubkey(path=path).pubkey) + addr = AddressUtil.p2sh_p2wpkh(pk, network=NETWORK) + test_obj.path_dic[str(addr)] = path + test_obj.addr_dic['p2sh-p2wpkh'] = addr + test_obj.desc_dic[str(addr)] = parse_descriptor( + 'sh(wpkh({}))'.format(str(pk)), network=NETWORK) + print('set p2sh-p2wpkh addr: ' + str(addr)) + + # multisig_key + path = '{}/0/'.format(ROOT_PATH) + path_list = [path + str(i + 1) for i in range(3)] + pk1 = str(test_obj.hdwallet.get_pubkey(path=path_list[0]).pubkey) + pk2 = str(test_obj.hdwallet.get_pubkey(path=path_list[1]).pubkey) + pk3 = str(test_obj.hdwallet.get_pubkey(path=path_list[2]).pubkey) + pk_list = [pk1, pk2, pk3] + req_num = 2 + desc_multi = 'multi({},{},{},{})'.format(req_num, pk1, pk2, pk3) + addr = AddressUtil.multisig( + req_num, pk_list, HashType.P2SH, network=NETWORK) + test_obj.path_dic[str(addr)] = path_list + test_obj.addr_dic['p2sh'] = addr + test_obj.desc_dic[str(addr)] = parse_descriptor( + 'sh({})'.format(desc_multi), network=NETWORK) + print('set p2sh addr: ' + str(addr)) + addr = AddressUtil.multisig( + req_num, pk_list, HashType.P2WSH, network=NETWORK) + test_obj.path_dic[str(addr)] = path_list + test_obj.addr_dic['p2wsh'] = addr + test_obj.desc_dic[str(addr)] = parse_descriptor( + 'wsh({})'.format(desc_multi), network=NETWORK) + print('set p2wsh addr: ' + str(addr)) + addr = AddressUtil.multisig( + req_num, pk_list, HashType.P2SH_P2WSH, network=NETWORK) + test_obj.path_dic[str(addr)] = path_list + test_obj.addr_dic['p2sh-p2wsh'] = addr + test_obj.desc_dic[str(addr)] = parse_descriptor( + 'sh(wsh({}))'.format(desc_multi), network=NETWORK) + print('set p2sh-p2wsh addr: ' + str(addr)) + + +def test_import_address(test_obj): + btc_rpc = test_obj.conn.get_rpc() + # fee address + btc_rpc.importaddress(str(test_obj.addr_dic['fee']), 'test_fee', False) + # pkh address + btc_rpc.importaddress(str(test_obj.addr_dic['main']), 'test_main', False) + btc_rpc.importaddress(str(test_obj.addr_dic['p2pkh']), 'test_pkh', False) + btc_rpc.importaddress(str(test_obj.addr_dic['p2wpkh']), 'test_wpkh', False) + btc_rpc.importaddress( + str(test_obj.addr_dic['p2sh-p2wpkh']), 'test_sh_wpkh', False) + # multisig_key + btc_rpc.importaddress(str(test_obj.addr_dic['p2sh']), 'test_sh', False) + btc_rpc.importaddress(str(test_obj.addr_dic['p2wsh']), 'test_wsh', False) + btc_rpc.importaddress( + str(test_obj.addr_dic['p2sh-p2wsh']), 'test_sh_wsh', False) + + +def test_generate(test_obj): + # generatetoaddress -> fee addresss + print(test_obj.addr_dic) + btc_rpc = test_obj.conn.get_rpc() + addr = str(test_obj.addr_dic['fee']) + btc_rpc.generatetoaddress(100, addr) + btc_rpc.generatetoaddress(5, addr) + time.sleep(2) + resp = get_utxo(btc_rpc, [addr]) + print(resp) + + +def test_bitcoin_pkh(test_obj): + btc_rpc = test_obj.conn.get_rpc() + # create tx (output wpkh, p2sh-segwit, pkh) + txouts = [ + TxOut(100000000, str(test_obj.addr_dic['p2pkh'])), + TxOut(100000000, str(test_obj.addr_dic['p2wpkh'])), + TxOut(100000000, str(test_obj.addr_dic['p2sh-p2wpkh'])), + ] + tx = Transaction.create(2, 0, [], txouts) + # fundrawtransaction + fee_addr = str(test_obj.addr_dic['fee']) + fee_desc = test_obj.desc_dic[fee_addr] + fee_sk = test_obj.hdwallet.get_privkey(path=FEE_PATH).privkey + utxos = get_utxo(btc_rpc, [fee_addr]) + utxo_list = convert_bitcoin_utxos(test_obj, utxos) + tx.fund_raw_transaction([], utxo_list, fee_addr, + target_amount=0, effective_fee_rate=20.0, + knapsack_min_change=1) + # add sign + for txin in tx.txin_list: + utxo = search_utxos(test_obj, utxo_list, txin.outpoint) + tx.sign_with_privkey(txin.outpoint, fee_desc.data.hash_type, fee_sk, + amount=utxo.amount, + sighashtype=SigHashType.ALL) + # broadcast + print(Transaction.parse_to_json(str(tx), network=NETWORK)) + btc_rpc.sendrawtransaction(str(tx)) + # generate block + btc_rpc.generatetoaddress(2, fee_addr) + time.sleep(2) + + # create tx (output wpkh only, input tx1-3) + txid = tx.txid + txin_list = [] + txin_utxo_list = [] + for index, txout in enumerate(tx.txout_list): + temp_addr = str(txout.get_address(network=NETWORK)) + if temp_addr == fee_addr: + continue + txin_list.append(TxIn(txid=txid, vout=index)) + if temp_addr not in test_obj.desc_dic: + test_obj.assertTrue(False, 'addr not found. [{}]:[{}]'.format( + index, temp_addr)) + desc = test_obj.desc_dic[temp_addr] + txin_utxo_list.append(UtxoData( + txid=txid, vout=index, amount=txout.amount, descriptor=desc)) + txouts2 = [ + TxOut(300000000, str(test_obj.addr_dic['main'])), + ] + tx2 = Transaction.create(2, 0, txin_list, txouts2) + main_addr = test_obj.addr_dic['main'] + utxos = get_utxo(btc_rpc, [fee_addr]) + utxo_list = convert_bitcoin_utxos(test_obj, utxos) + tx2.fund_raw_transaction(txin_utxo_list, utxo_list, fee_addr, + target_amount=0, effective_fee_rate=20.0, + knapsack_min_change=1) + # add sign + join_utxo_list = [] + join_utxo_list[len(join_utxo_list):len(join_utxo_list)] = utxo_list + join_utxo_list[len(join_utxo_list):len(join_utxo_list)] = txin_utxo_list + for txin in tx2.txin_list: + utxo = search_utxos(test_obj, join_utxo_list, txin.outpoint) + path = test_obj.path_dic[str(utxo.descriptor.data.address)] + sk = test_obj.hdwallet.get_privkey(path=path).privkey + tx2.sign_with_privkey(txin.outpoint, utxo.descriptor.data.hash_type, + sk, amount=utxo.amount, + sighashtype=SigHashType.ALL) + # broadcast + print(Transaction.parse_to_json(str(tx2), network=NETWORK)) + btc_rpc.sendrawtransaction(str(tx2)) + # generate block + btc_rpc.generatetoaddress(2, fee_addr) + time.sleep(2) + utxos = get_utxo(btc_rpc, [str(main_addr)]) + print('UTXO: {}'.format(utxos)) + + +def test_bitcoin_multisig(test_obj): + btc_rpc = test_obj.conn.get_rpc() + # create tx (output multisig) + txouts = [ + TxOut(100000000, str(test_obj.addr_dic['p2sh'])), + TxOut(100000000, str(test_obj.addr_dic['p2wsh'])), + TxOut(100000000, str(test_obj.addr_dic['p2sh-p2wsh'])), + ] + tx = Transaction.create(2, 0, [], txouts) + # fundrawtransaction + fee_addr = str(test_obj.addr_dic['fee']) + fee_desc = test_obj.desc_dic[fee_addr] + fee_sk = test_obj.hdwallet.get_privkey(path=FEE_PATH).privkey + utxos = get_utxo(btc_rpc, [fee_addr]) + utxo_list = convert_bitcoin_utxos(test_obj, utxos) + tx.fund_raw_transaction([], utxo_list, fee_addr, + target_amount=0, effective_fee_rate=20.0, + knapsack_min_change=1) + # add sign + for txin in tx.txin_list: + utxo = search_utxos(test_obj, utxo_list, txin.outpoint) + tx.sign_with_privkey(txin.outpoint, fee_desc.data.hash_type, fee_sk, + amount=utxo.amount, + sighashtype=SigHashType.ALL) + # broadcast + print(Transaction.parse_to_json(str(tx), network=NETWORK)) + btc_rpc.sendrawtransaction(str(tx)) + # generate block + btc_rpc.generatetoaddress(2, fee_addr) + time.sleep(2) + + # create tx (output wpkh only, input multisig tx1-3) + txid = tx.txid + txin_list = [] + txin_utxo_list = [] + for index, txout in enumerate(tx.txout_list): + temp_addr = str(txout.get_address(network=NETWORK)) + if temp_addr == fee_addr: + continue + txin_list.append(TxIn(txid=txid, vout=index)) + if temp_addr not in test_obj.desc_dic: + test_obj.assertTrue(False, 'addr not found. [{}]:[{}]'.format( + index, temp_addr)) + desc = test_obj.desc_dic[temp_addr] + txin_utxo_list.append(UtxoData( + txid=txid, vout=index, amount=txout.amount, descriptor=desc)) + txouts2 = [ + TxOut(300000000, str(test_obj.addr_dic['main'])), + ] + tx2 = Transaction.create(2, 0, txin_list, txouts2) + main_addr = test_obj.addr_dic['main'] + utxos = get_utxo(btc_rpc, [fee_addr]) + utxo_list = convert_bitcoin_utxos(test_obj, utxos) + tx2.fund_raw_transaction(txin_utxo_list, utxo_list, fee_addr, + target_amount=0, effective_fee_rate=20.0, + knapsack_min_change=1) + # add sign + + def multisig_sign(tx_obj, utxo, path_list): + sighash = tx_obj.get_sighash( + outpoint=utxo.outpoint, + hash_type=utxo.descriptor.data.hash_type, + amount=utxo.amount, + redeem_script=utxo.descriptor.data.redeem_script) + signature_list = [] + for path in path_list: + sk = test_obj.hdwallet.get_privkey(path=path).privkey + sig = sk.calculate_ec_signature(sighash) + sig.related_pubkey = sk.pubkey + signature_list.append(sig) + if len(signature_list) == 2: + break + tx_obj.add_multisig_sign( + utxo.outpoint, utxo.descriptor.data.hash_type, + utxo.descriptor.data.redeem_script, signature_list) + + join_utxo_list = [] + join_utxo_list[len(join_utxo_list):len(join_utxo_list)] = utxo_list + join_utxo_list[len(join_utxo_list):len(join_utxo_list)] = txin_utxo_list + for index, txin in enumerate(tx2.txin_list): + utxo = search_utxos(test_obj, join_utxo_list, txin.outpoint) + if not utxo.descriptor.data.redeem_script: + path = test_obj.path_dic[str(utxo.descriptor.data.address)] + sk = test_obj.hdwallet.get_privkey(path=path).privkey + tx2.sign_with_privkey(txin.outpoint, + utxo.descriptor.data.hash_type, + sk, amount=utxo.amount, + sighashtype=SigHashType.ALL) + else: + path_list = test_obj.path_dic[str(utxo.descriptor.data.address)] + multisig_sign(tx2, utxo, path_list) + # broadcast + print(Transaction.parse_to_json(str(tx2), network=NETWORK)) + btc_rpc.sendrawtransaction(str(tx2)) + # generate block + btc_rpc.generatetoaddress(2, fee_addr) + time.sleep(2) + utxos = get_utxo(btc_rpc, [str(main_addr)]) + print('UTXO: {}'.format(utxos)) + + +class TestBitcoin(unittest.TestCase): + def setUp(self): + logging.basicConfig() + logging.getLogger("BitcoinRPC").setLevel(logging.DEBUG) + + self.path_dic = {} + self.addr_dic = {} + self.desc_dic = {} + self.hdwallet = HDWallet.from_mnemonic( + MNEMONIC, passphrase=PASSPHRASE, network=NETWORK) + create_bitcoin_address(self) + self.conn = RpcWrapper( + port=18443, rpc_user='bitcoinrpc', rpc_password='password') + + def test_bitcoin(self): + ''' + To execute sequentially, define only one test + and call the test function in it. + ''' + test_import_address(self) + test_generate(self) + test_bitcoin_pkh(self) + test_bitcoin_multisig(self) + + +if __name__ == "__main__": + unittest.main() diff --git a/integration_test/tests/test_elements.py b/integration_test/tests/test_elements.py new file mode 100644 index 0000000..202ce43 --- /dev/null +++ b/integration_test/tests/test_elements.py @@ -0,0 +1,743 @@ +import unittest +from helper import RpcWrapper, get_utxo +from cfd.address import AddressUtil +from cfd.key import SigHashType +from cfd.hdwallet import HDWallet +from cfd.script import HashType +from cfd.descriptor import parse_descriptor +from cfd.transaction import Transaction +from cfd.confidential_address import ConfidentialAddress +from cfd.confidential_transaction import ConfidentialTransaction,\ + ConfidentialTxIn, ConfidentialTxOut, ElementsUtxoData,\ + TargetAmountData +from decimal import Decimal +import logging +import time + +MNEMONIC = [ + 'clerk', 'zoo', 'mercy', 'board', 'grab', 'service', 'impact', 'tortoise', + 'step', 'crash', 'load', 'aerobic', 'suggest', 'rack', 'refuse', 'can', + 'solve', 'become', 'upset', 'jump', 'token', 'anchor', 'apart', 'dog'] +PASSPHRASE = 'Unx3HmdQ' +NETWORK = 'elementsregtest' +MAINCHAIN_NETWORK = 'regtest' +ROOT_PATH = 'm/44h/0h/0h' +FEE_PATH = ROOT_PATH + '/1/0' +GEN_PATH = ROOT_PATH + '/1/1' +MULTISIG_CT_PATH_BASE = ROOT_PATH + '/0/100/' +BTC_AMOUNT = 100000000 +BTC_AMOUNT_BIT = 8 + + +def convert_elements_utxos(test_obj, utxo_list): + # {'txid': 'b8e25f336229b447e02eb18cc3f1201979eaea7fd9299c167407c8b97454f849', 'vout': 0, 'address': 'ert1qyq7xhec45m75m5nvhzuh47vsj3as7tqflljjgr', 'label': 'test_fee', 'scriptPubKey': '0014203c6be715a6fd4dd26cb8b97af990947b0f2c09', 'amount': Decimal('248.99999710'), 'assetcommitment': '0a42101f526b26b4f74d26c5ce566d77d6159894a8b50214b82d2f838dd0a3a418', 'asset': '5ac9f65c0efcc4775e0baec4ec03abdde22473cd3cf33c0419ca290e0751b225', 'amountcommitment': '0842192917a9b4adbd4e0d3ff7a71dc97004de57f94ef825b956e04531f6a87098', 'amountblinder': '2f79bce2b26efe065378cfb532907d77dfb426a90cf1181da597dc7ea05b303b', 'assetblinder': '0dfc94eb72987ee2781fa31b2881f132cce118b9005f3c1623224225b37c0eeb', 'confirmations': 111, 'spendable': False, 'solvable': False, 'safe': True} # noqa8 + utxos = [] + for utxo in utxo_list: + desc = test_obj.desc_dic[utxo['address']] + value = Decimal(str(utxo['amount'])) + value = value * BTC_AMOUNT + amount_commitment = utxo.get('amountcommitment', '') + asset_blinder = utxo.get('assetblinder', '') + amount_blinder = utxo.get('amountblinder', '') + data = ElementsUtxoData( + txid=utxo['txid'], vout=utxo['vout'], + amount=int(value), descriptor=desc, + value=amount_commitment, + asset=test_obj.pegged_asset, + asset_blinder=asset_blinder, + amount_blinder=amount_blinder) + utxos.append(data) + return utxos + + +def search_utxos(test_obj, utxo_list, outpoint): + for utxo in utxo_list: + if utxo.outpoint == outpoint: + return utxo + test_obj.assertTrue(False, 'UTXO is empty. outpoint={}'.format(outpoint)) + + +def create_bitcoin_address(test_obj): + # fee address + pk = str(test_obj.hdwallet.get_pubkey(path=FEE_PATH).pubkey) + addr = AddressUtil.p2wpkh(pk, network=NETWORK) + test_obj.path_dic[str(addr)] = FEE_PATH + test_obj.addr_dic['fee'] = addr + test_obj.desc_dic[str(addr)] = parse_descriptor( + 'wpkh({})'.format(str(pk)), network=NETWORK) + print('set fee addr: ' + str(addr)) + path2 = FEE_PATH + '/0' + sk = test_obj.hdwallet.get_privkey(path=path2).privkey + test_obj.blind_key_dic[str(addr)] = sk + ct_addr = ConfidentialAddress(addr, sk.pubkey) + test_obj.ct_addr_dic[str(addr)] = ct_addr + print('set fee ct_addr: ' + str(ct_addr)) + + # gen address + pk = str(test_obj.hdwallet.get_pubkey(path=GEN_PATH).pubkey) + addr = AddressUtil.p2wpkh(pk, network=NETWORK) + test_obj.path_dic[str(addr)] = FEE_PATH + test_obj.addr_dic['gen'] = addr + test_obj.desc_dic[str(addr)] = parse_descriptor( + 'wpkh({})'.format(str(pk)), network=NETWORK) + print('set gen addr: ' + str(addr)) + path2 = GEN_PATH + '/0' + sk = test_obj.hdwallet.get_privkey(path=path2).privkey + test_obj.blind_key_dic[str(addr)] = sk + ct_addr = ConfidentialAddress(addr, sk.pubkey) + test_obj.ct_addr_dic[str(addr)] = ct_addr + print('set gen ct_addr: ' + str(ct_addr)) + + # wpkh main address + path = '{}/0/0'.format(ROOT_PATH) + pk = str(test_obj.hdwallet.get_pubkey(path=path).pubkey) + addr = AddressUtil.p2wpkh(pk, network=NETWORK) + test_obj.path_dic[str(addr)] = path + test_obj.addr_dic['main'] = addr + test_obj.desc_dic[str(addr)] = parse_descriptor( + 'wpkh({})'.format(str(pk)), network=NETWORK) + print('set main addr: ' + str(addr)) + path2 = path + '/0' + sk = test_obj.hdwallet.get_privkey(path=path2).privkey + test_obj.blind_key_dic[str(addr)] = sk + ct_addr = ConfidentialAddress(addr, sk.pubkey) + test_obj.ct_addr_dic[str(addr)] = ct_addr + print('set main ct_addr: ' + str(ct_addr)) + + # pkh address + path = '{}/0/1'.format(ROOT_PATH) + pk = str(test_obj.hdwallet.get_pubkey(path=path).pubkey) + addr = AddressUtil.p2pkh(pk, network=NETWORK) + test_obj.path_dic[str(addr)] = path + test_obj.addr_dic['p2pkh'] = addr + test_obj.desc_dic[str(addr)] = parse_descriptor( + 'pkh({})'.format(str(pk)), network=NETWORK) + print('set p2pkh addr: ' + str(addr)) + path2 = path + '/0' + sk = test_obj.hdwallet.get_privkey(path=path2).privkey + test_obj.blind_key_dic[str(addr)] = sk + ct_addr = ConfidentialAddress(addr, sk.pubkey) + test_obj.ct_addr_dic[str(addr)] = ct_addr + print('set p2pkh ct_addr: ' + str(ct_addr)) + # wpkh address + path = '{}/0/2'.format(ROOT_PATH) + pk = str(test_obj.hdwallet.get_pubkey(path=path).pubkey) + addr = AddressUtil.p2wpkh(pk, network=NETWORK) + test_obj.path_dic[str(addr)] = path + test_obj.addr_dic['p2wpkh'] = addr + test_obj.desc_dic[str(addr)] = parse_descriptor( + 'wpkh({})'.format(str(pk)), network=NETWORK) + print('set p2wpkh addr: ' + str(addr)) + path2 = path + '/0' + sk = test_obj.hdwallet.get_privkey(path=path2).privkey + test_obj.blind_key_dic[str(addr)] = sk + ct_addr = ConfidentialAddress(addr, sk.pubkey) + test_obj.ct_addr_dic[str(addr)] = ct_addr + print('set p2wpkh ct_addr: ' + str(ct_addr)) + # p2sh-p2wpkh address + path = '{}/0/3'.format(ROOT_PATH) + pk = str(test_obj.hdwallet.get_pubkey(path=path).pubkey) + addr = AddressUtil.p2sh_p2wpkh(pk, network=NETWORK) + test_obj.path_dic[str(addr)] = path + test_obj.addr_dic['p2sh-p2wpkh'] = addr + test_obj.desc_dic[str(addr)] = parse_descriptor( + 'sh(wpkh({}))'.format(str(pk)), network=NETWORK) + print('set p2sh-p2wpkh addr: ' + str(addr)) + path2 = path + '/0' + sk = test_obj.hdwallet.get_privkey(path=path2).privkey + test_obj.blind_key_dic[str(addr)] = sk + ct_addr = ConfidentialAddress(addr, sk.pubkey) + test_obj.ct_addr_dic[str(addr)] = ct_addr + print('set p2sh-p2wpkh ct_addr: ' + str(ct_addr)) + + # multisig_key + path = '{}/0/'.format(ROOT_PATH) + path_list = [path + str(i + 1) for i in range(3)] + pk1 = str(test_obj.hdwallet.get_pubkey(path=path_list[0]).pubkey) + pk2 = str(test_obj.hdwallet.get_pubkey(path=path_list[1]).pubkey) + pk3 = str(test_obj.hdwallet.get_pubkey(path=path_list[2]).pubkey) + pk_list = [pk1, pk2, pk3] + req_num = 2 + desc_multi = 'multi({},{},{},{})'.format(req_num, pk1, pk2, pk3) + addr = AddressUtil.multisig( + req_num, pk_list, HashType.P2SH, network=NETWORK) + test_obj.path_dic[str(addr)] = path_list + test_obj.addr_dic['p2sh'] = addr + test_obj.desc_dic[str(addr)] = parse_descriptor( + 'sh({})'.format(desc_multi), network=NETWORK) + print('set p2sh addr: ' + str(addr)) + path2 = MULTISIG_CT_PATH_BASE + '1' + sk = test_obj.hdwallet.get_privkey(path=path2).privkey + test_obj.blind_key_dic[str(addr)] = sk + ct_addr = ConfidentialAddress(addr, sk.pubkey) + test_obj.ct_addr_dic[str(addr)] = ct_addr + print('set p2sh ct_addr: ' + str(ct_addr)) + addr = AddressUtil.multisig( + req_num, pk_list, HashType.P2WSH, network=NETWORK) + test_obj.path_dic[str(addr)] = path_list + test_obj.addr_dic['p2wsh'] = addr + test_obj.desc_dic[str(addr)] = parse_descriptor( + 'wsh({})'.format(desc_multi), network=NETWORK) + print('set p2wsh addr: ' + str(addr)) + path2 = MULTISIG_CT_PATH_BASE + '2' + sk = test_obj.hdwallet.get_privkey(path=path2).privkey + test_obj.blind_key_dic[str(addr)] = sk + ct_addr = ConfidentialAddress(addr, sk.pubkey) + test_obj.ct_addr_dic[str(addr)] = ct_addr + print('set p2wsh ct_addr: ' + str(ct_addr)) + addr = AddressUtil.multisig( + req_num, pk_list, HashType.P2SH_P2WSH, network=NETWORK) + test_obj.path_dic[str(addr)] = path_list + test_obj.addr_dic['p2sh-p2wsh'] = addr + test_obj.desc_dic[str(addr)] = parse_descriptor( + 'sh(wsh({}))'.format(desc_multi), network=NETWORK) + print('set p2sh-p2wsh addr: ' + str(addr)) + path2 = MULTISIG_CT_PATH_BASE + '3' + sk = test_obj.hdwallet.get_privkey(path=path2).privkey + test_obj.blind_key_dic[str(addr)] = sk + ct_addr = ConfidentialAddress(addr, sk.pubkey) + test_obj.ct_addr_dic[str(addr)] = ct_addr + print('set p2sh-p2wsh ct_addr: ' + str(ct_addr)) + + # master blinding key + path = '{}/0/1001'.format(ROOT_PATH) + sk = str(test_obj.hdwallet.get_privkey(path=path).privkey) + test_obj.master_blinding_key = sk + print('set master blinding key: ' + sk) + + +def test_import_address(test_obj): + btc_rpc = test_obj.btcConn.get_rpc() + elm_rpc = test_obj.elmConn.get_rpc() + + # get btc address from bitcoin-cli (for fee) + btc_addr = btc_rpc.getnewaddress('', 'bech32') + test_obj.addr_dic['btc'] = btc_addr + + # fee address + addr = str(test_obj.addr_dic['fee']) + elm_rpc.importaddress(str(test_obj.ct_addr_dic[addr]), 'test_fee', False) + elm_rpc.importblindingkey( + str(test_obj.ct_addr_dic[addr]), + str(test_obj.blind_key_dic[addr].hex)) + # pkh address + addr = str(test_obj.addr_dic['main']) + elm_rpc.importaddress(str(test_obj.ct_addr_dic[addr]), 'test_main', False) + elm_rpc.importblindingkey( + str(test_obj.ct_addr_dic[addr]), + str(test_obj.blind_key_dic[addr].hex)) + addr = str(test_obj.addr_dic['p2pkh']) + elm_rpc.importaddress(str(test_obj.ct_addr_dic[addr]), 'test_pkh', False) + elm_rpc.importblindingkey( + str(test_obj.ct_addr_dic[addr]), + str(test_obj.blind_key_dic[addr].hex)) + addr = str(test_obj.addr_dic['p2wpkh']) + elm_rpc.importaddress(str(test_obj.ct_addr_dic[addr]), 'test_wpkh', False) + elm_rpc.importblindingkey( + str(test_obj.ct_addr_dic[addr]), + str(test_obj.blind_key_dic[addr].hex)) + addr = str(test_obj.addr_dic['p2sh-p2wpkh']) + elm_rpc.importaddress( + str(test_obj.ct_addr_dic[addr]), 'test_sh_wpkh', False) + elm_rpc.importblindingkey( + str(test_obj.ct_addr_dic[addr]), + str(test_obj.blind_key_dic[addr].hex)) + # multisig_key + addr = str(test_obj.addr_dic['p2sh']) + elm_rpc.importaddress(str(test_obj.ct_addr_dic[addr]), 'test_sh', False) + elm_rpc.importblindingkey( + str(test_obj.ct_addr_dic[addr]), + str(test_obj.blind_key_dic[addr].hex)) + addr = str(test_obj.addr_dic['p2wsh']) + elm_rpc.importaddress(str(test_obj.ct_addr_dic[addr]), 'test_wsh', False) + elm_rpc.importblindingkey( + str(test_obj.ct_addr_dic[addr]), + str(test_obj.blind_key_dic[addr].hex)) + addr = str(test_obj.addr_dic['p2sh-p2wsh']) + elm_rpc.importaddress( + str(test_obj.ct_addr_dic[addr]), 'test_sh_wsh', False) + elm_rpc.importblindingkey( + str(test_obj.ct_addr_dic[addr]), + str(test_obj.blind_key_dic[addr].hex)) + + +def get_elements_config(test_obj): + elm_rpc = test_obj.elmConn.get_rpc() + # mainchain + test_obj.sidechaininfo = elm_rpc.getsidechaininfo() + test_obj.pegged_asset = test_obj.sidechaininfo['pegged_asset'] + test_obj.fedpegscript = test_obj.sidechaininfo['fedpegscript'] + test_obj.parent_blockhash = test_obj.sidechaininfo['parent_blockhash'] + test_obj.pegin_confirmation_depth =\ + test_obj.sidechaininfo['pegin_confirmation_depth'] + + +def update_pegin_tx(test_obj, pegin_tx, btc_tx, pegin_address): + pegin_tx2 = pegin_tx + btc_tx_obj = Transaction.from_hex(btc_tx) + btc_txid = btc_tx_obj.txid + btc_txout_index = btc_tx_obj.get_txout_index(address=pegin_address) + btc_amount = btc_tx_obj.txout_list[btc_txout_index].amount + btc_size = len(btc_tx) / 2 + + # decode + tx = ConfidentialTransaction.from_hex(pegin_tx) + target_script_pubkey = '' + target_amount = 0 + target_index = 0 + # fee_index = -1 + fee_amount = 0 + has_fee = len(tx.txout_list) == 2 + for index, txout in enumerate(tx.txout_list): + if txout.locking_script: + target_script_pubkey = txout.locking_script + target_amount = txout.amount + target_index = index + else: + fee_amount = txout.amount + # fee_index = index + # change script pubkey (string replace) + target_script_pubkey = '16' + target_script_pubkey + + fee_addr = test_obj.addr_dic['fee'] + new_script_pubkey = '16' + str(fee_addr.locking_script) + pegin_tx2 = pegin_tx.replace(target_script_pubkey, new_script_pubkey) + tx = ConfidentialTransaction.from_hex(pegin_tx2) + total_amount = target_amount + fee_amount + utxo_amount = 0 + if has_fee: + utxo_amount = total_amount - btc_amount + + # add txout + tx.add_txout(amount=1, + address=test_obj.ct_addr_dic[str(test_obj.addr_dic['main'])], + asset=test_obj.pegged_asset) + + # calc fee + pegin_utxo = ElementsUtxoData( + txid=btc_txid, vout=btc_txout_index, + amount=btc_amount, + descriptor='wpkh({})'.format('02' * 33), # dummy + asset=test_obj.pegged_asset, + is_pegin=True, pegin_btc_tx_size=int(btc_size), + fedpeg_script=test_obj.fedpegscript) + utxo_list = [pegin_utxo] + if utxo_amount > 0: + for txin in tx.txin_list: + if txin.outpoint.txid != btc_txid: + utxo = ElementsUtxoData( + outpoint=txin.outpoint, amount=utxo_amount, + descriptor='', asset=test_obj.pegged_asset) + utxo_list.append(utxo) + break + calc_fee, _, _ = tx.estimate_fee(utxo_list, test_obj.pegged_asset) + # update fee + tx.update_txout_fee_amount(calc_fee) + + # change amount + new_amount = total_amount - calc_fee - 1 + tx.update_txout_amount(target_index, new_amount) + + # blind + fee_ct_addr = test_obj.ct_addr_dic[str(fee_addr)] + tx.blind_txout(utxo_list, + confidential_address_list=[fee_ct_addr]) + return str(tx) + + +def test_generate_btc(test_obj): + # generatetoaddress -> fee address + print(test_obj.addr_dic) + btc_rpc = test_obj.btcConn.get_rpc() + + addr = str(test_obj.addr_dic['btc']) + btc_rpc.generatetoaddress(100, addr) + btc_rpc.generatetoaddress(5, addr) + time.sleep(2) + resp = get_utxo(btc_rpc, [addr]) + print(resp) + + +def test_pegin(test_obj): + btc_rpc = test_obj.btcConn.get_rpc() + elm_rpc = test_obj.elmConn.get_rpc() + + # generate pegin address by RPC + pegin_addr_info = elm_rpc.getpeginaddress() + pegin_address = pegin_addr_info['mainchain_address'] + claim_script = pegin_addr_info['claim_script'] + + for i in range(3): + try: + # send bitcoin + utxos = get_utxo(btc_rpc, []) + amount = 0 + for utxo in utxos: + amount += utxo['amount'] + amount -= 1 + if amount > 100: + amount = 100 + txid = btc_rpc.sendtoaddress(pegin_address, amount) + + # generate bitcoin 100 block + addr = str(test_obj.addr_dic['btc']) + btc_rpc.generatetoaddress(101, addr) + + # pegin transaction for fee address + tx_data = btc_rpc.gettransaction(txid)['hex'] + txout_proof = btc_rpc.gettxoutproof([txid]) + pegin_tx = elm_rpc.createrawpegin( + tx_data, txout_proof, claim_script)['hex'] + pegin_tx = update_pegin_tx( + test_obj, pegin_tx, tx_data, pegin_address) + pegin_tx = elm_rpc.signrawtransactionwithwallet(pegin_tx)['hex'] + # broadcast + print(ConfidentialTransaction.parse_to_json( + pegin_tx, network=NETWORK)) + txid = elm_rpc.sendrawtransaction(pegin_tx) + test_obj.tx_dic[txid] = pegin_tx + # generatetoaddress -> gen address + addr = str(test_obj.addr_dic['gen']) + elm_rpc.generatetoaddress(2, addr) + time.sleep(2) + except Exception as err: + print('Exception({})'.format(i)) + raise err + + # generatetoaddress -> gen address + addr = str(test_obj.addr_dic['gen']) + elm_rpc.generatetoaddress(100, addr) + elm_rpc.generatetoaddress(5, addr) + time.sleep(2) + fee_addr = test_obj.addr_dic['fee'] + utxos = get_utxo(elm_rpc, [str(fee_addr)]) + # utxos = get_utxo(elm_rpc, []) + print('UTXO: {}'.format(utxos)) + + +def test_elements_pkh(test_obj): + # btc_rpc = test_obj.btcConn.get_rpc() + elm_rpc = test_obj.elmConn.get_rpc() + # create tx (output wpkh, p2sh-segwit, pkh) + txouts = [ + ConfidentialTxOut( + 100000000, + test_obj.ct_addr_dic[str(test_obj.addr_dic['p2pkh'])], + asset=test_obj.pegged_asset), + ConfidentialTxOut( + 100000000, + test_obj.ct_addr_dic[str(test_obj.addr_dic['p2wpkh'])], + asset=test_obj.pegged_asset), + ConfidentialTxOut( + 100000000, + test_obj.ct_addr_dic[str(test_obj.addr_dic['p2sh-p2wpkh'])], + asset=test_obj.pegged_asset), + ] + tx = ConfidentialTransaction.create(2, 0, [], txouts) + # fundrawtransaction + fee_addr = str(test_obj.addr_dic['fee']) + fee_desc = test_obj.desc_dic[fee_addr] + fee_ct_addr = test_obj.ct_addr_dic[fee_addr] + fee_sk = test_obj.hdwallet.get_privkey(path=FEE_PATH).privkey + utxos = get_utxo(elm_rpc, [fee_addr]) + utxo_list = convert_elements_utxos(test_obj, utxos) + target_list = [TargetAmountData( + amount=1, + asset=test_obj.pegged_asset, + reserved_address=fee_ct_addr)] + tx.fund_raw_transaction([], utxo_list, + target_list=target_list, + fee_asset=test_obj.pegged_asset, + effective_fee_rate=0.1, + knapsack_min_change=1) + # blind + blind_utxo_list = [] + for txin in tx.txin_list: + blind_utxo_list.append(search_utxos( + test_obj, utxo_list, txin.outpoint)) + tx.blind_txout(blind_utxo_list) + # add sign + for txin in tx.txin_list: + utxo = search_utxos(test_obj, utxo_list, txin.outpoint) + tx.sign_with_privkey(txin.outpoint, fee_desc.data.hash_type, fee_sk, + value=utxo.value, + sighashtype=SigHashType.ALL) + # broadcast + print(ConfidentialTransaction.parse_to_json(str(tx), network=NETWORK)) + txid = elm_rpc.sendrawtransaction(str(tx)) + test_obj.tx_dic[txid] = tx + # generate block + elm_rpc.generatetoaddress(2, fee_addr) + time.sleep(2) + + # create tx (output wpkh only, input tx1-3) + txid = tx.txid + txin_list = [] + txin_utxo_list = [] + for index, txout in enumerate(tx.txout_list): + if not txout.locking_script: + continue + temp_addr = str(txout.get_address(network=NETWORK)) + if temp_addr == fee_addr: + continue + txin_list.append(ConfidentialTxIn(txid=txid, vout=index)) + if temp_addr not in test_obj.desc_dic: + test_obj.assertTrue(False, 'addr not found. [{}]:[{}]'.format( + index, temp_addr)) + desc = test_obj.desc_dic[temp_addr] + blind_key = test_obj.blind_key_dic[temp_addr] + unblind_data = tx.unblind_txout(index, blind_key) + txin_utxo_list.append(ElementsUtxoData( + txid=txid, vout=index, + amount=unblind_data.value.amount, + descriptor=desc, + value=txout.value.hex, + asset=test_obj.pegged_asset, + asset_blinder=unblind_data.asset_blinder, + amount_blinder=unblind_data.amount_blinder)) + txouts2 = [ + ConfidentialTxOut( + 300000000, + test_obj.ct_addr_dic[str(test_obj.addr_dic['main'])], + asset=test_obj.pegged_asset), + ] + tx2 = ConfidentialTransaction.create(2, 0, txin_list, txouts2) + main_addr = test_obj.addr_dic['main'] + utxos = get_utxo(elm_rpc, [fee_addr]) + utxo_list = convert_elements_utxos(test_obj, utxos) + target_list = [TargetAmountData( + amount=0, + asset=test_obj.pegged_asset, + reserved_address=fee_ct_addr)] + tx2.fund_raw_transaction(txin_utxo_list, utxo_list, + target_list=target_list, + fee_asset=test_obj.pegged_asset, + effective_fee_rate=0.1, + knapsack_min_change=1) + # blind + join_utxo_list = [] + join_utxo_list[len(join_utxo_list):len(join_utxo_list)] = utxo_list + join_utxo_list[len(join_utxo_list):len(join_utxo_list)] = txin_utxo_list + blind_utxo_list = [] + for txin in tx2.txin_list: + blind_utxo_list.append(search_utxos( + test_obj, join_utxo_list, txin.outpoint)) + tx2.blind_txout(blind_utxo_list) + # add sign + for txin in tx2.txin_list: + utxo = search_utxos(test_obj, blind_utxo_list, txin.outpoint) + path = test_obj.path_dic[str(utxo.descriptor.data.address)] + sk = test_obj.hdwallet.get_privkey(path=path).privkey + tx2.sign_with_privkey(txin.outpoint, utxo.descriptor.data.hash_type, + sk, value=utxo.value, + sighashtype=SigHashType.ALL) + # broadcast + print(ConfidentialTransaction.parse_to_json(str(tx2), network=NETWORK)) + txid = elm_rpc.sendrawtransaction(str(tx2)) + test_obj.tx_dic[txid] = tx2 + # generate block + elm_rpc.generatetoaddress(2, fee_addr) + time.sleep(2) + utxos = get_utxo(elm_rpc, [str(main_addr)]) + print('UTXO: {}'.format(utxos)) + + +def test_elements_multisig(test_obj): + # btc_rpc = test_obj.btcConn.get_rpc() + elm_rpc = test_obj.elmConn.get_rpc() + # create tx (output multisig) + txouts = [ + ConfidentialTxOut( + 100000000, + test_obj.ct_addr_dic[str(test_obj.addr_dic['p2sh'])], + asset=test_obj.pegged_asset), + ConfidentialTxOut( + 100000000, + test_obj.ct_addr_dic[str(test_obj.addr_dic['p2wsh'])], + asset=test_obj.pegged_asset), + ConfidentialTxOut( + 100000000, + test_obj.ct_addr_dic[str(test_obj.addr_dic['p2sh-p2wsh'])], + asset=test_obj.pegged_asset), + ] + tx = ConfidentialTransaction.create(2, 0, [], txouts) + # fundrawtransaction + fee_addr = str(test_obj.addr_dic['fee']) + fee_desc = test_obj.desc_dic[fee_addr] + fee_ct_addr = test_obj.ct_addr_dic[fee_addr] + fee_sk = test_obj.hdwallet.get_privkey(path=FEE_PATH).privkey + utxos = get_utxo(elm_rpc, [fee_addr]) + utxo_list = convert_elements_utxos(test_obj, utxos) + target_list = [TargetAmountData( + amount=1, + asset=test_obj.pegged_asset, + reserved_address=fee_ct_addr)] + tx.fund_raw_transaction([], utxo_list, + fee_asset=test_obj.pegged_asset, + target_list=target_list, + effective_fee_rate=0.1, + knapsack_min_change=1) + # blind + blind_utxo_list = [] + for txin in tx.txin_list: + blind_utxo_list.append(search_utxos( + test_obj, utxo_list, txin.outpoint)) + tx.blind_txout(blind_utxo_list) + # add sign + for txin in tx.txin_list: + utxo = search_utxos(test_obj, utxo_list, txin.outpoint) + tx.sign_with_privkey(txin.outpoint, fee_desc.data.hash_type, fee_sk, + value=utxo.value, + sighashtype=SigHashType.ALL) + # broadcast + print(ConfidentialTransaction.parse_to_json(str(tx), network=NETWORK)) + elm_rpc.sendrawtransaction(str(tx)) + # generate block + elm_rpc.generatetoaddress(2, fee_addr) + time.sleep(2) + + # create tx (output wpkh only, input multisig tx1-3) + txid = tx.txid + txin_list = [] + txin_utxo_list = [] + for index, txout in enumerate(tx.txout_list): + if not txout.locking_script: + continue + temp_addr = str(txout.get_address(network=NETWORK)) + if temp_addr == fee_addr: + continue + txin_list.append(ConfidentialTxIn(txid=txid, vout=index)) + if temp_addr not in test_obj.desc_dic: + test_obj.assertTrue(False, 'addr not found. [{}]:[{}]'.format( + index, temp_addr)) + desc = test_obj.desc_dic[temp_addr] + blind_key = test_obj.blind_key_dic[temp_addr] + unblind_data = tx.unblind_txout(index, blind_key) + txin_utxo_list.append(ElementsUtxoData( + txid=txid, vout=index, + amount=unblind_data.value.amount, + descriptor=desc, + value=txout.value.hex, + asset=test_obj.pegged_asset, + asset_blinder=unblind_data.asset_blinder, + amount_blinder=unblind_data.amount_blinder)) + txouts2 = [ + ConfidentialTxOut( + 300000000, + test_obj.ct_addr_dic[str(test_obj.addr_dic['main'])], + asset=test_obj.pegged_asset), + ] + tx2 = ConfidentialTransaction.create(2, 0, txin_list, txouts2) + main_addr = test_obj.addr_dic['main'] + utxos = get_utxo(elm_rpc, [fee_addr]) + utxo_list = convert_elements_utxos(test_obj, utxos) + target_list = [TargetAmountData( + amount=0, + asset=test_obj.pegged_asset, + reserved_address=fee_ct_addr)] + tx2.fund_raw_transaction(txin_utxo_list, utxo_list, + fee_asset=test_obj.pegged_asset, + target_list=target_list, + effective_fee_rate=0.1, + knapsack_min_change=1) + # blind + join_utxo_list = [] + join_utxo_list[len(join_utxo_list):len(join_utxo_list)] = utxo_list + join_utxo_list[len(join_utxo_list):len(join_utxo_list)] = txin_utxo_list + blind_utxo_list = [] + for txin in tx2.txin_list: + blind_utxo_list.append(search_utxos( + test_obj, join_utxo_list, txin.outpoint)) + tx2.blind_txout(blind_utxo_list) + + def multisig_sign(tx_obj, utxo, path_list): + sighash = tx_obj.get_sighash( + outpoint=utxo.outpoint, + hash_type=utxo.descriptor.data.hash_type, + value=utxo.value, + redeem_script=utxo.descriptor.data.redeem_script) + signature_list = [] + for path in path_list: + sk = test_obj.hdwallet.get_privkey(path=path).privkey + sig = sk.calculate_ec_signature(sighash) + sig.related_pubkey = sk.pubkey + signature_list.append(sig) + if len(signature_list) == 2: + break + tx_obj.add_multisig_sign( + utxo.outpoint, utxo.descriptor.data.hash_type, + utxo.descriptor.data.redeem_script, signature_list) + + # add sign + join_utxo_list = [] + join_utxo_list[len(join_utxo_list):len(join_utxo_list)] = utxo_list + join_utxo_list[len(join_utxo_list):len(join_utxo_list)] = txin_utxo_list + for index, txin in enumerate(tx2.txin_list): + utxo = search_utxos(test_obj, join_utxo_list, txin.outpoint) + if not utxo.descriptor.data.redeem_script: + path = test_obj.path_dic[str(utxo.descriptor.data.address)] + sk = test_obj.hdwallet.get_privkey(path=path).privkey + tx2.sign_with_privkey(txin.outpoint, + utxo.descriptor.data.hash_type, + sk, value=utxo.value, + sighashtype=SigHashType.ALL) + else: + path_list = test_obj.path_dic[str(utxo.descriptor.data.address)] + multisig_sign(tx2, utxo, path_list) + # broadcast + print(ConfidentialTransaction.parse_to_json(str(tx2), network=NETWORK)) + elm_rpc.sendrawtransaction(str(tx2)) + # generate block + elm_rpc.generatetoaddress(2, fee_addr) + time.sleep(2) + utxos = get_utxo(elm_rpc, [str(main_addr)]) + print('UTXO: {}'.format(utxos)) + + +class TestElements(unittest.TestCase): + def setUp(self): + logging.basicConfig() + logging.getLogger("BitcoinRPC").setLevel(logging.DEBUG) + + # FIXME get connection from config file. + + self.path_dic = {} + self.addr_dic = {} + self.desc_dic = {} + self.master_blinding_key = '' + self.ct_addr_dic = {} + self.blind_key_dic = {} + self.tx_dic = {} + self.sidechaininfo = {} + self.pegged_asset = '' + self.fedpegscript = '' + self.parent_blockhash = '' + self.pegin_confirmation_depth = 0 + + self.hdwallet = HDWallet.from_mnemonic( + MNEMONIC, passphrase=PASSPHRASE, network=MAINCHAIN_NETWORK) + create_bitcoin_address(self) + self.btcConn = RpcWrapper( + port=18443, rpc_user='bitcoinrpc', rpc_password='password') + self.elmConn = RpcWrapper( + port=18445, rpc_user='elementsrpc', rpc_password='password') + + def test_elements(self): + ''' + To execute sequentially, define only one test + and call the test function in it. + ''' + get_elements_config(self) + test_import_address(self) + test_generate_btc(self) + test_pegin(self) + test_elements_pkh(self) + test_elements_multisig(self) + # issue on RPC + # reissue + # send multi asset + # destroy amount + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/data/descriptor_test.json b/tests/data/descriptor_test.json index 4cf0ca1..c746196 100644 --- a/tests/data/descriptor_test.json +++ b/tests/data/descriptor_test.json @@ -112,7 +112,6 @@ "address": "3LKyvRN6SmYXGBNn8fcQvYxW9MGKtwcinN", "lockingScript": "a914cc6ffbc0bf31af759451068f90ba7a0272b6b33287", "hashType": "p2sh-p2wpkh", - "redeemScript": "00147fda9cf020c16cacf529c87d8de89bfc70b8c9cb", "includeMultisig": false, "scripts": [ { @@ -192,7 +191,7 @@ "address": "39XGHYpYmJV9sGFoGHZeU2rLkY6r1MJ6C1", "lockingScript": "a91455e8d5e8ee4f3604aba23c71c2684fa0a56a3a1287", "hashType": "p2sh-p2wsh", - "redeemScript": "0020fc5acc302aab97f821f9a61e1cc572e7968a603551e95d4ba12b51df6581482f", + "redeemScript": "76a914c42e7ef92fdb603af844d064faad95db9bcdfd3d88ac", "includeMultisig": false, "scripts": [ { @@ -374,7 +373,7 @@ "address": "3Hd7YQStg9gYpEt6hgK14ZHUABxSURzeuQ", "lockingScript": "a914aec509e284f909f769bb7dda299a717c87cc97ac87", "hashType": "p2sh-p2wsh", - "redeemScript": "0020ef8110fa7ddefb3e2d02b2c1b1480389b4bc93f606281570cfc20dba18066aee", + "redeemScript": "512103f28773c2d975288bc7d1d205c3748651b075fbc6610e58cddeeddf8f19405aa82103499fdf9e895e719cfd64e67f07d38e3226aa7b63678949e6e49b241a60e823e42102d7924d4f7d43ea965a465ae3095ff41131e5946f3c85f79e44adbcf8e27e080e53ae", "includeMultisig": true, "scripts": [ { @@ -589,7 +588,7 @@ "case": "p2sh miniscript", "request": { "isElements": false, - "descriptor": "sh(or_d(sha256(38df1c1f64a24a77b23393bca50dff872e31edc4f3b5aa3b90ad0b82f4f089b6},and_n(un:after(499999999},older(4194305))))", + "descriptor": "sh(or_d(sha256(38df1c1f64a24a77b23393bca50dff872e31edc4f3b5aa3b90ad0b82f4f089b6),and_n(un:after(499999999),older(4194305))))", "network": "mainnet" }, "expect": { @@ -607,17 +606,13 @@ "redeemScript": "82012088a82038df1c1f64a24a77b23393bca50dff872e31edc4f3b5aa3b90ad0b82f4f089b68773646304ff64cd1db19267006864006703010040b26868" } ] - }, - "exclude": [ - "python" - ], - "todo": "check current test case." + } }, { "case": "p2wsh miniscript", "request": { "isElements": false, - "descriptor": "wsh(thresh(2,multi(2,03a0434d9e47f3c86235477c7b1ae6ae5d3442d49b1943c2b752a68e2a47e247c7,036d2b085e9e382ed10b69fc311a03f8641ccfff21574de0927513a49d9a688a00},a:multi(1,036d2b085e9e382ed10b69fc311a03f8641ccfff21574de0927513a49d9a688a00},ac:pk_k(022f01e5e15cca351daff3843fb70f3c2f0a1bdd05e5af888a67784ef3e10a2a01)))", + "descriptor": "wsh(thresh(2,multi(2,03a0434d9e47f3c86235477c7b1ae6ae5d3442d49b1943c2b752a68e2a47e247c7,036d2b085e9e382ed10b69fc311a03f8641ccfff21574de0927513a49d9a688a00),a:multi(1,036d2b085e9e382ed10b69fc311a03f8641ccfff21574de0927513a49d9a688a00),ac:pk_k(022f01e5e15cca351daff3843fb70f3c2f0a1bdd05e5af888a67784ef3e10a2a01)))", "network": "mainnet" }, "expect": { @@ -636,17 +631,13 @@ "redeemScript": "522103a0434d9e47f3c86235477c7b1ae6ae5d3442d49b1943c2b752a68e2a47e247c721036d2b085e9e382ed10b69fc311a03f8641ccfff21574de0927513a49d9a688a0052ae6b5121036d2b085e9e382ed10b69fc311a03f8641ccfff21574de0927513a49d9a688a0051ae6c936b21022f01e5e15cca351daff3843fb70f3c2f0a1bdd05e5af888a67784ef3e10a2a01ac6c935287" } ] - }, - "exclude": [ - "python" - ], - "todo": "check current test case." + } }, { "case": "p2sh-p2wsh miniscript", "request": { "isElements": false, - "descriptor": "sh(wsh(c:or_i(andor(c:pk_h(xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB/1/0/*},pk_h(xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH/0/0/*},pk_h(02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5)},pk_k(02d7924d4f7d43ea965a465ae3095ff41131e5946f3c85f79e44adbcf8e27e080e))))", + "descriptor": "sh(wsh(c:or_i(andor(c:pk_h(xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB/1/0/*),pk_h(xpub69H7F5d8KSRgmmdJg2KhpAK8SR3DjMwAdkxj3ZuxV27CprR9LgpeyGmXUbC6wb7ERfvrnKZjXoUmmDznezpbZb7ap6r1D3tgFxHmwMkQTPH/0/0/*),pk_h(02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5)),pk_k(02d7924d4f7d43ea965a465ae3095ff41131e5946f3c85f79e44adbcf8e27e080e))))", "bip32DerivationPath": "44", "network": "mainnet" }, @@ -655,7 +646,7 @@ "address": "3GyYN9WnJBoMn8M5tuqVcFJq1BvbAcdPAt", "lockingScript": "a914a7a9f411001e3e3db96d7f02fc9ab1d0dc6aa69187", "hashType": "p2sh-p2wsh", - "redeemScript": "0020e29b7f3e543d581c99c92b59d45218b008b82c2d406bba3c7384d52e568124aa", + "redeemScript": "6376a914520e6e72bcd5b616bc744092139bd759c31d6bbe88ac6476a91406afd46bcdfd22ef94ac122aa11f241244a37ecc886776a9145ab62f0be26fe9d6205a155403f33e2ad2d31efe8868672102d7924d4f7d43ea965a465ae3095ff41131e5946f3c85f79e44adbcf8e27e080e68ac", "includeMultisig": false, "scripts": [ { @@ -673,11 +664,7 @@ "redeemScript": "6376a914520e6e72bcd5b616bc744092139bd759c31d6bbe88ac6476a91406afd46bcdfd22ef94ac122aa11f241244a37ecc886776a9145ab62f0be26fe9d6205a155403f33e2ad2d31efe8868672102d7924d4f7d43ea965a465ae3095ff41131e5946f3c85f79e44adbcf8e27e080e68ac" } ] - }, - "exclude": [ - "python" - ], - "todo": "check current test case." + } }, { "case": "Elements pkh liquidv1", @@ -795,7 +782,6 @@ "address": "XCEs4H1JSJ7ezGVRmkLeptqgvzPQpwLYHZ", "lockingScript": "a91409c370a0373e00cf7e3dc6bb2f37f9fb881baff487", "hashType": "p2sh-p2wpkh", - "redeemScript": "0014d1495dfca98f5d205b683a4eea1b10c7e29c1c92", "includeMultisig": false, "scripts": [ { @@ -1356,20 +1342,20 @@ } }, { - "case": "pkh hdkey-derive", + "case": "pkh hdkey-derive2", "request": { "scriptType": "p2pkh", "keyInfoList": [ { "parentExtkey": "tprv8ZgxMBicQKsPfFfgL33JxxEMtuXMCaUxXqetSSSVcsFcbsYzrDAw5SUG8UStm8G86cxBUANpv2kpEsB4GMEG6NfLVRZGzZCRLQrr8deFcfZ", - "keyPathFromParent": "0\"/1\"", + "keyPathFromParent": "0'/1'", "key": "tpubDBa2ey4mzydY8ZLZx3LR96vdTdVgnMXGDwQ8PDjkQNeRq82JRXGY8JLyiAKbidjJPLUXmSbJkcTWrPf89MkCGvhCYf8vUpJLPfmx3hyqC15/1/*" } ], "requireNum": 0 }, "expect": { - "descriptor": "pkh([00000000/0\"/1\"]tpubDBa2ey4mzydY8ZLZx3LR96vdTdVgnMXGDwQ8PDjkQNeRq82JRXGY8JLyiAKbidjJPLUXmSbJkcTWrPf89MkCGvhCYf8vUpJLPfmx3hyqC15/1/*)#y3xv4kyv" + "descriptor": "pkh([00000000/0'/1']tpubDBa2ey4mzydY8ZLZx3LR96vdTdVgnMXGDwQ8PDjkQNeRq82JRXGY8JLyiAKbidjJPLUXmSbJkcTWrPf89MkCGvhCYf8vUpJLPfmx3hyqC15/1/*)#y3xv4kyv" } }, { diff --git a/tests/data/elements_coin_test.json b/tests/data/elements_coin_test.json index c010fab..14c1755 100644 --- a/tests/data/elements_coin_test.json +++ b/tests/data/elements_coin_test.json @@ -64,10 +64,7 @@ ], "feeAmount": 3320, "utxoFeeAmount": 1820 - }, - "exclude": [ - "python" - ] + } }, { "case": "ElementsSelectCoinsTest03", diff --git a/tests/test_confidential_transaction.py b/tests/test_confidential_transaction.py index 87cca9d..1a80a40 100644 --- a/tests/test_confidential_transaction.py +++ b/tests/test_confidential_transaction.py @@ -1,3 +1,4 @@ +import json from unittest import TestCase from tests.util import load_json_file, get_json_file, exec_test,\ assert_equal, assert_error, assert_match, assert_message @@ -9,8 +10,8 @@ from cfd.transaction import OutPoint, TxIn from cfd.confidential_transaction import ConfidentialTxOut,\ ConfidentialTransaction, ElementsUtxoData, IssuanceKeyPair,\ - TargetAmountData, Issuance, UnblindData -import json + TargetAmountData, Issuance, UnblindData,\ + IssuanceAssetBlindData, IssuanceTokenBlindData def test_ct_transaction_func1(obj, name, case, req, exp, error): @@ -437,23 +438,29 @@ def test_ct_transaction_func4(obj, name, case, req, exp, error): for output in req.get('txouts', []): txout_map[str(output['index'])] = output['confidentialKey'] if issuance_key_map: - resp.blind(utxo_list, - issuance_key_map=issuance_key_map, - confidential_address_list=ct_addr_list, - direct_confidential_key_map=txout_map, - minimum_range_value=req.get('minimumRangeValue', 1), - exponent=req.get('exponent', 0), - minimum_bits=req.get('minimumBits', -1)) + blinder_list = resp.blind( + utxo_list, + issuance_key_map=issuance_key_map, + confidential_address_list=ct_addr_list, + direct_confidential_key_map=txout_map, + minimum_range_value=req.get('minimumRangeValue', 1), + exponent=req.get('exponent', 0), + minimum_bits=req.get('minimumBits', -1), + collect_blinder=True) else: - resp.blind_txout(utxo_list, - confidential_address_list=ct_addr_list, - direct_confidential_key_map=txout_map, - minimum_range_value=req.get( - 'minimumRangeValue', 1), - exponent=req.get('exponent', 0), - minimum_bits=req.get('minimumBits', -1)) - - resp = {'size': resp.size, 'vsize': resp.vsize} + blinder_list = resp.blind_txout( + utxo_list, + confidential_address_list=ct_addr_list, + direct_confidential_key_map=txout_map, + minimum_range_value=req.get( + 'minimumRangeValue', 1), + exponent=req.get('exponent', 0), + minimum_bits=req.get('minimumBits', -1), + collect_blinder=True) + + resp = {'size': resp.size, 'vsize': resp.vsize, + 'tx': resp, 'blinder_list': blinder_list, + 'req_output': req.get('txouts', [])} else: return False assert_error(obj, name, case, error) @@ -536,6 +543,63 @@ def test_ct_transaction_func4(obj, name, case, req, exp, error): obj.assertEqual(exp['maxVsize'], resp['vsize'], 'Fail: {}:{}:{}'.format( name, case, 'maxVsize')) + txout_list = resp['req_output'] + tx = resp['tx'] + blinding_keys = exp.get('blindingKeys', []) + issuance_list = exp.get('issuanceList', []) + txout_index_list = [] + for index, txout in enumerate(tx.txout_list): + if txout.value.has_blind(): + txout_index_list.append(index) + for blind_index, blinder in enumerate(resp['blinder_list']): + is_find = False + has_asset = isinstance(blinder, IssuanceAssetBlindData) + has_token = isinstance(blinder, IssuanceTokenBlindData) + if has_asset or has_token: + for exp_issuance in issuance_list: + outpoint = OutPoint( + exp_issuance['txid'], exp_issuance['vout']) + if outpoint == blinder.outpoint: + data = tx.unblind_issuance( + blinder.vout, + exp_issuance['assetBlindingKey'], + exp_issuance.get('tokenBlindingKey', '')) + is_find = True + data = data[0] if has_asset else data[1] + break + else: + for index, txout in enumerate(txout_list): + if txout['index'] == blinder.vout: + is_find = True + data = tx.unblind_txout( + blinder.vout, blinding_keys[index]) + break + if not is_find: + for index, txout_index in enumerate(txout_index_list): + if txout_index == blinder.vout: + is_find = True + data = tx.unblind_txout( + blinder.vout, blinding_keys[index]) + break + obj.assertEqual( + True, is_find, + f'Fail: {name}:{case}:blind_index:{blind_index}') + if is_find: + obj.assertEqual( + str(data.asset), str(blinder.asset), + f'Fail: {name}:{case}:asset:{blind_index}') + obj.assertEqual( + data.value.amount, blinder.value.amount, + f'Fail: {name}:{case}:value:{blind_index}') + obj.assertEqual( + str(data.asset_blinder), + str(blinder.asset_blinder), + f'Fail: {name}:{case}:asset_blinder:' + + f'{blind_index}') + obj.assertEqual( + str(data.amount_blinder), + str(blinder.amount_blinder), + f'Fail: {name}:{case}:blinder:{blind_index}') else: assert_equal(obj, name, case, exp, str(resp), 'blindingKey') @@ -560,6 +624,126 @@ def test_ct_transaction_func(obj, name, case, req, exp, error): raise Exception('unknown name: ' + name) +def test_parse_tx_func(obj, name, case, req, exp, error): + try: + ignore_list = ['empty hex string', 'invalid hex string(3 chars)', + 'invalid hex format', 'invalid elements network type'] + if case in ignore_list: + return # ignore testcase + + resp = ConfidentialTransaction.from_hex(req['hex']) + assert_error(obj, name, case, error) + + exp_vin_list = exp.get('vin', []) + exp_vout_list = exp.get('vout', []) + assert_match(obj, name, case, len(exp_vin_list), + len(resp.txin_list), 'vin.len') + assert_match(obj, name, case, len(exp_vout_list), + len(resp.txout_list), 'vout.len') + empty_32byte = '00' * 32 + for index, txin in enumerate(resp.txin_list): + exp_txin = exp_vin_list[index] + if 'coinbase' in exp_txin: + assert_match(obj, name, case, empty_32byte, + str(txin.outpoint.txid), 'txin.txid') + assert_match(obj, name, case, 0xffffffff, + txin.outpoint.vout, 'txin.vout') + assert_match(obj, name, case, exp_txin['coinbase'], + str(txin.script_sig), 'txin.coinbase') + else: + assert_match(obj, name, case, exp_txin['txid'], + str(txin.outpoint.txid), 'txin.txid') + assert_match(obj, name, case, exp_txin['vout'], + txin.outpoint.vout, 'txin.vout') + assert_match(obj, name, case, exp_txin['scriptSig']['hex'], + str(txin.script_sig), 'txin.scriptSig') + + assert_match(obj, name, case, exp_txin['sequence'], + txin.sequence, 'txin.sequence') + assert_match(obj, name, case, len(exp_txin.get('txinwitness', [])), + len(txin.witness_stack), 'txin.witness_stack.length') + for idx, stack in enumerate(txin.witness_stack): + assert_match(obj, name, case, exp_txin['txinwitness'][idx], + str(stack), f'txin.witness_stack[{idx}]') + is_pegin = True if len(txin.pegin_witness_stack) else False + assert_match(obj, name, case, exp_txin.get('is_pegin', False), + is_pegin, 'is_pegin') + if is_pegin: + assert_match(obj, name, case, len(exp_txin['pegin_witness']), + len(txin.pegin_witness_stack), + 'txin.pegin_witness_stack.length') + for idx, stack in enumerate(txin.pegin_witness_stack): + assert_match(obj, name, case, + exp_txin['pegin_witness'][idx], + str(stack), + f'txin.pegin_witness_stack[{idx}]') + is_issuance = False + if txin.issuance.asset_value.has_blind() or ( + txin.issuance.asset_value.amount > 0): + is_issuance = True + exp_is_issuance = True if 'issuance' in exp_txin else False + assert_match(obj, name, case, exp_is_issuance, + is_issuance, 'is_issuance') + if is_issuance: + exp_issuance = exp_txin['issuance'] + # assert_match(obj, name, case, + # exp_issuance['assetEntropy'], + # str(txin.issuance.entropy), + # 'txin.issuance.assetEntropy') + assert_match(obj, name, case, + exp_issuance['assetBlindingNonce'], + str(txin.issuance.nonce), + 'txin.issuance.assetBlindingNonce') + if 'assetamountcommitment' in exp_issuance: + assert_match(obj, name, case, + exp_issuance['assetamountcommitment'], + str(txin.issuance.asset_value.hex), + 'txin.issuance.assetamountcommitment') + else: + assert_match(obj, name, case, + exp_issuance['assetamount'], + txin.issuance.asset_value.amount, + 'txin.issuance.assetamount') + if 'tokenamountcommitment' in exp_issuance: + assert_match(obj, name, case, + exp_issuance['tokenamountcommitment'], + str(txin.issuance.token_value.hex), + 'txin.issuance.tokenamountcommitment') + elif 'tokenamount' in exp_issuance: + assert_match(obj, name, case, + exp_issuance['tokenamount'], + txin.issuance.token_value.amount, + 'txin.issuance.tokenamount') + + for index, txout in enumerate(resp.txout_list): + exp_txout = exp_vout_list[index] + if 'valuecommitment' in exp_txout: + assert_match(obj, name, case, exp_txout['valuecommitment'], + str(txout.value), 'txout.valuecommitment') + assert_match(obj, name, case, exp_txout['assetcommitment'], + str(txout.asset), 'txout.assetcommitment') + assert_match(obj, name, case, exp_txout['commitmentnonce'], + str(txout.nonce), 'txout.commitmentnonce') + else: + assert_match(obj, name, case, exp_txout['value'], + txout.value.amount, 'txout.value') + assert_match(obj, name, case, exp_txout['asset'], + str(txout.asset), 'txout.asset') + assert_match(obj, name, case, exp_txout['commitmentnonce'], + str(txout.nonce), 'txout.commitmentnonce') + + assert_match(obj, name, case, + exp_txout['scriptPubKey']['hex'], + str(txout.locking_script), 'txout.locking_script') + + except CfdError as err: + if not error: + print('{}:{} req={}'.format(name, case, req)) + raise err + assert_equal(obj, name, case, exp, err.message) + return True + + def test_elements_tx_func(obj, name, case, req, exp, error): try: if name == 'Elements.CoinSelection': @@ -726,6 +910,9 @@ def test_confidential_transaction(self): def test_elements_tx(self): exec_test(self, 'Elements', test_elements_tx_func) + def test_confidential_tx_parse(self): + exec_test(self, 'ConfidentialTransaction.Decode', test_parse_tx_func) + def test_string(self): asset = '0000000000000000000000000000000000000000000000000000000000000001' # noqa: E501 asset_blinder = '0000000000000000000000000000000000000000000000000000000000000002' # noqa: E501 @@ -743,3 +930,40 @@ def test_string(self): key_pair = IssuanceKeyPair() self.assertEqual('IssuanceKeyPair', str(key_pair)) + + def test_parse_unblind_tx(self): + tx_hex = '020000000001319bff5f4311e6255ecf4dd472650a6ef85fde7d11cd10d3e6ba5974174aeb560100000000ffffffff0201f38611eb688e6fcd06f25e2faf52b9f98364dc14c379ab085f1b57d56b4b1a6f0100000bd2cc1584c002deb65cc52301e1622f482a2f588b9800d2b8386ffabf74d6b2d73d17503a2f921976a9146a98a3f2935718df72518c00768ec67c589e0b2888ac01f38611eb688e6fcd06f25e2faf52b9f98364dc14c379ab085f1b57d56b4b1a6f0100000000004c4b40000000000000' # noqa: E501 + tx = ConfidentialTransaction.from_hex(tx_hex) + + self.assertEqual(2, len(tx.txout_list)) + self.assertEqual( + '6f1a4b6bd5571b5f08ab79c314dc6483f9b952af2f5ef206cd6f8e68eb1186f3', + str(tx.txout_list[0].asset)) + self.assertEqual( + 12999995000000, + tx.txout_list[0].value.amount) + self.assertEqual( + '0100000bd2cc1584c0', + tx.txout_list[0].value.hex) + self.assertEqual( + '02deb65cc52301e1622f482a2f588b9800d2b8386ffabf74d6b2d73d17503a2f92', # noqa: E501 + str(tx.txout_list[0].nonce)) + self.assertEqual( + '6f1a4b6bd5571b5f08ab79c314dc6483f9b952af2f5ef206cd6f8e68eb1186f3', + str(tx.txout_list[1].asset)) + self.assertEqual( + 5000000, + tx.txout_list[1].value.amount) + self.assertEqual( + '0100000000004c4b40', + tx.txout_list[1].value.hex) + self.assertEqual( + '', + str(tx.txout_list[1].nonce)) + + self.assertEqual(1, tx.get_txout_fee_index()) + self.assertEqual('Q6z1cAcrPxMCnsjAUjSgyT2DrSqRR6KZMr', + str(tx.txout_list[0].get_address())) + self.assertEqual( + 'VTpz4UGuFrPeMdFvW6dzq1vH3ZumciG6jmGnCUidgqsY5RHRxbGfLjndgUjzECCzQnNwAGoP8ohYdHXv', # noqa: E501 + str(tx.txout_list[0].get_address(is_confidential=True))) diff --git a/tests/test_descriptor.py b/tests/test_descriptor.py index e55120c..26a5261 100644 --- a/tests/test_descriptor.py +++ b/tests/test_descriptor.py @@ -38,28 +38,33 @@ def test_descriptor_func(obj, name, case, req, exp, error): def check_keys(target, exp, depth, index=-1): assert_equal(obj, name, case, exp, - target.key_type.as_str(), + target.key_type.as_str(), 'keyType', 'keyType:{}:{}'.format(depth, index)) assert_equal(obj, name, case, exp, - str(target), + str(target), 'key', 'key:{}:{}'.format(depth, index)) def check_descriptor_item(data, exp, depth=-1): assert_equal(obj, name, case, exp, data.script_type.as_str(), - 'type:{}'.format(depth)) + 'type', 'type:{}'.format(depth)) assert_equal(obj, name, case, exp, - data.address, 'address:{}'.format(depth)) + data.address, 'address', + 'address:{}'.format(depth)) assert_equal(obj, name, case, exp, - data.depth, 'depth:{}'.format(depth)) + data.depth, 'depth', 'depth:{}'.format(depth)) assert_equal(obj, name, case, exp, - data.hash_type, 'hashType:{}'.format(depth)) + data.hash_type, 'hashType', + 'hashType:{}'.format(depth)) assert_equal(obj, name, case, exp, - data.redeem_script, - 'lockingScript:{}'.format(depth)) + str(data.locking_script), + 'lockingScript', 'lockingScript:{}'.format(depth)) + assert_equal(obj, name, case, exp, + str(data.redeem_script), + 'redeemScript', 'redeemScript:{}'.format(depth)) assert_equal(obj, name, case, exp, data.multisig_require_num, - 'reqNum:{}'.format(depth)) + 'reqNum', 'reqNum:{}'.format(depth)) if data.key_data is not None: check_keys(data.key_data, exp, depth) diff --git a/tests/util.py b/tests/util.py index c52680e..3f67e46 100644 --- a/tests/util.py +++ b/tests/util.py @@ -59,7 +59,8 @@ def exec_test(test_obj, test_name, test_func): raise e -def assert_equal(test_obj, test_name, case, expect, value, param_name=''): +def assert_equal(test_obj, test_name, case, expect, value, + param_name='', log_name=''): if isinstance(value, bool) or isinstance(value, int): _value = value else: @@ -73,9 +74,10 @@ def assert_equal(test_obj, test_name, case, expect, value, param_name=''): err_msg, _value, 'Fail: {}:{}'.format(test_name, case)) elif param_name in expect: + fail_param_name = log_name if log_name else param_name test_obj.assertEqual( expect[param_name], _value, - 'Fail: {}:{}:{}'.format(test_name, case, param_name)) + 'Fail: {}:{}:{}'.format(test_name, case, fail_param_name)) def assert_match(test_obj, test_name, case, expect, value, param_name): diff --git a/tools/build_for_docker.sh b/tools/build_for_docker.sh new file mode 100755 index 0000000..f5c2462 --- /dev/null +++ b/tools/build_for_docker.sh @@ -0,0 +1,67 @@ +#!/bin/sh + +if [ -n "$CFD_SRC" ]; then +BASEDIR=$CFD_SRC +else +BASEDIR=`git rev-parse --show-toplevel` +fi + +if [ -z "$BASEDIR" ]; then +exit 1 +fi + +cd $BASEDIR + +if [ -n "$CFD_WORK" ]; then +WORKDIR=$CFD_WORK +else +WORKDIR=temp +fi + +echo "BASEDIR=$BASEDIR" +echo "WORKDIR=$WORKDIR" + +if [ -z "$WORKDIR" ]; then +exit 1 +fi + +rm -rf $WORKDIR/* +mkdir $WORKDIR +mkdir $WORKDIR/external +mkdir $WORKDIR/dist + +cp CMakeLists.txt $WORKDIR/ +cp VERSION $WORKDIR/ +cp LICENSE $WORKDIR/ +cp setup.* $WORKDIR/ +cp *.toml $WORKDIR/ +cp *.in $WORKDIR/ +cp *.md $WORKDIR/ +cp -rp cmake $WORKDIR/cmake +cp -rp external/CMakeLists.txt $WORKDIR/external +cp -rp external/template_CMakeLists.txt.in $WORKDIR/external +cp -rp local_resource $WORKDIR/local_resource +cp -rp cfd $WORKDIR/cfd +cp -rp tools $WORKDIR/tools +cp -rp tests $WORKDIR/tests +cp -rp tests $WORKDIR/tests + +cd $WORKDIR +if [ $? -gt 0 ]; then + echo "change directory NG." + exit 1 +fi + +echo "configure start." + +pip3 wheel . +if [ $? -gt 0 ]; then + echo "cmake build NG." + exit 1 +fi + +VER=`cat VERSION` + +mv ./*.whl $BASEDIR/cfd-$VER-py3-none-linux_x86_64.whl +rm -rf $BASEDIR/integration_test/*.whl +cp $BASEDIR/cfd-$VER-py3-none-linux_x86_64.whl $BASEDIR/integration_test/cfd-$VER-py3-none-linux_x86_64.whl