mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-08 12:19:24 +00:00
Compare commits
1 Commits
polkadot-s
...
tables
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f029471f9f |
5
.gitattributes
vendored
5
.gitattributes
vendored
@@ -1,5 +0,0 @@
|
|||||||
# Auto detect text files and perform LF normalization
|
|
||||||
* text=auto
|
|
||||||
* text eol=lf
|
|
||||||
|
|
||||||
*.pdf binary
|
|
||||||
21
.github/actions/LICENSE
vendored
21
.github/actions/LICENSE
vendored
@@ -1,21 +0,0 @@
|
|||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2022-2023 Luke Parker
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
47
.github/actions/bitcoin/action.yml
vendored
47
.github/actions/bitcoin/action.yml
vendored
@@ -1,47 +0,0 @@
|
|||||||
name: bitcoin-regtest
|
|
||||||
description: Spawns a regtest Bitcoin daemon
|
|
||||||
|
|
||||||
inputs:
|
|
||||||
version:
|
|
||||||
description: "Version to download and run"
|
|
||||||
required: false
|
|
||||||
default: 24.0.1
|
|
||||||
|
|
||||||
runs:
|
|
||||||
using: "composite"
|
|
||||||
steps:
|
|
||||||
- name: Bitcoin Daemon Cache
|
|
||||||
id: cache-bitcoind
|
|
||||||
uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84
|
|
||||||
with:
|
|
||||||
path: bitcoin.tar.gz
|
|
||||||
key: bitcoind-${{ runner.os }}-${{ runner.arch }}-${{ inputs.version }}
|
|
||||||
|
|
||||||
- name: Download the Bitcoin Daemon
|
|
||||||
if: steps.cache-bitcoind.outputs.cache-hit != 'true'
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
RUNNER_OS=linux
|
|
||||||
RUNNER_ARCH=x86_64
|
|
||||||
FILE=bitcoin-${{ inputs.version }}-$RUNNER_ARCH-$RUNNER_OS-gnu.tar.gz
|
|
||||||
|
|
||||||
wget https://bitcoincore.org/bin/bitcoin-core-${{ inputs.version }}/$FILE
|
|
||||||
mv $FILE bitcoin.tar.gz
|
|
||||||
|
|
||||||
- name: Extract the Bitcoin Daemon
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
tar xzvf bitcoin.tar.gz
|
|
||||||
cd bitcoin-${{ inputs.version }}
|
|
||||||
sudo mv bin/* /bin && sudo mv lib/* /lib
|
|
||||||
|
|
||||||
- name: Bitcoin Regtest Daemon
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
RPC_USER=serai
|
|
||||||
RPC_PASS=seraidex
|
|
||||||
|
|
||||||
bitcoind -txindex -regtest \
|
|
||||||
-rpcuser=$RPC_USER -rpcpassword=$RPC_PASS \
|
|
||||||
-rpcbind=127.0.0.1 -rpcbind=$(hostname) -rpcallowip=0.0.0.0/0 \
|
|
||||||
-daemon
|
|
||||||
41
.github/actions/build-dependencies/action.yml
vendored
41
.github/actions/build-dependencies/action.yml
vendored
@@ -1,41 +0,0 @@
|
|||||||
name: build-dependencies
|
|
||||||
description: Installs build dependencies for Serai
|
|
||||||
|
|
||||||
inputs:
|
|
||||||
github-token:
|
|
||||||
description: "GitHub token to install Protobuf with"
|
|
||||||
require: true
|
|
||||||
default:
|
|
||||||
|
|
||||||
runs:
|
|
||||||
using: "composite"
|
|
||||||
steps:
|
|
||||||
- name: Remove unused packages
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
sudo apt remove -y "*msbuild*" "*powershell*" "*nuget*" "*bazel*" "*ansible*" "*terraform*" "*heroku*" "*aws*" azure-cli
|
|
||||||
sudo apt remove -y "*nodejs*" "*npm*" "*yarn*" "*java*" "*kotlin*" "*golang*" "*swift*" "*julia*" "*fortran*" "*android*"
|
|
||||||
sudo apt remove -y "*apache2*" "*nginx*" "*firefox*" "*chromium*" "*chrome*" "*edge*"
|
|
||||||
sudo apt remove -y "*qemu*" "*sql*" "*texinfo*" "*imagemagick*"
|
|
||||||
sudo apt autoremove -y
|
|
||||||
sudo apt clean
|
|
||||||
docker system prune -a --volumes
|
|
||||||
|
|
||||||
- name: Install apt dependencies
|
|
||||||
shell: bash
|
|
||||||
run: sudo apt install -y ca-certificates
|
|
||||||
|
|
||||||
- name: Install Protobuf
|
|
||||||
uses: arduino/setup-protoc@a8b67ba40b37d35169e222f3bb352603327985b6
|
|
||||||
with:
|
|
||||||
repo-token: ${{ inputs.github-token }}
|
|
||||||
|
|
||||||
- name: Install solc
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
cargo install svm-rs
|
|
||||||
svm install 0.8.16
|
|
||||||
svm use 0.8.16
|
|
||||||
|
|
||||||
# - name: Cache Rust
|
|
||||||
# uses: Swatinem/rust-cache@a95ba195448af2da9b00fb742d14ffaaf3c21f43
|
|
||||||
44
.github/actions/monero-wallet-rpc/action.yml
vendored
44
.github/actions/monero-wallet-rpc/action.yml
vendored
@@ -1,44 +0,0 @@
|
|||||||
name: monero-wallet-rpc
|
|
||||||
description: Spawns a Monero Wallet-RPC.
|
|
||||||
|
|
||||||
inputs:
|
|
||||||
version:
|
|
||||||
description: "Version to download and run"
|
|
||||||
required: false
|
|
||||||
default: v0.18.2.0
|
|
||||||
|
|
||||||
runs:
|
|
||||||
using: "composite"
|
|
||||||
steps:
|
|
||||||
- name: Monero Wallet RPC Cache
|
|
||||||
id: cache-monero-wallet-rpc
|
|
||||||
uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84
|
|
||||||
with:
|
|
||||||
path: monero-wallet-rpc
|
|
||||||
key: monero-wallet-rpc-${{ runner.os }}-${{ runner.arch }}-${{ inputs.version }}
|
|
||||||
|
|
||||||
- name: Download the Monero Wallet RPC
|
|
||||||
if: steps.cache-monero-wallet-rpc.outputs.cache-hit != 'true'
|
|
||||||
# Calculates OS/ARCH to demonstrate it, yet then locks to linux-x64 due
|
|
||||||
# to the contained folder not following the same naming scheme and
|
|
||||||
# requiring further expansion not worth doing right now
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
RUNNER_OS=${{ runner.os }}
|
|
||||||
RUNNER_ARCH=${{ runner.arch }}
|
|
||||||
|
|
||||||
RUNNER_OS=${RUNNER_OS,,}
|
|
||||||
RUNNER_ARCH=${RUNNER_ARCH,,}
|
|
||||||
|
|
||||||
RUNNER_OS=linux
|
|
||||||
RUNNER_ARCH=x64
|
|
||||||
|
|
||||||
FILE=monero-$RUNNER_OS-$RUNNER_ARCH-${{ inputs.version }}.tar.bz2
|
|
||||||
wget https://downloads.getmonero.org/cli/$FILE
|
|
||||||
tar -xvf $FILE
|
|
||||||
|
|
||||||
mv monero-x86_64-linux-gnu-${{ inputs.version }}/monero-wallet-rpc monero-wallet-rpc
|
|
||||||
|
|
||||||
- name: Monero Wallet RPC
|
|
||||||
shell: bash
|
|
||||||
run: ./monero-wallet-rpc --disable-rpc-login --rpc-bind-port 6061 --allow-mismatched-daemon-version --wallet-dir ./ --detach
|
|
||||||
44
.github/actions/monero/action.yml
vendored
44
.github/actions/monero/action.yml
vendored
@@ -1,44 +0,0 @@
|
|||||||
name: monero-regtest
|
|
||||||
description: Spawns a regtest Monero daemon
|
|
||||||
|
|
||||||
inputs:
|
|
||||||
version:
|
|
||||||
description: "Version to download and run"
|
|
||||||
required: false
|
|
||||||
default: v0.18.2.0
|
|
||||||
|
|
||||||
runs:
|
|
||||||
using: "composite"
|
|
||||||
steps:
|
|
||||||
- name: Monero Daemon Cache
|
|
||||||
id: cache-monerod
|
|
||||||
uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84
|
|
||||||
with:
|
|
||||||
path: monerod
|
|
||||||
key: monerod-${{ runner.os }}-${{ runner.arch }}-${{ inputs.version }}
|
|
||||||
|
|
||||||
- name: Download the Monero Daemon
|
|
||||||
if: steps.cache-monerod.outputs.cache-hit != 'true'
|
|
||||||
# Calculates OS/ARCH to demonstrate it, yet then locks to linux-x64 due
|
|
||||||
# to the contained folder not following the same naming scheme and
|
|
||||||
# requiring further expansion not worth doing right now
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
RUNNER_OS=${{ runner.os }}
|
|
||||||
RUNNER_ARCH=${{ runner.arch }}
|
|
||||||
|
|
||||||
RUNNER_OS=${RUNNER_OS,,}
|
|
||||||
RUNNER_ARCH=${RUNNER_ARCH,,}
|
|
||||||
|
|
||||||
RUNNER_OS=linux
|
|
||||||
RUNNER_ARCH=x64
|
|
||||||
|
|
||||||
FILE=monero-$RUNNER_OS-$RUNNER_ARCH-${{ inputs.version }}.tar.bz2
|
|
||||||
wget https://downloads.getmonero.org/cli/$FILE
|
|
||||||
tar -xvf $FILE
|
|
||||||
|
|
||||||
mv monero-x86_64-linux-gnu-${{ inputs.version }}/monerod monerod
|
|
||||||
|
|
||||||
- name: Monero Regtest Daemon
|
|
||||||
shell: bash
|
|
||||||
run: ./monerod --regtest --offline --fixed-difficulty=1 --detach
|
|
||||||
45
.github/actions/test-dependencies/action.yml
vendored
45
.github/actions/test-dependencies/action.yml
vendored
@@ -1,45 +0,0 @@
|
|||||||
name: test-dependencies
|
|
||||||
description: Installs test dependencies for Serai
|
|
||||||
|
|
||||||
inputs:
|
|
||||||
github-token:
|
|
||||||
description: "GitHub token to install Protobuf with"
|
|
||||||
require: true
|
|
||||||
default:
|
|
||||||
|
|
||||||
monero-version:
|
|
||||||
description: "Monero version to download and run as a regtest node"
|
|
||||||
required: false
|
|
||||||
default: v0.18.2.0
|
|
||||||
|
|
||||||
bitcoin-version:
|
|
||||||
description: "Bitcoin version to download and run as a regtest node"
|
|
||||||
required: false
|
|
||||||
default: 24.0.1
|
|
||||||
|
|
||||||
runs:
|
|
||||||
using: "composite"
|
|
||||||
steps:
|
|
||||||
- name: Install Build Dependencies
|
|
||||||
uses: ./.github/actions/build-dependencies
|
|
||||||
with:
|
|
||||||
github-token: ${{ inputs.github-token }}
|
|
||||||
|
|
||||||
- name: Install Foundry
|
|
||||||
uses: foundry-rs/foundry-toolchain@cb603ca0abb544f301eaed59ac0baf579aa6aecf
|
|
||||||
with:
|
|
||||||
version: nightly-09fe3e041369a816365a020f715ad6f94dbce9f2
|
|
||||||
cache: false
|
|
||||||
|
|
||||||
- name: Run a Monero Regtest Node
|
|
||||||
uses: ./.github/actions/monero
|
|
||||||
with:
|
|
||||||
version: ${{ inputs.monero-version }}
|
|
||||||
|
|
||||||
- name: Run a Bitcoin Regtest Node
|
|
||||||
uses: ./.github/actions/bitcoin
|
|
||||||
with:
|
|
||||||
version: ${{ inputs.bitcoin-version }}
|
|
||||||
|
|
||||||
- name: Run a Monero Wallet-RPC
|
|
||||||
uses: ./.github/actions/monero-wallet-rpc
|
|
||||||
1
.github/nightly-version
vendored
1
.github/nightly-version
vendored
@@ -1 +0,0 @@
|
|||||||
nightly-2023-12-04
|
|
||||||
37
.github/workflows/coins-tests.yml
vendored
37
.github/workflows/coins-tests.yml
vendored
@@ -1,37 +0,0 @@
|
|||||||
name: coins/ Tests
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- develop
|
|
||||||
paths:
|
|
||||||
- "common/**"
|
|
||||||
- "crypto/**"
|
|
||||||
- "coins/**"
|
|
||||||
|
|
||||||
pull_request:
|
|
||||||
paths:
|
|
||||||
- "common/**"
|
|
||||||
- "crypto/**"
|
|
||||||
- "coins/**"
|
|
||||||
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
test-coins:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac
|
|
||||||
|
|
||||||
- name: Test Dependencies
|
|
||||||
uses: ./.github/actions/test-dependencies
|
|
||||||
with:
|
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Run Tests
|
|
||||||
run: |
|
|
||||||
GITHUB_CI=true RUST_BACKTRACE=1 cargo test --all-features \
|
|
||||||
-p bitcoin-serai \
|
|
||||||
-p ethereum-serai \
|
|
||||||
-p monero-generators \
|
|
||||||
-p monero-serai
|
|
||||||
33
.github/workflows/common-tests.yml
vendored
33
.github/workflows/common-tests.yml
vendored
@@ -1,33 +0,0 @@
|
|||||||
name: common/ Tests
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- develop
|
|
||||||
paths:
|
|
||||||
- "common/**"
|
|
||||||
|
|
||||||
pull_request:
|
|
||||||
paths:
|
|
||||||
- "common/**"
|
|
||||||
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
test-common:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac
|
|
||||||
|
|
||||||
- name: Build Dependencies
|
|
||||||
uses: ./.github/actions/build-dependencies
|
|
||||||
with:
|
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Run Tests
|
|
||||||
run: |
|
|
||||||
GITHUB_CI=true RUST_BACKTRACE=1 cargo test --all-features \
|
|
||||||
-p std-shims \
|
|
||||||
-p zalloc \
|
|
||||||
-p serai-db \
|
|
||||||
-p serai-env
|
|
||||||
44
.github/workflows/coordinator-tests.yml
vendored
44
.github/workflows/coordinator-tests.yml
vendored
@@ -1,44 +0,0 @@
|
|||||||
name: Coordinator Tests
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- develop
|
|
||||||
paths:
|
|
||||||
- "common/**"
|
|
||||||
- "crypto/**"
|
|
||||||
- "coins/**"
|
|
||||||
- "message-queue/**"
|
|
||||||
- "orchestration/message-queue/**"
|
|
||||||
- "coordinator/**"
|
|
||||||
- "orchestration/coordinator/**"
|
|
||||||
- "tests/docker/**"
|
|
||||||
- "tests/coordinator/**"
|
|
||||||
|
|
||||||
pull_request:
|
|
||||||
paths:
|
|
||||||
- "common/**"
|
|
||||||
- "crypto/**"
|
|
||||||
- "coins/**"
|
|
||||||
- "message-queue/**"
|
|
||||||
- "orchestration/message-queue/**"
|
|
||||||
- "coordinator/**"
|
|
||||||
- "orchestration/coordinator/**"
|
|
||||||
- "tests/docker/**"
|
|
||||||
- "tests/coordinator/**"
|
|
||||||
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac
|
|
||||||
|
|
||||||
- name: Install Build Dependencies
|
|
||||||
uses: ./.github/actions/build-dependencies
|
|
||||||
with:
|
|
||||||
github-token: ${{ inputs.github-token }}
|
|
||||||
|
|
||||||
- name: Run coordinator Docker tests
|
|
||||||
run: cd tests/coordinator && GITHUB_CI=true RUST_BACKTRACE=1 cargo test
|
|
||||||
42
.github/workflows/crypto-tests.yml
vendored
42
.github/workflows/crypto-tests.yml
vendored
@@ -1,42 +0,0 @@
|
|||||||
name: crypto/ Tests
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- develop
|
|
||||||
paths:
|
|
||||||
- "common/**"
|
|
||||||
- "crypto/**"
|
|
||||||
|
|
||||||
pull_request:
|
|
||||||
paths:
|
|
||||||
- "common/**"
|
|
||||||
- "crypto/**"
|
|
||||||
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
test-crypto:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac
|
|
||||||
|
|
||||||
- name: Build Dependencies
|
|
||||||
uses: ./.github/actions/build-dependencies
|
|
||||||
with:
|
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Run Tests
|
|
||||||
run: |
|
|
||||||
GITHUB_CI=true RUST_BACKTRACE=1 cargo test --all-features \
|
|
||||||
-p flexible-transcript \
|
|
||||||
-p ff-group-tests \
|
|
||||||
-p dalek-ff-group \
|
|
||||||
-p minimal-ed448 \
|
|
||||||
-p ciphersuite \
|
|
||||||
-p multiexp \
|
|
||||||
-p schnorr-signatures \
|
|
||||||
-p dleq \
|
|
||||||
-p dkg \
|
|
||||||
-p modular-frost \
|
|
||||||
-p frost-schnorrkel
|
|
||||||
24
.github/workflows/daily-deny.yml
vendored
24
.github/workflows/daily-deny.yml
vendored
@@ -1,24 +0,0 @@
|
|||||||
name: Daily Deny Check
|
|
||||||
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
- cron: "0 0 * * *"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
deny:
|
|
||||||
name: Run cargo deny
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac
|
|
||||||
|
|
||||||
- name: Advisory Cache
|
|
||||||
uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84
|
|
||||||
with:
|
|
||||||
path: ~/.cargo/advisory-db
|
|
||||||
key: rust-advisory-db
|
|
||||||
|
|
||||||
- name: Install cargo deny
|
|
||||||
run: cargo install --locked cargo-deny
|
|
||||||
|
|
||||||
- name: Run cargo deny
|
|
||||||
run: cargo deny -L error --all-features check
|
|
||||||
24
.github/workflows/full-stack-tests.yml
vendored
24
.github/workflows/full-stack-tests.yml
vendored
@@ -1,24 +0,0 @@
|
|||||||
name: Full Stack Tests
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- develop
|
|
||||||
|
|
||||||
pull_request:
|
|
||||||
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac
|
|
||||||
|
|
||||||
- name: Install Build Dependencies
|
|
||||||
uses: ./.github/actions/build-dependencies
|
|
||||||
with:
|
|
||||||
github-token: ${{ inputs.github-token }}
|
|
||||||
|
|
||||||
- name: Run Full Stack Docker tests
|
|
||||||
run: cd tests/full-stack && GITHUB_CI=true RUST_BACKTRACE=1 cargo test
|
|
||||||
86
.github/workflows/lint.yml
vendored
86
.github/workflows/lint.yml
vendored
@@ -1,86 +0,0 @@
|
|||||||
name: Lint
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- develop
|
|
||||||
pull_request:
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
clippy:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac
|
|
||||||
|
|
||||||
- name: Get nightly version to use
|
|
||||||
id: nightly
|
|
||||||
run: echo "version=$(cat .github/nightly-version)" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Build Dependencies
|
|
||||||
uses: ./.github/actions/build-dependencies
|
|
||||||
with:
|
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Install nightly rust
|
|
||||||
run: rustup toolchain install ${{ steps.nightly.outputs.version }} --profile minimal -t wasm32-unknown-unknown -c rust-src -c clippy
|
|
||||||
|
|
||||||
- name: Run Clippy
|
|
||||||
run: cargo +${{ steps.nightly.outputs.version }} clippy --all-features --all-targets -- -D warnings -A clippy::items_after_test_module
|
|
||||||
|
|
||||||
# Also verify the lockfile isn't dirty
|
|
||||||
# This happens when someone edits a Cargo.toml yet doesn't do anything
|
|
||||||
# which causes the lockfile to be updated
|
|
||||||
# The above clippy run will cause it to be updated, so checking there's
|
|
||||||
# no differences present now performs the desired check
|
|
||||||
- name: Verify lockfile
|
|
||||||
run: git diff | wc -l | grep -x "0"
|
|
||||||
|
|
||||||
deny:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac
|
|
||||||
|
|
||||||
- name: Advisory Cache
|
|
||||||
uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84
|
|
||||||
with:
|
|
||||||
path: ~/.cargo/advisory-db
|
|
||||||
key: rust-advisory-db
|
|
||||||
|
|
||||||
- name: Install cargo deny
|
|
||||||
run: cargo install --locked cargo-deny
|
|
||||||
|
|
||||||
- name: Run cargo deny
|
|
||||||
run: cargo deny -L error --all-features check
|
|
||||||
|
|
||||||
fmt:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac
|
|
||||||
|
|
||||||
- name: Get nightly version to use
|
|
||||||
id: nightly
|
|
||||||
run: echo "version=$(cat .github/nightly-version)" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Install nightly rust
|
|
||||||
run: rustup toolchain install ${{ steps.nightly.outputs.version }} --profile minimal -c rustfmt
|
|
||||||
|
|
||||||
- name: Run rustfmt
|
|
||||||
run: cargo +${{ steps.nightly.outputs.version }} fmt -- --check
|
|
||||||
|
|
||||||
dockerfiles:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac
|
|
||||||
- name: Verify Dockerfiles are up to date
|
|
||||||
# Runs the file which generates them and checks the diff has no lines
|
|
||||||
run: cd orchestration && ./dockerfiles.sh && git diff | wc -l | grep -x "0"
|
|
||||||
|
|
||||||
machete:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac
|
|
||||||
- name: Verify all dependencies are in use
|
|
||||||
run: |
|
|
||||||
cargo install cargo-machete
|
|
||||||
cargo machete
|
|
||||||
38
.github/workflows/message-queue-tests.yml
vendored
38
.github/workflows/message-queue-tests.yml
vendored
@@ -1,38 +0,0 @@
|
|||||||
name: Message Queue Tests
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- develop
|
|
||||||
paths:
|
|
||||||
- "common/**"
|
|
||||||
- "crypto/**"
|
|
||||||
- "message-queue/**"
|
|
||||||
- "orchestration/message-queue/**"
|
|
||||||
- "tests/docker/**"
|
|
||||||
- "tests/message-queue/**"
|
|
||||||
|
|
||||||
pull_request:
|
|
||||||
paths:
|
|
||||||
- "common/**"
|
|
||||||
- "crypto/**"
|
|
||||||
- "message-queue/**"
|
|
||||||
- "orchestration/message-queue/**"
|
|
||||||
- "tests/docker/**"
|
|
||||||
- "tests/message-queue/**"
|
|
||||||
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac
|
|
||||||
|
|
||||||
- name: Install Build Dependencies
|
|
||||||
uses: ./.github/actions/build-dependencies
|
|
||||||
with:
|
|
||||||
github-token: ${{ inputs.github-token }}
|
|
||||||
|
|
||||||
- name: Run message-queue Docker tests
|
|
||||||
run: cd tests/message-queue && GITHUB_CI=true RUST_BACKTRACE=1 cargo test
|
|
||||||
28
.github/workflows/mini-tests.yml
vendored
28
.github/workflows/mini-tests.yml
vendored
@@ -1,28 +0,0 @@
|
|||||||
name: mini/ Tests
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- develop
|
|
||||||
paths:
|
|
||||||
- "mini/**"
|
|
||||||
|
|
||||||
pull_request:
|
|
||||||
paths:
|
|
||||||
- "mini/**"
|
|
||||||
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
test-common:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac
|
|
||||||
|
|
||||||
- name: Build Dependencies
|
|
||||||
uses: ./.github/actions/build-dependencies
|
|
||||||
with:
|
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Run Tests
|
|
||||||
run: GITHUB_CI=true RUST_BACKTRACE=1 cargo test --all-features -p mini-serai
|
|
||||||
59
.github/workflows/monero-tests.yaml
vendored
59
.github/workflows/monero-tests.yaml
vendored
@@ -1,59 +0,0 @@
|
|||||||
name: Monero Tests
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- develop
|
|
||||||
paths:
|
|
||||||
- "coins/monero/**"
|
|
||||||
- "processor/**"
|
|
||||||
|
|
||||||
pull_request:
|
|
||||||
paths:
|
|
||||||
- "coins/monero/**"
|
|
||||||
- "processor/**"
|
|
||||||
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
# Only run these once since they will be consistent regardless of any node
|
|
||||||
unit-tests:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac
|
|
||||||
|
|
||||||
- name: Test Dependencies
|
|
||||||
uses: ./.github/actions/test-dependencies
|
|
||||||
with:
|
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Run Unit Tests Without Features
|
|
||||||
run: GITHUB_CI=true RUST_BACKTRACE=1 cargo test --package monero-serai --lib
|
|
||||||
|
|
||||||
# Doesn't run unit tests with features as the tests workflow will
|
|
||||||
|
|
||||||
integration-tests:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
# Test against all supported protocol versions
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
version: [v0.17.3.2, v0.18.2.0]
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac
|
|
||||||
|
|
||||||
- name: Test Dependencies
|
|
||||||
uses: ./.github/actions/test-dependencies
|
|
||||||
with:
|
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
monero-version: ${{ matrix.version }}
|
|
||||||
|
|
||||||
- name: Run Integration Tests Without Features
|
|
||||||
# Runs with the binaries feature so the binaries build
|
|
||||||
# https://github.com/rust-lang/cargo/issues/8396
|
|
||||||
run: GITHUB_CI=true RUST_BACKTRACE=1 cargo test --package monero-serai --features binaries --test '*'
|
|
||||||
|
|
||||||
- name: Run Integration Tests
|
|
||||||
# Don't run if the the tests workflow also will
|
|
||||||
if: ${{ matrix.version != 'v0.18.2.0' }}
|
|
||||||
run: GITHUB_CI=true RUST_BACKTRACE=1 cargo test --package monero-serai --all-features --test '*'
|
|
||||||
53
.github/workflows/monthly-nightly-update.yml
vendored
53
.github/workflows/monthly-nightly-update.yml
vendored
@@ -1,53 +0,0 @@
|
|||||||
name: Monthly Nightly Update
|
|
||||||
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
- cron: "0 0 1 * *"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
update:
|
|
||||||
name: Update nightly
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac
|
|
||||||
with:
|
|
||||||
submodules: "recursive"
|
|
||||||
|
|
||||||
- name: Write nightly version
|
|
||||||
run: echo $(date +"nightly-%Y-%m"-01) > .github/nightly-version
|
|
||||||
|
|
||||||
- name: Create the commit
|
|
||||||
run: |
|
|
||||||
git config user.name "GitHub Actions"
|
|
||||||
git config user.email "<>"
|
|
||||||
|
|
||||||
git checkout -b $(date +"nightly-%Y-%m")
|
|
||||||
|
|
||||||
git add .github/nightly-version
|
|
||||||
git commit -m "Update nightly"
|
|
||||||
git push -u origin $(date +"nightly-%Y-%m")
|
|
||||||
|
|
||||||
- name: Pull Request
|
|
||||||
uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410
|
|
||||||
with:
|
|
||||||
script: |
|
|
||||||
const { repo, owner } = context.repo;
|
|
||||||
|
|
||||||
const result = await github.rest.pulls.create({
|
|
||||||
title: (new Date()).toLocaleString(
|
|
||||||
false,
|
|
||||||
{ month: "long", year: "numeric" }
|
|
||||||
) + " - Rust Nightly Update",
|
|
||||||
owner,
|
|
||||||
repo,
|
|
||||||
head: "nightly-" + (new Date()).toISOString().split("-").splice(0, 2).join("-"),
|
|
||||||
base: "develop",
|
|
||||||
body: "PR auto-generated by a GitHub workflow."
|
|
||||||
});
|
|
||||||
|
|
||||||
github.rest.issues.addLabels({
|
|
||||||
owner,
|
|
||||||
repo,
|
|
||||||
issue_number: result.data.number,
|
|
||||||
labels: ["improvement"]
|
|
||||||
});
|
|
||||||
37
.github/workflows/no-std.yml
vendored
37
.github/workflows/no-std.yml
vendored
@@ -1,37 +0,0 @@
|
|||||||
name: no-std build
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- develop
|
|
||||||
paths:
|
|
||||||
- "common/**"
|
|
||||||
- "crypto/**"
|
|
||||||
- "coins/**"
|
|
||||||
- "tests/no-std/**"
|
|
||||||
|
|
||||||
pull_request:
|
|
||||||
paths:
|
|
||||||
- "common/**"
|
|
||||||
- "crypto/**"
|
|
||||||
- "coins/**"
|
|
||||||
- "tests/no-std/**"
|
|
||||||
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac
|
|
||||||
|
|
||||||
- name: Install Build Dependencies
|
|
||||||
uses: ./.github/actions/build-dependencies
|
|
||||||
with:
|
|
||||||
github-token: ${{ inputs.github-token }}
|
|
||||||
|
|
||||||
- name: Install RISC-V Toolchain
|
|
||||||
run: sudo apt update && sudo apt install -y gcc-riscv64-unknown-elf gcc-multilib && rustup target add riscv32imac-unknown-none-elf
|
|
||||||
|
|
||||||
- name: Verify no-std builds
|
|
||||||
run: cd tests/no-std && CFLAGS=-I/usr/include cargo build --target riscv32imac-unknown-none-elf
|
|
||||||
44
.github/workflows/processor-tests.yml
vendored
44
.github/workflows/processor-tests.yml
vendored
@@ -1,44 +0,0 @@
|
|||||||
name: Processor Tests
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- develop
|
|
||||||
paths:
|
|
||||||
- "common/**"
|
|
||||||
- "crypto/**"
|
|
||||||
- "coins/**"
|
|
||||||
- "message-queue/**"
|
|
||||||
- "orchestration/message-queue/**"
|
|
||||||
- "processor/**"
|
|
||||||
- "orchestration/processor/**"
|
|
||||||
- "tests/docker/**"
|
|
||||||
- "tests/processor/**"
|
|
||||||
|
|
||||||
pull_request:
|
|
||||||
paths:
|
|
||||||
- "common/**"
|
|
||||||
- "crypto/**"
|
|
||||||
- "coins/**"
|
|
||||||
- "message-queue/**"
|
|
||||||
- "orchestration/message-queue/**"
|
|
||||||
- "processor/**"
|
|
||||||
- "orchestration/processor/**"
|
|
||||||
- "tests/docker/**"
|
|
||||||
- "tests/processor/**"
|
|
||||||
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac
|
|
||||||
|
|
||||||
- name: Install Build Dependencies
|
|
||||||
uses: ./.github/actions/build-dependencies
|
|
||||||
with:
|
|
||||||
github-token: ${{ inputs.github-token }}
|
|
||||||
|
|
||||||
- name: Run processor Docker tests
|
|
||||||
run: cd tests/processor && GITHUB_CI=true RUST_BACKTRACE=1 cargo test
|
|
||||||
38
.github/workflows/reproducible-runtime.yml
vendored
38
.github/workflows/reproducible-runtime.yml
vendored
@@ -1,38 +0,0 @@
|
|||||||
name: Reproducible Runtime
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- develop
|
|
||||||
paths:
|
|
||||||
- "Cargo.lock"
|
|
||||||
- "common/**"
|
|
||||||
- "crypto/**"
|
|
||||||
- "substrate/**"
|
|
||||||
- "orchestration/runtime/**"
|
|
||||||
- "tests/reproducible-runtime/**"
|
|
||||||
|
|
||||||
pull_request:
|
|
||||||
paths:
|
|
||||||
- "Cargo.lock"
|
|
||||||
- "common/**"
|
|
||||||
- "crypto/**"
|
|
||||||
- "substrate/**"
|
|
||||||
- "orchestration/runtime/**"
|
|
||||||
- "tests/reproducible-runtime/**"
|
|
||||||
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac
|
|
||||||
|
|
||||||
- name: Install Build Dependencies
|
|
||||||
uses: ./.github/actions/build-dependencies
|
|
||||||
with:
|
|
||||||
github-token: ${{ inputs.github-token }}
|
|
||||||
|
|
||||||
- name: Run Reproducible Runtime tests
|
|
||||||
run: cd tests/reproducible-runtime && GITHUB_CI=true RUST_BACKTRACE=1 cargo test
|
|
||||||
86
.github/workflows/tests.yml
vendored
86
.github/workflows/tests.yml
vendored
@@ -1,86 +0,0 @@
|
|||||||
name: Tests
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- develop
|
|
||||||
paths:
|
|
||||||
- "common/**"
|
|
||||||
- "crypto/**"
|
|
||||||
- "coins/**"
|
|
||||||
- "message-queue/**"
|
|
||||||
- "processor/**"
|
|
||||||
- "coordinator/**"
|
|
||||||
- "substrate/**"
|
|
||||||
|
|
||||||
pull_request:
|
|
||||||
paths:
|
|
||||||
- "common/**"
|
|
||||||
- "crypto/**"
|
|
||||||
- "coins/**"
|
|
||||||
- "message-queue/**"
|
|
||||||
- "processor/**"
|
|
||||||
- "coordinator/**"
|
|
||||||
- "substrate/**"
|
|
||||||
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
test-infra:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac
|
|
||||||
|
|
||||||
- name: Build Dependencies
|
|
||||||
uses: ./.github/actions/build-dependencies
|
|
||||||
with:
|
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Run Tests
|
|
||||||
run: |
|
|
||||||
GITHUB_CI=true RUST_BACKTRACE=1 cargo test --all-features \
|
|
||||||
-p serai-message-queue \
|
|
||||||
-p serai-processor-messages \
|
|
||||||
-p serai-processor \
|
|
||||||
-p tendermint-machine \
|
|
||||||
-p tributary-chain \
|
|
||||||
-p serai-coordinator \
|
|
||||||
-p serai-docker-tests
|
|
||||||
|
|
||||||
test-substrate:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac
|
|
||||||
|
|
||||||
- name: Build Dependencies
|
|
||||||
uses: ./.github/actions/build-dependencies
|
|
||||||
with:
|
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Run Tests
|
|
||||||
run: |
|
|
||||||
GITHUB_CI=true RUST_BACKTRACE=1 cargo test --all-features \
|
|
||||||
-p serai-primitives \
|
|
||||||
-p serai-coins-primitives \
|
|
||||||
-p serai-coins-pallet \
|
|
||||||
-p serai-dex-pallet \
|
|
||||||
-p serai-validator-sets-primitives \
|
|
||||||
-p serai-validator-sets-pallet \
|
|
||||||
-p serai-in-instructions-primitives \
|
|
||||||
-p serai-in-instructions-pallet \
|
|
||||||
-p serai-signals-pallet \
|
|
||||||
-p serai-runtime \
|
|
||||||
-p serai-node
|
|
||||||
|
|
||||||
test-serai-client:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac
|
|
||||||
|
|
||||||
- name: Build Dependencies
|
|
||||||
uses: ./.github/actions/build-dependencies
|
|
||||||
with:
|
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Run Tests
|
|
||||||
run: GITHUB_CI=true RUST_BACKTRACE=1 cargo test --all-features -p serai-client
|
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,3 +1,2 @@
|
|||||||
target
|
target
|
||||||
.vscode
|
Cargo.lock
|
||||||
.test-logs
|
|
||||||
|
|||||||
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[submodule "coins/monero/c/monero"]
|
||||||
|
path = coins/monero/c/monero
|
||||||
|
url = https://github.com/monero-project/monero
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
edition = "2021"
|
|
||||||
tab_spaces = 2
|
|
||||||
|
|
||||||
max_width = 100
|
|
||||||
# Let the developer decide based on the 100 char line limit
|
|
||||||
use_small_heuristics = "Max"
|
|
||||||
|
|
||||||
error_on_line_overflow = true
|
|
||||||
error_on_unformatted = true
|
|
||||||
|
|
||||||
imports_granularity = "Crate"
|
|
||||||
reorder_imports = false
|
|
||||||
reorder_modules = false
|
|
||||||
|
|
||||||
unstable_features = true
|
|
||||||
spaces_around_ranges = true
|
|
||||||
binop_separator = "Back"
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
# Contributing
|
|
||||||
|
|
||||||
Contributions come in a variety of forms. Developing Serai, helping document it,
|
|
||||||
using its libraries in another project, using and testing it, and simply sharing
|
|
||||||
it are all valuable ways of contributing.
|
|
||||||
|
|
||||||
This document will specifically focus on contributions to this repository in the
|
|
||||||
form of code and documentation.
|
|
||||||
|
|
||||||
### Rules
|
|
||||||
|
|
||||||
- Stable native Rust, nightly wasm and tools.
|
|
||||||
- `cargo fmt` must be used.
|
|
||||||
- `cargo clippy` must pass, except for the ignored rules (`type_complexity` and
|
|
||||||
`dead_code`).
|
|
||||||
- The CI must pass.
|
|
||||||
|
|
||||||
- Only use uppercase variable names when relevant to cryptography.
|
|
||||||
|
|
||||||
- Use a two-space ident when possible.
|
|
||||||
- Put a space after comment markers.
|
|
||||||
- Don't use multiple newlines between sections of code.
|
|
||||||
- Have a newline before EOF.
|
|
||||||
|
|
||||||
### Guidelines
|
|
||||||
|
|
||||||
- Sort inputs as core, std, third party, and then Serai.
|
|
||||||
- Comment code reasonably.
|
|
||||||
- Include tests for new features.
|
|
||||||
- Sign commits.
|
|
||||||
|
|
||||||
### Submission
|
|
||||||
|
|
||||||
All submissions should be through GitHub. Contributions to a crate will be
|
|
||||||
licensed according to the crate's existing license, with the crate's copyright
|
|
||||||
holders (distinct from authors) having the right to re-license the crate via a
|
|
||||||
unanimous decision.
|
|
||||||
10373
Cargo.lock
generated
10373
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
165
Cargo.toml
165
Cargo.toml
@@ -1,181 +1,22 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
resolver = "2"
|
|
||||||
members = [
|
members = [
|
||||||
"common/std-shims",
|
|
||||||
"common/zalloc",
|
|
||||||
"common/db",
|
|
||||||
"common/env",
|
|
||||||
"common/request",
|
|
||||||
|
|
||||||
"crypto/transcript",
|
"crypto/transcript",
|
||||||
|
|
||||||
"crypto/ff-group-tests",
|
|
||||||
"crypto/dalek-ff-group",
|
"crypto/dalek-ff-group",
|
||||||
"crypto/ed448",
|
"crypto/tables",
|
||||||
"crypto/ciphersuite",
|
|
||||||
|
|
||||||
"crypto/multiexp",
|
"crypto/multiexp",
|
||||||
|
|
||||||
"crypto/schnorr",
|
|
||||||
"crypto/dleq",
|
"crypto/dleq",
|
||||||
"crypto/dkg",
|
|
||||||
"crypto/frost",
|
"crypto/frost",
|
||||||
"crypto/schnorrkel",
|
|
||||||
|
|
||||||
"coins/bitcoin",
|
|
||||||
"coins/ethereum",
|
|
||||||
"coins/monero/generators",
|
|
||||||
"coins/monero",
|
"coins/monero",
|
||||||
|
|
||||||
"message-queue",
|
|
||||||
|
|
||||||
"processor/messages",
|
|
||||||
"processor",
|
"processor",
|
||||||
|
|
||||||
"coordinator/tributary/tendermint",
|
|
||||||
"coordinator/tributary",
|
|
||||||
"coordinator",
|
|
||||||
|
|
||||||
"substrate/tree-cleanup/is-terminal",
|
|
||||||
"substrate/tree-cleanup/option-ext",
|
|
||||||
"substrate/tree-cleanup/directories-next",
|
|
||||||
"substrate/tree-cleanup/bandersnatch_vrfs",
|
|
||||||
"substrate/tree-cleanup/w3f-bls",
|
|
||||||
|
|
||||||
"substrate/primitives",
|
|
||||||
|
|
||||||
"substrate/coins/primitives",
|
|
||||||
"substrate/coins/pallet",
|
|
||||||
|
|
||||||
"substrate/in-instructions/primitives",
|
|
||||||
"substrate/in-instructions/pallet",
|
|
||||||
|
|
||||||
"substrate/validator-sets/primitives",
|
|
||||||
"substrate/validator-sets/pallet",
|
|
||||||
|
|
||||||
"substrate/signals/primitives",
|
|
||||||
"substrate/signals/pallet",
|
|
||||||
|
|
||||||
"substrate/abi",
|
|
||||||
|
|
||||||
"substrate/runtime",
|
"substrate/runtime",
|
||||||
"substrate/node",
|
"substrate/consensus",
|
||||||
|
"substrate/node"
|
||||||
"substrate/client",
|
|
||||||
|
|
||||||
"mini",
|
|
||||||
|
|
||||||
"tests/no-std",
|
|
||||||
|
|
||||||
"tests/docker",
|
|
||||||
"tests/message-queue",
|
|
||||||
"tests/processor",
|
|
||||||
"tests/coordinator",
|
|
||||||
"tests/full-stack",
|
|
||||||
"tests/reproducible-runtime",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
# Always compile Monero (and a variety of dependencies) with optimizations due
|
|
||||||
# to the extensive operations required for Bulletproofs
|
|
||||||
[profile.dev.package]
|
|
||||||
subtle = { opt-level = 3 }
|
|
||||||
curve25519-dalek = { opt-level = 3 }
|
|
||||||
|
|
||||||
ff = { opt-level = 3 }
|
|
||||||
group = { opt-level = 3 }
|
|
||||||
|
|
||||||
crypto-bigint = { opt-level = 3 }
|
|
||||||
dalek-ff-group = { opt-level = 3 }
|
|
||||||
minimal-ed448 = { opt-level = 3 }
|
|
||||||
|
|
||||||
multiexp = { opt-level = 3 }
|
|
||||||
|
|
||||||
monero-serai = { opt-level = 3 }
|
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
panic = "unwind"
|
panic = "unwind"
|
||||||
|
|
||||||
[patch.crates-io]
|
|
||||||
# https://github.com/rust-lang-nursery/lazy-static.rs/issues/201
|
|
||||||
lazy_static = { git = "https://github.com/rust-lang-nursery/lazy-static.rs", rev = "5735630d46572f1e5377c8f2ba0f79d18f53b10c" }
|
|
||||||
|
|
||||||
# subxt *can* pull these off crates.io yet there's no benefit to this
|
|
||||||
sp-core-hashing = { git = "https://github.com/serai-dex/polkadot-sdk", branch = "experimental" }
|
|
||||||
sp-std = { git = "https://github.com/serai-dex/polkadot-sdk", branch = "experimental" }
|
|
||||||
|
|
||||||
# is-terminal now has an std-based solution with an equivalent API
|
|
||||||
is-terminal = { path = "substrate/tree-cleanup/is-terminal" }
|
|
||||||
# So does matches
|
|
||||||
matches = { path = "substrate/tree-cleanup/matches" }
|
|
||||||
|
|
||||||
# directories-next was created because directories was unmaintained
|
|
||||||
# directories-next is now unmaintained while directories is maintained
|
|
||||||
# The directories author pulls in ridiculously pointless crates and prefers
|
|
||||||
# copyleft licenses
|
|
||||||
# Serai's polkadot-sdk consolidated to directories-next, as directories-next is
|
|
||||||
# acceptable and because we couldn't consolidate to directories without forking
|
|
||||||
# wasmtime
|
|
||||||
# The following two patches resolve everything
|
|
||||||
option-ext = { path = "substrate/tree-cleanup/option-ext" }
|
|
||||||
directories-next = { path = "substrate/tree-cleanup/directories-next" }
|
|
||||||
|
|
||||||
# mach is unmaintained, so this wraps mach2 as mach
|
|
||||||
mach = { path = "substrate/tree-cleanup/mach" }
|
|
||||||
|
|
||||||
# cargo believes the following are in-tree despite no features activating them
|
|
||||||
# We provide empty crates to not only prove they're unused, yet also clean up
|
|
||||||
# our Cargo.lock
|
|
||||||
w3f-bls = { path = "substrate/tree-cleanup/w3f-bls" }
|
|
||||||
[patch."https://github.com/w3f/ring-vrf"]
|
|
||||||
bandersnatch_vrfs = { path = "substrate/tree-cleanup/bandersnatch_vrfs" }
|
|
||||||
|
|
||||||
[workspace.lints.clippy]
|
|
||||||
unwrap_or_default = "allow"
|
|
||||||
borrow_as_ptr = "deny"
|
|
||||||
cast_lossless = "deny"
|
|
||||||
cast_possible_truncation = "deny"
|
|
||||||
cast_possible_wrap = "deny"
|
|
||||||
cast_precision_loss = "deny"
|
|
||||||
cast_ptr_alignment = "deny"
|
|
||||||
cast_sign_loss = "deny"
|
|
||||||
checked_conversions = "deny"
|
|
||||||
cloned_instead_of_copied = "deny"
|
|
||||||
enum_glob_use = "deny"
|
|
||||||
expl_impl_clone_on_copy = "deny"
|
|
||||||
explicit_into_iter_loop = "deny"
|
|
||||||
explicit_iter_loop = "deny"
|
|
||||||
flat_map_option = "deny"
|
|
||||||
float_cmp = "deny"
|
|
||||||
fn_params_excessive_bools = "deny"
|
|
||||||
ignored_unit_patterns = "deny"
|
|
||||||
implicit_clone = "deny"
|
|
||||||
inefficient_to_string = "deny"
|
|
||||||
invalid_upcast_comparisons = "deny"
|
|
||||||
large_stack_arrays = "deny"
|
|
||||||
linkedlist = "deny"
|
|
||||||
macro_use_imports = "deny"
|
|
||||||
manual_instant_elapsed = "deny"
|
|
||||||
manual_let_else = "deny"
|
|
||||||
manual_ok_or = "deny"
|
|
||||||
manual_string_new = "deny"
|
|
||||||
map_unwrap_or = "deny"
|
|
||||||
match_bool = "deny"
|
|
||||||
match_same_arms = "deny"
|
|
||||||
missing_fields_in_debug = "deny"
|
|
||||||
needless_continue = "deny"
|
|
||||||
needless_pass_by_value = "deny"
|
|
||||||
ptr_cast_constness = "deny"
|
|
||||||
range_minus_one = "deny"
|
|
||||||
range_plus_one = "deny"
|
|
||||||
redundant_closure_for_method_calls = "deny"
|
|
||||||
redundant_else = "deny"
|
|
||||||
string_add_assign = "deny"
|
|
||||||
unchecked_duration_subtraction = "deny"
|
|
||||||
uninlined_format_args = "deny"
|
|
||||||
unnecessary_box_returns = "deny"
|
|
||||||
unnecessary_join = "deny"
|
|
||||||
unnecessary_wraps = "deny"
|
|
||||||
unnested_or_patterns = "deny"
|
|
||||||
unused_async = "deny"
|
|
||||||
unused_self = "deny"
|
|
||||||
zero_sized_map_values = "deny"
|
|
||||||
|
|||||||
8
LICENSE
8
LICENSE
@@ -1,8 +0,0 @@
|
|||||||
Serai crates are licensed under one of two licenses, either MIT or AGPL-3.0,
|
|
||||||
depending on the crate in question. Each crate declares their license in their
|
|
||||||
`Cargo.toml` and includes a `LICENSE` file detailing its status. Additionally,
|
|
||||||
a full copy of the AGPL-3.0 License is included in the root of this repository
|
|
||||||
as a reference text. This copy should be provided with any distribution of a
|
|
||||||
crate licensed under the AGPL-3.0, as per its terms.
|
|
||||||
|
|
||||||
The GitHub actions (`.github/actions`) are licensed under the MIT license.
|
|
||||||
59
README.md
59
README.md
@@ -1,63 +1,22 @@
|
|||||||
# Serai
|
# Serai
|
||||||
|
|
||||||
Serai is a new DEX, built from the ground up, initially planning on listing
|
Serai is a new DEX, built from the ground up, initially planning on listing
|
||||||
Bitcoin, Ethereum, DAI, and Monero, offering a liquidity-pool-based trading
|
Bitcoin, Ethereum, Monero, DAI, and USDC, offering a liquidity pool trading
|
||||||
experience. Funds are stored in an economically secured threshold-multisig
|
experience. Funds are stored in an economically secured threshold multisig
|
||||||
wallet.
|
wallet.
|
||||||
|
|
||||||
[Getting Started](docs/Getting%20Started.md)
|
|
||||||
|
|
||||||
### Layout
|
### Layout
|
||||||
|
|
||||||
- `audits`: Audits for various parts of Serai.
|
- `docs` - Documentation on the Serai protocol.
|
||||||
|
|
||||||
- `docs`: Documentation on the Serai protocol.
|
- `coins` - Various coin libraries intended for usage in Serai yet also by the
|
||||||
|
|
||||||
- `common`: Crates containing utilities common to a variety of areas under
|
|
||||||
Serai, none neatly fitting under another category.
|
|
||||||
|
|
||||||
- `crypto`: A series of composable cryptographic libraries built around the
|
|
||||||
`ff`/`group` APIs, achieving a variety of tasks. These range from generic
|
|
||||||
infrastructure, to our IETF-compliant FROST implementation, to a DLEq proof as
|
|
||||||
needed for Bitcoin-Monero atomic swaps.
|
|
||||||
|
|
||||||
- `coins`: Various coin libraries intended for usage in Serai yet also by the
|
|
||||||
wider community. This means they will always support the functionality Serai
|
wider community. This means they will always support the functionality Serai
|
||||||
needs, yet won't disadvantage other use cases when possible.
|
needs, yet won't disadvantage other use cases when possible.
|
||||||
|
|
||||||
- `message-queue`: An ordered message server so services can talk to each other,
|
- `crypto` - A series of composable cryptographic libraries built around the
|
||||||
even when the other is offline.
|
`ff`/`group` APIs achieving a variety of tasks. These range from generic
|
||||||
|
infrastructure, to our IETF-compliant FROST implementation, to a DLEq proof as
|
||||||
|
needed for Bitcoin-Monero atomic swaps.
|
||||||
|
|
||||||
- `processor`: A generic chain processor to process data for Serai and process
|
- `processor` - A generic chain processor to process data for Serai and process
|
||||||
events from Serai, executing transactions as expected and needed.
|
events from Serai, executing transactions as expected and needed.
|
||||||
|
|
||||||
- `coordinator`: A service to manage processors and communicate over a P2P
|
|
||||||
network with other validators.
|
|
||||||
|
|
||||||
- `substrate`: Substrate crates used to instantiate the Serai network.
|
|
||||||
|
|
||||||
- `orchestration`: Dockerfiles and scripts to deploy a Serai node/test
|
|
||||||
environment.
|
|
||||||
|
|
||||||
- `tests`: Tests for various crates. Generally, `crate/src/tests` is used, or
|
|
||||||
`crate/tests`, yet any tests requiring crates' binaries are placed here.
|
|
||||||
|
|
||||||
### Security
|
|
||||||
|
|
||||||
Serai hosts a bug bounty program via
|
|
||||||
[Immunefi](https://immunefi.com/bounty/serai/). For in-scope critical
|
|
||||||
vulnerabilities, we will reward whitehats with up to $30,000.
|
|
||||||
|
|
||||||
Anything not in-scope should still be submitted through Immunefi, with rewards
|
|
||||||
issued at the discretion of the Immunefi program managers.
|
|
||||||
|
|
||||||
### Links
|
|
||||||
|
|
||||||
- [Website](https://serai.exchange/): https://serai.exchange/
|
|
||||||
- [Immunefi](https://immunefi.com/bounty/serai/): https://immunefi.com/bounty/serai/
|
|
||||||
- [Twitter](https://twitter.com/SeraiDEX): https://twitter.com/SeraiDEX
|
|
||||||
- [Mastodon](https://cryptodon.lol/@serai): https://cryptodon.lol/@serai
|
|
||||||
- [Discord](https://discord.gg/mpEUtJR3vz): https://discord.gg/mpEUtJR3vz
|
|
||||||
- [Matrix](https://matrix.to/#/#serai:matrix.org): https://matrix.to/#/#serai:matrix.org
|
|
||||||
- [Reddit](https://www.reddit.com/r/SeraiDEX/): https://www.reddit.com/r/SeraiDEX/
|
|
||||||
- [Telegram](https://t.me/SeraiDEX): https://t.me/SeraiDEX
|
|
||||||
|
|||||||
Binary file not shown.
@@ -1,21 +0,0 @@
|
|||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2023 Cypher Stack
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
# Cypher Stack /coins/bitcoin Audit, August 2023
|
|
||||||
|
|
||||||
This audit was over the /coins/bitcoin folder. It is encompassing up to commit
|
|
||||||
5121ca75199dff7bd34230880a1fdd793012068c.
|
|
||||||
|
|
||||||
Please see https://github.com/cypherstack/serai-btc-audit for provenance.
|
|
||||||
Binary file not shown.
@@ -1,21 +0,0 @@
|
|||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2023 Cypher Stack
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
# Cypher Stack /crypto Audit, March 2023
|
|
||||||
|
|
||||||
This audit was over the /crypto folder, excluding the ed448 crate, the `Ed448`
|
|
||||||
ciphersuite in the ciphersuite crate, and the `dleq/experimental` feature. It is
|
|
||||||
encompassing up to commit 669d2dbffc1dafb82a09d9419ea182667115df06.
|
|
||||||
|
|
||||||
Please see https://github.com/cypherstack/serai-audit for provenance.
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "bitcoin-serai"
|
|
||||||
version = "0.3.0"
|
|
||||||
description = "A Bitcoin library for FROST-signing transactions"
|
|
||||||
license = "MIT"
|
|
||||||
repository = "https://github.com/serai-dex/serai/tree/develop/coins/bitcoin"
|
|
||||||
authors = ["Luke Parker <lukeparker5132@gmail.com>", "Vrx <vrx00@proton.me>"]
|
|
||||||
edition = "2021"
|
|
||||||
rust-version = "1.74"
|
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
|
||||||
all-features = true
|
|
||||||
rustdoc-args = ["--cfg", "docsrs"]
|
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
std-shims = { version = "0.1.1", path = "../../common/std-shims", default-features = false }
|
|
||||||
|
|
||||||
thiserror = { version = "1", default-features = false, optional = true }
|
|
||||||
|
|
||||||
zeroize = { version = "^1.5", default-features = false }
|
|
||||||
rand_core = { version = "0.6", default-features = false }
|
|
||||||
|
|
||||||
bitcoin = { version = "0.31", default-features = false, features = ["no-std"] }
|
|
||||||
|
|
||||||
k256 = { version = "^0.13.1", default-features = false, features = ["arithmetic", "bits"] }
|
|
||||||
|
|
||||||
transcript = { package = "flexible-transcript", path = "../../crypto/transcript", version = "0.3", default-features = false, features = ["recommended"], optional = true }
|
|
||||||
frost = { package = "modular-frost", path = "../../crypto/frost", version = "0.8", default-features = false, features = ["secp256k1"], optional = true }
|
|
||||||
|
|
||||||
hex = { version = "0.4", default-features = false, optional = true }
|
|
||||||
serde = { version = "1", default-features = false, features = ["derive"], optional = true }
|
|
||||||
serde_json = { version = "1", default-features = false, optional = true }
|
|
||||||
simple-request = { path = "../../common/request", version = "0.1", default-features = false, features = ["tls", "basic-auth"], optional = true }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
secp256k1 = { version = "0.28", default-features = false, features = ["std"] }
|
|
||||||
|
|
||||||
frost = { package = "modular-frost", path = "../../crypto/frost", features = ["tests"] }
|
|
||||||
|
|
||||||
tokio = { version = "1", features = ["macros"] }
|
|
||||||
|
|
||||||
[features]
|
|
||||||
std = [
|
|
||||||
"std-shims/std",
|
|
||||||
|
|
||||||
"thiserror",
|
|
||||||
|
|
||||||
"zeroize/std",
|
|
||||||
"rand_core/std",
|
|
||||||
|
|
||||||
"bitcoin/std",
|
|
||||||
"bitcoin/serde",
|
|
||||||
|
|
||||||
"k256/std",
|
|
||||||
|
|
||||||
"transcript/std",
|
|
||||||
"frost",
|
|
||||||
|
|
||||||
"hex/std",
|
|
||||||
"serde/std",
|
|
||||||
"serde_json/std",
|
|
||||||
"simple-request",
|
|
||||||
]
|
|
||||||
hazmat = []
|
|
||||||
default = ["std"]
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2022-2023 Luke Parker
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
# bitcoin-serai
|
|
||||||
|
|
||||||
An application of [modular-frost](https://docs.rs/modular-frost) to Bitcoin
|
|
||||||
transactions, enabling extremely-efficient multisigs.
|
|
||||||
@@ -1,166 +0,0 @@
|
|||||||
use k256::{
|
|
||||||
elliptic_curve::sec1::{Tag, ToEncodedPoint},
|
|
||||||
ProjectivePoint,
|
|
||||||
};
|
|
||||||
|
|
||||||
use bitcoin::key::XOnlyPublicKey;
|
|
||||||
|
|
||||||
/// Get the x coordinate of a non-infinity, even point. Panics on invalid input.
|
|
||||||
pub fn x(key: &ProjectivePoint) -> [u8; 32] {
|
|
||||||
let encoded = key.to_encoded_point(true);
|
|
||||||
assert_eq!(encoded.tag(), Tag::CompressedEvenY, "x coordinate of odd key");
|
|
||||||
(*encoded.x().expect("point at infinity")).into()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Convert a non-infinity even point to a XOnlyPublicKey. Panics on invalid input.
|
|
||||||
pub fn x_only(key: &ProjectivePoint) -> XOnlyPublicKey {
|
|
||||||
XOnlyPublicKey::from_slice(&x(key)).expect("x_only was passed a point which was infinity or odd")
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Make a point even by adding the generator until it is even.
|
|
||||||
///
|
|
||||||
/// Returns the even point and the amount of additions required.
|
|
||||||
#[cfg(any(feature = "std", feature = "hazmat"))]
|
|
||||||
pub fn make_even(mut key: ProjectivePoint) -> (ProjectivePoint, u64) {
|
|
||||||
let mut c = 0;
|
|
||||||
while key.to_encoded_point(true).tag() == Tag::CompressedOddY {
|
|
||||||
key += ProjectivePoint::GENERATOR;
|
|
||||||
c += 1;
|
|
||||||
}
|
|
||||||
(key, c)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "std")]
|
|
||||||
mod frost_crypto {
|
|
||||||
use core::fmt::Debug;
|
|
||||||
use std_shims::{vec::Vec, io};
|
|
||||||
|
|
||||||
use zeroize::Zeroizing;
|
|
||||||
use rand_core::{RngCore, CryptoRng};
|
|
||||||
|
|
||||||
use bitcoin::hashes::{HashEngine, Hash, sha256::Hash as Sha256};
|
|
||||||
|
|
||||||
use transcript::Transcript;
|
|
||||||
|
|
||||||
use k256::{elliptic_curve::ops::Reduce, U256, Scalar};
|
|
||||||
|
|
||||||
use frost::{
|
|
||||||
curve::{Ciphersuite, Secp256k1},
|
|
||||||
Participant, ThresholdKeys, ThresholdView, FrostError,
|
|
||||||
algorithm::{Hram as HramTrait, Algorithm, Schnorr as FrostSchnorr},
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
/// A BIP-340 compatible HRAm for use with the modular-frost Schnorr Algorithm.
|
|
||||||
///
|
|
||||||
/// If passed an odd nonce, it will have the generator added until it is even.
|
|
||||||
///
|
|
||||||
/// If the key is odd, this will panic.
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
|
||||||
pub struct Hram;
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
impl HramTrait<Secp256k1> for Hram {
|
|
||||||
fn hram(R: &ProjectivePoint, A: &ProjectivePoint, m: &[u8]) -> Scalar {
|
|
||||||
// Convert the nonce to be even
|
|
||||||
let (R, _) = make_even(*R);
|
|
||||||
|
|
||||||
const TAG_HASH: Sha256 = Sha256::const_hash(b"BIP0340/challenge");
|
|
||||||
|
|
||||||
let mut data = Sha256::engine();
|
|
||||||
data.input(TAG_HASH.as_ref());
|
|
||||||
data.input(TAG_HASH.as_ref());
|
|
||||||
data.input(&x(&R));
|
|
||||||
data.input(&x(A));
|
|
||||||
data.input(m);
|
|
||||||
|
|
||||||
Scalar::reduce(U256::from_be_slice(Sha256::from_engine(data).as_ref()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// BIP-340 Schnorr signature algorithm.
|
|
||||||
///
|
|
||||||
/// This must be used with a ThresholdKeys whose group key is even. If it is odd, this will panic.
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct Schnorr<T: Sync + Clone + Debug + Transcript>(FrostSchnorr<Secp256k1, T, Hram>);
|
|
||||||
impl<T: Sync + Clone + Debug + Transcript> Schnorr<T> {
|
|
||||||
/// Construct a Schnorr algorithm continuing the specified transcript.
|
|
||||||
pub fn new(transcript: T) -> Schnorr<T> {
|
|
||||||
Schnorr(FrostSchnorr::new(transcript))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: Sync + Clone + Debug + Transcript> Algorithm<Secp256k1> for Schnorr<T> {
|
|
||||||
type Transcript = T;
|
|
||||||
type Addendum = ();
|
|
||||||
type Signature = [u8; 64];
|
|
||||||
|
|
||||||
fn transcript(&mut self) -> &mut Self::Transcript {
|
|
||||||
self.0.transcript()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn nonces(&self) -> Vec<Vec<ProjectivePoint>> {
|
|
||||||
self.0.nonces()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn preprocess_addendum<R: RngCore + CryptoRng>(
|
|
||||||
&mut self,
|
|
||||||
rng: &mut R,
|
|
||||||
keys: &ThresholdKeys<Secp256k1>,
|
|
||||||
) {
|
|
||||||
self.0.preprocess_addendum(rng, keys)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_addendum<R: io::Read>(&self, reader: &mut R) -> io::Result<Self::Addendum> {
|
|
||||||
self.0.read_addendum(reader)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn process_addendum(
|
|
||||||
&mut self,
|
|
||||||
view: &ThresholdView<Secp256k1>,
|
|
||||||
i: Participant,
|
|
||||||
addendum: (),
|
|
||||||
) -> Result<(), FrostError> {
|
|
||||||
self.0.process_addendum(view, i, addendum)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn sign_share(
|
|
||||||
&mut self,
|
|
||||||
params: &ThresholdView<Secp256k1>,
|
|
||||||
nonce_sums: &[Vec<<Secp256k1 as Ciphersuite>::G>],
|
|
||||||
nonces: Vec<Zeroizing<<Secp256k1 as Ciphersuite>::F>>,
|
|
||||||
msg: &[u8],
|
|
||||||
) -> <Secp256k1 as Ciphersuite>::F {
|
|
||||||
self.0.sign_share(params, nonce_sums, nonces, msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
fn verify(
|
|
||||||
&self,
|
|
||||||
group_key: ProjectivePoint,
|
|
||||||
nonces: &[Vec<ProjectivePoint>],
|
|
||||||
sum: Scalar,
|
|
||||||
) -> Option<Self::Signature> {
|
|
||||||
self.0.verify(group_key, nonces, sum).map(|mut sig| {
|
|
||||||
// Make the R of the final signature even
|
|
||||||
let offset;
|
|
||||||
(sig.R, offset) = make_even(sig.R);
|
|
||||||
// s = r + cx. Since we added to the r, add to s
|
|
||||||
sig.s += Scalar::from(offset);
|
|
||||||
// Convert to a Bitcoin signature by dropping the byte for the point's sign bit
|
|
||||||
sig.serialize()[1 ..].try_into().unwrap()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn verify_share(
|
|
||||||
&self,
|
|
||||||
verification_share: ProjectivePoint,
|
|
||||||
nonces: &[Vec<ProjectivePoint>],
|
|
||||||
share: Scalar,
|
|
||||||
) -> Result<Vec<(Scalar, ProjectivePoint)>, ()> {
|
|
||||||
self.0.verify_share(verification_share, nonces, share)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[cfg(feature = "std")]
|
|
||||||
pub use frost_crypto::*;
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
|
||||||
#![doc = include_str!("../README.md")]
|
|
||||||
#![cfg_attr(not(feature = "std"), no_std)]
|
|
||||||
|
|
||||||
#[cfg(not(feature = "std"))]
|
|
||||||
extern crate alloc;
|
|
||||||
|
|
||||||
/// The bitcoin Rust library.
|
|
||||||
pub use bitcoin;
|
|
||||||
|
|
||||||
/// Cryptographic helpers.
|
|
||||||
#[cfg(feature = "hazmat")]
|
|
||||||
pub mod crypto;
|
|
||||||
#[cfg(not(feature = "hazmat"))]
|
|
||||||
pub(crate) mod crypto;
|
|
||||||
|
|
||||||
/// Wallet functionality to create transactions.
|
|
||||||
pub mod wallet;
|
|
||||||
/// A minimal asynchronous Bitcoin RPC client.
|
|
||||||
#[cfg(feature = "std")]
|
|
||||||
pub mod rpc;
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests;
|
|
||||||
@@ -1,226 +0,0 @@
|
|||||||
use core::fmt::Debug;
|
|
||||||
use std::collections::HashSet;
|
|
||||||
|
|
||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
use serde::{Deserialize, de::DeserializeOwned};
|
|
||||||
use serde_json::json;
|
|
||||||
|
|
||||||
use simple_request::{hyper, Request, Client};
|
|
||||||
|
|
||||||
use bitcoin::{
|
|
||||||
hashes::{Hash, hex::FromHex},
|
|
||||||
consensus::encode,
|
|
||||||
Txid, Transaction, BlockHash, Block,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug, Deserialize)]
|
|
||||||
pub struct Error {
|
|
||||||
code: isize,
|
|
||||||
message: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
|
||||||
#[serde(untagged)]
|
|
||||||
enum RpcResponse<T> {
|
|
||||||
Ok { result: T },
|
|
||||||
Err { error: Error },
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A minimal asynchronous Bitcoin RPC client.
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct Rpc {
|
|
||||||
client: Client,
|
|
||||||
url: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug, Error)]
|
|
||||||
pub enum RpcError {
|
|
||||||
#[error("couldn't connect to node")]
|
|
||||||
ConnectionError,
|
|
||||||
#[error("request had an error: {0:?}")]
|
|
||||||
RequestError(Error),
|
|
||||||
#[error("node replied with invalid JSON")]
|
|
||||||
InvalidJson(serde_json::error::Category),
|
|
||||||
#[error("node sent an invalid response ({0})")]
|
|
||||||
InvalidResponse(&'static str),
|
|
||||||
#[error("node was missing expected methods")]
|
|
||||||
MissingMethods(HashSet<&'static str>),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Rpc {
|
|
||||||
/// Create a new connection to a Bitcoin RPC.
|
|
||||||
///
|
|
||||||
/// An RPC call is performed to ensure the node is reachable (and that an invalid URL wasn't
|
|
||||||
/// provided).
|
|
||||||
///
|
|
||||||
/// Additionally, a set of expected methods is checked to be offered by the Bitcoin RPC. If these
|
|
||||||
/// methods aren't provided, an error with the missing methods is returned. This ensures all RPC
|
|
||||||
/// routes explicitly provided by this library are at least possible.
|
|
||||||
///
|
|
||||||
/// Each individual RPC route may still fail at time-of-call, regardless of the arguments
|
|
||||||
/// provided to this library, if the RPC has an incompatible argument layout. That is not checked
|
|
||||||
/// at time of RPC creation.
|
|
||||||
pub async fn new(url: String) -> Result<Rpc, RpcError> {
|
|
||||||
let rpc = Rpc { client: Client::with_connection_pool(), url };
|
|
||||||
|
|
||||||
// Make an RPC request to verify the node is reachable and sane
|
|
||||||
let res: String = rpc.rpc_call("help", json!([])).await?;
|
|
||||||
|
|
||||||
// Verify all methods we expect are present
|
|
||||||
// If we had a more expanded RPC, due to differences in RPC versions, it wouldn't make sense to
|
|
||||||
// error if all methods weren't present
|
|
||||||
// We only provide a very minimal set of methods which have been largely consistent, hence why
|
|
||||||
// this is sane
|
|
||||||
let mut expected_methods = HashSet::from([
|
|
||||||
"help",
|
|
||||||
"getblockcount",
|
|
||||||
"getblockhash",
|
|
||||||
"getblockheader",
|
|
||||||
"getblock",
|
|
||||||
"sendrawtransaction",
|
|
||||||
"getrawtransaction",
|
|
||||||
]);
|
|
||||||
for line in res.split('\n') {
|
|
||||||
// This doesn't check if the arguments are as expected
|
|
||||||
// This is due to Bitcoin supporting a large amount of optional arguments, which
|
|
||||||
// occasionally change, with their own mechanism of text documentation, making matching off
|
|
||||||
// it a quite involved task
|
|
||||||
// Instead, once we've confirmed the methods are present, we assume our arguments are aligned
|
|
||||||
// Else we'll error at time of call
|
|
||||||
if expected_methods.remove(line.split(' ').next().unwrap_or("")) &&
|
|
||||||
expected_methods.is_empty()
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !expected_methods.is_empty() {
|
|
||||||
Err(RpcError::MissingMethods(expected_methods))?;
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(rpc)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Perform an arbitrary RPC call.
|
|
||||||
pub async fn rpc_call<Response: DeserializeOwned + Debug>(
|
|
||||||
&self,
|
|
||||||
method: &str,
|
|
||||||
params: serde_json::Value,
|
|
||||||
) -> Result<Response, RpcError> {
|
|
||||||
let mut request = Request::from(
|
|
||||||
hyper::Request::post(&self.url)
|
|
||||||
.header("Content-Type", "application/json")
|
|
||||||
.body(
|
|
||||||
serde_json::to_vec(&json!({ "jsonrpc": "2.0", "method": method, "params": params }))
|
|
||||||
.unwrap()
|
|
||||||
.into(),
|
|
||||||
)
|
|
||||||
.unwrap(),
|
|
||||||
);
|
|
||||||
request.with_basic_auth();
|
|
||||||
let mut res = self
|
|
||||||
.client
|
|
||||||
.request(request)
|
|
||||||
.await
|
|
||||||
.map_err(|_| RpcError::ConnectionError)?
|
|
||||||
.body()
|
|
||||||
.await
|
|
||||||
.map_err(|_| RpcError::ConnectionError)?;
|
|
||||||
|
|
||||||
let res: RpcResponse<Response> =
|
|
||||||
serde_json::from_reader(&mut res).map_err(|e| RpcError::InvalidJson(e.classify()))?;
|
|
||||||
match res {
|
|
||||||
RpcResponse::Ok { result } => Ok(result),
|
|
||||||
RpcResponse::Err { error } => Err(RpcError::RequestError(error)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the latest block's number.
|
|
||||||
///
|
|
||||||
/// The genesis block's 'number' is zero. They increment from there.
|
|
||||||
pub async fn get_latest_block_number(&self) -> Result<usize, RpcError> {
|
|
||||||
// getblockcount doesn't return the amount of blocks on the current chain, yet the "height"
|
|
||||||
// of the current chain. The "height" of the current chain is defined as the "height" of the
|
|
||||||
// tip block of the current chain. The "height" of a block is defined as the amount of blocks
|
|
||||||
// present when the block was created. Accordingly, the genesis block has height 0, and
|
|
||||||
// getblockcount will return 0 when it's only the only block, despite their being one block.
|
|
||||||
self.rpc_call("getblockcount", json!([])).await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the hash of a block by the block's number.
|
|
||||||
pub async fn get_block_hash(&self, number: usize) -> Result<[u8; 32], RpcError> {
|
|
||||||
let mut hash = self
|
|
||||||
.rpc_call::<BlockHash>("getblockhash", json!([number]))
|
|
||||||
.await?
|
|
||||||
.as_raw_hash()
|
|
||||||
.to_byte_array();
|
|
||||||
// bitcoin stores the inner bytes in reverse order.
|
|
||||||
hash.reverse();
|
|
||||||
Ok(hash)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get a block's number by its hash.
|
|
||||||
pub async fn get_block_number(&self, hash: &[u8; 32]) -> Result<usize, RpcError> {
|
|
||||||
#[derive(Deserialize, Debug)]
|
|
||||||
struct Number {
|
|
||||||
height: usize,
|
|
||||||
}
|
|
||||||
Ok(self.rpc_call::<Number>("getblockheader", json!([hex::encode(hash)])).await?.height)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get a block by its hash.
|
|
||||||
pub async fn get_block(&self, hash: &[u8; 32]) -> Result<Block, RpcError> {
|
|
||||||
let hex = self.rpc_call::<String>("getblock", json!([hex::encode(hash), 0])).await?;
|
|
||||||
let bytes: Vec<u8> = FromHex::from_hex(&hex)
|
|
||||||
.map_err(|_| RpcError::InvalidResponse("node didn't use hex to encode the block"))?;
|
|
||||||
let block: Block = encode::deserialize(&bytes)
|
|
||||||
.map_err(|_| RpcError::InvalidResponse("node sent an improperly serialized block"))?;
|
|
||||||
|
|
||||||
let mut block_hash = *block.block_hash().as_raw_hash().as_byte_array();
|
|
||||||
block_hash.reverse();
|
|
||||||
if hash != &block_hash {
|
|
||||||
Err(RpcError::InvalidResponse("node replied with a different block"))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(block)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Publish a transaction.
|
|
||||||
pub async fn send_raw_transaction(&self, tx: &Transaction) -> Result<Txid, RpcError> {
|
|
||||||
let txid = match self.rpc_call("sendrawtransaction", json!([encode::serialize_hex(tx)])).await {
|
|
||||||
Ok(txid) => txid,
|
|
||||||
Err(e) => {
|
|
||||||
// A const from Bitcoin's bitcoin/src/rpc/protocol.h
|
|
||||||
const RPC_VERIFY_ALREADY_IN_CHAIN: isize = -27;
|
|
||||||
// If this was already successfully published, consider this having succeeded
|
|
||||||
if let RpcError::RequestError(Error { code, .. }) = e {
|
|
||||||
if code == RPC_VERIFY_ALREADY_IN_CHAIN {
|
|
||||||
return Ok(tx.txid());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e)?
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if txid != tx.txid() {
|
|
||||||
Err(RpcError::InvalidResponse("returned TX ID inequals calculated TX ID"))?;
|
|
||||||
}
|
|
||||||
Ok(txid)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get a transaction by its hash.
|
|
||||||
pub async fn get_transaction(&self, hash: &[u8; 32]) -> Result<Transaction, RpcError> {
|
|
||||||
let hex = self.rpc_call::<String>("getrawtransaction", json!([hex::encode(hash)])).await?;
|
|
||||||
let bytes: Vec<u8> = FromHex::from_hex(&hex)
|
|
||||||
.map_err(|_| RpcError::InvalidResponse("node didn't use hex to encode the transaction"))?;
|
|
||||||
let tx: Transaction = encode::deserialize(&bytes)
|
|
||||||
.map_err(|_| RpcError::InvalidResponse("node sent an improperly serialized transaction"))?;
|
|
||||||
|
|
||||||
let mut tx_hash = *tx.txid().as_raw_hash().as_byte_array();
|
|
||||||
tx_hash.reverse();
|
|
||||||
if hash != &tx_hash {
|
|
||||||
Err(RpcError::InvalidResponse("node replied with a different transaction"))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(tx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
use rand_core::OsRng;
|
|
||||||
|
|
||||||
use secp256k1::{Secp256k1 as BContext, Message, schnorr::Signature};
|
|
||||||
|
|
||||||
use k256::Scalar;
|
|
||||||
use transcript::{Transcript, RecommendedTranscript};
|
|
||||||
use frost::{
|
|
||||||
curve::Secp256k1,
|
|
||||||
Participant,
|
|
||||||
tests::{algorithm_machines, key_gen, sign},
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
bitcoin::hashes::{Hash as HashTrait, sha256::Hash},
|
|
||||||
crypto::{x_only, make_even, Schnorr},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_algorithm() {
|
|
||||||
let mut keys = key_gen::<_, Secp256k1>(&mut OsRng);
|
|
||||||
const MESSAGE: &[u8] = b"Hello, World!";
|
|
||||||
|
|
||||||
for keys in keys.values_mut() {
|
|
||||||
let (_, offset) = make_even(keys.group_key());
|
|
||||||
*keys = keys.offset(Scalar::from(offset));
|
|
||||||
}
|
|
||||||
|
|
||||||
let algo =
|
|
||||||
Schnorr::<RecommendedTranscript>::new(RecommendedTranscript::new(b"bitcoin-serai sign test"));
|
|
||||||
let sig = sign(
|
|
||||||
&mut OsRng,
|
|
||||||
&algo,
|
|
||||||
keys.clone(),
|
|
||||||
algorithm_machines(&mut OsRng, &algo, &keys),
|
|
||||||
Hash::hash(MESSAGE).as_ref(),
|
|
||||||
);
|
|
||||||
|
|
||||||
BContext::new()
|
|
||||||
.verify_schnorr(
|
|
||||||
&Signature::from_slice(&sig)
|
|
||||||
.expect("couldn't convert produced signature to secp256k1::Signature"),
|
|
||||||
&Message::from(Hash::hash(MESSAGE)),
|
|
||||||
&x_only(&keys[&Participant::new(1).unwrap()].group_key()),
|
|
||||||
)
|
|
||||||
.unwrap()
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
mod crypto;
|
|
||||||
@@ -1,188 +0,0 @@
|
|||||||
use std_shims::{
|
|
||||||
vec::Vec,
|
|
||||||
collections::HashMap,
|
|
||||||
io::{self, Write},
|
|
||||||
};
|
|
||||||
#[cfg(feature = "std")]
|
|
||||||
use std_shims::io::Read;
|
|
||||||
|
|
||||||
use k256::{
|
|
||||||
elliptic_curve::sec1::{Tag, ToEncodedPoint},
|
|
||||||
Scalar, ProjectivePoint,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[cfg(feature = "std")]
|
|
||||||
use frost::{
|
|
||||||
curve::{Ciphersuite, Secp256k1},
|
|
||||||
ThresholdKeys,
|
|
||||||
};
|
|
||||||
|
|
||||||
use bitcoin::{
|
|
||||||
consensus::encode::serialize, key::TweakedPublicKey, address::Payload, OutPoint, ScriptBuf,
|
|
||||||
TxOut, Transaction, Block,
|
|
||||||
};
|
|
||||||
#[cfg(feature = "std")]
|
|
||||||
use bitcoin::consensus::encode::Decodable;
|
|
||||||
|
|
||||||
use crate::crypto::x_only;
|
|
||||||
#[cfg(feature = "std")]
|
|
||||||
use crate::crypto::make_even;
|
|
||||||
|
|
||||||
#[cfg(feature = "std")]
|
|
||||||
mod send;
|
|
||||||
#[cfg(feature = "std")]
|
|
||||||
pub use send::*;
|
|
||||||
|
|
||||||
/// Tweak keys to ensure they're usable with Bitcoin.
|
|
||||||
///
|
|
||||||
/// Taproot keys, which these keys are used as, must be even. This offsets the keys until they're
|
|
||||||
/// even.
|
|
||||||
#[cfg(feature = "std")]
|
|
||||||
pub fn tweak_keys(keys: &ThresholdKeys<Secp256k1>) -> ThresholdKeys<Secp256k1> {
|
|
||||||
let (_, offset) = make_even(keys.group_key());
|
|
||||||
keys.offset(Scalar::from(offset))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return the Taproot address payload for a public key.
|
|
||||||
///
|
|
||||||
/// If the key is odd, this will return None.
|
|
||||||
pub fn address_payload(key: ProjectivePoint) -> Option<Payload> {
|
|
||||||
if key.to_encoded_point(true).tag() != Tag::CompressedEvenY {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(Payload::p2tr_tweaked(TweakedPublicKey::dangerous_assume_tweaked(x_only(&key))))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A spendable output.
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
|
||||||
pub struct ReceivedOutput {
|
|
||||||
// The scalar offset to obtain the key usable to spend this output.
|
|
||||||
offset: Scalar,
|
|
||||||
// The output to spend.
|
|
||||||
output: TxOut,
|
|
||||||
// The TX ID and vout of the output to spend.
|
|
||||||
outpoint: OutPoint,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ReceivedOutput {
|
|
||||||
/// The offset for this output.
|
|
||||||
pub fn offset(&self) -> Scalar {
|
|
||||||
self.offset
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The Bitcoin output for this output.
|
|
||||||
pub fn output(&self) -> &TxOut {
|
|
||||||
&self.output
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The outpoint for this output.
|
|
||||||
pub fn outpoint(&self) -> &OutPoint {
|
|
||||||
&self.outpoint
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The value of this output.
|
|
||||||
pub fn value(&self) -> u64 {
|
|
||||||
self.output.value.to_sat()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read a ReceivedOutput from a generic satisfying Read.
|
|
||||||
#[cfg(feature = "std")]
|
|
||||||
pub fn read<R: Read>(r: &mut R) -> io::Result<ReceivedOutput> {
|
|
||||||
Ok(ReceivedOutput {
|
|
||||||
offset: Secp256k1::read_F(r)?,
|
|
||||||
output: TxOut::consensus_decode(r).map_err(|_| io::Error::other("invalid TxOut"))?,
|
|
||||||
outpoint: OutPoint::consensus_decode(r).map_err(|_| io::Error::other("invalid OutPoint"))?,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Write a ReceivedOutput to a generic satisfying Write.
|
|
||||||
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
|
||||||
w.write_all(&self.offset.to_bytes())?;
|
|
||||||
w.write_all(&serialize(&self.output))?;
|
|
||||||
w.write_all(&serialize(&self.outpoint))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Serialize a ReceivedOutput to a `Vec<u8>`.
|
|
||||||
pub fn serialize(&self) -> Vec<u8> {
|
|
||||||
let mut res = Vec::new();
|
|
||||||
self.write(&mut res).unwrap();
|
|
||||||
res
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A transaction scanner capable of being used with HDKD schemes.
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct Scanner {
|
|
||||||
key: ProjectivePoint,
|
|
||||||
scripts: HashMap<ScriptBuf, Scalar>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Scanner {
|
|
||||||
/// Construct a Scanner for a key.
|
|
||||||
///
|
|
||||||
/// Returns None if this key can't be scanned for.
|
|
||||||
pub fn new(key: ProjectivePoint) -> Option<Scanner> {
|
|
||||||
let mut scripts = HashMap::new();
|
|
||||||
scripts.insert(address_payload(key)?.script_pubkey(), Scalar::ZERO);
|
|
||||||
Some(Scanner { key, scripts })
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Register an offset to scan for.
|
|
||||||
///
|
|
||||||
/// Due to Bitcoin's requirement that points are even, not every offset may be used.
|
|
||||||
/// If an offset isn't usable, it will be incremented until it is. If this offset is already
|
|
||||||
/// present, None is returned. Else, Some(offset) will be, with the used offset.
|
|
||||||
///
|
|
||||||
/// This means offsets are surjective, not bijective, and the order offsets are registered in
|
|
||||||
/// may determine the validity of future offsets.
|
|
||||||
pub fn register_offset(&mut self, mut offset: Scalar) -> Option<Scalar> {
|
|
||||||
// This loop will terminate as soon as an even point is found, with any point having a ~50%
|
|
||||||
// chance of being even
|
|
||||||
// That means this should terminate within a very small amount of iterations
|
|
||||||
loop {
|
|
||||||
match address_payload(self.key + (ProjectivePoint::GENERATOR * offset)) {
|
|
||||||
Some(address) => {
|
|
||||||
let script = address.script_pubkey();
|
|
||||||
if self.scripts.contains_key(&script) {
|
|
||||||
None?;
|
|
||||||
}
|
|
||||||
self.scripts.insert(script, offset);
|
|
||||||
return Some(offset);
|
|
||||||
}
|
|
||||||
None => offset += Scalar::ONE,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Scan a transaction.
|
|
||||||
pub fn scan_transaction(&self, tx: &Transaction) -> Vec<ReceivedOutput> {
|
|
||||||
let mut res = Vec::new();
|
|
||||||
for (vout, output) in tx.output.iter().enumerate() {
|
|
||||||
// If the vout index exceeds 2**32, stop scanning outputs
|
|
||||||
let Ok(vout) = u32::try_from(vout) else { break };
|
|
||||||
|
|
||||||
if let Some(offset) = self.scripts.get(&output.script_pubkey) {
|
|
||||||
res.push(ReceivedOutput {
|
|
||||||
offset: *offset,
|
|
||||||
output: output.clone(),
|
|
||||||
outpoint: OutPoint::new(tx.txid(), vout),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
res
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Scan a block.
|
|
||||||
///
|
|
||||||
/// This will also scan the coinbase transaction which is bound by maturity. If received outputs
|
|
||||||
/// must be immediately spendable, a post-processing pass is needed to remove those outputs.
|
|
||||||
/// Alternatively, scan_transaction can be called on `block.txdata[1 ..]`.
|
|
||||||
pub fn scan_block(&self, block: &Block) -> Vec<ReceivedOutput> {
|
|
||||||
let mut res = Vec::new();
|
|
||||||
for tx in &block.txdata {
|
|
||||||
res.extend(self.scan_transaction(tx));
|
|
||||||
}
|
|
||||||
res
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,446 +0,0 @@
|
|||||||
use std_shims::{
|
|
||||||
io::{self, Read},
|
|
||||||
collections::HashMap,
|
|
||||||
};
|
|
||||||
|
|
||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
use rand_core::{RngCore, CryptoRng};
|
|
||||||
|
|
||||||
use transcript::{Transcript, RecommendedTranscript};
|
|
||||||
|
|
||||||
use k256::{elliptic_curve::sec1::ToEncodedPoint, Scalar};
|
|
||||||
use frost::{curve::Secp256k1, Participant, ThresholdKeys, FrostError, sign::*};
|
|
||||||
|
|
||||||
use bitcoin::{
|
|
||||||
hashes::Hash,
|
|
||||||
sighash::{TapSighashType, SighashCache, Prevouts},
|
|
||||||
absolute::LockTime,
|
|
||||||
script::{PushBytesBuf, ScriptBuf},
|
|
||||||
transaction::{Version, Transaction},
|
|
||||||
OutPoint, Sequence, Witness, TxIn, Amount, TxOut, Address,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
crypto::Schnorr,
|
|
||||||
wallet::{ReceivedOutput, address_payload},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[rustfmt::skip]
|
|
||||||
// https://github.com/bitcoin/bitcoin/blob/306ccd4927a2efe325c8d84be1bdb79edeb29b04/src/policy/policy.cpp#L26-L63
|
|
||||||
// As the above notes, a lower amount may not be considered dust if contained in a SegWit output
|
|
||||||
// This doesn't bother with delineation due to how marginal these values are, and because it isn't
|
|
||||||
// worth the complexity to implement differentation
|
|
||||||
pub const DUST: u64 = 546;
|
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug, Error)]
|
|
||||||
pub enum TransactionError {
|
|
||||||
#[error("no inputs were specified")]
|
|
||||||
NoInputs,
|
|
||||||
#[error("no outputs were created")]
|
|
||||||
NoOutputs,
|
|
||||||
#[error("a specified payment's amount was less than bitcoin's required minimum")]
|
|
||||||
DustPayment,
|
|
||||||
#[error("too much data was specified")]
|
|
||||||
TooMuchData,
|
|
||||||
#[error("fee was too low to pass the default minimum fee rate")]
|
|
||||||
TooLowFee,
|
|
||||||
#[error("not enough funds for these payments")]
|
|
||||||
NotEnoughFunds,
|
|
||||||
#[error("transaction was too large")]
|
|
||||||
TooLargeTransaction,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A signable transaction, clone-able across attempts.
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
|
||||||
pub struct SignableTransaction {
|
|
||||||
tx: Transaction,
|
|
||||||
offsets: Vec<Scalar>,
|
|
||||||
prevouts: Vec<TxOut>,
|
|
||||||
needed_fee: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SignableTransaction {
|
|
||||||
fn calculate_weight(inputs: usize, payments: &[(Address, u64)], change: Option<&Address>) -> u64 {
|
|
||||||
// Expand this a full transaction in order to use the bitcoin library's weight function
|
|
||||||
let mut tx = Transaction {
|
|
||||||
version: Version(2),
|
|
||||||
lock_time: LockTime::ZERO,
|
|
||||||
input: vec![
|
|
||||||
TxIn {
|
|
||||||
// This is a fixed size
|
|
||||||
// See https://developer.bitcoin.org/reference/transactions.html#raw-transaction-format
|
|
||||||
previous_output: OutPoint::default(),
|
|
||||||
// This is empty for a Taproot spend
|
|
||||||
script_sig: ScriptBuf::new(),
|
|
||||||
// This is fixed size, yet we do use Sequence::MAX
|
|
||||||
sequence: Sequence::MAX,
|
|
||||||
// Our witnesses contains a single 64-byte signature
|
|
||||||
witness: Witness::from_slice(&[vec![0; 64]])
|
|
||||||
};
|
|
||||||
inputs
|
|
||||||
],
|
|
||||||
output: payments
|
|
||||||
.iter()
|
|
||||||
// The payment is a fixed size so we don't have to use it here
|
|
||||||
// The script pub key is not of a fixed size and does have to be used here
|
|
||||||
.map(|payment| TxOut {
|
|
||||||
value: Amount::from_sat(payment.1),
|
|
||||||
script_pubkey: payment.0.script_pubkey(),
|
|
||||||
})
|
|
||||||
.collect(),
|
|
||||||
};
|
|
||||||
if let Some(change) = change {
|
|
||||||
// Use a 0 value since we're currently unsure what the change amount will be, and since
|
|
||||||
// the value is fixed size (so any value could be used here)
|
|
||||||
tx.output.push(TxOut { value: Amount::ZERO, script_pubkey: change.script_pubkey() });
|
|
||||||
}
|
|
||||||
u64::from(tx.weight())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the fee necessary for this transaction to achieve the fee rate specified at
|
|
||||||
/// construction.
|
|
||||||
///
|
|
||||||
/// The actual fee this transaction will use is `sum(inputs) - sum(outputs)`.
|
|
||||||
pub fn needed_fee(&self) -> u64 {
|
|
||||||
self.needed_fee
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the fee this transaction will use.
|
|
||||||
pub fn fee(&self) -> u64 {
|
|
||||||
self.prevouts.iter().map(|prevout| prevout.value.to_sat()).sum::<u64>() -
|
|
||||||
self.tx.output.iter().map(|prevout| prevout.value.to_sat()).sum::<u64>()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a new SignableTransaction.
|
|
||||||
///
|
|
||||||
/// If a change address is specified, any leftover funds will be sent to it if the leftover funds
|
|
||||||
/// exceed the minimum output amount. If a change address isn't specified, all leftover funds
|
|
||||||
/// will become part of the paid fee.
|
|
||||||
///
|
|
||||||
/// If data is specified, an OP_RETURN output will be added with it.
|
|
||||||
pub fn new(
|
|
||||||
mut inputs: Vec<ReceivedOutput>,
|
|
||||||
payments: &[(Address, u64)],
|
|
||||||
change: Option<&Address>,
|
|
||||||
data: Option<Vec<u8>>,
|
|
||||||
fee_per_weight: u64,
|
|
||||||
) -> Result<SignableTransaction, TransactionError> {
|
|
||||||
if inputs.is_empty() {
|
|
||||||
Err(TransactionError::NoInputs)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if payments.is_empty() && change.is_none() && data.is_none() {
|
|
||||||
Err(TransactionError::NoOutputs)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (_, amount) in payments {
|
|
||||||
if *amount < DUST {
|
|
||||||
Err(TransactionError::DustPayment)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if data.as_ref().map_or(0, Vec::len) > 80 {
|
|
||||||
Err(TransactionError::TooMuchData)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let input_sat = inputs.iter().map(|input| input.output.value.to_sat()).sum::<u64>();
|
|
||||||
let offsets = inputs.iter().map(|input| input.offset).collect();
|
|
||||||
let tx_ins = inputs
|
|
||||||
.iter()
|
|
||||||
.map(|input| TxIn {
|
|
||||||
previous_output: input.outpoint,
|
|
||||||
script_sig: ScriptBuf::new(),
|
|
||||||
sequence: Sequence::MAX,
|
|
||||||
witness: Witness::new(),
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
let payment_sat = payments.iter().map(|payment| payment.1).sum::<u64>();
|
|
||||||
let mut tx_outs = payments
|
|
||||||
.iter()
|
|
||||||
.map(|payment| TxOut {
|
|
||||||
value: Amount::from_sat(payment.1),
|
|
||||||
script_pubkey: payment.0.script_pubkey(),
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
// Add the OP_RETURN output
|
|
||||||
if let Some(data) = data {
|
|
||||||
tx_outs.push(TxOut {
|
|
||||||
value: Amount::ZERO,
|
|
||||||
script_pubkey: ScriptBuf::new_op_return(
|
|
||||||
PushBytesBuf::try_from(data)
|
|
||||||
.expect("data didn't fit into PushBytes depsite being checked"),
|
|
||||||
),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut weight = Self::calculate_weight(tx_ins.len(), payments, None);
|
|
||||||
let mut needed_fee = fee_per_weight * weight;
|
|
||||||
|
|
||||||
// "Virtual transaction size" is weight ceildiv 4 per
|
|
||||||
// https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki
|
|
||||||
|
|
||||||
// https://github.com/bitcoin/bitcoin/blob/306ccd4927a2efe325c8d84be1bdb79edeb29b04/
|
|
||||||
// src/policy/policy.cpp#L295-L298
|
|
||||||
// implements this as expected
|
|
||||||
|
|
||||||
// Technically, it takes whatever's greater, the weight or the amount of signature operations
|
|
||||||
// multiplied by DEFAULT_BYTES_PER_SIGOP (20)
|
|
||||||
// We only use 1 signature per input, and our inputs have a weight exceeding 20
|
|
||||||
// Accordingly, our inputs' weight will always be greater than the cost of the signature ops
|
|
||||||
let vsize = weight.div_ceil(4);
|
|
||||||
debug_assert_eq!(
|
|
||||||
u64::try_from(bitcoin::policy::get_virtual_tx_size(
|
|
||||||
weight.try_into().unwrap(),
|
|
||||||
tx_ins.len().try_into().unwrap()
|
|
||||||
))
|
|
||||||
.unwrap(),
|
|
||||||
vsize
|
|
||||||
);
|
|
||||||
// Technically, if there isn't change, this TX may still pay enough of a fee to pass the
|
|
||||||
// minimum fee. Such edge cases aren't worth programming when they go against intent, as the
|
|
||||||
// specified fee rate is too low to be valid
|
|
||||||
// bitcoin::policy::DEFAULT_MIN_RELAY_TX_FEE is in sats/kilo-vbyte
|
|
||||||
if needed_fee < ((u64::from(bitcoin::policy::DEFAULT_MIN_RELAY_TX_FEE) * vsize) / 1000) {
|
|
||||||
Err(TransactionError::TooLowFee)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if input_sat < (payment_sat + needed_fee) {
|
|
||||||
Err(TransactionError::NotEnoughFunds)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there's a change address, check if there's change to give it
|
|
||||||
if let Some(change) = change {
|
|
||||||
let weight_with_change = Self::calculate_weight(tx_ins.len(), payments, Some(change));
|
|
||||||
let fee_with_change = fee_per_weight * weight_with_change;
|
|
||||||
if let Some(value) = input_sat.checked_sub(payment_sat + fee_with_change) {
|
|
||||||
if value >= DUST {
|
|
||||||
tx_outs
|
|
||||||
.push(TxOut { value: Amount::from_sat(value), script_pubkey: change.script_pubkey() });
|
|
||||||
weight = weight_with_change;
|
|
||||||
needed_fee = fee_with_change;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if tx_outs.is_empty() {
|
|
||||||
Err(TransactionError::NoOutputs)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if weight > u64::from(bitcoin::policy::MAX_STANDARD_TX_WEIGHT) {
|
|
||||||
Err(TransactionError::TooLargeTransaction)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(SignableTransaction {
|
|
||||||
tx: Transaction {
|
|
||||||
version: Version(2),
|
|
||||||
lock_time: LockTime::ZERO,
|
|
||||||
input: tx_ins,
|
|
||||||
output: tx_outs,
|
|
||||||
},
|
|
||||||
offsets,
|
|
||||||
prevouts: inputs.drain(..).map(|input| input.output).collect(),
|
|
||||||
needed_fee,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the TX ID of the transaction this will create.
|
|
||||||
pub fn txid(&self) -> [u8; 32] {
|
|
||||||
let mut res = self.tx.txid().to_byte_array();
|
|
||||||
res.reverse();
|
|
||||||
res
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the outputs this transaction will create.
|
|
||||||
pub fn outputs(&self) -> &[TxOut] {
|
|
||||||
&self.tx.output
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a multisig machine for this transaction.
|
|
||||||
///
|
|
||||||
/// Returns None if the wrong keys are used.
|
|
||||||
pub fn multisig(
|
|
||||||
self,
|
|
||||||
keys: &ThresholdKeys<Secp256k1>,
|
|
||||||
mut transcript: RecommendedTranscript,
|
|
||||||
) -> Option<TransactionMachine> {
|
|
||||||
transcript.domain_separate(b"bitcoin_transaction");
|
|
||||||
transcript.append_message(b"root_key", keys.group_key().to_encoded_point(true).as_bytes());
|
|
||||||
|
|
||||||
// Transcript the inputs and outputs
|
|
||||||
let tx = &self.tx;
|
|
||||||
for input in &tx.input {
|
|
||||||
transcript.append_message(b"input_hash", input.previous_output.txid);
|
|
||||||
transcript.append_message(b"input_output_index", input.previous_output.vout.to_le_bytes());
|
|
||||||
}
|
|
||||||
for payment in &tx.output {
|
|
||||||
transcript.append_message(b"output_script", payment.script_pubkey.as_bytes());
|
|
||||||
transcript.append_message(b"output_amount", payment.value.to_sat().to_le_bytes());
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut sigs = vec![];
|
|
||||||
for i in 0 .. tx.input.len() {
|
|
||||||
let mut transcript = transcript.clone();
|
|
||||||
// This unwrap is safe since any transaction with this many inputs violates the maximum
|
|
||||||
// size allowed under standards, which this lib will error on creation of
|
|
||||||
transcript.append_message(b"signing_input", u32::try_from(i).unwrap().to_le_bytes());
|
|
||||||
|
|
||||||
let offset = keys.clone().offset(self.offsets[i]);
|
|
||||||
if address_payload(offset.group_key())?.script_pubkey() != self.prevouts[i].script_pubkey {
|
|
||||||
None?;
|
|
||||||
}
|
|
||||||
|
|
||||||
sigs.push(AlgorithmMachine::new(
|
|
||||||
Schnorr::new(transcript),
|
|
||||||
keys.clone().offset(self.offsets[i]),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(TransactionMachine { tx: self, sigs })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A FROST signing machine to produce a Bitcoin transaction.
|
|
||||||
///
|
|
||||||
/// This does not support caching its preprocess. When sign is called, the message must be empty.
|
|
||||||
/// This will panic if either `cache` is called or the message isn't empty.
|
|
||||||
pub struct TransactionMachine {
|
|
||||||
tx: SignableTransaction,
|
|
||||||
sigs: Vec<AlgorithmMachine<Secp256k1, Schnorr<RecommendedTranscript>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PreprocessMachine for TransactionMachine {
|
|
||||||
type Preprocess = Vec<Preprocess<Secp256k1, ()>>;
|
|
||||||
type Signature = Transaction;
|
|
||||||
type SignMachine = TransactionSignMachine;
|
|
||||||
|
|
||||||
fn preprocess<R: RngCore + CryptoRng>(
|
|
||||||
mut self,
|
|
||||||
rng: &mut R,
|
|
||||||
) -> (Self::SignMachine, Self::Preprocess) {
|
|
||||||
let mut preprocesses = Vec::with_capacity(self.sigs.len());
|
|
||||||
let sigs = self
|
|
||||||
.sigs
|
|
||||||
.drain(..)
|
|
||||||
.map(|sig| {
|
|
||||||
let (sig, preprocess) = sig.preprocess(rng);
|
|
||||||
preprocesses.push(preprocess);
|
|
||||||
sig
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
(TransactionSignMachine { tx: self.tx, sigs }, preprocesses)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct TransactionSignMachine {
|
|
||||||
tx: SignableTransaction,
|
|
||||||
sigs: Vec<AlgorithmSignMachine<Secp256k1, Schnorr<RecommendedTranscript>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SignMachine<Transaction> for TransactionSignMachine {
|
|
||||||
type Params = ();
|
|
||||||
type Keys = ThresholdKeys<Secp256k1>;
|
|
||||||
type Preprocess = Vec<Preprocess<Secp256k1, ()>>;
|
|
||||||
type SignatureShare = Vec<SignatureShare<Secp256k1>>;
|
|
||||||
type SignatureMachine = TransactionSignatureMachine;
|
|
||||||
|
|
||||||
fn cache(self) -> CachedPreprocess {
|
|
||||||
unimplemented!(
|
|
||||||
"Bitcoin transactions don't support caching their preprocesses due to {}",
|
|
||||||
"being already bound to a specific transaction"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn from_cache(
|
|
||||||
(): (),
|
|
||||||
_: ThresholdKeys<Secp256k1>,
|
|
||||||
_: CachedPreprocess,
|
|
||||||
) -> (Self, Self::Preprocess) {
|
|
||||||
unimplemented!(
|
|
||||||
"Bitcoin transactions don't support caching their preprocesses due to {}",
|
|
||||||
"being already bound to a specific transaction"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_preprocess<R: Read>(&self, reader: &mut R) -> io::Result<Self::Preprocess> {
|
|
||||||
self.sigs.iter().map(|sig| sig.read_preprocess(reader)).collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn sign(
|
|
||||||
mut self,
|
|
||||||
commitments: HashMap<Participant, Self::Preprocess>,
|
|
||||||
msg: &[u8],
|
|
||||||
) -> Result<(TransactionSignatureMachine, Self::SignatureShare), FrostError> {
|
|
||||||
if !msg.is_empty() {
|
|
||||||
panic!("message was passed to the TransactionMachine when it generates its own");
|
|
||||||
}
|
|
||||||
|
|
||||||
let commitments = (0 .. self.sigs.len())
|
|
||||||
.map(|c| {
|
|
||||||
commitments
|
|
||||||
.iter()
|
|
||||||
.map(|(l, commitments)| (*l, commitments[c].clone()))
|
|
||||||
.collect::<HashMap<_, _>>()
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
let mut cache = SighashCache::new(&self.tx.tx);
|
|
||||||
// Sign committing to all inputs
|
|
||||||
let prevouts = Prevouts::All(&self.tx.prevouts);
|
|
||||||
|
|
||||||
let mut shares = Vec::with_capacity(self.sigs.len());
|
|
||||||
let sigs = self
|
|
||||||
.sigs
|
|
||||||
.drain(..)
|
|
||||||
.enumerate()
|
|
||||||
.map(|(i, sig)| {
|
|
||||||
let (sig, share) = sig.sign(
|
|
||||||
commitments[i].clone(),
|
|
||||||
cache
|
|
||||||
.taproot_key_spend_signature_hash(i, &prevouts, TapSighashType::Default)
|
|
||||||
// This should never happen since the inputs align with the TX the cache was
|
|
||||||
// constructed with, and because i is always < prevouts.len()
|
|
||||||
.expect("taproot_key_spend_signature_hash failed to return a hash")
|
|
||||||
.as_ref(),
|
|
||||||
)?;
|
|
||||||
shares.push(share);
|
|
||||||
Ok(sig)
|
|
||||||
})
|
|
||||||
.collect::<Result<_, _>>()?;
|
|
||||||
|
|
||||||
Ok((TransactionSignatureMachine { tx: self.tx.tx, sigs }, shares))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct TransactionSignatureMachine {
|
|
||||||
tx: Transaction,
|
|
||||||
sigs: Vec<AlgorithmSignatureMachine<Secp256k1, Schnorr<RecommendedTranscript>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SignatureMachine<Transaction> for TransactionSignatureMachine {
|
|
||||||
type SignatureShare = Vec<SignatureShare<Secp256k1>>;
|
|
||||||
|
|
||||||
fn read_share<R: Read>(&self, reader: &mut R) -> io::Result<Self::SignatureShare> {
|
|
||||||
self.sigs.iter().map(|sig| sig.read_share(reader)).collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn complete(
|
|
||||||
mut self,
|
|
||||||
mut shares: HashMap<Participant, Self::SignatureShare>,
|
|
||||||
) -> Result<Transaction, FrostError> {
|
|
||||||
for (input, schnorr) in self.tx.input.iter_mut().zip(self.sigs.drain(..)) {
|
|
||||||
let sig = schnorr.complete(
|
|
||||||
shares.iter_mut().map(|(l, shares)| (*l, shares.remove(0))).collect::<HashMap<_, _>>(),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let mut witness = Witness::new();
|
|
||||||
witness.push(sig);
|
|
||||||
input.witness = witness;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(self.tx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
use bitcoin_serai::{bitcoin::hashes::Hash as HashTrait, rpc::RpcError};
|
|
||||||
|
|
||||||
mod runner;
|
|
||||||
use runner::rpc;
|
|
||||||
|
|
||||||
async_sequential! {
|
|
||||||
async fn test_rpc() {
|
|
||||||
let rpc = rpc().await;
|
|
||||||
|
|
||||||
// Test get_latest_block_number and get_block_hash by round tripping them
|
|
||||||
let latest = rpc.get_latest_block_number().await.unwrap();
|
|
||||||
let hash = rpc.get_block_hash(latest).await.unwrap();
|
|
||||||
assert_eq!(rpc.get_block_number(&hash).await.unwrap(), latest);
|
|
||||||
|
|
||||||
// Test this actually is the latest block number by checking asking for the next block's errors
|
|
||||||
assert!(matches!(rpc.get_block_hash(latest + 1).await, Err(RpcError::RequestError(_))));
|
|
||||||
|
|
||||||
// Test get_block by checking the received block's hash matches the request
|
|
||||||
let block = rpc.get_block(&hash).await.unwrap();
|
|
||||||
// Hashes are stored in reverse. It's bs from Satoshi
|
|
||||||
let mut block_hash = *block.block_hash().as_raw_hash().as_byte_array();
|
|
||||||
block_hash.reverse();
|
|
||||||
assert_eq!(hash, block_hash);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
use std::sync::OnceLock;
|
|
||||||
|
|
||||||
use bitcoin_serai::rpc::Rpc;
|
|
||||||
|
|
||||||
use tokio::sync::Mutex;
|
|
||||||
|
|
||||||
static SEQUENTIAL_CELL: OnceLock<Mutex<()>> = OnceLock::new();
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
pub fn SEQUENTIAL() -> &'static Mutex<()> {
|
|
||||||
SEQUENTIAL_CELL.get_or_init(|| Mutex::new(()))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub(crate) async fn rpc() -> Rpc {
|
|
||||||
let rpc = Rpc::new("http://serai:seraidex@127.0.0.1:18443".to_string()).await.unwrap();
|
|
||||||
|
|
||||||
// If this node has already been interacted with, clear its chain
|
|
||||||
if rpc.get_latest_block_number().await.unwrap() > 0 {
|
|
||||||
rpc
|
|
||||||
.rpc_call(
|
|
||||||
"invalidateblock",
|
|
||||||
serde_json::json!([hex::encode(rpc.get_block_hash(1).await.unwrap())]),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
rpc
|
|
||||||
}
|
|
||||||
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! async_sequential {
|
|
||||||
($(async fn $name: ident() $body: block)*) => {
|
|
||||||
$(
|
|
||||||
#[tokio::test]
|
|
||||||
async fn $name() {
|
|
||||||
let guard = runner::SEQUENTIAL().lock().await;
|
|
||||||
let local = tokio::task::LocalSet::new();
|
|
||||||
local.run_until(async move {
|
|
||||||
if let Err(err) = tokio::task::spawn_local(async move { $body }).await {
|
|
||||||
drop(guard);
|
|
||||||
Err(err).unwrap()
|
|
||||||
}
|
|
||||||
}).await;
|
|
||||||
}
|
|
||||||
)*
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,365 +0,0 @@
|
|||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use rand_core::{RngCore, OsRng};
|
|
||||||
|
|
||||||
use transcript::{Transcript, RecommendedTranscript};
|
|
||||||
|
|
||||||
use k256::{
|
|
||||||
elliptic_curve::{
|
|
||||||
group::{ff::Field, Group},
|
|
||||||
sec1::{Tag, ToEncodedPoint},
|
|
||||||
},
|
|
||||||
Scalar, ProjectivePoint,
|
|
||||||
};
|
|
||||||
use frost::{
|
|
||||||
curve::Secp256k1,
|
|
||||||
Participant, ThresholdKeys,
|
|
||||||
tests::{THRESHOLD, key_gen, sign_without_caching},
|
|
||||||
};
|
|
||||||
|
|
||||||
use bitcoin_serai::{
|
|
||||||
bitcoin::{
|
|
||||||
hashes::Hash as HashTrait,
|
|
||||||
blockdata::opcodes::all::OP_RETURN,
|
|
||||||
script::{PushBytesBuf, Instruction, Instructions, Script},
|
|
||||||
address::NetworkChecked,
|
|
||||||
OutPoint, Amount, TxOut, Transaction, Network, Address,
|
|
||||||
},
|
|
||||||
wallet::{
|
|
||||||
tweak_keys, address_payload, ReceivedOutput, Scanner, TransactionError, SignableTransaction,
|
|
||||||
},
|
|
||||||
rpc::Rpc,
|
|
||||||
};
|
|
||||||
|
|
||||||
mod runner;
|
|
||||||
use runner::rpc;
|
|
||||||
|
|
||||||
const FEE: u64 = 20;
|
|
||||||
|
|
||||||
fn is_even(key: ProjectivePoint) -> bool {
|
|
||||||
key.to_encoded_point(true).tag() == Tag::CompressedEvenY
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn send_and_get_output(rpc: &Rpc, scanner: &Scanner, key: ProjectivePoint) -> ReceivedOutput {
|
|
||||||
let block_number = rpc.get_latest_block_number().await.unwrap() + 1;
|
|
||||||
|
|
||||||
rpc
|
|
||||||
.rpc_call::<Vec<String>>(
|
|
||||||
"generatetoaddress",
|
|
||||||
serde_json::json!([
|
|
||||||
1,
|
|
||||||
Address::<NetworkChecked>::new(Network::Regtest, address_payload(key).unwrap())
|
|
||||||
]),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// Mine until maturity
|
|
||||||
rpc
|
|
||||||
.rpc_call::<Vec<String>>(
|
|
||||||
"generatetoaddress",
|
|
||||||
serde_json::json!([100, Address::p2sh(Script::new(), Network::Regtest).unwrap()]),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let block = rpc.get_block(&rpc.get_block_hash(block_number).await.unwrap()).await.unwrap();
|
|
||||||
|
|
||||||
let mut outputs = scanner.scan_block(&block);
|
|
||||||
assert_eq!(outputs, scanner.scan_transaction(&block.txdata[0]));
|
|
||||||
|
|
||||||
assert_eq!(outputs.len(), 1);
|
|
||||||
assert_eq!(outputs[0].outpoint(), &OutPoint::new(block.txdata[0].txid(), 0));
|
|
||||||
assert_eq!(outputs[0].value(), block.txdata[0].output[0].value.to_sat());
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
ReceivedOutput::read::<&[u8]>(&mut outputs[0].serialize().as_ref()).unwrap(),
|
|
||||||
outputs[0]
|
|
||||||
);
|
|
||||||
|
|
||||||
outputs.swap_remove(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn keys() -> (HashMap<Participant, ThresholdKeys<Secp256k1>>, ProjectivePoint) {
|
|
||||||
let mut keys = key_gen(&mut OsRng);
|
|
||||||
for keys in keys.values_mut() {
|
|
||||||
*keys = tweak_keys(keys);
|
|
||||||
}
|
|
||||||
let key = keys.values().next().unwrap().group_key();
|
|
||||||
(keys, key)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn sign(
|
|
||||||
keys: &HashMap<Participant, ThresholdKeys<Secp256k1>>,
|
|
||||||
tx: &SignableTransaction,
|
|
||||||
) -> Transaction {
|
|
||||||
let mut machines = HashMap::new();
|
|
||||||
for i in (1 ..= THRESHOLD).map(|i| Participant::new(i).unwrap()) {
|
|
||||||
machines.insert(
|
|
||||||
i,
|
|
||||||
tx.clone()
|
|
||||||
.multisig(&keys[&i].clone(), RecommendedTranscript::new(b"bitcoin-serai Test Transaction"))
|
|
||||||
.unwrap(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
sign_without_caching(&mut OsRng, machines, &[])
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_tweak_keys() {
|
|
||||||
let mut even = false;
|
|
||||||
let mut odd = false;
|
|
||||||
|
|
||||||
// Generate keys until we get an even set and an odd set
|
|
||||||
while !(even && odd) {
|
|
||||||
let mut keys = key_gen(&mut OsRng).drain().next().unwrap().1;
|
|
||||||
if is_even(keys.group_key()) {
|
|
||||||
// Tweaking should do nothing
|
|
||||||
assert_eq!(tweak_keys(&keys).group_key(), keys.group_key());
|
|
||||||
|
|
||||||
even = true;
|
|
||||||
} else {
|
|
||||||
let tweaked = tweak_keys(&keys).group_key();
|
|
||||||
assert_ne!(tweaked, keys.group_key());
|
|
||||||
// Tweaking should produce an even key
|
|
||||||
assert!(is_even(tweaked));
|
|
||||||
|
|
||||||
// Verify it uses the smallest possible offset
|
|
||||||
while keys.group_key().to_encoded_point(true).tag() == Tag::CompressedOddY {
|
|
||||||
keys = keys.offset(Scalar::ONE);
|
|
||||||
}
|
|
||||||
assert_eq!(tweaked, keys.group_key());
|
|
||||||
|
|
||||||
odd = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async_sequential! {
|
|
||||||
async fn test_scanner() {
|
|
||||||
// Test Scanners are creatable for even keys.
|
|
||||||
for _ in 0 .. 128 {
|
|
||||||
let key = ProjectivePoint::random(&mut OsRng);
|
|
||||||
assert_eq!(Scanner::new(key).is_some(), is_even(key));
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut key = ProjectivePoint::random(&mut OsRng);
|
|
||||||
while !is_even(key) {
|
|
||||||
key += ProjectivePoint::GENERATOR;
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
let mut scanner = Scanner::new(key).unwrap();
|
|
||||||
for _ in 0 .. 128 {
|
|
||||||
let mut offset = Scalar::random(&mut OsRng);
|
|
||||||
let registered = scanner.register_offset(offset).unwrap();
|
|
||||||
// Registering this again should return None
|
|
||||||
assert!(scanner.register_offset(offset).is_none());
|
|
||||||
|
|
||||||
// We can only register offsets resulting in even keys
|
|
||||||
// Make this even
|
|
||||||
while !is_even(key + (ProjectivePoint::GENERATOR * offset)) {
|
|
||||||
offset += Scalar::ONE;
|
|
||||||
}
|
|
||||||
// Ensure it matches the registered offset
|
|
||||||
assert_eq!(registered, offset);
|
|
||||||
// Assert registering this again fails
|
|
||||||
assert!(scanner.register_offset(offset).is_none());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let rpc = rpc().await;
|
|
||||||
let mut scanner = Scanner::new(key).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(send_and_get_output(&rpc, &scanner, key).await.offset(), Scalar::ZERO);
|
|
||||||
|
|
||||||
// Register an offset and test receiving to it
|
|
||||||
let offset = scanner.register_offset(Scalar::random(&mut OsRng)).unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
send_and_get_output(&rpc, &scanner, key + (ProjectivePoint::GENERATOR * offset))
|
|
||||||
.await
|
|
||||||
.offset(),
|
|
||||||
offset
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn test_transaction_errors() {
|
|
||||||
let (_, key) = keys();
|
|
||||||
|
|
||||||
let rpc = rpc().await;
|
|
||||||
let scanner = Scanner::new(key).unwrap();
|
|
||||||
|
|
||||||
let output = send_and_get_output(&rpc, &scanner, key).await;
|
|
||||||
assert_eq!(output.offset(), Scalar::ZERO);
|
|
||||||
|
|
||||||
let inputs = vec![output];
|
|
||||||
let addr = || Address::<NetworkChecked>::new(Network::Regtest, address_payload(key).unwrap());
|
|
||||||
let payments = vec![(addr(), 1000)];
|
|
||||||
|
|
||||||
assert!(SignableTransaction::new(inputs.clone(), &payments, None, None, FEE).is_ok());
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
SignableTransaction::new(vec![], &payments, None, None, FEE),
|
|
||||||
Err(TransactionError::NoInputs)
|
|
||||||
);
|
|
||||||
|
|
||||||
// No change
|
|
||||||
assert!(SignableTransaction::new(inputs.clone(), &[(addr(), 1000)], None, None, FEE).is_ok());
|
|
||||||
// Consolidation TX
|
|
||||||
assert!(SignableTransaction::new(inputs.clone(), &[], Some(&addr()), None, FEE).is_ok());
|
|
||||||
// Data
|
|
||||||
assert!(SignableTransaction::new(inputs.clone(), &[], None, Some(vec![]), FEE).is_ok());
|
|
||||||
// No outputs
|
|
||||||
assert_eq!(
|
|
||||||
SignableTransaction::new(inputs.clone(), &[], None, None, FEE),
|
|
||||||
Err(TransactionError::NoOutputs),
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
SignableTransaction::new(inputs.clone(), &[(addr(), 1)], None, None, FEE),
|
|
||||||
Err(TransactionError::DustPayment),
|
|
||||||
);
|
|
||||||
|
|
||||||
assert!(
|
|
||||||
SignableTransaction::new(inputs.clone(), &payments, None, Some(vec![0; 80]), FEE).is_ok()
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
SignableTransaction::new(inputs.clone(), &payments, None, Some(vec![0; 81]), FEE),
|
|
||||||
Err(TransactionError::TooMuchData),
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
SignableTransaction::new(inputs.clone(), &[], Some(&addr()), None, 0),
|
|
||||||
Err(TransactionError::TooLowFee),
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
SignableTransaction::new(inputs.clone(), &[(addr(), inputs[0].value() * 2)], None, None, FEE),
|
|
||||||
Err(TransactionError::NotEnoughFunds),
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
SignableTransaction::new(inputs, &vec![(addr(), 1000); 10000], None, None, FEE),
|
|
||||||
Err(TransactionError::TooLargeTransaction),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn test_send() {
|
|
||||||
let (keys, key) = keys();
|
|
||||||
|
|
||||||
let rpc = rpc().await;
|
|
||||||
let mut scanner = Scanner::new(key).unwrap();
|
|
||||||
|
|
||||||
// Get inputs, one not offset and one offset
|
|
||||||
let output = send_and_get_output(&rpc, &scanner, key).await;
|
|
||||||
assert_eq!(output.offset(), Scalar::ZERO);
|
|
||||||
|
|
||||||
let offset = scanner.register_offset(Scalar::random(&mut OsRng)).unwrap();
|
|
||||||
let offset_key = key + (ProjectivePoint::GENERATOR * offset);
|
|
||||||
let offset_output = send_and_get_output(&rpc, &scanner, offset_key).await;
|
|
||||||
assert_eq!(offset_output.offset(), offset);
|
|
||||||
|
|
||||||
// Declare payments, change, fee
|
|
||||||
let payments = [
|
|
||||||
(Address::<NetworkChecked>::new(Network::Regtest, address_payload(key).unwrap()), 1005),
|
|
||||||
(Address::<NetworkChecked>::new(Network::Regtest, address_payload(offset_key).unwrap()), 1007)
|
|
||||||
];
|
|
||||||
|
|
||||||
let change_offset = scanner.register_offset(Scalar::random(&mut OsRng)).unwrap();
|
|
||||||
let change_key = key + (ProjectivePoint::GENERATOR * change_offset);
|
|
||||||
let change_addr =
|
|
||||||
Address::<NetworkChecked>::new(Network::Regtest, address_payload(change_key).unwrap());
|
|
||||||
|
|
||||||
// Create and sign the TX
|
|
||||||
let tx = SignableTransaction::new(
|
|
||||||
vec![output.clone(), offset_output.clone()],
|
|
||||||
&payments,
|
|
||||||
Some(&change_addr),
|
|
||||||
None,
|
|
||||||
FEE
|
|
||||||
).unwrap();
|
|
||||||
let needed_fee = tx.needed_fee();
|
|
||||||
let expected_id = tx.txid();
|
|
||||||
let tx = sign(&keys, &tx);
|
|
||||||
|
|
||||||
assert_eq!(tx.output.len(), 3);
|
|
||||||
|
|
||||||
// Ensure we can scan it
|
|
||||||
let outputs = scanner.scan_transaction(&tx);
|
|
||||||
for (o, output) in outputs.iter().enumerate() {
|
|
||||||
assert_eq!(output.outpoint(), &OutPoint::new(tx.txid(), u32::try_from(o).unwrap()));
|
|
||||||
assert_eq!(&ReceivedOutput::read::<&[u8]>(&mut output.serialize().as_ref()).unwrap(), output);
|
|
||||||
}
|
|
||||||
|
|
||||||
assert_eq!(outputs[0].offset(), Scalar::ZERO);
|
|
||||||
assert_eq!(outputs[1].offset(), offset);
|
|
||||||
assert_eq!(outputs[2].offset(), change_offset);
|
|
||||||
|
|
||||||
// Make sure the payments were properly created
|
|
||||||
for ((output, scanned), payment) in tx.output.iter().zip(outputs.iter()).zip(payments.iter()) {
|
|
||||||
assert_eq!(
|
|
||||||
output,
|
|
||||||
&TxOut { script_pubkey: payment.0.script_pubkey(), value: Amount::from_sat(payment.1) },
|
|
||||||
);
|
|
||||||
assert_eq!(scanned.value(), payment.1 );
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make sure the change is correct
|
|
||||||
assert_eq!(needed_fee, u64::from(tx.weight()) * FEE);
|
|
||||||
let input_value = output.value() + offset_output.value();
|
|
||||||
let output_value = tx.output.iter().map(|output| output.value.to_sat()).sum::<u64>();
|
|
||||||
assert_eq!(input_value - output_value, needed_fee);
|
|
||||||
|
|
||||||
let change_amount =
|
|
||||||
input_value - payments.iter().map(|payment| payment.1).sum::<u64>() - needed_fee;
|
|
||||||
assert_eq!(
|
|
||||||
tx.output[2],
|
|
||||||
TxOut { script_pubkey: change_addr.script_pubkey(), value: Amount::from_sat(change_amount) },
|
|
||||||
);
|
|
||||||
|
|
||||||
// This also tests send_raw_transaction and get_transaction, which the RPC test can't
|
|
||||||
// effectively test
|
|
||||||
rpc.send_raw_transaction(&tx).await.unwrap();
|
|
||||||
let mut hash = *tx.txid().as_raw_hash().as_byte_array();
|
|
||||||
hash.reverse();
|
|
||||||
assert_eq!(tx, rpc.get_transaction(&hash).await.unwrap());
|
|
||||||
assert_eq!(expected_id, hash);
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn test_data() {
|
|
||||||
let (keys, key) = keys();
|
|
||||||
|
|
||||||
let rpc = rpc().await;
|
|
||||||
let scanner = Scanner::new(key).unwrap();
|
|
||||||
|
|
||||||
let output = send_and_get_output(&rpc, &scanner, key).await;
|
|
||||||
assert_eq!(output.offset(), Scalar::ZERO);
|
|
||||||
|
|
||||||
let data_len = 60 + usize::try_from(OsRng.next_u64() % 21).unwrap();
|
|
||||||
let mut data = vec![0; data_len];
|
|
||||||
OsRng.fill_bytes(&mut data);
|
|
||||||
|
|
||||||
let tx = sign(
|
|
||||||
&keys,
|
|
||||||
&SignableTransaction::new(
|
|
||||||
vec![output],
|
|
||||||
&[],
|
|
||||||
Some(&Address::<NetworkChecked>::new(Network::Regtest, address_payload(key).unwrap())),
|
|
||||||
Some(data.clone()),
|
|
||||||
FEE
|
|
||||||
).unwrap()
|
|
||||||
);
|
|
||||||
|
|
||||||
assert!(tx.output[0].script_pubkey.is_op_return());
|
|
||||||
let check = |mut instructions: Instructions| {
|
|
||||||
assert_eq!(instructions.next().unwrap().unwrap(), Instruction::Op(OP_RETURN));
|
|
||||||
assert_eq!(
|
|
||||||
instructions.next().unwrap().unwrap(),
|
|
||||||
Instruction::PushBytes(&PushBytesBuf::try_from(data.clone()).unwrap()),
|
|
||||||
);
|
|
||||||
assert!(instructions.next().is_none());
|
|
||||||
};
|
|
||||||
check(tx.output[0].script_pubkey.instructions());
|
|
||||||
check(tx.output[0].script_pubkey.instructions_minimal());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
3
coins/ethereum/.gitignore
vendored
3
coins/ethereum/.gitignore
vendored
@@ -1,3 +0,0 @@
|
|||||||
# solidity build outputs
|
|
||||||
cache
|
|
||||||
artifacts
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "ethereum-serai"
|
|
||||||
version = "0.1.0"
|
|
||||||
description = "An Ethereum library supporting Schnorr signing and on-chain verification"
|
|
||||||
license = "AGPL-3.0-only"
|
|
||||||
repository = "https://github.com/serai-dex/serai/tree/develop/coins/ethereum"
|
|
||||||
authors = ["Luke Parker <lukeparker5132@gmail.com>", "Elizabeth Binks <elizabethjbinks@gmail.com>"]
|
|
||||||
edition = "2021"
|
|
||||||
publish = false
|
|
||||||
rust-version = "1.74"
|
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
|
||||||
all-features = true
|
|
||||||
rustdoc-args = ["--cfg", "docsrs"]
|
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
thiserror = { version = "1", default-features = false }
|
|
||||||
eyre = { version = "0.6", default-features = false }
|
|
||||||
|
|
||||||
sha3 = { version = "0.10", default-features = false, features = ["std"] }
|
|
||||||
|
|
||||||
group = { version = "0.13", default-features = false }
|
|
||||||
k256 = { version = "^0.13.1", default-features = false, features = ["std", "ecdsa"] }
|
|
||||||
frost = { package = "modular-frost", path = "../../crypto/frost", features = ["secp256k1", "tests"] }
|
|
||||||
|
|
||||||
ethers-core = { version = "2", default-features = false }
|
|
||||||
ethers-providers = { version = "2", default-features = false }
|
|
||||||
ethers-contract = { version = "2", default-features = false, features = ["abigen", "providers"] }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
rand_core = { version = "0.6", default-features = false, features = ["std"] }
|
|
||||||
|
|
||||||
hex = { version = "0.4", default-features = false, features = ["std"] }
|
|
||||||
serde = { version = "1", default-features = false, features = ["std"] }
|
|
||||||
serde_json = { version = "1", default-features = false, features = ["std"] }
|
|
||||||
|
|
||||||
sha2 = { version = "0.10", default-features = false, features = ["std"] }
|
|
||||||
|
|
||||||
tokio = { version = "1", features = ["macros"] }
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
AGPL-3.0-only license
|
|
||||||
|
|
||||||
Copyright (c) 2022-2023 Luke Parker
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU Affero General Public License Version 3 as
|
|
||||||
published by the Free Software Foundation.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU Affero General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
# Ethereum
|
|
||||||
|
|
||||||
This package contains Ethereum-related functionality, specifically deploying and
|
|
||||||
interacting with Serai contracts.
|
|
||||||
|
|
||||||
### Dependencies
|
|
||||||
|
|
||||||
- solc
|
|
||||||
- [Foundry](https://github.com/foundry-rs/foundry)
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
fn main() {
|
|
||||||
println!("cargo:rerun-if-changed=contracts");
|
|
||||||
println!("cargo:rerun-if-changed=artifacts");
|
|
||||||
|
|
||||||
#[rustfmt::skip]
|
|
||||||
let args = [
|
|
||||||
"--base-path", ".",
|
|
||||||
"-o", "./artifacts", "--overwrite",
|
|
||||||
"--bin", "--abi",
|
|
||||||
"--optimize",
|
|
||||||
"./contracts/Schnorr.sol"
|
|
||||||
];
|
|
||||||
|
|
||||||
assert!(std::process::Command::new("solc").args(args).status().unwrap().success());
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
//SPDX-License-Identifier: AGPLv3
|
|
||||||
pragma solidity ^0.8.0;
|
|
||||||
|
|
||||||
// see https://github.com/noot/schnorr-verify for implementation details
|
|
||||||
contract Schnorr {
|
|
||||||
// secp256k1 group order
|
|
||||||
uint256 constant public Q =
|
|
||||||
0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141;
|
|
||||||
|
|
||||||
// parity := public key y-coord parity (27 or 28)
|
|
||||||
// px := public key x-coord
|
|
||||||
// message := 32-byte message
|
|
||||||
// s := schnorr signature
|
|
||||||
// e := schnorr signature challenge
|
|
||||||
function verify(
|
|
||||||
uint8 parity,
|
|
||||||
bytes32 px,
|
|
||||||
bytes32 message,
|
|
||||||
bytes32 s,
|
|
||||||
bytes32 e
|
|
||||||
) public view returns (bool) {
|
|
||||||
// ecrecover = (m, v, r, s);
|
|
||||||
bytes32 sp = bytes32(Q - mulmod(uint256(s), uint256(px), Q));
|
|
||||||
bytes32 ep = bytes32(Q - mulmod(uint256(e), uint256(px), Q));
|
|
||||||
|
|
||||||
require(sp != 0);
|
|
||||||
// the ecrecover precompile implementation checks that the `r` and `s`
|
|
||||||
// inputs are non-zero (in this case, `px` and `ep`), thus we don't need to
|
|
||||||
// check if they're zero.will make me
|
|
||||||
address R = ecrecover(sp, parity, px, ep);
|
|
||||||
require(R != address(0), "ecrecover failed");
|
|
||||||
return e == keccak256(
|
|
||||||
abi.encodePacked(R, uint8(parity), px, block.chainid, message)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
use thiserror::Error;
|
|
||||||
use eyre::{eyre, Result};
|
|
||||||
|
|
||||||
use ethers_providers::{Provider, Http};
|
|
||||||
use ethers_contract::abigen;
|
|
||||||
|
|
||||||
use crate::crypto::ProcessedSignature;
|
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
|
||||||
pub enum EthereumError {
|
|
||||||
#[error("failed to verify Schnorr signature")]
|
|
||||||
VerificationError,
|
|
||||||
}
|
|
||||||
|
|
||||||
abigen!(Schnorr, "./artifacts/Schnorr.abi");
|
|
||||||
|
|
||||||
pub async fn call_verify(
|
|
||||||
contract: &Schnorr<Provider<Http>>,
|
|
||||||
params: &ProcessedSignature,
|
|
||||||
) -> Result<()> {
|
|
||||||
if contract
|
|
||||||
.verify(
|
|
||||||
params.parity + 27,
|
|
||||||
params.px.to_bytes().into(),
|
|
||||||
params.message,
|
|
||||||
params.s.to_bytes().into(),
|
|
||||||
params.e.to_bytes().into(),
|
|
||||||
)
|
|
||||||
.call()
|
|
||||||
.await?
|
|
||||||
{
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(eyre!(EthereumError::VerificationError))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
use sha3::{Digest, Keccak256};
|
|
||||||
|
|
||||||
use group::Group;
|
|
||||||
use k256::{
|
|
||||||
elliptic_curve::{
|
|
||||||
bigint::ArrayEncoding, ops::Reduce, point::DecompressPoint, sec1::ToEncodedPoint,
|
|
||||||
},
|
|
||||||
AffinePoint, ProjectivePoint, Scalar, U256,
|
|
||||||
};
|
|
||||||
|
|
||||||
use frost::{algorithm::Hram, curve::Secp256k1};
|
|
||||||
|
|
||||||
pub fn keccak256(data: &[u8]) -> [u8; 32] {
|
|
||||||
Keccak256::digest(data).into()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn hash_to_scalar(data: &[u8]) -> Scalar {
|
|
||||||
Scalar::reduce(U256::from_be_slice(&keccak256(data)))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn address(point: &ProjectivePoint) -> [u8; 20] {
|
|
||||||
let encoded_point = point.to_encoded_point(false);
|
|
||||||
keccak256(&encoded_point.as_ref()[1 .. 65])[12 .. 32].try_into().unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn ecrecover(message: Scalar, v: u8, r: Scalar, s: Scalar) -> Option<[u8; 20]> {
|
|
||||||
if r.is_zero().into() || s.is_zero().into() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
let R = AffinePoint::decompress(&r.to_bytes(), v.into());
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
if let Some(R) = Option::<AffinePoint>::from(R) {
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
let R = ProjectivePoint::from(R);
|
|
||||||
|
|
||||||
let r = r.invert().unwrap();
|
|
||||||
let u1 = ProjectivePoint::GENERATOR * (-message * r);
|
|
||||||
let u2 = R * (s * r);
|
|
||||||
let key: ProjectivePoint = u1 + u2;
|
|
||||||
if !bool::from(key.is_identity()) {
|
|
||||||
return Some(address(&key));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Default)]
|
|
||||||
pub struct EthereumHram {}
|
|
||||||
impl Hram<Secp256k1> for EthereumHram {
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
fn hram(R: &ProjectivePoint, A: &ProjectivePoint, m: &[u8]) -> Scalar {
|
|
||||||
let a_encoded_point = A.to_encoded_point(true);
|
|
||||||
let mut a_encoded = a_encoded_point.as_ref().to_owned();
|
|
||||||
a_encoded[0] += 25; // Ethereum uses 27/28 for point parity
|
|
||||||
let mut data = address(R).to_vec();
|
|
||||||
data.append(&mut a_encoded);
|
|
||||||
data.append(&mut m.to_vec());
|
|
||||||
Scalar::reduce(U256::from_be_slice(&keccak256(&data)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct ProcessedSignature {
|
|
||||||
pub s: Scalar,
|
|
||||||
pub px: Scalar,
|
|
||||||
pub parity: u8,
|
|
||||||
pub message: [u8; 32],
|
|
||||||
pub e: Scalar,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
pub fn preprocess_signature_for_ecrecover(
|
|
||||||
m: [u8; 32],
|
|
||||||
R: &ProjectivePoint,
|
|
||||||
s: Scalar,
|
|
||||||
A: &ProjectivePoint,
|
|
||||||
chain_id: U256,
|
|
||||||
) -> (Scalar, Scalar) {
|
|
||||||
let processed_sig = process_signature_for_contract(m, R, s, A, chain_id);
|
|
||||||
let sr = processed_sig.s.mul(&processed_sig.px).negate();
|
|
||||||
let er = processed_sig.e.mul(&processed_sig.px).negate();
|
|
||||||
(sr, er)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
pub fn process_signature_for_contract(
|
|
||||||
m: [u8; 32],
|
|
||||||
R: &ProjectivePoint,
|
|
||||||
s: Scalar,
|
|
||||||
A: &ProjectivePoint,
|
|
||||||
chain_id: U256,
|
|
||||||
) -> ProcessedSignature {
|
|
||||||
let encoded_pk = A.to_encoded_point(true);
|
|
||||||
let px = &encoded_pk.as_ref()[1 .. 33];
|
|
||||||
let px_scalar = Scalar::reduce(U256::from_be_slice(px));
|
|
||||||
let e = EthereumHram::hram(R, A, &[chain_id.to_be_byte_array().as_slice(), &m].concat());
|
|
||||||
ProcessedSignature {
|
|
||||||
s,
|
|
||||||
px: px_scalar,
|
|
||||||
parity: &encoded_pk.as_ref()[0] - 2,
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
message: m,
|
|
||||||
e,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
pub mod contract;
|
|
||||||
pub mod crypto;
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
use std::{convert::TryFrom, sync::Arc, time::Duration, fs::File};
|
|
||||||
|
|
||||||
use rand_core::OsRng;
|
|
||||||
|
|
||||||
use ::k256::{
|
|
||||||
elliptic_curve::{bigint::ArrayEncoding, PrimeField},
|
|
||||||
U256,
|
|
||||||
};
|
|
||||||
|
|
||||||
use ethers_core::{
|
|
||||||
types::Signature,
|
|
||||||
abi::Abi,
|
|
||||||
utils::{keccak256, Anvil, AnvilInstance},
|
|
||||||
};
|
|
||||||
use ethers_contract::ContractFactory;
|
|
||||||
use ethers_providers::{Middleware, Provider, Http};
|
|
||||||
|
|
||||||
use frost::{
|
|
||||||
curve::Secp256k1,
|
|
||||||
Participant,
|
|
||||||
algorithm::IetfSchnorr,
|
|
||||||
tests::{key_gen, algorithm_machines, sign},
|
|
||||||
};
|
|
||||||
|
|
||||||
use ethereum_serai::{
|
|
||||||
crypto,
|
|
||||||
contract::{Schnorr, call_verify},
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO: Replace with a contract deployment from an unknown account, so the environment solely has
|
|
||||||
// to fund the deployer, not create/pass a wallet
|
|
||||||
pub async fn deploy_schnorr_verifier_contract(
|
|
||||||
chain_id: u32,
|
|
||||||
client: Arc<Provider<Http>>,
|
|
||||||
wallet: &k256::ecdsa::SigningKey,
|
|
||||||
) -> eyre::Result<Schnorr<Provider<Http>>> {
|
|
||||||
let abi: Abi = serde_json::from_reader(File::open("./artifacts/Schnorr.abi").unwrap()).unwrap();
|
|
||||||
|
|
||||||
let hex_bin_buf = std::fs::read_to_string("./artifacts/Schnorr.bin").unwrap();
|
|
||||||
let hex_bin =
|
|
||||||
if let Some(stripped) = hex_bin_buf.strip_prefix("0x") { stripped } else { &hex_bin_buf };
|
|
||||||
let bin = hex::decode(hex_bin).unwrap();
|
|
||||||
let factory = ContractFactory::new(abi, bin.into(), client.clone());
|
|
||||||
|
|
||||||
let mut deployment_tx = factory.deploy(())?.tx;
|
|
||||||
deployment_tx.set_chain_id(chain_id);
|
|
||||||
deployment_tx.set_gas(500_000);
|
|
||||||
let (max_fee_per_gas, max_priority_fee_per_gas) = client.estimate_eip1559_fees(None).await?;
|
|
||||||
deployment_tx.as_eip1559_mut().unwrap().max_fee_per_gas = Some(max_fee_per_gas);
|
|
||||||
deployment_tx.as_eip1559_mut().unwrap().max_priority_fee_per_gas = Some(max_priority_fee_per_gas);
|
|
||||||
|
|
||||||
let sig_hash = deployment_tx.sighash();
|
|
||||||
let (sig, rid) = wallet.sign_prehash_recoverable(sig_hash.as_ref()).unwrap();
|
|
||||||
|
|
||||||
// EIP-155 v
|
|
||||||
let mut v = u64::from(rid.to_byte());
|
|
||||||
assert!((v == 0) || (v == 1));
|
|
||||||
v += u64::from((chain_id * 2) + 35);
|
|
||||||
|
|
||||||
let r = sig.r().to_repr();
|
|
||||||
let r_ref: &[u8] = r.as_ref();
|
|
||||||
let s = sig.s().to_repr();
|
|
||||||
let s_ref: &[u8] = s.as_ref();
|
|
||||||
let deployment_tx = deployment_tx.rlp_signed(&Signature { r: r_ref.into(), s: s_ref.into(), v });
|
|
||||||
|
|
||||||
let pending_tx = client.send_raw_transaction(deployment_tx).await?;
|
|
||||||
|
|
||||||
let mut receipt;
|
|
||||||
while {
|
|
||||||
receipt = client.get_transaction_receipt(pending_tx.tx_hash()).await?;
|
|
||||||
receipt.is_none()
|
|
||||||
} {
|
|
||||||
tokio::time::sleep(Duration::from_secs(6)).await;
|
|
||||||
}
|
|
||||||
let receipt = receipt.unwrap();
|
|
||||||
assert!(receipt.status == Some(1.into()));
|
|
||||||
|
|
||||||
let contract = Schnorr::new(receipt.contract_address.unwrap(), client.clone());
|
|
||||||
Ok(contract)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn deploy_test_contract() -> (u32, AnvilInstance, Schnorr<Provider<Http>>) {
|
|
||||||
let anvil = Anvil::new().spawn();
|
|
||||||
|
|
||||||
let provider =
|
|
||||||
Provider::<Http>::try_from(anvil.endpoint()).unwrap().interval(Duration::from_millis(10u64));
|
|
||||||
let chain_id = provider.get_chainid().await.unwrap().as_u32();
|
|
||||||
let wallet = anvil.keys()[0].clone().into();
|
|
||||||
let client = Arc::new(provider);
|
|
||||||
|
|
||||||
(chain_id, anvil, deploy_schnorr_verifier_contract(chain_id, client, &wallet).await.unwrap())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_deploy_contract() {
|
|
||||||
deploy_test_contract().await;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_ecrecover_hack() {
|
|
||||||
let (chain_id, _anvil, contract) = deploy_test_contract().await;
|
|
||||||
let chain_id = U256::from(chain_id);
|
|
||||||
|
|
||||||
let keys = key_gen::<_, Secp256k1>(&mut OsRng);
|
|
||||||
let group_key = keys[&Participant::new(1).unwrap()].group_key();
|
|
||||||
|
|
||||||
const MESSAGE: &[u8] = b"Hello, World!";
|
|
||||||
let hashed_message = keccak256(MESSAGE);
|
|
||||||
|
|
||||||
let full_message = &[chain_id.to_be_byte_array().as_slice(), &hashed_message].concat();
|
|
||||||
|
|
||||||
let algo = IetfSchnorr::<Secp256k1, crypto::EthereumHram>::ietf();
|
|
||||||
let sig = sign(
|
|
||||||
&mut OsRng,
|
|
||||||
&algo,
|
|
||||||
keys.clone(),
|
|
||||||
algorithm_machines(&mut OsRng, &algo, &keys),
|
|
||||||
full_message,
|
|
||||||
);
|
|
||||||
let mut processed_sig =
|
|
||||||
crypto::process_signature_for_contract(hashed_message, &sig.R, sig.s, &group_key, chain_id);
|
|
||||||
|
|
||||||
call_verify(&contract, &processed_sig).await.unwrap();
|
|
||||||
|
|
||||||
// test invalid signature fails
|
|
||||||
processed_sig.message[0] = 0;
|
|
||||||
assert!(call_verify(&contract, &processed_sig).await.is_err());
|
|
||||||
}
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
use k256::{
|
|
||||||
elliptic_curve::{bigint::ArrayEncoding, ops::Reduce, sec1::ToEncodedPoint},
|
|
||||||
ProjectivePoint, Scalar, U256,
|
|
||||||
};
|
|
||||||
use frost::{curve::Secp256k1, Participant};
|
|
||||||
|
|
||||||
use ethereum_serai::crypto::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_ecrecover() {
|
|
||||||
use rand_core::OsRng;
|
|
||||||
use sha2::Sha256;
|
|
||||||
use sha3::{Digest, Keccak256};
|
|
||||||
use k256::ecdsa::{hazmat::SignPrimitive, signature::DigestVerifier, SigningKey, VerifyingKey};
|
|
||||||
|
|
||||||
let private = SigningKey::random(&mut OsRng);
|
|
||||||
let public = VerifyingKey::from(&private);
|
|
||||||
|
|
||||||
const MESSAGE: &[u8] = b"Hello, World!";
|
|
||||||
let (sig, recovery_id) = private
|
|
||||||
.as_nonzero_scalar()
|
|
||||||
.try_sign_prehashed_rfc6979::<Sha256>(&Keccak256::digest(MESSAGE), b"")
|
|
||||||
.unwrap();
|
|
||||||
#[allow(clippy::unit_cmp)] // Intended to assert this wasn't changed to Result<bool>
|
|
||||||
{
|
|
||||||
assert_eq!(public.verify_digest(Keccak256::new_with_prefix(MESSAGE), &sig).unwrap(), ());
|
|
||||||
}
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
ecrecover(hash_to_scalar(MESSAGE), recovery_id.unwrap().is_y_odd().into(), *sig.r(), *sig.s())
|
|
||||||
.unwrap(),
|
|
||||||
address(&ProjectivePoint::from(public.as_affine()))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_signing() {
|
|
||||||
use frost::{
|
|
||||||
algorithm::IetfSchnorr,
|
|
||||||
tests::{algorithm_machines, key_gen, sign},
|
|
||||||
};
|
|
||||||
use rand_core::OsRng;
|
|
||||||
|
|
||||||
let keys = key_gen::<_, Secp256k1>(&mut OsRng);
|
|
||||||
let _group_key = keys[&Participant::new(1).unwrap()].group_key();
|
|
||||||
|
|
||||||
const MESSAGE: &[u8] = b"Hello, World!";
|
|
||||||
|
|
||||||
let algo = IetfSchnorr::<Secp256k1, EthereumHram>::ietf();
|
|
||||||
let _sig =
|
|
||||||
sign(&mut OsRng, &algo, keys.clone(), algorithm_machines(&mut OsRng, &algo, &keys), MESSAGE);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_ecrecover_hack() {
|
|
||||||
use frost::{
|
|
||||||
algorithm::IetfSchnorr,
|
|
||||||
tests::{algorithm_machines, key_gen, sign},
|
|
||||||
};
|
|
||||||
use rand_core::OsRng;
|
|
||||||
|
|
||||||
let keys = key_gen::<_, Secp256k1>(&mut OsRng);
|
|
||||||
let group_key = keys[&Participant::new(1).unwrap()].group_key();
|
|
||||||
let group_key_encoded = group_key.to_encoded_point(true);
|
|
||||||
let group_key_compressed = group_key_encoded.as_ref();
|
|
||||||
let group_key_x = Scalar::reduce(U256::from_be_slice(&group_key_compressed[1 .. 33]));
|
|
||||||
|
|
||||||
const MESSAGE: &[u8] = b"Hello, World!";
|
|
||||||
let hashed_message = keccak256(MESSAGE);
|
|
||||||
let chain_id = U256::ONE;
|
|
||||||
|
|
||||||
let full_message = &[chain_id.to_be_byte_array().as_slice(), &hashed_message].concat();
|
|
||||||
|
|
||||||
let algo = IetfSchnorr::<Secp256k1, EthereumHram>::ietf();
|
|
||||||
let sig = sign(
|
|
||||||
&mut OsRng,
|
|
||||||
&algo,
|
|
||||||
keys.clone(),
|
|
||||||
algorithm_machines(&mut OsRng, &algo, &keys),
|
|
||||||
full_message,
|
|
||||||
);
|
|
||||||
|
|
||||||
let (sr, er) =
|
|
||||||
preprocess_signature_for_ecrecover(hashed_message, &sig.R, sig.s, &group_key, chain_id);
|
|
||||||
let q = ecrecover(sr, group_key_compressed[0] - 2, group_key_x, er).unwrap();
|
|
||||||
assert_eq!(q, address(&sig.R));
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
mod contract;
|
|
||||||
mod crypto;
|
|
||||||
1
coins/monero/.gitignore
vendored
Normal file
1
coins/monero/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
c/.build
|
||||||
@@ -1,113 +1,52 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "monero-serai"
|
name = "monero-serai"
|
||||||
version = "0.1.4-alpha"
|
version = "0.1.0"
|
||||||
description = "A modern Monero transaction library"
|
description = "A modern Monero wallet library"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
repository = "https://github.com/serai-dex/serai/tree/develop/coins/monero"
|
|
||||||
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
|
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.74"
|
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
|
||||||
all-features = true
|
|
||||||
rustdoc-args = ["--cfg", "docsrs"]
|
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
std-shims = { path = "../../common/std-shims", version = "^0.1.1", default-features = false }
|
|
||||||
|
|
||||||
async-trait = { version = "0.1", default-features = false }
|
|
||||||
thiserror = { version = "1", default-features = false, optional = true }
|
|
||||||
|
|
||||||
zeroize = { version = "^1.5", default-features = false, features = ["zeroize_derive"] }
|
|
||||||
subtle = { version = "^2.4", default-features = false }
|
|
||||||
|
|
||||||
rand_core = { version = "0.6", default-features = false }
|
|
||||||
# Used to send transactions
|
|
||||||
rand = { version = "0.8", default-features = false }
|
|
||||||
rand_chacha = { version = "0.3", default-features = false }
|
|
||||||
# Used to select decoys
|
|
||||||
rand_distr = { version = "0.4", default-features = false }
|
|
||||||
|
|
||||||
sha3 = { version = "0.10", default-features = false }
|
|
||||||
pbkdf2 = { version = "0.12", features = ["simple"], default-features = false }
|
|
||||||
|
|
||||||
curve25519-dalek = { version = "4", default-features = false, features = ["alloc", "zeroize", "precomputed-tables"] }
|
|
||||||
|
|
||||||
# Used for the hash to curve, along with the more complicated proofs
|
|
||||||
group = { version = "0.13", default-features = false }
|
|
||||||
dalek-ff-group = { path = "../../crypto/dalek-ff-group", version = "0.4", default-features = false }
|
|
||||||
multiexp = { path = "../../crypto/multiexp", version = "0.4", default-features = false, features = ["batch"] }
|
|
||||||
|
|
||||||
# Needed for multisig
|
|
||||||
transcript = { package = "flexible-transcript", path = "../../crypto/transcript", version = "0.3", default-features = false, features = ["recommended"], optional = true }
|
|
||||||
dleq = { path = "../../crypto/dleq", version = "0.4", default-features = false, features = ["serialize"], optional = true }
|
|
||||||
frost = { package = "modular-frost", path = "../../crypto/frost", version = "0.8", default-features = false, features = ["ed25519"], optional = true }
|
|
||||||
|
|
||||||
monero-generators = { path = "generators", version = "0.4", default-features = false }
|
|
||||||
|
|
||||||
async-lock = { version = "3", default-features = false, optional = true }
|
|
||||||
|
|
||||||
hex-literal = "0.4"
|
|
||||||
hex = { version = "0.4", default-features = false, features = ["alloc"] }
|
|
||||||
serde = { version = "1", default-features = false, features = ["derive", "alloc"] }
|
|
||||||
serde_json = { version = "1", default-features = false, features = ["alloc"] }
|
|
||||||
|
|
||||||
base58-monero = { version = "2", default-features = false, features = ["check"] }
|
|
||||||
|
|
||||||
# Used for the provided HTTP RPC
|
|
||||||
digest_auth = { version = "0.3", default-features = false, optional = true }
|
|
||||||
simple-request = { path = "../../common/request", version = "0.1", default-features = false, features = ["tls"], optional = true }
|
|
||||||
tokio = { version = "1", default-features = false, optional = true }
|
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
dalek-ff-group = { path = "../../crypto/dalek-ff-group", version = "0.4", default-features = false }
|
cc = "1.0"
|
||||||
monero-generators = { path = "generators", version = "0.4", default-features = false }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dependencies]
|
||||||
tokio = { version = "1", features = ["sync", "macros"] }
|
hex-literal = "0.3"
|
||||||
|
lazy_static = "1"
|
||||||
|
thiserror = "1"
|
||||||
|
|
||||||
frost = { package = "modular-frost", path = "../../crypto/frost", features = ["tests"] }
|
rand_core = "0.6"
|
||||||
|
rand_chacha = { version = "0.3", optional = true }
|
||||||
|
rand = "0.8"
|
||||||
|
rand_distr = "0.4"
|
||||||
|
|
||||||
|
subtle = "2.4"
|
||||||
|
|
||||||
|
tiny-keccak = { version = "2", features = ["keccak"] }
|
||||||
|
blake2 = { version = "0.10", optional = true }
|
||||||
|
|
||||||
|
curve25519-dalek = { version = "3", features = ["std"] }
|
||||||
|
|
||||||
|
group = { version = "0.12" }
|
||||||
|
dalek-ff-group = { path = "../../crypto/dalek-ff-group" }
|
||||||
|
|
||||||
|
transcript = { package = "flexible-transcript", path = "../../crypto/transcript", features = ["recommended"], optional = true }
|
||||||
|
frost = { package = "modular-frost", path = "../../crypto/frost", features = ["ed25519"], optional = true }
|
||||||
|
dleq = { package = "dleq-serai", path = "../../crypto/dleq", features = ["serialize"], optional = true }
|
||||||
|
|
||||||
|
hex = "0.4"
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_json = "1.0"
|
||||||
|
|
||||||
|
base58-monero = "1"
|
||||||
|
monero-epee-bin-serde = "1.0"
|
||||||
|
monero = "0.16"
|
||||||
|
|
||||||
|
reqwest = { version = "0.11", features = ["json"] }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
std = [
|
|
||||||
"std-shims/std",
|
|
||||||
|
|
||||||
"thiserror",
|
|
||||||
|
|
||||||
"zeroize/std",
|
|
||||||
"subtle/std",
|
|
||||||
|
|
||||||
"rand_core/std",
|
|
||||||
"rand/std",
|
|
||||||
"rand_chacha/std",
|
|
||||||
"rand_distr/std",
|
|
||||||
|
|
||||||
"sha3/std",
|
|
||||||
"pbkdf2/std",
|
|
||||||
|
|
||||||
"multiexp/std",
|
|
||||||
|
|
||||||
"transcript/std",
|
|
||||||
"dleq/std",
|
|
||||||
|
|
||||||
"monero-generators/std",
|
|
||||||
|
|
||||||
"async-lock?/std",
|
|
||||||
|
|
||||||
"hex/std",
|
|
||||||
"serde/std",
|
|
||||||
"serde_json/std",
|
|
||||||
|
|
||||||
"base58-monero/std",
|
|
||||||
]
|
|
||||||
|
|
||||||
cache-distribution = ["async-lock"]
|
|
||||||
http-rpc = ["digest_auth", "simple-request", "tokio"]
|
|
||||||
multisig = ["transcript", "frost", "dleq", "std"]
|
|
||||||
binaries = ["tokio/rt-multi-thread", "tokio/macros", "http-rpc"]
|
|
||||||
experimental = []
|
experimental = []
|
||||||
|
multisig = ["rand_chacha", "blake2", "transcript", "frost", "dleq"]
|
||||||
|
|
||||||
default = ["std", "http-rpc"]
|
[dev-dependencies]
|
||||||
|
sha2 = "0.10"
|
||||||
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2022-2023 Luke Parker
|
Copyright (c) 2022 Luke Parker
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
@@ -4,46 +4,4 @@ A modern Monero transaction library intended for usage in wallets. It prides
|
|||||||
itself on accuracy, correctness, and removing common pit falls developers may
|
itself on accuracy, correctness, and removing common pit falls developers may
|
||||||
face.
|
face.
|
||||||
|
|
||||||
monero-serai also offers the following features:
|
Threshold multisignature support is available via the `multisig` feature.
|
||||||
|
|
||||||
- Featured Addresses
|
|
||||||
- A FROST-based multisig orders of magnitude more performant than Monero's
|
|
||||||
|
|
||||||
### Purpose and support
|
|
||||||
|
|
||||||
monero-serai was written for Serai, a decentralized exchange aiming to support
|
|
||||||
Monero. Despite this, monero-serai is intended to be a widely usable library,
|
|
||||||
accurate to Monero. monero-serai guarantees the functionality needed for Serai,
|
|
||||||
yet will not deprive functionality from other users.
|
|
||||||
|
|
||||||
Various legacy transaction formats are not currently implemented, yet we are
|
|
||||||
willing to add support for them. There aren't active development efforts around
|
|
||||||
them however.
|
|
||||||
|
|
||||||
### Caveats
|
|
||||||
|
|
||||||
This library DOES attempt to do the following:
|
|
||||||
|
|
||||||
- Create on-chain transactions identical to how wallet2 would (unless told not
|
|
||||||
to)
|
|
||||||
- Not be detectable as monero-serai when scanning outputs
|
|
||||||
- Not reveal spent outputs to the connected RPC node
|
|
||||||
|
|
||||||
This library DOES NOT attempt to do the following:
|
|
||||||
|
|
||||||
- Have identical RPC behavior when creating transactions
|
|
||||||
- Be a wallet
|
|
||||||
|
|
||||||
This means that monero-serai shouldn't be fingerprintable on-chain. It also
|
|
||||||
shouldn't be fingerprintable if a targeted attack occurs to detect if the
|
|
||||||
receiving wallet is monero-serai or wallet2. It also should be generally safe
|
|
||||||
for usage with remote nodes.
|
|
||||||
|
|
||||||
It won't hide from remote nodes it's monero-serai however, potentially
|
|
||||||
allowing a remote node to profile you. The implications of this are left to the
|
|
||||||
user to consider.
|
|
||||||
|
|
||||||
It also won't act as a wallet, just as a transaction library. wallet2 has
|
|
||||||
several *non-transaction-level* policies, such as always attempting to use two
|
|
||||||
inputs to create transactions. These are considered out of scope to
|
|
||||||
monero-serai.
|
|
||||||
|
|||||||
@@ -1,67 +1,72 @@
|
|||||||
use std::{
|
use std::{env, path::Path, process::Command};
|
||||||
io::Write,
|
|
||||||
env,
|
|
||||||
path::Path,
|
|
||||||
fs::{File, remove_file},
|
|
||||||
};
|
|
||||||
|
|
||||||
use dalek_ff_group::EdwardsPoint;
|
|
||||||
|
|
||||||
use monero_generators::bulletproofs_generators;
|
|
||||||
|
|
||||||
fn serialize(generators_string: &mut String, points: &[EdwardsPoint]) {
|
|
||||||
for generator in points {
|
|
||||||
generators_string.extend(
|
|
||||||
format!(
|
|
||||||
"
|
|
||||||
dalek_ff_group::EdwardsPoint(
|
|
||||||
curve25519_dalek::edwards::CompressedEdwardsY({:?}).decompress().unwrap()
|
|
||||||
),
|
|
||||||
",
|
|
||||||
generator.compress().to_bytes()
|
|
||||||
)
|
|
||||||
.chars(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn generators(prefix: &'static str, path: &str) {
|
|
||||||
let generators = bulletproofs_generators(prefix.as_bytes());
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
let mut G_str = String::new();
|
|
||||||
serialize(&mut G_str, &generators.G);
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
let mut H_str = String::new();
|
|
||||||
serialize(&mut H_str, &generators.H);
|
|
||||||
|
|
||||||
let path = Path::new(&env::var("OUT_DIR").unwrap()).join(path);
|
|
||||||
let _ = remove_file(&path);
|
|
||||||
File::create(&path)
|
|
||||||
.unwrap()
|
|
||||||
.write_all(
|
|
||||||
format!(
|
|
||||||
"
|
|
||||||
pub(crate) static GENERATORS_CELL: OnceLock<Generators> = OnceLock::new();
|
|
||||||
pub fn GENERATORS() -> &'static Generators {{
|
|
||||||
GENERATORS_CELL.get_or_init(|| Generators {{
|
|
||||||
G: vec![
|
|
||||||
{G_str}
|
|
||||||
],
|
|
||||||
H: vec![
|
|
||||||
{H_str}
|
|
||||||
],
|
|
||||||
}})
|
|
||||||
}}
|
|
||||||
",
|
|
||||||
)
|
|
||||||
.as_bytes(),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
println!("cargo:rerun-if-changed=build.rs");
|
if !Command::new("git").args(&["submodule", "update", "--init", "--recursive"]).status().unwrap().success() {
|
||||||
|
panic!("git failed to init submodules");
|
||||||
|
}
|
||||||
|
|
||||||
generators("bulletproof", "generators.rs");
|
if !Command ::new("mkdir").args(&["-p", ".build"])
|
||||||
generators("bulletproof_plus", "generators_plus.rs");
|
.current_dir(&Path::new("c")).status().unwrap().success() {
|
||||||
|
panic!("failed to create a directory to track build progress");
|
||||||
|
}
|
||||||
|
|
||||||
|
let out_dir = &env::var("OUT_DIR").unwrap();
|
||||||
|
|
||||||
|
// Use a file to signal if Monero was already built, as that should never be rebuilt
|
||||||
|
// If the signaling file was deleted, run this script again to rebuild Monero though
|
||||||
|
println!("cargo:rerun-if-changed=c/.build/monero");
|
||||||
|
if !Path::new("c/.build/monero").exists() {
|
||||||
|
if !Command::new("make").arg(format!("-j{}", &env::var("THREADS").unwrap_or("2".to_string())))
|
||||||
|
.current_dir(&Path::new("c/monero")).status().unwrap().success() {
|
||||||
|
panic!("make failed to build Monero. Please check your dependencies");
|
||||||
|
}
|
||||||
|
|
||||||
|
if !Command::new("touch").arg("monero")
|
||||||
|
.current_dir(&Path::new("c/.build")).status().unwrap().success() {
|
||||||
|
panic!("failed to create a file to label Monero as built");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("cargo:rerun-if-changed=c/wrapper.cpp");
|
||||||
|
cc::Build::new()
|
||||||
|
.static_flag(true)
|
||||||
|
.warnings(false)
|
||||||
|
.extra_warnings(false)
|
||||||
|
.flag("-Wno-deprecated-declarations")
|
||||||
|
|
||||||
|
.include("c/monero/external/supercop/include")
|
||||||
|
.include("c/monero/contrib/epee/include")
|
||||||
|
.include("c/monero/src")
|
||||||
|
.include("c/monero/build/release/generated_include")
|
||||||
|
|
||||||
|
.define("AUTO_INITIALIZE_EASYLOGGINGPP", None)
|
||||||
|
.include("c/monero/external/easylogging++")
|
||||||
|
.file("c/monero/external/easylogging++/easylogging++.cc")
|
||||||
|
|
||||||
|
.file("c/monero/src/common/aligned.c")
|
||||||
|
.file("c/monero/src/common/perf_timer.cpp")
|
||||||
|
|
||||||
|
.include("c/monero/src/crypto")
|
||||||
|
.file("c/monero/src/crypto/crypto-ops-data.c")
|
||||||
|
.file("c/monero/src/crypto/crypto-ops.c")
|
||||||
|
.file("c/monero/src/crypto/keccak.c")
|
||||||
|
.file("c/monero/src/crypto/hash.c")
|
||||||
|
|
||||||
|
.include("c/monero/src/device")
|
||||||
|
.file("c/monero/src/device/device_default.cpp")
|
||||||
|
|
||||||
|
.include("c/monero/src/ringct")
|
||||||
|
.file("c/monero/src/ringct/rctCryptoOps.c")
|
||||||
|
.file("c/monero/src/ringct/rctTypes.cpp")
|
||||||
|
.file("c/monero/src/ringct/rctOps.cpp")
|
||||||
|
.file("c/monero/src/ringct/multiexp.cc")
|
||||||
|
.file("c/monero/src/ringct/bulletproofs.cc")
|
||||||
|
.file("c/monero/src/ringct/rctSigs.cpp")
|
||||||
|
|
||||||
|
.file("c/wrapper.cpp")
|
||||||
|
.compile("wrapper");
|
||||||
|
|
||||||
|
println!("cargo:rustc-link-search={}", out_dir);
|
||||||
|
println!("cargo:rustc-link-lib=wrapper");
|
||||||
|
println!("cargo:rustc-link-lib=stdc++");
|
||||||
}
|
}
|
||||||
|
|||||||
1
coins/monero/c/monero
Submodule
1
coins/monero/c/monero
Submodule
Submodule coins/monero/c/monero added at 424e4de16b
158
coins/monero/c/wrapper.cpp
Normal file
158
coins/monero/c/wrapper.cpp
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
#include <mutex>
|
||||||
|
|
||||||
|
#include "device/device_default.hpp"
|
||||||
|
|
||||||
|
#include "ringct/bulletproofs.h"
|
||||||
|
#include "ringct/rctSigs.h"
|
||||||
|
|
||||||
|
typedef std::lock_guard<std::mutex> lock;
|
||||||
|
|
||||||
|
std::mutex rng_mutex;
|
||||||
|
uint8_t rng_entropy[64];
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
void rng(uint8_t* seed) {
|
||||||
|
// Set the first half to the seed
|
||||||
|
memcpy(rng_entropy, seed, 32);
|
||||||
|
// Set the second half to the hash of a DST to ensure a lack of collisions
|
||||||
|
crypto::cn_fast_hash("RNG_entropy_seed", 16, (char*) &rng_entropy[32]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" void monero_wide_reduce(uint8_t* value);
|
||||||
|
namespace crypto {
|
||||||
|
void generate_random_bytes_not_thread_safe(size_t n, void* value) {
|
||||||
|
size_t written = 0;
|
||||||
|
while (written != n) {
|
||||||
|
uint8_t hash[32];
|
||||||
|
crypto::cn_fast_hash(rng_entropy, 64, (char*) hash);
|
||||||
|
// Step the RNG by setting the latter half to the most recent result
|
||||||
|
// Does not leak the RNG, even if the values are leaked (which they are
|
||||||
|
// expected to be) due to the first half remaining constant and
|
||||||
|
// undisclosed
|
||||||
|
memcpy(&rng_entropy[32], hash, 32);
|
||||||
|
|
||||||
|
size_t next = n - written;
|
||||||
|
if (next > 32) {
|
||||||
|
next = 32;
|
||||||
|
}
|
||||||
|
memcpy(&((uint8_t*) value)[written], hash, next);
|
||||||
|
written += next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void random32_unbiased(unsigned char *bytes) {
|
||||||
|
uint8_t value[64];
|
||||||
|
generate_random_bytes_not_thread_safe(64, value);
|
||||||
|
monero_wide_reduce(value);
|
||||||
|
memcpy(bytes, value, 32);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
void c_hash_to_point(uint8_t* point) {
|
||||||
|
rct::key key_point;
|
||||||
|
ge_p3 e_p3;
|
||||||
|
memcpy(key_point.bytes, point, 32);
|
||||||
|
rct::hash_to_p3(e_p3, key_point);
|
||||||
|
ge_p3_tobytes(point, &e_p3);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t* c_generate_bp(uint8_t* seed, uint8_t len, uint64_t* a, uint8_t* m) {
|
||||||
|
lock guard(rng_mutex);
|
||||||
|
rng(seed);
|
||||||
|
|
||||||
|
rct::keyV masks;
|
||||||
|
std::vector<uint64_t> amounts;
|
||||||
|
masks.resize(len);
|
||||||
|
amounts.resize(len);
|
||||||
|
for (uint8_t i = 0; i < len; i++) {
|
||||||
|
memcpy(masks[i].bytes, m + (i * 32), 32);
|
||||||
|
amounts[i] = a[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
rct::Bulletproof bp = rct::bulletproof_PROVE(amounts, masks);
|
||||||
|
|
||||||
|
std::stringstream ss;
|
||||||
|
binary_archive<true> ba(ss);
|
||||||
|
::serialization::serialize(ba, bp);
|
||||||
|
uint8_t* res = (uint8_t*) calloc(ss.str().size(), 1);
|
||||||
|
memcpy(res, ss.str().data(), ss.str().size());
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool c_verify_bp(
|
||||||
|
uint8_t* seed,
|
||||||
|
uint s_len,
|
||||||
|
uint8_t* s,
|
||||||
|
uint8_t c_len,
|
||||||
|
uint8_t* c
|
||||||
|
) {
|
||||||
|
// BPs are batch verified which use RNG based weights to ensure individual
|
||||||
|
// integrity
|
||||||
|
// That's why this must also have control over RNG, to prevent interrupting
|
||||||
|
// multisig signing while not using known seeds. Considering this doesn't
|
||||||
|
// actually define a batch, and it's only verifying a single BP,
|
||||||
|
// it'd probably be fine, but...
|
||||||
|
lock guard(rng_mutex);
|
||||||
|
rng(seed);
|
||||||
|
|
||||||
|
rct::Bulletproof bp;
|
||||||
|
std::stringstream ss;
|
||||||
|
std::string str;
|
||||||
|
str.assign((char*) s, (size_t) s_len);
|
||||||
|
ss << str;
|
||||||
|
binary_archive<false> ba(ss);
|
||||||
|
::serialization::serialize(ba, bp);
|
||||||
|
if (!ss.good()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bp.V.resize(c_len);
|
||||||
|
for (uint8_t i = 0; i < c_len; i++) {
|
||||||
|
memcpy(bp.V[i].bytes, &c[i * 32], 32);
|
||||||
|
}
|
||||||
|
|
||||||
|
try { return rct::bulletproof_VERIFY(bp); } catch(...) { return false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
bool c_verify_clsag(
|
||||||
|
uint s_len,
|
||||||
|
uint8_t* s,
|
||||||
|
uint8_t k_len,
|
||||||
|
uint8_t* k,
|
||||||
|
uint8_t* I,
|
||||||
|
uint8_t* p,
|
||||||
|
uint8_t* m
|
||||||
|
) {
|
||||||
|
rct::clsag clsag;
|
||||||
|
std::stringstream ss;
|
||||||
|
std::string str;
|
||||||
|
str.assign((char*) s, (size_t) s_len);
|
||||||
|
ss << str;
|
||||||
|
binary_archive<false> ba(ss);
|
||||||
|
::serialization::serialize(ba, clsag);
|
||||||
|
if (!ss.good()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
rct::ctkeyV keys;
|
||||||
|
keys.resize(k_len);
|
||||||
|
for (uint8_t i = 0; i < k_len; i++) {
|
||||||
|
memcpy(keys[i].dest.bytes, &k[(i * 2) * 32], 32);
|
||||||
|
memcpy(keys[i].mask.bytes, &k[((i * 2) + 1) * 32], 32);
|
||||||
|
}
|
||||||
|
|
||||||
|
memcpy(clsag.I.bytes, I, 32);
|
||||||
|
|
||||||
|
rct::key pseudo_out;
|
||||||
|
memcpy(pseudo_out.bytes, p, 32);
|
||||||
|
|
||||||
|
rct::key msg;
|
||||||
|
memcpy(msg.bytes, m, 32);
|
||||||
|
|
||||||
|
try {
|
||||||
|
return verRctCLSAGSimple(msg, clsag, keys, pseudo_out);
|
||||||
|
} catch(...) { return false; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "monero-generators"
|
|
||||||
version = "0.4.0"
|
|
||||||
description = "Monero's hash_to_point and generators"
|
|
||||||
license = "MIT"
|
|
||||||
repository = "https://github.com/serai-dex/serai/tree/develop/coins/monero/generators"
|
|
||||||
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
|
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
|
||||||
all-features = true
|
|
||||||
rustdoc-args = ["--cfg", "docsrs"]
|
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
std-shims = { path = "../../../common/std-shims", version = "^0.1.1", default-features = false }
|
|
||||||
|
|
||||||
subtle = { version = "^2.4", default-features = false }
|
|
||||||
|
|
||||||
sha3 = { version = "0.10", default-features = false }
|
|
||||||
|
|
||||||
curve25519-dalek = { version = "4", default-features = false, features = ["alloc", "zeroize", "precomputed-tables"] }
|
|
||||||
|
|
||||||
group = { version = "0.13", default-features = false }
|
|
||||||
dalek-ff-group = { path = "../../../crypto/dalek-ff-group", version = "0.4", default-features = false }
|
|
||||||
|
|
||||||
[features]
|
|
||||||
std = ["std-shims/std", "subtle/std", "sha3/std", "dalek-ff-group/std"]
|
|
||||||
default = ["std"]
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2022-2023 Luke Parker
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
# Monero Generators
|
|
||||||
|
|
||||||
Generators used by Monero in both its Pedersen commitments and Bulletproofs(+).
|
|
||||||
An implementation of Monero's `ge_fromfe_frombytes_vartime`, simply called
|
|
||||||
`hash_to_point` here, is included, as needed to generate generators.
|
|
||||||
|
|
||||||
This library is usable under no-std when the `std` feature is disabled.
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
use subtle::ConditionallySelectable;
|
|
||||||
|
|
||||||
use curve25519_dalek::edwards::{EdwardsPoint, CompressedEdwardsY};
|
|
||||||
|
|
||||||
use group::ff::{Field, PrimeField};
|
|
||||||
use dalek_ff_group::FieldElement;
|
|
||||||
|
|
||||||
use crate::hash;
|
|
||||||
|
|
||||||
/// Monero's hash to point function, as named `ge_fromfe_frombytes_vartime`.
|
|
||||||
pub fn hash_to_point(bytes: [u8; 32]) -> EdwardsPoint {
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
let A = FieldElement::from(486662u64);
|
|
||||||
|
|
||||||
let v = FieldElement::from_square(hash(&bytes)).double();
|
|
||||||
let w = v + FieldElement::ONE;
|
|
||||||
let x = w.square() + (-A.square() * v);
|
|
||||||
|
|
||||||
// This isn't the complete X, yet its initial value
|
|
||||||
// We don't calculate the full X, and instead solely calculate Y, letting dalek reconstruct X
|
|
||||||
// While inefficient, it solves API boundaries and reduces the amount of work done here
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
let X = {
|
|
||||||
let u = w;
|
|
||||||
let v = x;
|
|
||||||
let v3 = v * v * v;
|
|
||||||
let uv3 = u * v3;
|
|
||||||
let v7 = v3 * v3 * v;
|
|
||||||
let uv7 = u * v7;
|
|
||||||
uv3 * uv7.pow((-FieldElement::from(5u8)) * FieldElement::from(8u8).invert().unwrap())
|
|
||||||
};
|
|
||||||
let x = X.square() * x;
|
|
||||||
|
|
||||||
let y = w - x;
|
|
||||||
let non_zero_0 = !y.is_zero();
|
|
||||||
let y_if_non_zero_0 = w + x;
|
|
||||||
let sign = non_zero_0 & (!y_if_non_zero_0.is_zero());
|
|
||||||
|
|
||||||
let mut z = -A;
|
|
||||||
z *= FieldElement::conditional_select(&v, &FieldElement::from(1u8), sign);
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
let Z = z + w;
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
let mut Y = z - w;
|
|
||||||
|
|
||||||
Y *= Z.invert().unwrap();
|
|
||||||
let mut bytes = Y.to_repr();
|
|
||||||
bytes[31] |= sign.unwrap_u8() << 7;
|
|
||||||
|
|
||||||
CompressedEdwardsY(bytes).decompress().unwrap().mul_by_cofactor()
|
|
||||||
}
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
//! Generators used by Monero in both its Pedersen commitments and Bulletproofs(+).
|
|
||||||
//!
|
|
||||||
//! An implementation of Monero's `ge_fromfe_frombytes_vartime`, simply called
|
|
||||||
//! `hash_to_point` here, is included, as needed to generate generators.
|
|
||||||
|
|
||||||
#![cfg_attr(not(feature = "std"), no_std)]
|
|
||||||
|
|
||||||
use std_shims::{sync::OnceLock, vec::Vec};
|
|
||||||
|
|
||||||
use sha3::{Digest, Keccak256};
|
|
||||||
|
|
||||||
use curve25519_dalek::edwards::{EdwardsPoint as DalekPoint, CompressedEdwardsY};
|
|
||||||
|
|
||||||
use group::{Group, GroupEncoding};
|
|
||||||
use dalek_ff_group::EdwardsPoint;
|
|
||||||
|
|
||||||
mod varint;
|
|
||||||
use varint::write_varint;
|
|
||||||
|
|
||||||
mod hash_to_point;
|
|
||||||
pub use hash_to_point::hash_to_point;
|
|
||||||
|
|
||||||
fn hash(data: &[u8]) -> [u8; 32] {
|
|
||||||
Keccak256::digest(data).into()
|
|
||||||
}
|
|
||||||
|
|
||||||
static H_CELL: OnceLock<DalekPoint> = OnceLock::new();
|
|
||||||
/// Monero's alternate generator `H`, used for amounts in Pedersen commitments.
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
pub fn H() -> DalekPoint {
|
|
||||||
*H_CELL.get_or_init(|| {
|
|
||||||
CompressedEdwardsY(hash(&EdwardsPoint::generator().to_bytes()))
|
|
||||||
.decompress()
|
|
||||||
.unwrap()
|
|
||||||
.mul_by_cofactor()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
static H_POW_2_CELL: OnceLock<[DalekPoint; 64]> = OnceLock::new();
|
|
||||||
/// Monero's alternate generator `H`, multiplied by 2**i for i in 1 ..= 64.
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
pub fn H_pow_2() -> &'static [DalekPoint; 64] {
|
|
||||||
H_POW_2_CELL.get_or_init(|| {
|
|
||||||
let mut res = [H(); 64];
|
|
||||||
for i in 1 .. 64 {
|
|
||||||
res[i] = res[i - 1] + res[i - 1];
|
|
||||||
}
|
|
||||||
res
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const MAX_M: usize = 16;
|
|
||||||
const N: usize = 64;
|
|
||||||
const MAX_MN: usize = MAX_M * N;
|
|
||||||
|
|
||||||
/// Container struct for Bulletproofs(+) generators.
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
pub struct Generators {
|
|
||||||
pub G: Vec<EdwardsPoint>,
|
|
||||||
pub H: Vec<EdwardsPoint>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Generate generators as needed for Bulletproofs(+), as Monero does.
|
|
||||||
pub fn bulletproofs_generators(dst: &'static [u8]) -> Generators {
|
|
||||||
let mut res = Generators { G: Vec::with_capacity(MAX_MN), H: Vec::with_capacity(MAX_MN) };
|
|
||||||
for i in 0 .. MAX_MN {
|
|
||||||
let i = 2 * i;
|
|
||||||
|
|
||||||
let mut even = H().compress().to_bytes().to_vec();
|
|
||||||
even.extend(dst);
|
|
||||||
let mut odd = even.clone();
|
|
||||||
|
|
||||||
write_varint(&i.try_into().unwrap(), &mut even).unwrap();
|
|
||||||
write_varint(&(i + 1).try_into().unwrap(), &mut odd).unwrap();
|
|
||||||
res.H.push(EdwardsPoint(hash_to_point(hash(&even))));
|
|
||||||
res.G.push(EdwardsPoint(hash_to_point(hash(&odd))));
|
|
||||||
}
|
|
||||||
res
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
use std_shims::io::{self, Write};
|
|
||||||
|
|
||||||
const VARINT_CONTINUATION_MASK: u8 = 0b1000_0000;
|
|
||||||
pub(crate) fn write_varint<W: Write>(varint: &u64, w: &mut W) -> io::Result<()> {
|
|
||||||
let mut varint = *varint;
|
|
||||||
while {
|
|
||||||
let mut b = u8::try_from(varint & u64::from(!VARINT_CONTINUATION_MASK)).unwrap();
|
|
||||||
varint >>= 7;
|
|
||||||
if varint != 0 {
|
|
||||||
b |= VARINT_CONTINUATION_MASK;
|
|
||||||
}
|
|
||||||
w.write_all(&[b])?;
|
|
||||||
varint != 0
|
|
||||||
} {}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
@@ -1,323 +0,0 @@
|
|||||||
#[cfg(feature = "binaries")]
|
|
||||||
mod binaries {
|
|
||||||
pub(crate) use std::sync::Arc;
|
|
||||||
|
|
||||||
pub(crate) use curve25519_dalek::{
|
|
||||||
scalar::Scalar,
|
|
||||||
edwards::{CompressedEdwardsY, EdwardsPoint},
|
|
||||||
};
|
|
||||||
|
|
||||||
pub(crate) use multiexp::BatchVerifier;
|
|
||||||
|
|
||||||
pub(crate) use serde::Deserialize;
|
|
||||||
pub(crate) use serde_json::json;
|
|
||||||
|
|
||||||
pub(crate) use monero_serai::{
|
|
||||||
Commitment,
|
|
||||||
ringct::RctPrunable,
|
|
||||||
transaction::{Input, Transaction},
|
|
||||||
block::Block,
|
|
||||||
rpc::{RpcError, Rpc, HttpRpc},
|
|
||||||
};
|
|
||||||
|
|
||||||
pub(crate) use tokio::task::JoinHandle;
|
|
||||||
|
|
||||||
pub(crate) async fn check_block(rpc: Arc<Rpc<HttpRpc>>, block_i: usize) {
|
|
||||||
let hash = loop {
|
|
||||||
match rpc.get_block_hash(block_i).await {
|
|
||||||
Ok(hash) => break hash,
|
|
||||||
Err(RpcError::ConnectionError(e)) => {
|
|
||||||
println!("get_block_hash ConnectionError: {e}");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
Err(e) => panic!("couldn't get block {block_i}'s hash: {e:?}"),
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO: Grab the JSON to also check it was deserialized correctly
|
|
||||||
#[derive(Deserialize, Debug)]
|
|
||||||
struct BlockResponse {
|
|
||||||
blob: String,
|
|
||||||
}
|
|
||||||
let res: BlockResponse = loop {
|
|
||||||
match rpc.json_rpc_call("get_block", Some(json!({ "hash": hex::encode(hash) }))).await {
|
|
||||||
Ok(res) => break res,
|
|
||||||
Err(RpcError::ConnectionError(e)) => {
|
|
||||||
println!("get_block ConnectionError: {e}");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
Err(e) => panic!("couldn't get block {block_i} via block.hash(): {e:?}"),
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let blob = hex::decode(res.blob).expect("node returned non-hex block");
|
|
||||||
let block = Block::read(&mut blob.as_slice())
|
|
||||||
.unwrap_or_else(|e| panic!("couldn't deserialize block {block_i}: {e}"));
|
|
||||||
assert_eq!(block.hash(), hash, "hash differs");
|
|
||||||
assert_eq!(block.serialize(), blob, "serialization differs");
|
|
||||||
|
|
||||||
let txs_len = 1 + block.txs.len();
|
|
||||||
|
|
||||||
if !block.txs.is_empty() {
|
|
||||||
#[derive(Deserialize, Debug)]
|
|
||||||
struct TransactionResponse {
|
|
||||||
tx_hash: String,
|
|
||||||
as_hex: String,
|
|
||||||
}
|
|
||||||
#[derive(Deserialize, Debug)]
|
|
||||||
struct TransactionsResponse {
|
|
||||||
#[serde(default)]
|
|
||||||
missed_tx: Vec<String>,
|
|
||||||
txs: Vec<TransactionResponse>,
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut hashes_hex = block.txs.iter().map(hex::encode).collect::<Vec<_>>();
|
|
||||||
let mut all_txs = vec![];
|
|
||||||
while !hashes_hex.is_empty() {
|
|
||||||
let txs: TransactionsResponse = loop {
|
|
||||||
match rpc
|
|
||||||
.rpc_call(
|
|
||||||
"get_transactions",
|
|
||||||
Some(json!({
|
|
||||||
"txs_hashes": hashes_hex.drain(.. hashes_hex.len().min(100)).collect::<Vec<_>>(),
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(txs) => break txs,
|
|
||||||
Err(RpcError::ConnectionError(e)) => {
|
|
||||||
println!("get_transactions ConnectionError: {e}");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
Err(e) => panic!("couldn't call get_transactions: {e:?}"),
|
|
||||||
}
|
|
||||||
};
|
|
||||||
assert!(txs.missed_tx.is_empty());
|
|
||||||
all_txs.extend(txs.txs);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut batch = BatchVerifier::new(block.txs.len());
|
|
||||||
for (tx_hash, tx_res) in block.txs.into_iter().zip(all_txs) {
|
|
||||||
assert_eq!(
|
|
||||||
tx_res.tx_hash,
|
|
||||||
hex::encode(tx_hash),
|
|
||||||
"node returned a transaction with different hash"
|
|
||||||
);
|
|
||||||
|
|
||||||
let tx = Transaction::read(
|
|
||||||
&mut hex::decode(&tx_res.as_hex).expect("node returned non-hex transaction").as_slice(),
|
|
||||||
)
|
|
||||||
.expect("couldn't deserialize transaction");
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
hex::encode(tx.serialize()),
|
|
||||||
tx_res.as_hex,
|
|
||||||
"Transaction serialization was different"
|
|
||||||
);
|
|
||||||
assert_eq!(tx.hash(), tx_hash, "Transaction hash was different");
|
|
||||||
|
|
||||||
if matches!(tx.rct_signatures.prunable, RctPrunable::Null) {
|
|
||||||
assert_eq!(tx.prefix.version, 1);
|
|
||||||
assert!(!tx.signatures.is_empty());
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let sig_hash = tx.signature_hash();
|
|
||||||
// Verify all proofs we support proving for
|
|
||||||
// This is due to having debug_asserts calling verify within their proving, and CLSAG
|
|
||||||
// multisig explicitly calling verify as part of its signing process
|
|
||||||
// Accordingly, making sure our signature_hash algorithm is correct is great, and further
|
|
||||||
// making sure the verification functions are valid is appreciated
|
|
||||||
match tx.rct_signatures.prunable {
|
|
||||||
RctPrunable::Null |
|
|
||||||
RctPrunable::AggregateMlsagBorromean { .. } |
|
|
||||||
RctPrunable::MlsagBorromean { .. } => {}
|
|
||||||
RctPrunable::MlsagBulletproofs { bulletproofs, .. } => {
|
|
||||||
assert!(bulletproofs.batch_verify(
|
|
||||||
&mut rand_core::OsRng,
|
|
||||||
&mut batch,
|
|
||||||
(),
|
|
||||||
&tx.rct_signatures.base.commitments
|
|
||||||
));
|
|
||||||
}
|
|
||||||
RctPrunable::Clsag { bulletproofs, clsags, pseudo_outs } => {
|
|
||||||
assert!(bulletproofs.batch_verify(
|
|
||||||
&mut rand_core::OsRng,
|
|
||||||
&mut batch,
|
|
||||||
(),
|
|
||||||
&tx.rct_signatures.base.commitments
|
|
||||||
));
|
|
||||||
|
|
||||||
for (i, clsag) in clsags.into_iter().enumerate() {
|
|
||||||
let (amount, key_offsets, image) = match &tx.prefix.inputs[i] {
|
|
||||||
Input::Gen(_) => panic!("Input::Gen"),
|
|
||||||
Input::ToKey { amount, key_offsets, key_image } => (amount, key_offsets, key_image),
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut running_sum = 0;
|
|
||||||
let mut actual_indexes = vec![];
|
|
||||||
for offset in key_offsets {
|
|
||||||
running_sum += offset;
|
|
||||||
actual_indexes.push(running_sum);
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_outs(
|
|
||||||
rpc: &Rpc<HttpRpc>,
|
|
||||||
amount: u64,
|
|
||||||
indexes: &[u64],
|
|
||||||
) -> Vec<[EdwardsPoint; 2]> {
|
|
||||||
#[derive(Deserialize, Debug)]
|
|
||||||
struct Out {
|
|
||||||
key: String,
|
|
||||||
mask: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
|
||||||
struct Outs {
|
|
||||||
outs: Vec<Out>,
|
|
||||||
}
|
|
||||||
|
|
||||||
let outs: Outs = loop {
|
|
||||||
match rpc
|
|
||||||
.rpc_call(
|
|
||||||
"get_outs",
|
|
||||||
Some(json!({
|
|
||||||
"get_txid": true,
|
|
||||||
"outputs": indexes.iter().map(|o| json!({
|
|
||||||
"amount": amount,
|
|
||||||
"index": o
|
|
||||||
})).collect::<Vec<_>>()
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(outs) => break outs,
|
|
||||||
Err(RpcError::ConnectionError(e)) => {
|
|
||||||
println!("get_outs ConnectionError: {e}");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
Err(e) => panic!("couldn't connect to RPC to get outs: {e:?}"),
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let rpc_point = |point: &str| {
|
|
||||||
CompressedEdwardsY(
|
|
||||||
hex::decode(point)
|
|
||||||
.expect("invalid hex for ring member")
|
|
||||||
.try_into()
|
|
||||||
.expect("invalid point len for ring member"),
|
|
||||||
)
|
|
||||||
.decompress()
|
|
||||||
.expect("invalid point for ring member")
|
|
||||||
};
|
|
||||||
|
|
||||||
outs
|
|
||||||
.outs
|
|
||||||
.iter()
|
|
||||||
.map(|out| {
|
|
||||||
let mask = rpc_point(&out.mask);
|
|
||||||
if amount != 0 {
|
|
||||||
assert_eq!(mask, Commitment::new(Scalar::from(1u8), amount).calculate());
|
|
||||||
}
|
|
||||||
[rpc_point(&out.key), mask]
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
clsag
|
|
||||||
.verify(
|
|
||||||
&get_outs(&rpc, amount.unwrap_or(0), &actual_indexes).await,
|
|
||||||
image,
|
|
||||||
&pseudo_outs[i],
|
|
||||||
&sig_hash,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assert!(batch.verify_vartime());
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("Deserialized, hashed, and reserialized {block_i} with {txs_len} TXs");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "binaries")]
|
|
||||||
#[tokio::main]
|
|
||||||
async fn main() {
|
|
||||||
use binaries::*;
|
|
||||||
|
|
||||||
let args = std::env::args().collect::<Vec<String>>();
|
|
||||||
|
|
||||||
// Read start block as the first arg
|
|
||||||
let mut block_i = args[1].parse::<usize>().expect("invalid start block");
|
|
||||||
|
|
||||||
// How many blocks to work on at once
|
|
||||||
let async_parallelism: usize =
|
|
||||||
args.get(2).unwrap_or(&"8".to_string()).parse::<usize>().expect("invalid parallelism argument");
|
|
||||||
|
|
||||||
// Read further args as RPC URLs
|
|
||||||
let default_nodes = vec![
|
|
||||||
"http://xmr-node.cakewallet.com:18081".to_string(),
|
|
||||||
"https://node.sethforprivacy.com".to_string(),
|
|
||||||
];
|
|
||||||
let mut specified_nodes = vec![];
|
|
||||||
{
|
|
||||||
let mut i = 0;
|
|
||||||
loop {
|
|
||||||
let Some(node) = args.get(3 + i) else { break };
|
|
||||||
specified_nodes.push(node.clone());
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let nodes = if specified_nodes.is_empty() { default_nodes } else { specified_nodes };
|
|
||||||
|
|
||||||
let rpc = |url: String| async move {
|
|
||||||
HttpRpc::new(url.clone())
|
|
||||||
.await
|
|
||||||
.unwrap_or_else(|_| panic!("couldn't create HttpRpc connected to {url}"))
|
|
||||||
};
|
|
||||||
let main_rpc = rpc(nodes[0].clone()).await;
|
|
||||||
let mut rpcs = vec![];
|
|
||||||
for i in 0 .. async_parallelism {
|
|
||||||
rpcs.push(Arc::new(rpc(nodes[i % nodes.len()].clone()).await));
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut rpc_i = 0;
|
|
||||||
let mut handles: Vec<JoinHandle<()>> = vec![];
|
|
||||||
let mut height = 0;
|
|
||||||
loop {
|
|
||||||
let new_height = main_rpc.get_height().await.expect("couldn't call get_height");
|
|
||||||
if new_height == height {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
height = new_height;
|
|
||||||
|
|
||||||
while block_i < height {
|
|
||||||
if handles.len() >= async_parallelism {
|
|
||||||
// Guarantee one handle is complete
|
|
||||||
handles.swap_remove(0).await.unwrap();
|
|
||||||
|
|
||||||
// Remove all of the finished handles
|
|
||||||
let mut i = 0;
|
|
||||||
while i < handles.len() {
|
|
||||||
if handles[i].is_finished() {
|
|
||||||
handles.swap_remove(i).await.unwrap();
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handles.push(tokio::spawn(check_block(rpcs[rpc_i].clone(), block_i)));
|
|
||||||
rpc_i = (rpc_i + 1) % rpcs.len();
|
|
||||||
block_i += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(feature = "binaries"))]
|
|
||||||
fn main() {
|
|
||||||
panic!("To run binaries, please build with `--feature binaries`.");
|
|
||||||
}
|
|
||||||
@@ -1,31 +1,19 @@
|
|||||||
use std_shims::{
|
|
||||||
vec::Vec,
|
|
||||||
io::{self, Read, Write},
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
hash,
|
|
||||||
merkle::merkle_root,
|
|
||||||
serialize::*,
|
serialize::*,
|
||||||
transaction::{Input, Transaction},
|
transaction::Transaction
|
||||||
};
|
};
|
||||||
|
|
||||||
const CORRECT_BLOCK_HASH_202612: [u8; 32] =
|
#[derive(Clone, PartialEq, Debug)]
|
||||||
hex_literal::hex!("426d16cff04c71f8b16340b722dc4010a2dd3831c22041431f772547ba6e331a");
|
|
||||||
const EXISTING_BLOCK_HASH_202612: [u8; 32] =
|
|
||||||
hex_literal::hex!("bbd604d2ba11ba27935e006ed39c9bfdd99b76bf4a50654bc1e1e61217962698");
|
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
|
||||||
pub struct BlockHeader {
|
pub struct BlockHeader {
|
||||||
pub major_version: u8,
|
pub major_version: u64,
|
||||||
pub minor_version: u8,
|
pub minor_version: u64,
|
||||||
pub timestamp: u64,
|
pub timestamp: u64,
|
||||||
pub previous: [u8; 32],
|
pub previous: [u8; 32],
|
||||||
pub nonce: u32,
|
pub nonce: u32
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BlockHeader {
|
impl BlockHeader {
|
||||||
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
pub fn serialize<W: std::io::Write>(&self, w: &mut W) -> std::io::Result<()> {
|
||||||
write_varint(&self.major_version, w)?;
|
write_varint(&self.major_version, w)?;
|
||||||
write_varint(&self.minor_version, w)?;
|
write_varint(&self.minor_version, w)?;
|
||||||
write_varint(&self.timestamp, w)?;
|
write_varint(&self.timestamp, w)?;
|
||||||
@@ -33,91 +21,46 @@ impl BlockHeader {
|
|||||||
w.write_all(&self.nonce.to_le_bytes())
|
w.write_all(&self.nonce.to_le_bytes())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn serialize(&self) -> Vec<u8> {
|
pub fn deserialize<R: std::io::Read>(r: &mut R) -> std::io::Result<BlockHeader> {
|
||||||
let mut serialized = vec![];
|
Ok(
|
||||||
self.write(&mut serialized).unwrap();
|
BlockHeader {
|
||||||
serialized
|
major_version: read_varint(r)?,
|
||||||
}
|
minor_version: read_varint(r)?,
|
||||||
|
timestamp: read_varint(r)?,
|
||||||
pub fn read<R: Read>(r: &mut R) -> io::Result<BlockHeader> {
|
previous: { let mut previous = [0; 32]; r.read_exact(&mut previous)?; previous },
|
||||||
Ok(BlockHeader {
|
nonce: { let mut nonce = [0; 4]; r.read_exact(&mut nonce)?; u32::from_le_bytes(nonce) }
|
||||||
major_version: read_varint(r)?,
|
}
|
||||||
minor_version: read_varint(r)?,
|
)
|
||||||
timestamp: read_varint(r)?,
|
|
||||||
previous: read_bytes(r)?,
|
|
||||||
nonce: read_bytes(r).map(u32::from_le_bytes)?,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
#[derive(Clone, PartialEq, Debug)]
|
||||||
pub struct Block {
|
pub struct Block {
|
||||||
pub header: BlockHeader,
|
pub header: BlockHeader,
|
||||||
pub miner_tx: Transaction,
|
pub miner_tx: Transaction,
|
||||||
pub txs: Vec<[u8; 32]>,
|
pub txs: Vec<[u8; 32]>
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Block {
|
impl Block {
|
||||||
pub fn number(&self) -> usize {
|
pub fn serialize<W: std::io::Write>(&self, w: &mut W) -> std::io::Result<()> {
|
||||||
match self.miner_tx.prefix.inputs.first() {
|
self.header.serialize(w)?;
|
||||||
Some(Input::Gen(number)) => (*number).try_into().unwrap(),
|
self.miner_tx.serialize(w)?;
|
||||||
_ => panic!("invalid block, miner TX didn't have a Input::Gen"),
|
write_varint(&self.txs.len().try_into().unwrap(), w)?;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
|
||||||
self.header.write(w)?;
|
|
||||||
self.miner_tx.write(w)?;
|
|
||||||
write_varint(&self.txs.len(), w)?;
|
|
||||||
for tx in &self.txs {
|
for tx in &self.txs {
|
||||||
w.write_all(tx)?;
|
w.write_all(tx)?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn tx_merkle_root(&self) -> [u8; 32] {
|
pub fn deserialize<R: std::io::Read>(r: &mut R) -> std::io::Result<Block> {
|
||||||
merkle_root(self.miner_tx.hash(), &self.txs)
|
Ok(
|
||||||
}
|
Block {
|
||||||
|
header: BlockHeader::deserialize(r)?,
|
||||||
/// Serialize the block as required for the proof of work hash.
|
miner_tx: Transaction::deserialize(r)?,
|
||||||
///
|
txs: (0 .. read_varint(r)?).map(
|
||||||
/// This is distinct from the serialization required for the block hash. To get the block hash,
|
|_| { let mut tx = [0; 32]; r.read_exact(&mut tx).map(|_| tx) }
|
||||||
/// use the [`Block::hash`] function.
|
).collect::<Result<_, _>>()?
|
||||||
pub fn serialize_hashable(&self) -> Vec<u8> {
|
}
|
||||||
let mut blob = self.header.serialize();
|
)
|
||||||
blob.extend_from_slice(&self.tx_merkle_root());
|
|
||||||
write_varint(&(1 + u64::try_from(self.txs.len()).unwrap()), &mut blob).unwrap();
|
|
||||||
|
|
||||||
blob
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn hash(&self) -> [u8; 32] {
|
|
||||||
let mut hashable = self.serialize_hashable();
|
|
||||||
// Monero pre-appends a VarInt of the block hashing blobs length before getting the block hash
|
|
||||||
// but doesn't do this when getting the proof of work hash :)
|
|
||||||
let mut hashing_blob = Vec::with_capacity(8 + hashable.len());
|
|
||||||
write_varint(&u64::try_from(hashable.len()).unwrap(), &mut hashing_blob).unwrap();
|
|
||||||
hashing_blob.append(&mut hashable);
|
|
||||||
|
|
||||||
let hash = hash(&hashing_blob);
|
|
||||||
if hash == CORRECT_BLOCK_HASH_202612 {
|
|
||||||
return EXISTING_BLOCK_HASH_202612;
|
|
||||||
};
|
|
||||||
|
|
||||||
hash
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn serialize(&self) -> Vec<u8> {
|
|
||||||
let mut serialized = vec![];
|
|
||||||
self.write(&mut serialized).unwrap();
|
|
||||||
serialized
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn read<R: Read>(r: &mut R) -> io::Result<Block> {
|
|
||||||
Ok(Block {
|
|
||||||
header: BlockHeader::read(r)?,
|
|
||||||
miner_tx: Transaction::read(r)?,
|
|
||||||
txs: (0_usize .. read_varint(r)?).map(|_| read_bytes(r)).collect::<Result<_, _>>()?,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
76
coins/monero/src/frost.rs
Normal file
76
coins/monero/src/frost.rs
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
use std::io::Read;
|
||||||
|
|
||||||
|
use thiserror::Error;
|
||||||
|
use rand_core::{RngCore, CryptoRng};
|
||||||
|
|
||||||
|
use curve25519_dalek::{scalar::Scalar, edwards::EdwardsPoint};
|
||||||
|
|
||||||
|
use group::{Group, GroupEncoding};
|
||||||
|
|
||||||
|
use transcript::{Transcript, RecommendedTranscript};
|
||||||
|
use dalek_ff_group as dfg;
|
||||||
|
use dleq::DLEqProof;
|
||||||
|
|
||||||
|
#[derive(Clone, Error, Debug)]
|
||||||
|
pub enum MultisigError {
|
||||||
|
#[error("internal error ({0})")]
|
||||||
|
InternalError(String),
|
||||||
|
#[error("invalid discrete log equality proof")]
|
||||||
|
InvalidDLEqProof(u16),
|
||||||
|
#[error("invalid key image {0}")]
|
||||||
|
InvalidKeyImage(u16)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn transcript() -> RecommendedTranscript {
|
||||||
|
RecommendedTranscript::new(b"monero_key_image_dleq")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub(crate) fn write_dleq<R: RngCore + CryptoRng>(
|
||||||
|
rng: &mut R,
|
||||||
|
H: EdwardsPoint,
|
||||||
|
x: Scalar
|
||||||
|
) -> Vec<u8> {
|
||||||
|
let mut res = Vec::with_capacity(64);
|
||||||
|
DLEqProof::prove(
|
||||||
|
rng,
|
||||||
|
// Doesn't take in a larger transcript object due to the usage of this
|
||||||
|
// Every prover would immediately write their own DLEq proof, when they can only do so in
|
||||||
|
// the proper order if they want to reach consensus
|
||||||
|
// It'd be a poor API to have CLSAG define a new transcript solely to pass here, just to try to
|
||||||
|
// merge later in some form, when it should instead just merge xH (as it does)
|
||||||
|
&mut transcript(),
|
||||||
|
&[dfg::EdwardsPoint::generator(), dfg::EdwardsPoint(H)],
|
||||||
|
dfg::Scalar(x)
|
||||||
|
).serialize(&mut res).unwrap();
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub(crate) fn read_dleq<Re: Read>(
|
||||||
|
serialized: &mut Re,
|
||||||
|
H: EdwardsPoint,
|
||||||
|
l: u16,
|
||||||
|
xG: dfg::EdwardsPoint
|
||||||
|
) -> Result<dfg::EdwardsPoint, MultisigError> {
|
||||||
|
let mut bytes = [0; 32];
|
||||||
|
serialized.read_exact(&mut bytes).map_err(|_| MultisigError::InvalidDLEqProof(l))?;
|
||||||
|
// dfg ensures the point is torsion free
|
||||||
|
let xH = Option::<dfg::EdwardsPoint>::from(
|
||||||
|
dfg::EdwardsPoint::from_bytes(&bytes)).ok_or(MultisigError::InvalidDLEqProof(l)
|
||||||
|
)?;
|
||||||
|
// Ensure this is a canonical point
|
||||||
|
if xH.to_bytes() != bytes {
|
||||||
|
Err(MultisigError::InvalidDLEqProof(l))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
DLEqProof::<dfg::EdwardsPoint>::deserialize(
|
||||||
|
serialized
|
||||||
|
).map_err(|_| MultisigError::InvalidDLEqProof(l))?.verify(
|
||||||
|
&mut transcript(),
|
||||||
|
&[dfg::EdwardsPoint::generator(), dfg::EdwardsPoint(H)],
|
||||||
|
&[xG, xH]
|
||||||
|
).map_err(|_| MultisigError::InvalidDLEqProof(l))?;
|
||||||
|
|
||||||
|
Ok(xH)
|
||||||
|
}
|
||||||
@@ -1,225 +1,100 @@
|
|||||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
use std::slice;
|
||||||
#![doc = include_str!("../README.md")]
|
|
||||||
#![cfg_attr(not(feature = "std"), no_std)]
|
|
||||||
|
|
||||||
#[cfg(not(feature = "std"))]
|
|
||||||
#[macro_use]
|
|
||||||
extern crate alloc;
|
|
||||||
|
|
||||||
use std_shims::{sync::OnceLock, io};
|
|
||||||
|
|
||||||
|
use lazy_static::lazy_static;
|
||||||
use rand_core::{RngCore, CryptoRng};
|
use rand_core::{RngCore, CryptoRng};
|
||||||
|
|
||||||
use zeroize::{Zeroize, ZeroizeOnDrop};
|
use subtle::ConstantTimeEq;
|
||||||
|
|
||||||
use sha3::{Digest, Keccak256};
|
use tiny_keccak::{Hasher, Keccak};
|
||||||
|
|
||||||
use curve25519_dalek::{constants::ED25519_BASEPOINT_TABLE, scalar::Scalar, edwards::EdwardsPoint};
|
use curve25519_dalek::{
|
||||||
|
constants::ED25519_BASEPOINT_TABLE,
|
||||||
|
scalar::Scalar,
|
||||||
|
edwards::{EdwardsPoint, EdwardsBasepointTable, CompressedEdwardsY}
|
||||||
|
};
|
||||||
|
|
||||||
pub use monero_generators::H;
|
#[cfg(feature = "multisig")]
|
||||||
|
pub mod frost;
|
||||||
mod merkle;
|
|
||||||
|
|
||||||
mod serialize;
|
mod serialize;
|
||||||
use serialize::{read_byte, read_u16};
|
|
||||||
|
|
||||||
/// UnreducedScalar struct with functionality for recovering incorrectly reduced scalars.
|
|
||||||
mod unreduced_scalar;
|
|
||||||
|
|
||||||
/// Ring Signature structs and functionality.
|
|
||||||
pub mod ring_signatures;
|
|
||||||
|
|
||||||
/// RingCT structs and functionality.
|
|
||||||
pub mod ringct;
|
pub mod ringct;
|
||||||
use ringct::RctType;
|
|
||||||
|
|
||||||
/// Transaction structs.
|
|
||||||
pub mod transaction;
|
pub mod transaction;
|
||||||
/// Block structs.
|
|
||||||
pub mod block;
|
pub mod block;
|
||||||
|
|
||||||
/// Monero daemon RPC interface.
|
|
||||||
pub mod rpc;
|
pub mod rpc;
|
||||||
/// Wallet functionality, enabling scanning and sending transactions.
|
|
||||||
pub mod wallet;
|
pub mod wallet;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|
||||||
static INV_EIGHT_CELL: OnceLock<Scalar> = OnceLock::new();
|
lazy_static! {
|
||||||
|
static ref H: EdwardsPoint = CompressedEdwardsY(
|
||||||
|
hex::decode("8b655970153799af2aeadc9ff1add0ea6c7251d54154cfa92c173a0dd39c1f94").unwrap().try_into().unwrap()
|
||||||
|
).decompress().unwrap();
|
||||||
|
static ref H_TABLE: EdwardsBasepointTable = EdwardsBasepointTable::create(&*H);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function from libsodium our subsection of Monero relies on. Implementing it here means we don't
|
||||||
|
// need to link against libsodium
|
||||||
|
#[no_mangle]
|
||||||
|
unsafe extern "C" fn crypto_verify_32(a: *const u8, b: *const u8) -> isize {
|
||||||
|
isize::from(
|
||||||
|
slice::from_raw_parts(a, 32).ct_eq(slice::from_raw_parts(b, 32)).unwrap_u8()
|
||||||
|
) - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Offer a wide reduction to C. Our seeded RNG prevented Monero from defining an unbiased scalar
|
||||||
|
// generation function, and in order to not use Monero code (which would require propagating its
|
||||||
|
// license), the function was rewritten. It was rewritten with wide reduction, instead of rejection
|
||||||
|
// sampling however, hence the need for this function
|
||||||
|
#[no_mangle]
|
||||||
|
unsafe extern "C" fn monero_wide_reduce(value: *mut u8) {
|
||||||
|
let res = Scalar::from_bytes_mod_order_wide(
|
||||||
|
std::slice::from_raw_parts(value, 64).try_into().unwrap()
|
||||||
|
);
|
||||||
|
for (i, b) in res.to_bytes().iter().enumerate() {
|
||||||
|
value.add(i).write(*b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
pub(crate) fn INV_EIGHT() -> Scalar {
|
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
|
||||||
*INV_EIGHT_CELL.get_or_init(|| Scalar::from(8u8).invert())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Monero protocol version.
|
|
||||||
///
|
|
||||||
/// v15 is omitted as v15 was simply v14 and v16 being active at the same time, with regards to the
|
|
||||||
/// transactions supported. Accordingly, v16 should be used during v15.
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
|
|
||||||
#[allow(non_camel_case_types)]
|
|
||||||
pub enum Protocol {
|
|
||||||
v14,
|
|
||||||
v16,
|
|
||||||
Custom {
|
|
||||||
ring_len: usize,
|
|
||||||
bp_plus: bool,
|
|
||||||
optimal_rct_type: RctType,
|
|
||||||
view_tags: bool,
|
|
||||||
v16_fee: bool,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Protocol {
|
|
||||||
/// Amount of ring members under this protocol version.
|
|
||||||
pub fn ring_len(&self) -> usize {
|
|
||||||
match self {
|
|
||||||
Protocol::v14 => 11,
|
|
||||||
Protocol::v16 => 16,
|
|
||||||
Protocol::Custom { ring_len, .. } => *ring_len,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Whether or not the specified version uses Bulletproofs or Bulletproofs+.
|
|
||||||
///
|
|
||||||
/// This method will likely be reworked when versions not using Bulletproofs at all are added.
|
|
||||||
pub fn bp_plus(&self) -> bool {
|
|
||||||
match self {
|
|
||||||
Protocol::v14 => false,
|
|
||||||
Protocol::v16 => true,
|
|
||||||
Protocol::Custom { bp_plus, .. } => *bp_plus,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Make this an Option when we support pre-RCT protocols
|
|
||||||
pub fn optimal_rct_type(&self) -> RctType {
|
|
||||||
match self {
|
|
||||||
Protocol::v14 => RctType::Clsag,
|
|
||||||
Protocol::v16 => RctType::BulletproofsPlus,
|
|
||||||
Protocol::Custom { optimal_rct_type, .. } => *optimal_rct_type,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Whether or not the specified version uses view tags.
|
|
||||||
pub fn view_tags(&self) -> bool {
|
|
||||||
match self {
|
|
||||||
Protocol::v14 => false,
|
|
||||||
Protocol::v16 => true,
|
|
||||||
Protocol::Custom { view_tags, .. } => *view_tags,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Whether or not the specified version uses the fee algorithm from Monero
|
|
||||||
/// hard fork version 16 (released in v18 binaries).
|
|
||||||
pub fn v16_fee(&self) -> bool {
|
|
||||||
match self {
|
|
||||||
Protocol::v14 => false,
|
|
||||||
Protocol::v16 => true,
|
|
||||||
Protocol::Custom { v16_fee, .. } => *v16_fee,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn write<W: io::Write>(&self, w: &mut W) -> io::Result<()> {
|
|
||||||
match self {
|
|
||||||
Protocol::v14 => w.write_all(&[0, 14]),
|
|
||||||
Protocol::v16 => w.write_all(&[0, 16]),
|
|
||||||
Protocol::Custom { ring_len, bp_plus, optimal_rct_type, view_tags, v16_fee } => {
|
|
||||||
// Custom, version 0
|
|
||||||
w.write_all(&[1, 0])?;
|
|
||||||
w.write_all(&u16::try_from(*ring_len).unwrap().to_le_bytes())?;
|
|
||||||
w.write_all(&[u8::from(*bp_plus)])?;
|
|
||||||
w.write_all(&[optimal_rct_type.to_byte()])?;
|
|
||||||
w.write_all(&[u8::from(*view_tags)])?;
|
|
||||||
w.write_all(&[u8::from(*v16_fee)])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn read<R: io::Read>(r: &mut R) -> io::Result<Protocol> {
|
|
||||||
Ok(match read_byte(r)? {
|
|
||||||
// Monero protocol
|
|
||||||
0 => match read_byte(r)? {
|
|
||||||
14 => Protocol::v14,
|
|
||||||
16 => Protocol::v16,
|
|
||||||
_ => Err(io::Error::other("unrecognized monero protocol"))?,
|
|
||||||
},
|
|
||||||
// Custom
|
|
||||||
1 => match read_byte(r)? {
|
|
||||||
0 => Protocol::Custom {
|
|
||||||
ring_len: read_u16(r)?.into(),
|
|
||||||
bp_plus: match read_byte(r)? {
|
|
||||||
0 => false,
|
|
||||||
1 => true,
|
|
||||||
_ => Err(io::Error::other("invalid bool serialization"))?,
|
|
||||||
},
|
|
||||||
optimal_rct_type: RctType::from_byte(read_byte(r)?)
|
|
||||||
.ok_or_else(|| io::Error::other("invalid RctType serialization"))?,
|
|
||||||
view_tags: match read_byte(r)? {
|
|
||||||
0 => false,
|
|
||||||
1 => true,
|
|
||||||
_ => Err(io::Error::other("invalid bool serialization"))?,
|
|
||||||
},
|
|
||||||
v16_fee: match read_byte(r)? {
|
|
||||||
0 => false,
|
|
||||||
1 => true,
|
|
||||||
_ => Err(io::Error::other("invalid bool serialization"))?,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
_ => Err(io::Error::other("unrecognized custom protocol serialization"))?,
|
|
||||||
},
|
|
||||||
_ => Err(io::Error::other("unrecognized protocol serialization"))?,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Transparent structure representing a Pedersen commitment's contents.
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
#[derive(Clone, PartialEq, Eq, Zeroize, ZeroizeOnDrop)]
|
|
||||||
pub struct Commitment {
|
pub struct Commitment {
|
||||||
pub mask: Scalar,
|
pub mask: Scalar,
|
||||||
pub amount: u64,
|
pub amount: u64
|
||||||
}
|
|
||||||
|
|
||||||
impl core::fmt::Debug for Commitment {
|
|
||||||
fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
|
|
||||||
fmt.debug_struct("Commitment").field("amount", &self.amount).finish_non_exhaustive()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Commitment {
|
impl Commitment {
|
||||||
/// A commitment to zero, defined with a mask of 1 (as to not be the identity).
|
|
||||||
pub fn zero() -> Commitment {
|
pub fn zero() -> Commitment {
|
||||||
Commitment { mask: Scalar::ONE, amount: 0 }
|
Commitment { mask: Scalar::one(), amount: 0}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new(mask: Scalar, amount: u64) -> Commitment {
|
pub fn new(mask: Scalar, amount: u64) -> Commitment {
|
||||||
Commitment { mask, amount }
|
Commitment { mask, amount }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Calculate a Pedersen commitment, as a point, from the transparent structure.
|
|
||||||
pub fn calculate(&self) -> EdwardsPoint {
|
pub fn calculate(&self) -> EdwardsPoint {
|
||||||
(&self.mask * ED25519_BASEPOINT_TABLE) + (Scalar::from(self.amount) * H())
|
(&self.mask * &ED25519_BASEPOINT_TABLE) + (&Scalar::from(self.amount) * &*H_TABLE)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Support generating a random scalar using a modern rand, as dalek's is notoriously dated.
|
// Allows using a modern rand as dalek's is notoriously dated
|
||||||
pub fn random_scalar<R: RngCore + CryptoRng>(rng: &mut R) -> Scalar {
|
pub fn random_scalar<R: RngCore + CryptoRng>(rng: &mut R) -> Scalar {
|
||||||
let mut r = [0; 64];
|
let mut r = [0; 64];
|
||||||
rng.fill_bytes(&mut r);
|
rng.fill_bytes(&mut r);
|
||||||
Scalar::from_bytes_mod_order_wide(&r)
|
Scalar::from_bytes_mod_order_wide(&r)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn hash(data: &[u8]) -> [u8; 32] {
|
pub fn hash(data: &[u8]) -> [u8; 32] {
|
||||||
Keccak256::digest(data).into()
|
let mut keccak = Keccak::v256();
|
||||||
|
keccak.update(data);
|
||||||
|
let mut res = [0; 32];
|
||||||
|
keccak.finalize(&mut res);
|
||||||
|
res
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Hash the provided data to a scalar via keccak256(data) % l.
|
|
||||||
pub fn hash_to_scalar(data: &[u8]) -> Scalar {
|
pub fn hash_to_scalar(data: &[u8]) -> Scalar {
|
||||||
let scalar = Scalar::from_bytes_mod_order(hash(data));
|
Scalar::from_bytes_mod_order(hash(&data))
|
||||||
// Monero will explicitly error in this case
|
|
||||||
// This library acknowledges its practical impossibility of it occurring, and doesn't bother to
|
|
||||||
// code in logic to handle it. That said, if it ever occurs, something must happen in order to
|
|
||||||
// not generate/verify a proof we believe to be valid when it isn't
|
|
||||||
assert!(scalar != Scalar::ZERO, "ZERO HASH: {data:?}");
|
|
||||||
scalar
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,55 +0,0 @@
|
|||||||
use std_shims::vec::Vec;
|
|
||||||
|
|
||||||
use crate::hash;
|
|
||||||
|
|
||||||
pub(crate) fn merkle_root(root: [u8; 32], leafs: &[[u8; 32]]) -> [u8; 32] {
|
|
||||||
match leafs.len() {
|
|
||||||
0 => root,
|
|
||||||
1 => hash(&[root, leafs[0]].concat()),
|
|
||||||
_ => {
|
|
||||||
let mut hashes = Vec::with_capacity(1 + leafs.len());
|
|
||||||
hashes.push(root);
|
|
||||||
hashes.extend(leafs);
|
|
||||||
|
|
||||||
// Monero preprocess this so the length is a power of 2
|
|
||||||
let mut high_pow_2 = 4; // 4 is the lowest value this can be
|
|
||||||
while high_pow_2 < hashes.len() {
|
|
||||||
high_pow_2 *= 2;
|
|
||||||
}
|
|
||||||
let low_pow_2 = high_pow_2 / 2;
|
|
||||||
|
|
||||||
// Merge right-most hashes until we're at the low_pow_2
|
|
||||||
{
|
|
||||||
let overage = hashes.len() - low_pow_2;
|
|
||||||
let mut rightmost = hashes.drain((low_pow_2 - overage) ..);
|
|
||||||
// This is true since we took overage from beneath and above low_pow_2, taking twice as
|
|
||||||
// many elements as overage
|
|
||||||
debug_assert_eq!(rightmost.len() % 2, 0);
|
|
||||||
|
|
||||||
let mut paired_hashes = Vec::with_capacity(overage);
|
|
||||||
while let Some(left) = rightmost.next() {
|
|
||||||
let right = rightmost.next().unwrap();
|
|
||||||
paired_hashes.push(hash(&[left.as_ref(), &right].concat()));
|
|
||||||
}
|
|
||||||
drop(rightmost);
|
|
||||||
|
|
||||||
hashes.extend(paired_hashes);
|
|
||||||
assert_eq!(hashes.len(), low_pow_2);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Do a traditional pairing off
|
|
||||||
let mut new_hashes = Vec::with_capacity(hashes.len() / 2);
|
|
||||||
while hashes.len() > 1 {
|
|
||||||
let mut i = 0;
|
|
||||||
while i < hashes.len() {
|
|
||||||
new_hashes.push(hash(&[hashes[i], hashes[i + 1]].concat()));
|
|
||||||
i += 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
hashes = new_hashes;
|
|
||||||
new_hashes = Vec::with_capacity(hashes.len() / 2);
|
|
||||||
}
|
|
||||||
hashes[0]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
use std_shims::{
|
|
||||||
io::{self, *},
|
|
||||||
vec::Vec,
|
|
||||||
};
|
|
||||||
|
|
||||||
use zeroize::Zeroize;
|
|
||||||
|
|
||||||
use curve25519_dalek::{EdwardsPoint, Scalar};
|
|
||||||
|
|
||||||
use monero_generators::hash_to_point;
|
|
||||||
|
|
||||||
use crate::{serialize::*, hash_to_scalar};
|
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
|
|
||||||
pub struct Signature {
|
|
||||||
c: Scalar,
|
|
||||||
r: Scalar,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Signature {
|
|
||||||
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
|
||||||
write_scalar(&self.c, w)?;
|
|
||||||
write_scalar(&self.r, w)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn read<R: Read>(r: &mut R) -> io::Result<Signature> {
|
|
||||||
Ok(Signature { c: read_scalar(r)?, r: read_scalar(r)? })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
|
|
||||||
pub struct RingSignature {
|
|
||||||
sigs: Vec<Signature>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RingSignature {
|
|
||||||
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
|
||||||
for sig in &self.sigs {
|
|
||||||
sig.write(w)?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn read<R: Read>(members: usize, r: &mut R) -> io::Result<RingSignature> {
|
|
||||||
Ok(RingSignature { sigs: read_raw_vec(Signature::read, members, r)? })
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn verify(&self, msg: &[u8; 32], ring: &[EdwardsPoint], key_image: &EdwardsPoint) -> bool {
|
|
||||||
if ring.len() != self.sigs.len() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut buf = Vec::with_capacity(32 + (32 * 2 * ring.len()));
|
|
||||||
buf.extend_from_slice(msg);
|
|
||||||
|
|
||||||
let mut sum = Scalar::ZERO;
|
|
||||||
|
|
||||||
for (ring_member, sig) in ring.iter().zip(&self.sigs) {
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
let Li = EdwardsPoint::vartime_double_scalar_mul_basepoint(&sig.c, ring_member, &sig.r);
|
|
||||||
buf.extend_from_slice(Li.compress().as_bytes());
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
let Ri = (sig.r * hash_to_point(ring_member.compress().to_bytes())) + (sig.c * key_image);
|
|
||||||
buf.extend_from_slice(Ri.compress().as_bytes());
|
|
||||||
|
|
||||||
sum += sig.c;
|
|
||||||
}
|
|
||||||
|
|
||||||
sum == hash_to_scalar(&buf)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
use core::fmt::Debug;
|
|
||||||
use std_shims::io::{self, Read, Write};
|
|
||||||
|
|
||||||
use curve25519_dalek::{traits::Identity, Scalar, EdwardsPoint};
|
|
||||||
|
|
||||||
use monero_generators::H_pow_2;
|
|
||||||
|
|
||||||
use crate::{hash_to_scalar, unreduced_scalar::UnreducedScalar, serialize::*};
|
|
||||||
|
|
||||||
/// 64 Borromean ring signatures.
|
|
||||||
///
|
|
||||||
/// s0 and s1 are stored as `UnreducedScalar`s due to Monero not requiring they were reduced.
|
|
||||||
/// `UnreducedScalar` preserves their original byte encoding and implements a custom reduction
|
|
||||||
/// algorithm which was in use.
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
|
||||||
pub struct BorromeanSignatures {
|
|
||||||
pub s0: [UnreducedScalar; 64],
|
|
||||||
pub s1: [UnreducedScalar; 64],
|
|
||||||
pub ee: Scalar,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BorromeanSignatures {
|
|
||||||
pub fn read<R: Read>(r: &mut R) -> io::Result<BorromeanSignatures> {
|
|
||||||
Ok(BorromeanSignatures {
|
|
||||||
s0: read_array(UnreducedScalar::read, r)?,
|
|
||||||
s1: read_array(UnreducedScalar::read, r)?,
|
|
||||||
ee: read_scalar(r)?,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
|
||||||
for s0 in &self.s0 {
|
|
||||||
s0.write(w)?;
|
|
||||||
}
|
|
||||||
for s1 in &self.s1 {
|
|
||||||
s1.write(w)?;
|
|
||||||
}
|
|
||||||
write_scalar(&self.ee, w)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn verify(&self, keys_a: &[EdwardsPoint], keys_b: &[EdwardsPoint]) -> bool {
|
|
||||||
let mut transcript = [0; 2048];
|
|
||||||
|
|
||||||
for i in 0 .. 64 {
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
let LL = EdwardsPoint::vartime_double_scalar_mul_basepoint(
|
|
||||||
&self.ee,
|
|
||||||
&keys_a[i],
|
|
||||||
&self.s0[i].recover_monero_slide_scalar(),
|
|
||||||
);
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
let LV = EdwardsPoint::vartime_double_scalar_mul_basepoint(
|
|
||||||
&hash_to_scalar(LL.compress().as_bytes()),
|
|
||||||
&keys_b[i],
|
|
||||||
&self.s1[i].recover_monero_slide_scalar(),
|
|
||||||
);
|
|
||||||
transcript[(i * 32) .. ((i + 1) * 32)].copy_from_slice(LV.compress().as_bytes());
|
|
||||||
}
|
|
||||||
|
|
||||||
hash_to_scalar(&transcript) == self.ee
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A range proof premised on Borromean ring signatures.
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
|
||||||
pub struct BorromeanRange {
|
|
||||||
pub sigs: BorromeanSignatures,
|
|
||||||
pub bit_commitments: [EdwardsPoint; 64],
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BorromeanRange {
|
|
||||||
pub fn read<R: Read>(r: &mut R) -> io::Result<BorromeanRange> {
|
|
||||||
Ok(BorromeanRange {
|
|
||||||
sigs: BorromeanSignatures::read(r)?,
|
|
||||||
bit_commitments: read_array(read_point, r)?,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
|
||||||
self.sigs.write(w)?;
|
|
||||||
write_raw_vec(write_point, &self.bit_commitments, w)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn verify(&self, commitment: &EdwardsPoint) -> bool {
|
|
||||||
if &self.bit_commitments.iter().sum::<EdwardsPoint>() != commitment {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
let H_pow_2 = H_pow_2();
|
|
||||||
let mut commitments_sub_one = [EdwardsPoint::identity(); 64];
|
|
||||||
for i in 0 .. 64 {
|
|
||||||
commitments_sub_one[i] = self.bit_commitments[i] - H_pow_2[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
self.sigs.verify(&self.bit_commitments, &commitments_sub_one)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
161
coins/monero/src/ringct/bulletproofs.rs
Normal file
161
coins/monero/src/ringct/bulletproofs.rs
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
#![allow(non_snake_case)]
|
||||||
|
|
||||||
|
use rand_core::{RngCore, CryptoRng};
|
||||||
|
|
||||||
|
use curve25519_dalek::{scalar::Scalar, edwards::EdwardsPoint};
|
||||||
|
|
||||||
|
use crate::{Commitment, wallet::TransactionError, serialize::*};
|
||||||
|
|
||||||
|
pub(crate) const MAX_OUTPUTS: usize = 16;
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq, Debug)]
|
||||||
|
pub struct Bulletproofs {
|
||||||
|
pub A: EdwardsPoint,
|
||||||
|
pub S: EdwardsPoint,
|
||||||
|
pub T1: EdwardsPoint,
|
||||||
|
pub T2: EdwardsPoint,
|
||||||
|
pub taux: Scalar,
|
||||||
|
pub mu: Scalar,
|
||||||
|
pub L: Vec<EdwardsPoint>,
|
||||||
|
pub R: Vec<EdwardsPoint>,
|
||||||
|
pub a: Scalar,
|
||||||
|
pub b: Scalar,
|
||||||
|
pub t: Scalar
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Bulletproofs {
|
||||||
|
pub(crate) fn fee_weight(outputs: usize) -> usize {
|
||||||
|
let proofs = 6 + usize::try_from(usize::BITS - (outputs - 1).leading_zeros()).unwrap();
|
||||||
|
let len = (9 + (2 * proofs)) * 32;
|
||||||
|
|
||||||
|
let mut clawback = 0;
|
||||||
|
let padded = 1 << (proofs - 6);
|
||||||
|
if padded > 2 {
|
||||||
|
const BP_BASE: usize = 368;
|
||||||
|
clawback = ((BP_BASE * padded) - len) * 4 / 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
len + clawback
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new<R: RngCore + CryptoRng>(rng: &mut R, outputs: &[Commitment]) -> Result<Bulletproofs, TransactionError> {
|
||||||
|
if outputs.len() > MAX_OUTPUTS {
|
||||||
|
return Err(TransactionError::TooManyOutputs)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut seed = [0; 32];
|
||||||
|
rng.fill_bytes(&mut seed);
|
||||||
|
|
||||||
|
let masks = outputs.iter().map(|commitment| commitment.mask.to_bytes()).collect::<Vec<_>>();
|
||||||
|
let amounts = outputs.iter().map(|commitment| commitment.amount).collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let res;
|
||||||
|
unsafe {
|
||||||
|
#[link(name = "wrapper")]
|
||||||
|
extern "C" {
|
||||||
|
fn free(ptr: *const u8);
|
||||||
|
fn c_generate_bp(seed: *const u8, len: u8, amounts: *const u64, masks: *const [u8; 32]) -> *const u8;
|
||||||
|
}
|
||||||
|
|
||||||
|
let ptr = c_generate_bp(
|
||||||
|
seed.as_ptr(),
|
||||||
|
u8::try_from(outputs.len()).unwrap(),
|
||||||
|
amounts.as_ptr(),
|
||||||
|
masks.as_ptr()
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut len = 6 * 32;
|
||||||
|
len += (2 * (1 + (usize::from(ptr.add(len).read()) * 32))) + (3 * 32);
|
||||||
|
res = Bulletproofs::deserialize(
|
||||||
|
// Wrap in a cursor to provide a mutable Reader
|
||||||
|
&mut std::io::Cursor::new(std::slice::from_raw_parts(ptr, len))
|
||||||
|
).expect("Couldn't deserialize Bulletproofs from Monero");
|
||||||
|
free(ptr);
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn verify<R: RngCore + CryptoRng>(&self, rng: &mut R, commitments: &[EdwardsPoint]) -> bool {
|
||||||
|
if commitments.len() > 16 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut seed = [0; 32];
|
||||||
|
rng.fill_bytes(&mut seed);
|
||||||
|
|
||||||
|
let mut serialized = Vec::with_capacity((9 + (2 * self.L.len())) * 32);
|
||||||
|
self.serialize(&mut serialized).unwrap();
|
||||||
|
let commitments: Vec<[u8; 32]> = commitments.iter().map(
|
||||||
|
|commitment| (commitment * Scalar::from(8u8).invert()).compress().to_bytes()
|
||||||
|
).collect();
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
#[link(name = "wrapper")]
|
||||||
|
extern "C" {
|
||||||
|
fn c_verify_bp(
|
||||||
|
seed: *const u8,
|
||||||
|
serialized_len: usize,
|
||||||
|
serialized: *const u8,
|
||||||
|
commitments_len: u8,
|
||||||
|
commitments: *const [u8; 32]
|
||||||
|
) -> bool;
|
||||||
|
}
|
||||||
|
|
||||||
|
c_verify_bp(
|
||||||
|
seed.as_ptr(),
|
||||||
|
serialized.len(),
|
||||||
|
serialized.as_ptr(),
|
||||||
|
u8::try_from(commitments.len()).unwrap(),
|
||||||
|
commitments.as_ptr()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn serialize_core<
|
||||||
|
W: std::io::Write,
|
||||||
|
F: Fn(&[EdwardsPoint], &mut W) -> std::io::Result<()>
|
||||||
|
>(&self, w: &mut W, specific_write_vec: F) -> std::io::Result<()> {
|
||||||
|
write_point(&self.A, w)?;
|
||||||
|
write_point(&self.S, w)?;
|
||||||
|
write_point(&self.T1, w)?;
|
||||||
|
write_point(&self.T2, w)?;
|
||||||
|
write_scalar(&self.taux, w)?;
|
||||||
|
write_scalar(&self.mu, w)?;
|
||||||
|
specific_write_vec(&self.L, w)?;
|
||||||
|
specific_write_vec(&self.R, w)?;
|
||||||
|
write_scalar(&self.a, w)?;
|
||||||
|
write_scalar(&self.b, w)?;
|
||||||
|
write_scalar(&self.t, w)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn signature_serialize<W: std::io::Write>(&self, w: &mut W) -> std::io::Result<()> {
|
||||||
|
self.serialize_core(w, |points, w| write_raw_vec(write_point, points, w))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn serialize<W: std::io::Write>(&self, w: &mut W) -> std::io::Result<()> {
|
||||||
|
self.serialize_core(w, |points, w| write_vec(write_point, points, w))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deserialize<R: std::io::Read>(r: &mut R) -> std::io::Result<Bulletproofs> {
|
||||||
|
let bp = Bulletproofs {
|
||||||
|
A: read_point(r)?,
|
||||||
|
S: read_point(r)?,
|
||||||
|
T1: read_point(r)?,
|
||||||
|
T2: read_point(r)?,
|
||||||
|
taux: read_scalar(r)?,
|
||||||
|
mu: read_scalar(r)?,
|
||||||
|
L: read_vec(read_point, r)?,
|
||||||
|
R: read_vec(read_point, r)?,
|
||||||
|
a: read_scalar(r)?,
|
||||||
|
b: read_scalar(r)?,
|
||||||
|
t: read_scalar(r)?
|
||||||
|
};
|
||||||
|
|
||||||
|
if bp.L.len() != bp.R.len() {
|
||||||
|
Err(std::io::Error::new(std::io::ErrorKind::Other, "mismatched L/R len"))?;
|
||||||
|
}
|
||||||
|
Ok(bp)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,151 +0,0 @@
|
|||||||
use std_shims::{vec::Vec, sync::OnceLock};
|
|
||||||
|
|
||||||
use rand_core::{RngCore, CryptoRng};
|
|
||||||
|
|
||||||
use subtle::{Choice, ConditionallySelectable};
|
|
||||||
|
|
||||||
use curve25519_dalek::edwards::EdwardsPoint as DalekPoint;
|
|
||||||
|
|
||||||
use group::{ff::Field, Group};
|
|
||||||
use dalek_ff_group::{Scalar, EdwardsPoint};
|
|
||||||
|
|
||||||
use multiexp::multiexp as multiexp_const;
|
|
||||||
|
|
||||||
pub(crate) use monero_generators::Generators;
|
|
||||||
|
|
||||||
use crate::{INV_EIGHT as DALEK_INV_EIGHT, H as DALEK_H, Commitment, hash_to_scalar as dalek_hash};
|
|
||||||
pub(crate) use crate::ringct::bulletproofs::scalar_vector::*;
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn INV_EIGHT() -> Scalar {
|
|
||||||
Scalar(DALEK_INV_EIGHT())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn H() -> EdwardsPoint {
|
|
||||||
EdwardsPoint(DALEK_H())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn hash_to_scalar(data: &[u8]) -> Scalar {
|
|
||||||
Scalar(dalek_hash(data))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Components common between variants
|
|
||||||
pub(crate) const MAX_M: usize = 16;
|
|
||||||
pub(crate) const LOG_N: usize = 6; // 2 << 6 == N
|
|
||||||
pub(crate) const N: usize = 64;
|
|
||||||
|
|
||||||
pub(crate) fn prove_multiexp(pairs: &[(Scalar, EdwardsPoint)]) -> EdwardsPoint {
|
|
||||||
multiexp_const(pairs) * INV_EIGHT()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn vector_exponent(
|
|
||||||
generators: &Generators,
|
|
||||||
a: &ScalarVector,
|
|
||||||
b: &ScalarVector,
|
|
||||||
) -> EdwardsPoint {
|
|
||||||
debug_assert_eq!(a.len(), b.len());
|
|
||||||
(a * &generators.G[.. a.len()]) + (b * &generators.H[.. b.len()])
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn hash_cache(cache: &mut Scalar, mash: &[[u8; 32]]) -> Scalar {
|
|
||||||
let slice =
|
|
||||||
&[cache.to_bytes().as_ref(), mash.iter().copied().flatten().collect::<Vec<_>>().as_ref()]
|
|
||||||
.concat();
|
|
||||||
*cache = hash_to_scalar(slice);
|
|
||||||
*cache
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn MN(outputs: usize) -> (usize, usize, usize) {
|
|
||||||
let mut logM = 0;
|
|
||||||
let mut M;
|
|
||||||
while {
|
|
||||||
M = 1 << logM;
|
|
||||||
(M <= MAX_M) && (M < outputs)
|
|
||||||
} {
|
|
||||||
logM += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
(logM + LOG_N, M, M * N)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn bit_decompose(commitments: &[Commitment]) -> (ScalarVector, ScalarVector) {
|
|
||||||
let (_, M, MN) = MN(commitments.len());
|
|
||||||
|
|
||||||
let sv = commitments.iter().map(|c| Scalar::from(c.amount)).collect::<Vec<_>>();
|
|
||||||
let mut aL = ScalarVector::new(MN);
|
|
||||||
let mut aR = ScalarVector::new(MN);
|
|
||||||
|
|
||||||
for j in 0 .. M {
|
|
||||||
for i in (0 .. N).rev() {
|
|
||||||
let bit =
|
|
||||||
if j < sv.len() { Choice::from((sv[j][i / 8] >> (i % 8)) & 1) } else { Choice::from(0) };
|
|
||||||
aL.0[(j * N) + i] = Scalar::conditional_select(&Scalar::ZERO, &Scalar::ONE, bit);
|
|
||||||
aR.0[(j * N) + i] = Scalar::conditional_select(&-Scalar::ONE, &Scalar::ZERO, bit);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
(aL, aR)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn hash_commitments<C: IntoIterator<Item = DalekPoint>>(
|
|
||||||
commitments: C,
|
|
||||||
) -> (Scalar, Vec<EdwardsPoint>) {
|
|
||||||
let V = commitments.into_iter().map(|c| EdwardsPoint(c) * INV_EIGHT()).collect::<Vec<_>>();
|
|
||||||
(hash_to_scalar(&V.iter().flat_map(|V| V.compress().to_bytes()).collect::<Vec<_>>()), V)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn alpha_rho<R: RngCore + CryptoRng>(
|
|
||||||
rng: &mut R,
|
|
||||||
generators: &Generators,
|
|
||||||
aL: &ScalarVector,
|
|
||||||
aR: &ScalarVector,
|
|
||||||
) -> (Scalar, EdwardsPoint) {
|
|
||||||
let ar = Scalar::random(rng);
|
|
||||||
(ar, (vector_exponent(generators, aL, aR) + (EdwardsPoint::generator() * ar)) * INV_EIGHT())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn LR_statements(
|
|
||||||
a: &ScalarVector,
|
|
||||||
G_i: &[EdwardsPoint],
|
|
||||||
b: &ScalarVector,
|
|
||||||
H_i: &[EdwardsPoint],
|
|
||||||
cL: Scalar,
|
|
||||||
U: EdwardsPoint,
|
|
||||||
) -> Vec<(Scalar, EdwardsPoint)> {
|
|
||||||
let mut res = a
|
|
||||||
.0
|
|
||||||
.iter()
|
|
||||||
.copied()
|
|
||||||
.zip(G_i.iter().copied())
|
|
||||||
.chain(b.0.iter().copied().zip(H_i.iter().copied()))
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
res.push((cL, U));
|
|
||||||
res
|
|
||||||
}
|
|
||||||
|
|
||||||
static TWO_N_CELL: OnceLock<ScalarVector> = OnceLock::new();
|
|
||||||
pub(crate) fn TWO_N() -> &'static ScalarVector {
|
|
||||||
TWO_N_CELL.get_or_init(|| ScalarVector::powers(Scalar::from(2u8), N))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn challenge_products(w: &[Scalar], winv: &[Scalar]) -> Vec<Scalar> {
|
|
||||||
let mut products = vec![Scalar::ZERO; 1 << w.len()];
|
|
||||||
products[0] = winv[0];
|
|
||||||
products[1] = w[0];
|
|
||||||
for j in 1 .. w.len() {
|
|
||||||
let mut slots = (1 << (j + 1)) - 1;
|
|
||||||
while slots > 0 {
|
|
||||||
products[slots] = products[slots / 2] * w[j];
|
|
||||||
products[slots - 1] = products[slots / 2] * winv[j];
|
|
||||||
slots = slots.saturating_sub(2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sanity check as if the above failed to populate, it'd be critical
|
|
||||||
for w in &products {
|
|
||||||
debug_assert!(!bool::from(w.is_zero()));
|
|
||||||
}
|
|
||||||
|
|
||||||
products
|
|
||||||
}
|
|
||||||
@@ -1,229 +0,0 @@
|
|||||||
#![allow(non_snake_case)]
|
|
||||||
|
|
||||||
use std_shims::{
|
|
||||||
vec::Vec,
|
|
||||||
io::{self, Read, Write},
|
|
||||||
};
|
|
||||||
|
|
||||||
use rand_core::{RngCore, CryptoRng};
|
|
||||||
|
|
||||||
use zeroize::{Zeroize, Zeroizing};
|
|
||||||
|
|
||||||
use curve25519_dalek::edwards::EdwardsPoint;
|
|
||||||
use multiexp::BatchVerifier;
|
|
||||||
|
|
||||||
use crate::{Commitment, wallet::TransactionError, serialize::*};
|
|
||||||
|
|
||||||
pub(crate) mod scalar_vector;
|
|
||||||
pub(crate) mod core;
|
|
||||||
use self::core::LOG_N;
|
|
||||||
|
|
||||||
pub(crate) mod original;
|
|
||||||
use self::original::OriginalStruct;
|
|
||||||
|
|
||||||
pub(crate) mod plus;
|
|
||||||
use self::plus::*;
|
|
||||||
|
|
||||||
pub(crate) const MAX_OUTPUTS: usize = self::core::MAX_M;
|
|
||||||
|
|
||||||
/// Bulletproofs enum, supporting the original and plus formulations.
|
|
||||||
#[allow(clippy::large_enum_variant)]
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
|
||||||
pub enum Bulletproofs {
|
|
||||||
Original(OriginalStruct),
|
|
||||||
Plus(AggregateRangeProof),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Bulletproofs {
|
|
||||||
fn bp_fields(plus: bool) -> usize {
|
|
||||||
if plus {
|
|
||||||
6
|
|
||||||
} else {
|
|
||||||
9
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://github.com/monero-project/monero/blob/94e67bf96bbc010241f29ada6abc89f49a81759c/
|
|
||||||
// src/cryptonote_basic/cryptonote_format_utils.cpp#L106-L124
|
|
||||||
pub(crate) fn calculate_bp_clawback(plus: bool, n_outputs: usize) -> (usize, usize) {
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
let mut LR_len = 0;
|
|
||||||
let mut n_padded_outputs = 1;
|
|
||||||
while n_padded_outputs < n_outputs {
|
|
||||||
LR_len += 1;
|
|
||||||
n_padded_outputs = 1 << LR_len;
|
|
||||||
}
|
|
||||||
LR_len += LOG_N;
|
|
||||||
|
|
||||||
let mut bp_clawback = 0;
|
|
||||||
if n_padded_outputs > 2 {
|
|
||||||
let fields = Bulletproofs::bp_fields(plus);
|
|
||||||
let base = ((fields + (2 * (LOG_N + 1))) * 32) / 2;
|
|
||||||
let size = (fields + (2 * LR_len)) * 32;
|
|
||||||
bp_clawback = ((base * n_padded_outputs) - size) * 4 / 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
(bp_clawback, LR_len)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn fee_weight(plus: bool, outputs: usize) -> usize {
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
let (bp_clawback, LR_len) = Bulletproofs::calculate_bp_clawback(plus, outputs);
|
|
||||||
32 * (Bulletproofs::bp_fields(plus) + (2 * LR_len)) + 2 + bp_clawback
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Prove the list of commitments are within [0 .. 2^64).
|
|
||||||
pub fn prove<R: RngCore + CryptoRng>(
|
|
||||||
rng: &mut R,
|
|
||||||
outputs: &[Commitment],
|
|
||||||
plus: bool,
|
|
||||||
) -> Result<Bulletproofs, TransactionError> {
|
|
||||||
if outputs.is_empty() {
|
|
||||||
Err(TransactionError::NoOutputs)?;
|
|
||||||
}
|
|
||||||
if outputs.len() > MAX_OUTPUTS {
|
|
||||||
Err(TransactionError::TooManyOutputs)?;
|
|
||||||
}
|
|
||||||
Ok(if !plus {
|
|
||||||
Bulletproofs::Original(OriginalStruct::prove(rng, outputs))
|
|
||||||
} else {
|
|
||||||
use dalek_ff_group::EdwardsPoint as DfgPoint;
|
|
||||||
Bulletproofs::Plus(
|
|
||||||
AggregateRangeStatement::new(outputs.iter().map(|com| DfgPoint(com.calculate())).collect())
|
|
||||||
.unwrap()
|
|
||||||
.prove(rng, &Zeroizing::new(AggregateRangeWitness::new(outputs).unwrap()))
|
|
||||||
.unwrap(),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Verify the given Bulletproofs.
|
|
||||||
#[must_use]
|
|
||||||
pub fn verify<R: RngCore + CryptoRng>(&self, rng: &mut R, commitments: &[EdwardsPoint]) -> bool {
|
|
||||||
match self {
|
|
||||||
Bulletproofs::Original(bp) => bp.verify(rng, commitments),
|
|
||||||
Bulletproofs::Plus(bp) => {
|
|
||||||
let mut verifier = BatchVerifier::new(1);
|
|
||||||
// If this commitment is torsioned (which is allowed), this won't be a well-formed
|
|
||||||
// dfg::EdwardsPoint (expected to be of prime-order)
|
|
||||||
// The actual BP+ impl will perform a torsion clear though, making this safe
|
|
||||||
// TODO: Have AggregateRangeStatement take in dalek EdwardsPoint for clarity on this
|
|
||||||
let Some(statement) = AggregateRangeStatement::new(
|
|
||||||
commitments.iter().map(|c| dalek_ff_group::EdwardsPoint(*c)).collect(),
|
|
||||||
) else {
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
if !statement.verify(rng, &mut verifier, (), bp.clone()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
verifier.verify_vartime()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Accumulate the verification for the given Bulletproofs into the specified BatchVerifier.
|
|
||||||
/// Returns false if the Bulletproofs aren't sane, without mutating the BatchVerifier.
|
|
||||||
/// Returns true if the Bulletproofs are sane, regardless of their validity.
|
|
||||||
#[must_use]
|
|
||||||
pub fn batch_verify<ID: Copy + Zeroize, R: RngCore + CryptoRng>(
|
|
||||||
&self,
|
|
||||||
rng: &mut R,
|
|
||||||
verifier: &mut BatchVerifier<ID, dalek_ff_group::EdwardsPoint>,
|
|
||||||
id: ID,
|
|
||||||
commitments: &[EdwardsPoint],
|
|
||||||
) -> bool {
|
|
||||||
match self {
|
|
||||||
Bulletproofs::Original(bp) => bp.batch_verify(rng, verifier, id, commitments),
|
|
||||||
Bulletproofs::Plus(bp) => {
|
|
||||||
let Some(statement) = AggregateRangeStatement::new(
|
|
||||||
commitments.iter().map(|c| dalek_ff_group::EdwardsPoint(*c)).collect(),
|
|
||||||
) else {
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
statement.verify(rng, verifier, id, bp.clone())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn write_core<W: Write, F: Fn(&[EdwardsPoint], &mut W) -> io::Result<()>>(
|
|
||||||
&self,
|
|
||||||
w: &mut W,
|
|
||||||
specific_write_vec: F,
|
|
||||||
) -> io::Result<()> {
|
|
||||||
match self {
|
|
||||||
Bulletproofs::Original(bp) => {
|
|
||||||
write_point(&bp.A, w)?;
|
|
||||||
write_point(&bp.S, w)?;
|
|
||||||
write_point(&bp.T1, w)?;
|
|
||||||
write_point(&bp.T2, w)?;
|
|
||||||
write_scalar(&bp.taux, w)?;
|
|
||||||
write_scalar(&bp.mu, w)?;
|
|
||||||
specific_write_vec(&bp.L, w)?;
|
|
||||||
specific_write_vec(&bp.R, w)?;
|
|
||||||
write_scalar(&bp.a, w)?;
|
|
||||||
write_scalar(&bp.b, w)?;
|
|
||||||
write_scalar(&bp.t, w)
|
|
||||||
}
|
|
||||||
|
|
||||||
Bulletproofs::Plus(bp) => {
|
|
||||||
write_point(&bp.A.0, w)?;
|
|
||||||
write_point(&bp.wip.A.0, w)?;
|
|
||||||
write_point(&bp.wip.B.0, w)?;
|
|
||||||
write_scalar(&bp.wip.r_answer.0, w)?;
|
|
||||||
write_scalar(&bp.wip.s_answer.0, w)?;
|
|
||||||
write_scalar(&bp.wip.delta_answer.0, w)?;
|
|
||||||
specific_write_vec(&bp.wip.L.iter().copied().map(|L| L.0).collect::<Vec<_>>(), w)?;
|
|
||||||
specific_write_vec(&bp.wip.R.iter().copied().map(|R| R.0).collect::<Vec<_>>(), w)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn signature_write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
|
||||||
self.write_core(w, |points, w| write_raw_vec(write_point, points, w))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
|
||||||
self.write_core(w, |points, w| write_vec(write_point, points, w))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn serialize(&self) -> Vec<u8> {
|
|
||||||
let mut serialized = vec![];
|
|
||||||
self.write(&mut serialized).unwrap();
|
|
||||||
serialized
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read Bulletproofs.
|
|
||||||
pub fn read<R: Read>(r: &mut R) -> io::Result<Bulletproofs> {
|
|
||||||
Ok(Bulletproofs::Original(OriginalStruct {
|
|
||||||
A: read_point(r)?,
|
|
||||||
S: read_point(r)?,
|
|
||||||
T1: read_point(r)?,
|
|
||||||
T2: read_point(r)?,
|
|
||||||
taux: read_scalar(r)?,
|
|
||||||
mu: read_scalar(r)?,
|
|
||||||
L: read_vec(read_point, r)?,
|
|
||||||
R: read_vec(read_point, r)?,
|
|
||||||
a: read_scalar(r)?,
|
|
||||||
b: read_scalar(r)?,
|
|
||||||
t: read_scalar(r)?,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read Bulletproofs+.
|
|
||||||
pub fn read_plus<R: Read>(r: &mut R) -> io::Result<Bulletproofs> {
|
|
||||||
use dalek_ff_group::{Scalar as DfgScalar, EdwardsPoint as DfgPoint};
|
|
||||||
|
|
||||||
Ok(Bulletproofs::Plus(AggregateRangeProof {
|
|
||||||
A: DfgPoint(read_point(r)?),
|
|
||||||
wip: WipProof {
|
|
||||||
A: DfgPoint(read_point(r)?),
|
|
||||||
B: DfgPoint(read_point(r)?),
|
|
||||||
r_answer: DfgScalar(read_scalar(r)?),
|
|
||||||
s_answer: DfgScalar(read_scalar(r)?),
|
|
||||||
delta_answer: DfgScalar(read_scalar(r)?),
|
|
||||||
L: read_vec(read_point, r)?.into_iter().map(DfgPoint).collect(),
|
|
||||||
R: read_vec(read_point, r)?.into_iter().map(DfgPoint).collect(),
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,309 +0,0 @@
|
|||||||
use std_shims::{vec::Vec, sync::OnceLock};
|
|
||||||
|
|
||||||
use rand_core::{RngCore, CryptoRng};
|
|
||||||
|
|
||||||
use zeroize::Zeroize;
|
|
||||||
|
|
||||||
use curve25519_dalek::{scalar::Scalar as DalekScalar, edwards::EdwardsPoint as DalekPoint};
|
|
||||||
|
|
||||||
use group::{ff::Field, Group};
|
|
||||||
use dalek_ff_group::{ED25519_BASEPOINT_POINT as G, Scalar, EdwardsPoint};
|
|
||||||
|
|
||||||
use multiexp::BatchVerifier;
|
|
||||||
|
|
||||||
use crate::{Commitment, ringct::bulletproofs::core::*};
|
|
||||||
|
|
||||||
include!(concat!(env!("OUT_DIR"), "/generators.rs"));
|
|
||||||
|
|
||||||
static IP12_CELL: OnceLock<Scalar> = OnceLock::new();
|
|
||||||
pub(crate) fn IP12() -> Scalar {
|
|
||||||
*IP12_CELL.get_or_init(|| inner_product(&ScalarVector(vec![Scalar::ONE; N]), TWO_N()))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
|
||||||
pub struct OriginalStruct {
|
|
||||||
pub(crate) A: DalekPoint,
|
|
||||||
pub(crate) S: DalekPoint,
|
|
||||||
pub(crate) T1: DalekPoint,
|
|
||||||
pub(crate) T2: DalekPoint,
|
|
||||||
pub(crate) taux: DalekScalar,
|
|
||||||
pub(crate) mu: DalekScalar,
|
|
||||||
pub(crate) L: Vec<DalekPoint>,
|
|
||||||
pub(crate) R: Vec<DalekPoint>,
|
|
||||||
pub(crate) a: DalekScalar,
|
|
||||||
pub(crate) b: DalekScalar,
|
|
||||||
pub(crate) t: DalekScalar,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl OriginalStruct {
|
|
||||||
pub(crate) fn prove<R: RngCore + CryptoRng>(
|
|
||||||
rng: &mut R,
|
|
||||||
commitments: &[Commitment],
|
|
||||||
) -> OriginalStruct {
|
|
||||||
let (logMN, M, MN) = MN(commitments.len());
|
|
||||||
|
|
||||||
let (aL, aR) = bit_decompose(commitments);
|
|
||||||
let commitments_points = commitments.iter().map(Commitment::calculate).collect::<Vec<_>>();
|
|
||||||
let (mut cache, _) = hash_commitments(commitments_points.clone());
|
|
||||||
|
|
||||||
let (sL, sR) =
|
|
||||||
ScalarVector((0 .. (MN * 2)).map(|_| Scalar::random(&mut *rng)).collect::<Vec<_>>()).split();
|
|
||||||
|
|
||||||
let generators = GENERATORS();
|
|
||||||
let (mut alpha, A) = alpha_rho(&mut *rng, generators, &aL, &aR);
|
|
||||||
let (mut rho, S) = alpha_rho(&mut *rng, generators, &sL, &sR);
|
|
||||||
|
|
||||||
let y = hash_cache(&mut cache, &[A.compress().to_bytes(), S.compress().to_bytes()]);
|
|
||||||
let mut cache = hash_to_scalar(&y.to_bytes());
|
|
||||||
let z = cache;
|
|
||||||
|
|
||||||
let l0 = &aL - z;
|
|
||||||
let l1 = sL;
|
|
||||||
|
|
||||||
let mut zero_twos = Vec::with_capacity(MN);
|
|
||||||
let zpow = ScalarVector::powers(z, M + 2);
|
|
||||||
for j in 0 .. M {
|
|
||||||
for i in 0 .. N {
|
|
||||||
zero_twos.push(zpow[j + 2] * TWO_N()[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let yMN = ScalarVector::powers(y, MN);
|
|
||||||
let r0 = (&(aR + z) * &yMN) + ScalarVector(zero_twos);
|
|
||||||
let r1 = yMN * sR;
|
|
||||||
|
|
||||||
let (T1, T2, x, mut taux) = {
|
|
||||||
let t1 = inner_product(&l0, &r1) + inner_product(&l1, &r0);
|
|
||||||
let t2 = inner_product(&l1, &r1);
|
|
||||||
|
|
||||||
let mut tau1 = Scalar::random(&mut *rng);
|
|
||||||
let mut tau2 = Scalar::random(&mut *rng);
|
|
||||||
|
|
||||||
let T1 = prove_multiexp(&[(t1, H()), (tau1, EdwardsPoint::generator())]);
|
|
||||||
let T2 = prove_multiexp(&[(t2, H()), (tau2, EdwardsPoint::generator())]);
|
|
||||||
|
|
||||||
let x =
|
|
||||||
hash_cache(&mut cache, &[z.to_bytes(), T1.compress().to_bytes(), T2.compress().to_bytes()]);
|
|
||||||
|
|
||||||
let taux = (tau2 * (x * x)) + (tau1 * x);
|
|
||||||
|
|
||||||
tau1.zeroize();
|
|
||||||
tau2.zeroize();
|
|
||||||
(T1, T2, x, taux)
|
|
||||||
};
|
|
||||||
|
|
||||||
let mu = (x * rho) + alpha;
|
|
||||||
alpha.zeroize();
|
|
||||||
rho.zeroize();
|
|
||||||
|
|
||||||
for (i, gamma) in commitments.iter().map(|c| Scalar(c.mask)).enumerate() {
|
|
||||||
taux += zpow[i + 2] * gamma;
|
|
||||||
}
|
|
||||||
|
|
||||||
let l = &l0 + &(l1 * x);
|
|
||||||
let r = &r0 + &(r1 * x);
|
|
||||||
|
|
||||||
let t = inner_product(&l, &r);
|
|
||||||
|
|
||||||
let x_ip =
|
|
||||||
hash_cache(&mut cache, &[x.to_bytes(), taux.to_bytes(), mu.to_bytes(), t.to_bytes()]);
|
|
||||||
|
|
||||||
let mut a = l;
|
|
||||||
let mut b = r;
|
|
||||||
|
|
||||||
let yinv = y.invert().unwrap();
|
|
||||||
let yinvpow = ScalarVector::powers(yinv, MN);
|
|
||||||
|
|
||||||
let mut G_proof = generators.G[.. a.len()].to_vec();
|
|
||||||
let mut H_proof = generators.H[.. a.len()].to_vec();
|
|
||||||
H_proof.iter_mut().zip(yinvpow.0.iter()).for_each(|(this_H, yinvpow)| *this_H *= yinvpow);
|
|
||||||
let U = H() * x_ip;
|
|
||||||
|
|
||||||
let mut L = Vec::with_capacity(logMN);
|
|
||||||
let mut R = Vec::with_capacity(logMN);
|
|
||||||
|
|
||||||
while a.len() != 1 {
|
|
||||||
let (aL, aR) = a.split();
|
|
||||||
let (bL, bR) = b.split();
|
|
||||||
|
|
||||||
let cL = inner_product(&aL, &bR);
|
|
||||||
let cR = inner_product(&aR, &bL);
|
|
||||||
|
|
||||||
let (G_L, G_R) = G_proof.split_at(aL.len());
|
|
||||||
let (H_L, H_R) = H_proof.split_at(aL.len());
|
|
||||||
|
|
||||||
let L_i = prove_multiexp(&LR_statements(&aL, G_R, &bR, H_L, cL, U));
|
|
||||||
let R_i = prove_multiexp(&LR_statements(&aR, G_L, &bL, H_R, cR, U));
|
|
||||||
L.push(L_i);
|
|
||||||
R.push(R_i);
|
|
||||||
|
|
||||||
let w = hash_cache(&mut cache, &[L_i.compress().to_bytes(), R_i.compress().to_bytes()]);
|
|
||||||
let winv = w.invert().unwrap();
|
|
||||||
|
|
||||||
a = (aL * w) + (aR * winv);
|
|
||||||
b = (bL * winv) + (bR * w);
|
|
||||||
|
|
||||||
if a.len() != 1 {
|
|
||||||
G_proof = hadamard_fold(G_L, G_R, winv, w);
|
|
||||||
H_proof = hadamard_fold(H_L, H_R, w, winv);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let res = OriginalStruct {
|
|
||||||
A: *A,
|
|
||||||
S: *S,
|
|
||||||
T1: *T1,
|
|
||||||
T2: *T2,
|
|
||||||
taux: *taux,
|
|
||||||
mu: *mu,
|
|
||||||
L: L.drain(..).map(|L| *L).collect(),
|
|
||||||
R: R.drain(..).map(|R| *R).collect(),
|
|
||||||
a: *a[0],
|
|
||||||
b: *b[0],
|
|
||||||
t: *t,
|
|
||||||
};
|
|
||||||
debug_assert!(res.verify(rng, &commitments_points));
|
|
||||||
res
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
fn verify_core<ID: Copy + Zeroize, R: RngCore + CryptoRng>(
|
|
||||||
&self,
|
|
||||||
rng: &mut R,
|
|
||||||
verifier: &mut BatchVerifier<ID, EdwardsPoint>,
|
|
||||||
id: ID,
|
|
||||||
commitments: &[DalekPoint],
|
|
||||||
) -> bool {
|
|
||||||
// Verify commitments are valid
|
|
||||||
if commitments.is_empty() || (commitments.len() > MAX_M) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify L and R are properly sized
|
|
||||||
if self.L.len() != self.R.len() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
let (logMN, M, MN) = MN(commitments.len());
|
|
||||||
if self.L.len() != logMN {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rebuild all challenges
|
|
||||||
let (mut cache, commitments) = hash_commitments(commitments.iter().copied());
|
|
||||||
let y = hash_cache(&mut cache, &[self.A.compress().to_bytes(), self.S.compress().to_bytes()]);
|
|
||||||
|
|
||||||
let z = hash_to_scalar(&y.to_bytes());
|
|
||||||
cache = z;
|
|
||||||
|
|
||||||
let x = hash_cache(
|
|
||||||
&mut cache,
|
|
||||||
&[z.to_bytes(), self.T1.compress().to_bytes(), self.T2.compress().to_bytes()],
|
|
||||||
);
|
|
||||||
|
|
||||||
let x_ip = hash_cache(
|
|
||||||
&mut cache,
|
|
||||||
&[x.to_bytes(), self.taux.to_bytes(), self.mu.to_bytes(), self.t.to_bytes()],
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut w = Vec::with_capacity(logMN);
|
|
||||||
let mut winv = Vec::with_capacity(logMN);
|
|
||||||
for (L, R) in self.L.iter().zip(&self.R) {
|
|
||||||
w.push(hash_cache(&mut cache, &[L.compress().to_bytes(), R.compress().to_bytes()]));
|
|
||||||
winv.push(cache.invert().unwrap());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert the proof from * INV_EIGHT to its actual form
|
|
||||||
let normalize = |point: &DalekPoint| EdwardsPoint(point.mul_by_cofactor());
|
|
||||||
|
|
||||||
let L = self.L.iter().map(normalize).collect::<Vec<_>>();
|
|
||||||
let R = self.R.iter().map(normalize).collect::<Vec<_>>();
|
|
||||||
let T1 = normalize(&self.T1);
|
|
||||||
let T2 = normalize(&self.T2);
|
|
||||||
let A = normalize(&self.A);
|
|
||||||
let S = normalize(&self.S);
|
|
||||||
|
|
||||||
let commitments = commitments.iter().map(EdwardsPoint::mul_by_cofactor).collect::<Vec<_>>();
|
|
||||||
|
|
||||||
// Verify it
|
|
||||||
let mut proof = Vec::with_capacity(4 + commitments.len());
|
|
||||||
|
|
||||||
let zpow = ScalarVector::powers(z, M + 3);
|
|
||||||
let ip1y = ScalarVector::powers(y, M * N).sum();
|
|
||||||
let mut k = -(zpow[2] * ip1y);
|
|
||||||
for j in 1 ..= M {
|
|
||||||
k -= zpow[j + 2] * IP12();
|
|
||||||
}
|
|
||||||
let y1 = Scalar(self.t) - ((z * ip1y) + k);
|
|
||||||
proof.push((-y1, H()));
|
|
||||||
|
|
||||||
proof.push((-Scalar(self.taux), G));
|
|
||||||
|
|
||||||
for (j, commitment) in commitments.iter().enumerate() {
|
|
||||||
proof.push((zpow[j + 2], *commitment));
|
|
||||||
}
|
|
||||||
|
|
||||||
proof.push((x, T1));
|
|
||||||
proof.push((x * x, T2));
|
|
||||||
verifier.queue(&mut *rng, id, proof);
|
|
||||||
|
|
||||||
proof = Vec::with_capacity(4 + (2 * (MN + logMN)));
|
|
||||||
let z3 = (Scalar(self.t) - (Scalar(self.a) * Scalar(self.b))) * x_ip;
|
|
||||||
proof.push((z3, H()));
|
|
||||||
proof.push((-Scalar(self.mu), G));
|
|
||||||
|
|
||||||
proof.push((Scalar::ONE, A));
|
|
||||||
proof.push((x, S));
|
|
||||||
|
|
||||||
{
|
|
||||||
let ypow = ScalarVector::powers(y, MN);
|
|
||||||
let yinv = y.invert().unwrap();
|
|
||||||
let yinvpow = ScalarVector::powers(yinv, MN);
|
|
||||||
|
|
||||||
let w_cache = challenge_products(&w, &winv);
|
|
||||||
|
|
||||||
let generators = GENERATORS();
|
|
||||||
for i in 0 .. MN {
|
|
||||||
let g = (Scalar(self.a) * w_cache[i]) + z;
|
|
||||||
proof.push((-g, generators.G[i]));
|
|
||||||
|
|
||||||
let mut h = Scalar(self.b) * yinvpow[i] * w_cache[(!i) & (MN - 1)];
|
|
||||||
h -= ((zpow[(i / N) + 2] * TWO_N()[i % N]) + (z * ypow[i])) * yinvpow[i];
|
|
||||||
proof.push((-h, generators.H[i]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for i in 0 .. logMN {
|
|
||||||
proof.push((w[i] * w[i], L[i]));
|
|
||||||
proof.push((winv[i] * winv[i], R[i]));
|
|
||||||
}
|
|
||||||
verifier.queue(rng, id, proof);
|
|
||||||
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub(crate) fn verify<R: RngCore + CryptoRng>(
|
|
||||||
&self,
|
|
||||||
rng: &mut R,
|
|
||||||
commitments: &[DalekPoint],
|
|
||||||
) -> bool {
|
|
||||||
let mut verifier = BatchVerifier::new(1);
|
|
||||||
if self.verify_core(rng, &mut verifier, (), commitments) {
|
|
||||||
verifier.verify_vartime()
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub(crate) fn batch_verify<ID: Copy + Zeroize, R: RngCore + CryptoRng>(
|
|
||||||
&self,
|
|
||||||
rng: &mut R,
|
|
||||||
verifier: &mut BatchVerifier<ID, EdwardsPoint>,
|
|
||||||
id: ID,
|
|
||||||
commitments: &[DalekPoint],
|
|
||||||
) -> bool {
|
|
||||||
self.verify_core(rng, verifier, id, commitments)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,249 +0,0 @@
|
|||||||
use std_shims::vec::Vec;
|
|
||||||
|
|
||||||
use rand_core::{RngCore, CryptoRng};
|
|
||||||
|
|
||||||
use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing};
|
|
||||||
|
|
||||||
use multiexp::{multiexp, multiexp_vartime, BatchVerifier};
|
|
||||||
use group::{
|
|
||||||
ff::{Field, PrimeField},
|
|
||||||
Group, GroupEncoding,
|
|
||||||
};
|
|
||||||
use dalek_ff_group::{Scalar, EdwardsPoint};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
Commitment,
|
|
||||||
ringct::{
|
|
||||||
bulletproofs::core::{MAX_M, N},
|
|
||||||
bulletproofs::plus::{
|
|
||||||
ScalarVector, PointVector, GeneratorsList, Generators,
|
|
||||||
transcript::*,
|
|
||||||
weighted_inner_product::{WipStatement, WipWitness, WipProof},
|
|
||||||
padded_pow_of_2, u64_decompose,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// Figure 3
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub(crate) struct AggregateRangeStatement {
|
|
||||||
generators: Generators,
|
|
||||||
V: Vec<EdwardsPoint>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Zeroize for AggregateRangeStatement {
|
|
||||||
fn zeroize(&mut self) {
|
|
||||||
self.V.zeroize();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Zeroize, ZeroizeOnDrop)]
|
|
||||||
pub(crate) struct AggregateRangeWitness {
|
|
||||||
values: Vec<u64>,
|
|
||||||
gammas: Vec<Scalar>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AggregateRangeWitness {
|
|
||||||
pub(crate) fn new(commitments: &[Commitment]) -> Option<Self> {
|
|
||||||
if commitments.is_empty() || (commitments.len() > MAX_M) {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut values = Vec::with_capacity(commitments.len());
|
|
||||||
let mut gammas = Vec::with_capacity(commitments.len());
|
|
||||||
for commitment in commitments {
|
|
||||||
values.push(commitment.amount);
|
|
||||||
gammas.push(Scalar(commitment.mask));
|
|
||||||
}
|
|
||||||
Some(AggregateRangeWitness { values, gammas })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
|
|
||||||
pub struct AggregateRangeProof {
|
|
||||||
pub(crate) A: EdwardsPoint,
|
|
||||||
pub(crate) wip: WipProof,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AggregateRangeStatement {
|
|
||||||
pub(crate) fn new(V: Vec<EdwardsPoint>) -> Option<Self> {
|
|
||||||
if V.is_empty() || (V.len() > MAX_M) {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(Self { generators: Generators::new(), V })
|
|
||||||
}
|
|
||||||
|
|
||||||
fn transcript_A(transcript: &mut Scalar, A: EdwardsPoint) -> (Scalar, Scalar) {
|
|
||||||
let y = hash_to_scalar(&[transcript.to_repr().as_ref(), A.to_bytes().as_ref()].concat());
|
|
||||||
let z = hash_to_scalar(y.to_bytes().as_ref());
|
|
||||||
*transcript = z;
|
|
||||||
(y, z)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn d_j(j: usize, m: usize) -> ScalarVector {
|
|
||||||
let mut d_j = Vec::with_capacity(m * N);
|
|
||||||
for _ in 0 .. (j - 1) * N {
|
|
||||||
d_j.push(Scalar::ZERO);
|
|
||||||
}
|
|
||||||
d_j.append(&mut ScalarVector::powers(Scalar::from(2u8), N).0);
|
|
||||||
for _ in 0 .. (m - j) * N {
|
|
||||||
d_j.push(Scalar::ZERO);
|
|
||||||
}
|
|
||||||
ScalarVector(d_j)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn compute_A_hat(
|
|
||||||
mut V: PointVector,
|
|
||||||
generators: &Generators,
|
|
||||||
transcript: &mut Scalar,
|
|
||||||
mut A: EdwardsPoint,
|
|
||||||
) -> (Scalar, ScalarVector, Scalar, Scalar, ScalarVector, EdwardsPoint) {
|
|
||||||
let (y, z) = Self::transcript_A(transcript, A);
|
|
||||||
A = A.mul_by_cofactor();
|
|
||||||
|
|
||||||
while V.len() < padded_pow_of_2(V.len()) {
|
|
||||||
V.0.push(EdwardsPoint::identity());
|
|
||||||
}
|
|
||||||
let mn = V.len() * N;
|
|
||||||
|
|
||||||
let mut z_pow = Vec::with_capacity(V.len());
|
|
||||||
|
|
||||||
let mut d = ScalarVector::new(mn);
|
|
||||||
for j in 1 ..= V.len() {
|
|
||||||
z_pow.push(z.pow(Scalar::from(2 * u64::try_from(j).unwrap()))); // TODO: Optimize this
|
|
||||||
d = d.add_vec(&Self::d_j(j, V.len()).mul(z_pow[j - 1]));
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut ascending_y = ScalarVector(vec![y]);
|
|
||||||
for i in 1 .. d.len() {
|
|
||||||
ascending_y.0.push(ascending_y[i - 1] * y);
|
|
||||||
}
|
|
||||||
let y_pows = ascending_y.clone().sum();
|
|
||||||
|
|
||||||
let mut descending_y = ascending_y.clone();
|
|
||||||
descending_y.0.reverse();
|
|
||||||
|
|
||||||
let d_descending_y = d.mul_vec(&descending_y);
|
|
||||||
|
|
||||||
let y_mn_plus_one = descending_y[0] * y;
|
|
||||||
|
|
||||||
let mut commitment_accum = EdwardsPoint::identity();
|
|
||||||
for (j, commitment) in V.0.iter().enumerate() {
|
|
||||||
commitment_accum += *commitment * z_pow[j];
|
|
||||||
}
|
|
||||||
|
|
||||||
let neg_z = -z;
|
|
||||||
let mut A_terms = Vec::with_capacity((generators.len() * 2) + 2);
|
|
||||||
for (i, d_y_z) in d_descending_y.add(z).0.drain(..).enumerate() {
|
|
||||||
A_terms.push((neg_z, generators.generator(GeneratorsList::GBold1, i)));
|
|
||||||
A_terms.push((d_y_z, generators.generator(GeneratorsList::HBold1, i)));
|
|
||||||
}
|
|
||||||
A_terms.push((y_mn_plus_one, commitment_accum));
|
|
||||||
A_terms.push((
|
|
||||||
((y_pows * z) - (d.sum() * y_mn_plus_one * z) - (y_pows * z.square())),
|
|
||||||
Generators::g(),
|
|
||||||
));
|
|
||||||
|
|
||||||
(y, d_descending_y, y_mn_plus_one, z, ScalarVector(z_pow), A + multiexp_vartime(&A_terms))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn prove<R: RngCore + CryptoRng>(
|
|
||||||
self,
|
|
||||||
rng: &mut R,
|
|
||||||
witness: &AggregateRangeWitness,
|
|
||||||
) -> Option<AggregateRangeProof> {
|
|
||||||
// Check for consistency with the witness
|
|
||||||
if self.V.len() != witness.values.len() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
for (commitment, (value, gamma)) in
|
|
||||||
self.V.iter().zip(witness.values.iter().zip(witness.gammas.iter()))
|
|
||||||
{
|
|
||||||
if Commitment::new(**gamma, *value).calculate() != **commitment {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let Self { generators, V } = self;
|
|
||||||
// Monero expects all of these points to be torsion-free
|
|
||||||
// Generally, for Bulletproofs, it sends points * INV_EIGHT and then performs a torsion clear
|
|
||||||
// by multiplying by 8
|
|
||||||
// This also restores the original value due to the preprocessing
|
|
||||||
// Commitments aren't transmitted INV_EIGHT though, so this multiplies by INV_EIGHT to enable
|
|
||||||
// clearing its cofactor without mutating the value
|
|
||||||
// For some reason, these values are transcripted * INV_EIGHT, not as transmitted
|
|
||||||
let mut V = V.into_iter().map(|V| EdwardsPoint(V.0 * crate::INV_EIGHT())).collect::<Vec<_>>();
|
|
||||||
let mut transcript = initial_transcript(V.iter());
|
|
||||||
V.iter_mut().for_each(|V| *V = V.mul_by_cofactor());
|
|
||||||
|
|
||||||
// Pad V
|
|
||||||
while V.len() < padded_pow_of_2(V.len()) {
|
|
||||||
V.push(EdwardsPoint::identity());
|
|
||||||
}
|
|
||||||
|
|
||||||
let generators = generators.reduce(V.len() * N);
|
|
||||||
|
|
||||||
let mut d_js = Vec::with_capacity(V.len());
|
|
||||||
let mut a_l = ScalarVector(Vec::with_capacity(V.len() * N));
|
|
||||||
for j in 1 ..= V.len() {
|
|
||||||
d_js.push(Self::d_j(j, V.len()));
|
|
||||||
a_l.0.append(&mut u64_decompose(*witness.values.get(j - 1).unwrap_or(&0)).0);
|
|
||||||
}
|
|
||||||
|
|
||||||
let a_r = a_l.sub(Scalar::ONE);
|
|
||||||
|
|
||||||
let alpha = Scalar::random(&mut *rng);
|
|
||||||
|
|
||||||
let mut A_terms = Vec::with_capacity((generators.len() * 2) + 1);
|
|
||||||
for (i, a_l) in a_l.0.iter().enumerate() {
|
|
||||||
A_terms.push((*a_l, generators.generator(GeneratorsList::GBold1, i)));
|
|
||||||
}
|
|
||||||
for (i, a_r) in a_r.0.iter().enumerate() {
|
|
||||||
A_terms.push((*a_r, generators.generator(GeneratorsList::HBold1, i)));
|
|
||||||
}
|
|
||||||
A_terms.push((alpha, Generators::h()));
|
|
||||||
let mut A = multiexp(&A_terms);
|
|
||||||
A_terms.zeroize();
|
|
||||||
|
|
||||||
// Multiply by INV_EIGHT per earlier commentary
|
|
||||||
A.0 *= crate::INV_EIGHT();
|
|
||||||
|
|
||||||
let (y, d_descending_y, y_mn_plus_one, z, z_pow, A_hat) =
|
|
||||||
Self::compute_A_hat(PointVector(V), &generators, &mut transcript, A);
|
|
||||||
|
|
||||||
let a_l = a_l.sub(z);
|
|
||||||
let a_r = a_r.add_vec(&d_descending_y).add(z);
|
|
||||||
let mut alpha = alpha;
|
|
||||||
for j in 1 ..= witness.gammas.len() {
|
|
||||||
alpha += z_pow[j - 1] * witness.gammas[j - 1] * y_mn_plus_one;
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(AggregateRangeProof {
|
|
||||||
A,
|
|
||||||
wip: WipStatement::new(generators, A_hat, y)
|
|
||||||
.prove(rng, transcript, &Zeroizing::new(WipWitness::new(a_l, a_r, alpha).unwrap()))
|
|
||||||
.unwrap(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn verify<Id: Copy + Zeroize, R: RngCore + CryptoRng>(
|
|
||||||
self,
|
|
||||||
rng: &mut R,
|
|
||||||
verifier: &mut BatchVerifier<Id, EdwardsPoint>,
|
|
||||||
id: Id,
|
|
||||||
proof: AggregateRangeProof,
|
|
||||||
) -> bool {
|
|
||||||
let Self { generators, V } = self;
|
|
||||||
|
|
||||||
let mut V = V.into_iter().map(|V| EdwardsPoint(V.0 * crate::INV_EIGHT())).collect::<Vec<_>>();
|
|
||||||
let mut transcript = initial_transcript(V.iter());
|
|
||||||
V.iter_mut().for_each(|V| *V = V.mul_by_cofactor());
|
|
||||||
|
|
||||||
let generators = generators.reduce(V.len() * N);
|
|
||||||
|
|
||||||
let (y, _, _, _, _, A_hat) =
|
|
||||||
Self::compute_A_hat(PointVector(V), &generators, &mut transcript, proof.A);
|
|
||||||
WipStatement::new(generators, A_hat, y).verify(rng, verifier, id, transcript, proof.wip)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
#![allow(non_snake_case)]
|
|
||||||
|
|
||||||
use group::Group;
|
|
||||||
use dalek_ff_group::{Scalar, EdwardsPoint};
|
|
||||||
|
|
||||||
mod scalar_vector;
|
|
||||||
pub(crate) use scalar_vector::{ScalarVector, weighted_inner_product};
|
|
||||||
mod point_vector;
|
|
||||||
pub(crate) use point_vector::PointVector;
|
|
||||||
|
|
||||||
pub(crate) mod transcript;
|
|
||||||
pub(crate) mod weighted_inner_product;
|
|
||||||
pub(crate) use weighted_inner_product::*;
|
|
||||||
pub(crate) mod aggregate_range_proof;
|
|
||||||
pub(crate) use aggregate_range_proof::*;
|
|
||||||
|
|
||||||
pub(crate) fn padded_pow_of_2(i: usize) -> usize {
|
|
||||||
let mut next_pow_of_2 = 1;
|
|
||||||
while next_pow_of_2 < i {
|
|
||||||
next_pow_of_2 <<= 1;
|
|
||||||
}
|
|
||||||
next_pow_of_2
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
|
|
||||||
pub(crate) enum GeneratorsList {
|
|
||||||
GBold1,
|
|
||||||
HBold1,
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Table these
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub(crate) struct Generators {
|
|
||||||
g_bold1: &'static [EdwardsPoint],
|
|
||||||
h_bold1: &'static [EdwardsPoint],
|
|
||||||
}
|
|
||||||
|
|
||||||
mod generators {
|
|
||||||
use std_shims::sync::OnceLock;
|
|
||||||
use monero_generators::Generators;
|
|
||||||
include!(concat!(env!("OUT_DIR"), "/generators_plus.rs"));
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Generators {
|
|
||||||
#[allow(clippy::new_without_default)]
|
|
||||||
pub(crate) fn new() -> Self {
|
|
||||||
let gens = generators::GENERATORS();
|
|
||||||
Generators { g_bold1: &gens.G, h_bold1: &gens.H }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn len(&self) -> usize {
|
|
||||||
self.g_bold1.len()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn g() -> EdwardsPoint {
|
|
||||||
dalek_ff_group::EdwardsPoint(crate::H())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn h() -> EdwardsPoint {
|
|
||||||
EdwardsPoint::generator()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn generator(&self, list: GeneratorsList, i: usize) -> EdwardsPoint {
|
|
||||||
match list {
|
|
||||||
GeneratorsList::GBold1 => self.g_bold1[i],
|
|
||||||
GeneratorsList::HBold1 => self.h_bold1[i],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn reduce(&self, generators: usize) -> Self {
|
|
||||||
// Round to the nearest power of 2
|
|
||||||
let generators = padded_pow_of_2(generators);
|
|
||||||
assert!(generators <= self.g_bold1.len());
|
|
||||||
|
|
||||||
Generators { g_bold1: &self.g_bold1[.. generators], h_bold1: &self.h_bold1[.. generators] }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Returns the little-endian decomposition.
|
|
||||||
fn u64_decompose(value: u64) -> ScalarVector {
|
|
||||||
let mut bits = ScalarVector::new(64);
|
|
||||||
for bit in 0 .. 64 {
|
|
||||||
bits[bit] = Scalar::from((value >> bit) & 1);
|
|
||||||
}
|
|
||||||
bits
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
use core::ops::{Index, IndexMut};
|
|
||||||
use std_shims::vec::Vec;
|
|
||||||
|
|
||||||
use zeroize::{Zeroize, ZeroizeOnDrop};
|
|
||||||
|
|
||||||
use dalek_ff_group::EdwardsPoint;
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
use multiexp::multiexp;
|
|
||||||
#[cfg(test)]
|
|
||||||
use crate::ringct::bulletproofs::plus::ScalarVector;
|
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug, Zeroize, ZeroizeOnDrop)]
|
|
||||||
pub(crate) struct PointVector(pub(crate) Vec<EdwardsPoint>);
|
|
||||||
|
|
||||||
impl Index<usize> for PointVector {
|
|
||||||
type Output = EdwardsPoint;
|
|
||||||
fn index(&self, index: usize) -> &EdwardsPoint {
|
|
||||||
&self.0[index]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IndexMut<usize> for PointVector {
|
|
||||||
fn index_mut(&mut self, index: usize) -> &mut EdwardsPoint {
|
|
||||||
&mut self.0[index]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PointVector {
|
|
||||||
#[cfg(test)]
|
|
||||||
pub(crate) fn multiexp(&self, vector: &ScalarVector) -> EdwardsPoint {
|
|
||||||
debug_assert_eq!(self.len(), vector.len());
|
|
||||||
let mut res = Vec::with_capacity(self.len());
|
|
||||||
for (point, scalar) in self.0.iter().copied().zip(vector.0.iter().copied()) {
|
|
||||||
res.push((scalar, point));
|
|
||||||
}
|
|
||||||
multiexp(&res)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn len(&self) -> usize {
|
|
||||||
self.0.len()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn split(mut self) -> (Self, Self) {
|
|
||||||
debug_assert!(self.len() > 1);
|
|
||||||
let r = self.0.split_off(self.0.len() / 2);
|
|
||||||
debug_assert_eq!(self.len(), r.len());
|
|
||||||
(self, PointVector(r))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,114 +0,0 @@
|
|||||||
use core::{
|
|
||||||
borrow::Borrow,
|
|
||||||
ops::{Index, IndexMut},
|
|
||||||
};
|
|
||||||
use std_shims::vec::Vec;
|
|
||||||
|
|
||||||
use zeroize::Zeroize;
|
|
||||||
|
|
||||||
use group::ff::Field;
|
|
||||||
use dalek_ff_group::Scalar;
|
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
|
|
||||||
pub(crate) struct ScalarVector(pub(crate) Vec<Scalar>);
|
|
||||||
|
|
||||||
impl Index<usize> for ScalarVector {
|
|
||||||
type Output = Scalar;
|
|
||||||
fn index(&self, index: usize) -> &Scalar {
|
|
||||||
&self.0[index]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IndexMut<usize> for ScalarVector {
|
|
||||||
fn index_mut(&mut self, index: usize) -> &mut Scalar {
|
|
||||||
&mut self.0[index]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ScalarVector {
|
|
||||||
pub(crate) fn new(len: usize) -> Self {
|
|
||||||
ScalarVector(vec![Scalar::ZERO; len])
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn add(&self, scalar: impl Borrow<Scalar>) -> Self {
|
|
||||||
let mut res = self.clone();
|
|
||||||
for val in &mut res.0 {
|
|
||||||
*val += scalar.borrow();
|
|
||||||
}
|
|
||||||
res
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn sub(&self, scalar: impl Borrow<Scalar>) -> Self {
|
|
||||||
let mut res = self.clone();
|
|
||||||
for val in &mut res.0 {
|
|
||||||
*val -= scalar.borrow();
|
|
||||||
}
|
|
||||||
res
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn mul(&self, scalar: impl Borrow<Scalar>) -> Self {
|
|
||||||
let mut res = self.clone();
|
|
||||||
for val in &mut res.0 {
|
|
||||||
*val *= scalar.borrow();
|
|
||||||
}
|
|
||||||
res
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn add_vec(&self, vector: &Self) -> Self {
|
|
||||||
debug_assert_eq!(self.len(), vector.len());
|
|
||||||
let mut res = self.clone();
|
|
||||||
for (i, val) in res.0.iter_mut().enumerate() {
|
|
||||||
*val += vector.0[i];
|
|
||||||
}
|
|
||||||
res
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn mul_vec(&self, vector: &Self) -> Self {
|
|
||||||
debug_assert_eq!(self.len(), vector.len());
|
|
||||||
let mut res = self.clone();
|
|
||||||
for (i, val) in res.0.iter_mut().enumerate() {
|
|
||||||
*val *= vector.0[i];
|
|
||||||
}
|
|
||||||
res
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn inner_product(&self, vector: &Self) -> Scalar {
|
|
||||||
self.mul_vec(vector).sum()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn powers(x: Scalar, len: usize) -> Self {
|
|
||||||
debug_assert!(len != 0);
|
|
||||||
|
|
||||||
let mut res = Vec::with_capacity(len);
|
|
||||||
res.push(Scalar::ONE);
|
|
||||||
res.push(x);
|
|
||||||
for i in 2 .. len {
|
|
||||||
res.push(res[i - 1] * x);
|
|
||||||
}
|
|
||||||
res.truncate(len);
|
|
||||||
ScalarVector(res)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn sum(mut self) -> Scalar {
|
|
||||||
self.0.drain(..).sum()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn len(&self) -> usize {
|
|
||||||
self.0.len()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn split(mut self) -> (Self, Self) {
|
|
||||||
debug_assert!(self.len() > 1);
|
|
||||||
let r = self.0.split_off(self.0.len() / 2);
|
|
||||||
debug_assert_eq!(self.len(), r.len());
|
|
||||||
(self, ScalarVector(r))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn weighted_inner_product(
|
|
||||||
a: &ScalarVector,
|
|
||||||
b: &ScalarVector,
|
|
||||||
y: &ScalarVector,
|
|
||||||
) -> Scalar {
|
|
||||||
a.inner_product(&b.mul_vec(y))
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
use std_shims::{sync::OnceLock, vec::Vec};
|
|
||||||
|
|
||||||
use dalek_ff_group::{Scalar, EdwardsPoint};
|
|
||||||
|
|
||||||
use monero_generators::{hash_to_point as raw_hash_to_point};
|
|
||||||
use crate::{hash, hash_to_scalar as dalek_hash};
|
|
||||||
|
|
||||||
// Monero starts BP+ transcripts with the following constant.
|
|
||||||
static TRANSCRIPT_CELL: OnceLock<[u8; 32]> = OnceLock::new();
|
|
||||||
pub(crate) fn TRANSCRIPT() -> [u8; 32] {
|
|
||||||
// Why this uses a hash_to_point is completely unknown.
|
|
||||||
*TRANSCRIPT_CELL
|
|
||||||
.get_or_init(|| raw_hash_to_point(hash(b"bulletproof_plus_transcript")).compress().to_bytes())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn hash_to_scalar(data: &[u8]) -> Scalar {
|
|
||||||
Scalar(dalek_hash(data))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn initial_transcript(commitments: core::slice::Iter<'_, EdwardsPoint>) -> Scalar {
|
|
||||||
let commitments_hash =
|
|
||||||
hash_to_scalar(&commitments.flat_map(|V| V.compress().to_bytes()).collect::<Vec<_>>());
|
|
||||||
hash_to_scalar(&[TRANSCRIPT().as_ref(), &commitments_hash.to_bytes()].concat())
|
|
||||||
}
|
|
||||||
@@ -1,447 +0,0 @@
|
|||||||
use std_shims::vec::Vec;
|
|
||||||
|
|
||||||
use rand_core::{RngCore, CryptoRng};
|
|
||||||
|
|
||||||
use zeroize::{Zeroize, ZeroizeOnDrop};
|
|
||||||
|
|
||||||
use multiexp::{multiexp, multiexp_vartime, BatchVerifier};
|
|
||||||
use group::{
|
|
||||||
ff::{Field, PrimeField},
|
|
||||||
GroupEncoding,
|
|
||||||
};
|
|
||||||
use dalek_ff_group::{Scalar, EdwardsPoint};
|
|
||||||
|
|
||||||
use crate::ringct::bulletproofs::plus::{
|
|
||||||
ScalarVector, PointVector, GeneratorsList, Generators, padded_pow_of_2, weighted_inner_product,
|
|
||||||
transcript::*,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Figure 1
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub(crate) struct WipStatement {
|
|
||||||
generators: Generators,
|
|
||||||
P: EdwardsPoint,
|
|
||||||
y: ScalarVector,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Zeroize for WipStatement {
|
|
||||||
fn zeroize(&mut self) {
|
|
||||||
self.P.zeroize();
|
|
||||||
self.y.zeroize();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Zeroize, ZeroizeOnDrop)]
|
|
||||||
pub(crate) struct WipWitness {
|
|
||||||
a: ScalarVector,
|
|
||||||
b: ScalarVector,
|
|
||||||
alpha: Scalar,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WipWitness {
|
|
||||||
pub(crate) fn new(mut a: ScalarVector, mut b: ScalarVector, alpha: Scalar) -> Option<Self> {
|
|
||||||
if a.0.is_empty() || (a.len() != b.len()) {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pad to the nearest power of 2
|
|
||||||
let missing = padded_pow_of_2(a.len()) - a.len();
|
|
||||||
a.0.reserve(missing);
|
|
||||||
b.0.reserve(missing);
|
|
||||||
for _ in 0 .. missing {
|
|
||||||
a.0.push(Scalar::ZERO);
|
|
||||||
b.0.push(Scalar::ZERO);
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(Self { a, b, alpha })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
|
|
||||||
pub(crate) struct WipProof {
|
|
||||||
pub(crate) L: Vec<EdwardsPoint>,
|
|
||||||
pub(crate) R: Vec<EdwardsPoint>,
|
|
||||||
pub(crate) A: EdwardsPoint,
|
|
||||||
pub(crate) B: EdwardsPoint,
|
|
||||||
pub(crate) r_answer: Scalar,
|
|
||||||
pub(crate) s_answer: Scalar,
|
|
||||||
pub(crate) delta_answer: Scalar,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WipStatement {
|
|
||||||
pub(crate) fn new(generators: Generators, P: EdwardsPoint, y: Scalar) -> Self {
|
|
||||||
debug_assert_eq!(generators.len(), padded_pow_of_2(generators.len()));
|
|
||||||
|
|
||||||
// y ** n
|
|
||||||
let mut y_vec = ScalarVector::new(generators.len());
|
|
||||||
y_vec[0] = y;
|
|
||||||
for i in 1 .. y_vec.len() {
|
|
||||||
y_vec[i] = y_vec[i - 1] * y;
|
|
||||||
}
|
|
||||||
|
|
||||||
Self { generators, P, y: y_vec }
|
|
||||||
}
|
|
||||||
|
|
||||||
fn transcript_L_R(transcript: &mut Scalar, L: EdwardsPoint, R: EdwardsPoint) -> Scalar {
|
|
||||||
let e = hash_to_scalar(
|
|
||||||
&[transcript.to_repr().as_ref(), L.to_bytes().as_ref(), R.to_bytes().as_ref()].concat(),
|
|
||||||
);
|
|
||||||
*transcript = e;
|
|
||||||
e
|
|
||||||
}
|
|
||||||
|
|
||||||
fn transcript_A_B(transcript: &mut Scalar, A: EdwardsPoint, B: EdwardsPoint) -> Scalar {
|
|
||||||
let e = hash_to_scalar(
|
|
||||||
&[transcript.to_repr().as_ref(), A.to_bytes().as_ref(), B.to_bytes().as_ref()].concat(),
|
|
||||||
);
|
|
||||||
*transcript = e;
|
|
||||||
e
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prover's variant of the shared code block to calculate G/H/P when n > 1
|
|
||||||
// Returns each permutation of G/H since the prover needs to do operation on each permutation
|
|
||||||
// P is dropped as it's unused in the prover's path
|
|
||||||
// TODO: It'd still probably be faster to keep in terms of the original generators, both between
|
|
||||||
// the reduced amount of group operations and the potential tabling of the generators under
|
|
||||||
// multiexp
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
fn next_G_H(
|
|
||||||
transcript: &mut Scalar,
|
|
||||||
mut g_bold1: PointVector,
|
|
||||||
mut g_bold2: PointVector,
|
|
||||||
mut h_bold1: PointVector,
|
|
||||||
mut h_bold2: PointVector,
|
|
||||||
L: EdwardsPoint,
|
|
||||||
R: EdwardsPoint,
|
|
||||||
y_inv_n_hat: Scalar,
|
|
||||||
) -> (Scalar, Scalar, Scalar, Scalar, PointVector, PointVector) {
|
|
||||||
debug_assert_eq!(g_bold1.len(), g_bold2.len());
|
|
||||||
debug_assert_eq!(h_bold1.len(), h_bold2.len());
|
|
||||||
debug_assert_eq!(g_bold1.len(), h_bold1.len());
|
|
||||||
|
|
||||||
let e = Self::transcript_L_R(transcript, L, R);
|
|
||||||
let inv_e = e.invert().unwrap();
|
|
||||||
|
|
||||||
// This vartime is safe as all of these arguments are public
|
|
||||||
let mut new_g_bold = Vec::with_capacity(g_bold1.len());
|
|
||||||
let e_y_inv = e * y_inv_n_hat;
|
|
||||||
for g_bold in g_bold1.0.drain(..).zip(g_bold2.0.drain(..)) {
|
|
||||||
new_g_bold.push(multiexp_vartime(&[(inv_e, g_bold.0), (e_y_inv, g_bold.1)]));
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut new_h_bold = Vec::with_capacity(h_bold1.len());
|
|
||||||
for h_bold in h_bold1.0.drain(..).zip(h_bold2.0.drain(..)) {
|
|
||||||
new_h_bold.push(multiexp_vartime(&[(e, h_bold.0), (inv_e, h_bold.1)]));
|
|
||||||
}
|
|
||||||
|
|
||||||
let e_square = e.square();
|
|
||||||
let inv_e_square = inv_e.square();
|
|
||||||
|
|
||||||
(e, inv_e, e_square, inv_e_square, PointVector(new_g_bold), PointVector(new_h_bold))
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
This has room for optimization worth investigating further. It currently takes
|
|
||||||
an iterative approach. It can be optimized further via divide and conquer.
|
|
||||||
|
|
||||||
Assume there are 4 challenges.
|
|
||||||
|
|
||||||
Iterative approach (current):
|
|
||||||
1. Do the optimal multiplications across challenge column 0 and 1.
|
|
||||||
2. Do the optimal multiplications across that result and column 2.
|
|
||||||
3. Do the optimal multiplications across that result and column 3.
|
|
||||||
|
|
||||||
Divide and conquer (worth investigating further):
|
|
||||||
1. Do the optimal multiplications across challenge column 0 and 1.
|
|
||||||
2. Do the optimal multiplications across challenge column 2 and 3.
|
|
||||||
3. Multiply both results together.
|
|
||||||
|
|
||||||
When there are 4 challenges (n=16), the iterative approach does 28 multiplications
|
|
||||||
versus divide and conquer's 24.
|
|
||||||
*/
|
|
||||||
fn challenge_products(challenges: &[(Scalar, Scalar)]) -> Vec<Scalar> {
|
|
||||||
let mut products = vec![Scalar::ONE; 1 << challenges.len()];
|
|
||||||
|
|
||||||
if !challenges.is_empty() {
|
|
||||||
products[0] = challenges[0].1;
|
|
||||||
products[1] = challenges[0].0;
|
|
||||||
|
|
||||||
for (j, challenge) in challenges.iter().enumerate().skip(1) {
|
|
||||||
let mut slots = (1 << (j + 1)) - 1;
|
|
||||||
while slots > 0 {
|
|
||||||
products[slots] = products[slots / 2] * challenge.0;
|
|
||||||
products[slots - 1] = products[slots / 2] * challenge.1;
|
|
||||||
|
|
||||||
slots = slots.saturating_sub(2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sanity check since if the above failed to populate, it'd be critical
|
|
||||||
for product in &products {
|
|
||||||
debug_assert!(!bool::from(product.is_zero()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
products
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn prove<R: RngCore + CryptoRng>(
|
|
||||||
self,
|
|
||||||
rng: &mut R,
|
|
||||||
mut transcript: Scalar,
|
|
||||||
witness: &WipWitness,
|
|
||||||
) -> Option<WipProof> {
|
|
||||||
let WipStatement { generators, P, mut y } = self;
|
|
||||||
#[cfg(not(debug_assertions))]
|
|
||||||
let _ = P;
|
|
||||||
|
|
||||||
if generators.len() != witness.a.len() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let (g, h) = (Generators::g(), Generators::h());
|
|
||||||
let mut g_bold = vec![];
|
|
||||||
let mut h_bold = vec![];
|
|
||||||
for i in 0 .. generators.len() {
|
|
||||||
g_bold.push(generators.generator(GeneratorsList::GBold1, i));
|
|
||||||
h_bold.push(generators.generator(GeneratorsList::HBold1, i));
|
|
||||||
}
|
|
||||||
let mut g_bold = PointVector(g_bold);
|
|
||||||
let mut h_bold = PointVector(h_bold);
|
|
||||||
|
|
||||||
// Check P has the expected relationship
|
|
||||||
#[cfg(debug_assertions)]
|
|
||||||
{
|
|
||||||
let mut P_terms = witness
|
|
||||||
.a
|
|
||||||
.0
|
|
||||||
.iter()
|
|
||||||
.copied()
|
|
||||||
.zip(g_bold.0.iter().copied())
|
|
||||||
.chain(witness.b.0.iter().copied().zip(h_bold.0.iter().copied()))
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
P_terms.push((weighted_inner_product(&witness.a, &witness.b, &y), g));
|
|
||||||
P_terms.push((witness.alpha, h));
|
|
||||||
debug_assert_eq!(multiexp(&P_terms), P);
|
|
||||||
P_terms.zeroize();
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut a = witness.a.clone();
|
|
||||||
let mut b = witness.b.clone();
|
|
||||||
let mut alpha = witness.alpha;
|
|
||||||
|
|
||||||
// From here on, g_bold.len() is used as n
|
|
||||||
debug_assert_eq!(g_bold.len(), a.len());
|
|
||||||
|
|
||||||
let mut L_vec = vec![];
|
|
||||||
let mut R_vec = vec![];
|
|
||||||
|
|
||||||
// else n > 1 case from figure 1
|
|
||||||
while g_bold.len() > 1 {
|
|
||||||
let (a1, a2) = a.clone().split();
|
|
||||||
let (b1, b2) = b.clone().split();
|
|
||||||
let (g_bold1, g_bold2) = g_bold.split();
|
|
||||||
let (h_bold1, h_bold2) = h_bold.split();
|
|
||||||
|
|
||||||
let n_hat = g_bold1.len();
|
|
||||||
debug_assert_eq!(a1.len(), n_hat);
|
|
||||||
debug_assert_eq!(a2.len(), n_hat);
|
|
||||||
debug_assert_eq!(b1.len(), n_hat);
|
|
||||||
debug_assert_eq!(b2.len(), n_hat);
|
|
||||||
debug_assert_eq!(g_bold1.len(), n_hat);
|
|
||||||
debug_assert_eq!(g_bold2.len(), n_hat);
|
|
||||||
debug_assert_eq!(h_bold1.len(), n_hat);
|
|
||||||
debug_assert_eq!(h_bold2.len(), n_hat);
|
|
||||||
|
|
||||||
let y_n_hat = y[n_hat - 1];
|
|
||||||
y.0.truncate(n_hat);
|
|
||||||
|
|
||||||
let d_l = Scalar::random(&mut *rng);
|
|
||||||
let d_r = Scalar::random(&mut *rng);
|
|
||||||
|
|
||||||
let c_l = weighted_inner_product(&a1, &b2, &y);
|
|
||||||
let c_r = weighted_inner_product(&(a2.mul(y_n_hat)), &b1, &y);
|
|
||||||
|
|
||||||
// TODO: Calculate these with a batch inversion
|
|
||||||
let y_inv_n_hat = y_n_hat.invert().unwrap();
|
|
||||||
|
|
||||||
let mut L_terms = a1
|
|
||||||
.mul(y_inv_n_hat)
|
|
||||||
.0
|
|
||||||
.drain(..)
|
|
||||||
.zip(g_bold2.0.iter().copied())
|
|
||||||
.chain(b2.0.iter().copied().zip(h_bold1.0.iter().copied()))
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
L_terms.push((c_l, g));
|
|
||||||
L_terms.push((d_l, h));
|
|
||||||
let L = multiexp(&L_terms) * Scalar(crate::INV_EIGHT());
|
|
||||||
L_vec.push(L);
|
|
||||||
L_terms.zeroize();
|
|
||||||
|
|
||||||
let mut R_terms = a2
|
|
||||||
.mul(y_n_hat)
|
|
||||||
.0
|
|
||||||
.drain(..)
|
|
||||||
.zip(g_bold1.0.iter().copied())
|
|
||||||
.chain(b1.0.iter().copied().zip(h_bold2.0.iter().copied()))
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
R_terms.push((c_r, g));
|
|
||||||
R_terms.push((d_r, h));
|
|
||||||
let R = multiexp(&R_terms) * Scalar(crate::INV_EIGHT());
|
|
||||||
R_vec.push(R);
|
|
||||||
R_terms.zeroize();
|
|
||||||
|
|
||||||
let (e, inv_e, e_square, inv_e_square);
|
|
||||||
(e, inv_e, e_square, inv_e_square, g_bold, h_bold) =
|
|
||||||
Self::next_G_H(&mut transcript, g_bold1, g_bold2, h_bold1, h_bold2, L, R, y_inv_n_hat);
|
|
||||||
|
|
||||||
a = a1.mul(e).add_vec(&a2.mul(y_n_hat * inv_e));
|
|
||||||
b = b1.mul(inv_e).add_vec(&b2.mul(e));
|
|
||||||
alpha += (d_l * e_square) + (d_r * inv_e_square);
|
|
||||||
|
|
||||||
debug_assert_eq!(g_bold.len(), a.len());
|
|
||||||
debug_assert_eq!(g_bold.len(), h_bold.len());
|
|
||||||
debug_assert_eq!(g_bold.len(), b.len());
|
|
||||||
}
|
|
||||||
|
|
||||||
// n == 1 case from figure 1
|
|
||||||
debug_assert_eq!(g_bold.len(), 1);
|
|
||||||
debug_assert_eq!(h_bold.len(), 1);
|
|
||||||
|
|
||||||
debug_assert_eq!(a.len(), 1);
|
|
||||||
debug_assert_eq!(b.len(), 1);
|
|
||||||
|
|
||||||
let r = Scalar::random(&mut *rng);
|
|
||||||
let s = Scalar::random(&mut *rng);
|
|
||||||
let delta = Scalar::random(&mut *rng);
|
|
||||||
let eta = Scalar::random(&mut *rng);
|
|
||||||
|
|
||||||
let ry = r * y[0];
|
|
||||||
|
|
||||||
let mut A_terms =
|
|
||||||
vec![(r, g_bold[0]), (s, h_bold[0]), ((ry * b[0]) + (s * y[0] * a[0]), g), (delta, h)];
|
|
||||||
let A = multiexp(&A_terms) * Scalar(crate::INV_EIGHT());
|
|
||||||
A_terms.zeroize();
|
|
||||||
|
|
||||||
let mut B_terms = vec![(ry * s, g), (eta, h)];
|
|
||||||
let B = multiexp(&B_terms) * Scalar(crate::INV_EIGHT());
|
|
||||||
B_terms.zeroize();
|
|
||||||
|
|
||||||
let e = Self::transcript_A_B(&mut transcript, A, B);
|
|
||||||
|
|
||||||
let r_answer = r + (a[0] * e);
|
|
||||||
let s_answer = s + (b[0] * e);
|
|
||||||
let delta_answer = eta + (delta * e) + (alpha * e.square());
|
|
||||||
|
|
||||||
Some(WipProof { L: L_vec, R: R_vec, A, B, r_answer, s_answer, delta_answer })
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn verify<Id: Copy + Zeroize, R: RngCore + CryptoRng>(
|
|
||||||
self,
|
|
||||||
rng: &mut R,
|
|
||||||
verifier: &mut BatchVerifier<Id, EdwardsPoint>,
|
|
||||||
id: Id,
|
|
||||||
mut transcript: Scalar,
|
|
||||||
mut proof: WipProof,
|
|
||||||
) -> bool {
|
|
||||||
let WipStatement { generators, P, y } = self;
|
|
||||||
|
|
||||||
let (g, h) = (Generators::g(), Generators::h());
|
|
||||||
|
|
||||||
// Verify the L/R lengths
|
|
||||||
{
|
|
||||||
let mut lr_len = 0;
|
|
||||||
while (1 << lr_len) < generators.len() {
|
|
||||||
lr_len += 1;
|
|
||||||
}
|
|
||||||
if (proof.L.len() != lr_len) ||
|
|
||||||
(proof.R.len() != lr_len) ||
|
|
||||||
(generators.len() != (1 << lr_len))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let inv_y = {
|
|
||||||
let inv_y = y[0].invert().unwrap();
|
|
||||||
let mut res = Vec::with_capacity(y.len());
|
|
||||||
res.push(inv_y);
|
|
||||||
while res.len() < y.len() {
|
|
||||||
res.push(inv_y * res.last().unwrap());
|
|
||||||
}
|
|
||||||
res
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut P_terms = vec![(Scalar::ONE, P)];
|
|
||||||
P_terms.reserve(6 + (2 * generators.len()) + proof.L.len());
|
|
||||||
|
|
||||||
let mut challenges = Vec::with_capacity(proof.L.len());
|
|
||||||
let product_cache = {
|
|
||||||
let mut es = Vec::with_capacity(proof.L.len());
|
|
||||||
for (L, R) in proof.L.iter_mut().zip(proof.R.iter_mut()) {
|
|
||||||
es.push(Self::transcript_L_R(&mut transcript, *L, *R));
|
|
||||||
*L = L.mul_by_cofactor();
|
|
||||||
*R = R.mul_by_cofactor();
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut inv_es = es.clone();
|
|
||||||
let mut scratch = vec![Scalar::ZERO; es.len()];
|
|
||||||
group::ff::BatchInverter::invert_with_external_scratch(&mut inv_es, &mut scratch);
|
|
||||||
drop(scratch);
|
|
||||||
|
|
||||||
debug_assert_eq!(es.len(), inv_es.len());
|
|
||||||
debug_assert_eq!(es.len(), proof.L.len());
|
|
||||||
debug_assert_eq!(es.len(), proof.R.len());
|
|
||||||
for ((e, inv_e), (L, R)) in
|
|
||||||
es.drain(..).zip(inv_es.drain(..)).zip(proof.L.iter().zip(proof.R.iter()))
|
|
||||||
{
|
|
||||||
debug_assert_eq!(e.invert().unwrap(), inv_e);
|
|
||||||
|
|
||||||
challenges.push((e, inv_e));
|
|
||||||
|
|
||||||
let e_square = e.square();
|
|
||||||
let inv_e_square = inv_e.square();
|
|
||||||
P_terms.push((e_square, *L));
|
|
||||||
P_terms.push((inv_e_square, *R));
|
|
||||||
}
|
|
||||||
|
|
||||||
Self::challenge_products(&challenges)
|
|
||||||
};
|
|
||||||
|
|
||||||
let e = Self::transcript_A_B(&mut transcript, proof.A, proof.B);
|
|
||||||
proof.A = proof.A.mul_by_cofactor();
|
|
||||||
proof.B = proof.B.mul_by_cofactor();
|
|
||||||
let neg_e_square = -e.square();
|
|
||||||
|
|
||||||
let mut multiexp = P_terms;
|
|
||||||
multiexp.reserve(4 + (2 * generators.len()));
|
|
||||||
for (scalar, _) in &mut multiexp {
|
|
||||||
*scalar *= neg_e_square;
|
|
||||||
}
|
|
||||||
|
|
||||||
let re = proof.r_answer * e;
|
|
||||||
for i in 0 .. generators.len() {
|
|
||||||
let mut scalar = product_cache[i] * re;
|
|
||||||
if i > 0 {
|
|
||||||
scalar *= inv_y[i - 1];
|
|
||||||
}
|
|
||||||
multiexp.push((scalar, generators.generator(GeneratorsList::GBold1, i)));
|
|
||||||
}
|
|
||||||
|
|
||||||
let se = proof.s_answer * e;
|
|
||||||
for i in 0 .. generators.len() {
|
|
||||||
multiexp.push((
|
|
||||||
se * product_cache[product_cache.len() - 1 - i],
|
|
||||||
generators.generator(GeneratorsList::HBold1, i),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
multiexp.push((-e, proof.A));
|
|
||||||
multiexp.push((proof.r_answer * y[0] * proof.s_answer, g));
|
|
||||||
multiexp.push((proof.delta_answer, h));
|
|
||||||
multiexp.push((-Scalar::ONE, proof.B));
|
|
||||||
|
|
||||||
verifier.queue(rng, id, multiexp);
|
|
||||||
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,114 +0,0 @@
|
|||||||
use core::ops::{Add, Sub, Mul, Index};
|
|
||||||
use std_shims::vec::Vec;
|
|
||||||
|
|
||||||
use zeroize::{Zeroize, ZeroizeOnDrop};
|
|
||||||
|
|
||||||
use group::ff::Field;
|
|
||||||
use dalek_ff_group::{Scalar, EdwardsPoint};
|
|
||||||
|
|
||||||
use multiexp::multiexp;
|
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug, Zeroize, ZeroizeOnDrop)]
|
|
||||||
pub(crate) struct ScalarVector(pub(crate) Vec<Scalar>);
|
|
||||||
macro_rules! math_op {
|
|
||||||
($Op: ident, $op: ident, $f: expr) => {
|
|
||||||
#[allow(clippy::redundant_closure_call)]
|
|
||||||
impl $Op<Scalar> for ScalarVector {
|
|
||||||
type Output = ScalarVector;
|
|
||||||
fn $op(self, b: Scalar) -> ScalarVector {
|
|
||||||
ScalarVector(self.0.iter().map(|a| $f((a, &b))).collect())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::redundant_closure_call)]
|
|
||||||
impl $Op<Scalar> for &ScalarVector {
|
|
||||||
type Output = ScalarVector;
|
|
||||||
fn $op(self, b: Scalar) -> ScalarVector {
|
|
||||||
ScalarVector(self.0.iter().map(|a| $f((a, &b))).collect())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::redundant_closure_call)]
|
|
||||||
impl $Op<ScalarVector> for ScalarVector {
|
|
||||||
type Output = ScalarVector;
|
|
||||||
fn $op(self, b: ScalarVector) -> ScalarVector {
|
|
||||||
debug_assert_eq!(self.len(), b.len());
|
|
||||||
ScalarVector(self.0.iter().zip(b.0.iter()).map($f).collect())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::redundant_closure_call)]
|
|
||||||
impl $Op<&ScalarVector> for &ScalarVector {
|
|
||||||
type Output = ScalarVector;
|
|
||||||
fn $op(self, b: &ScalarVector) -> ScalarVector {
|
|
||||||
debug_assert_eq!(self.len(), b.len());
|
|
||||||
ScalarVector(self.0.iter().zip(b.0.iter()).map($f).collect())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
math_op!(Add, add, |(a, b): (&Scalar, &Scalar)| *a + *b);
|
|
||||||
math_op!(Sub, sub, |(a, b): (&Scalar, &Scalar)| *a - *b);
|
|
||||||
math_op!(Mul, mul, |(a, b): (&Scalar, &Scalar)| *a * *b);
|
|
||||||
|
|
||||||
impl ScalarVector {
|
|
||||||
pub(crate) fn new(len: usize) -> ScalarVector {
|
|
||||||
ScalarVector(vec![Scalar::ZERO; len])
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn powers(x: Scalar, len: usize) -> ScalarVector {
|
|
||||||
debug_assert!(len != 0);
|
|
||||||
|
|
||||||
let mut res = Vec::with_capacity(len);
|
|
||||||
res.push(Scalar::ONE);
|
|
||||||
for i in 1 .. len {
|
|
||||||
res.push(res[i - 1] * x);
|
|
||||||
}
|
|
||||||
ScalarVector(res)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn sum(mut self) -> Scalar {
|
|
||||||
self.0.drain(..).sum()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn len(&self) -> usize {
|
|
||||||
self.0.len()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn split(self) -> (ScalarVector, ScalarVector) {
|
|
||||||
let (l, r) = self.0.split_at(self.0.len() / 2);
|
|
||||||
(ScalarVector(l.to_vec()), ScalarVector(r.to_vec()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Index<usize> for ScalarVector {
|
|
||||||
type Output = Scalar;
|
|
||||||
fn index(&self, index: usize) -> &Scalar {
|
|
||||||
&self.0[index]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn inner_product(a: &ScalarVector, b: &ScalarVector) -> Scalar {
|
|
||||||
(a * b).sum()
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Mul<&[EdwardsPoint]> for &ScalarVector {
|
|
||||||
type Output = EdwardsPoint;
|
|
||||||
fn mul(self, b: &[EdwardsPoint]) -> EdwardsPoint {
|
|
||||||
debug_assert_eq!(self.len(), b.len());
|
|
||||||
multiexp(&self.0.iter().copied().zip(b.iter().copied()).collect::<Vec<_>>())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn hadamard_fold(
|
|
||||||
l: &[EdwardsPoint],
|
|
||||||
r: &[EdwardsPoint],
|
|
||||||
a: Scalar,
|
|
||||||
b: Scalar,
|
|
||||||
) -> Vec<EdwardsPoint> {
|
|
||||||
let mut res = Vec::with_capacity(l.len() / 2);
|
|
||||||
for i in 0 .. l.len() {
|
|
||||||
res.push(multiexp(&[(a, l[i]), (b, r[i])]));
|
|
||||||
}
|
|
||||||
res
|
|
||||||
}
|
|
||||||
@@ -1,71 +1,65 @@
|
|||||||
#![allow(non_snake_case)]
|
#![allow(non_snake_case)]
|
||||||
|
|
||||||
use core::ops::Deref;
|
use lazy_static::lazy_static;
|
||||||
use std_shims::{
|
use thiserror::Error;
|
||||||
vec::Vec,
|
|
||||||
io::{self, Read, Write},
|
|
||||||
};
|
|
||||||
|
|
||||||
use rand_core::{RngCore, CryptoRng};
|
use rand_core::{RngCore, CryptoRng};
|
||||||
|
|
||||||
use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing};
|
|
||||||
use subtle::{ConstantTimeEq, Choice, CtOption};
|
|
||||||
|
|
||||||
use curve25519_dalek::{
|
use curve25519_dalek::{
|
||||||
constants::ED25519_BASEPOINT_TABLE,
|
constants::ED25519_BASEPOINT_TABLE,
|
||||||
scalar::Scalar,
|
scalar::Scalar,
|
||||||
traits::{IsIdentity, VartimePrecomputedMultiscalarMul},
|
traits::VartimePrecomputedMultiscalarMul,
|
||||||
edwards::{EdwardsPoint, VartimeEdwardsPrecomputation},
|
edwards::{EdwardsPoint, VartimeEdwardsPrecomputation}
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
INV_EIGHT, Commitment, random_scalar, hash_to_scalar, wallet::decoys::Decoys,
|
Commitment, random_scalar, hash_to_scalar,
|
||||||
ringct::hash_to_point, serialize::*,
|
transaction::RING_LEN,
|
||||||
|
wallet::decoys::Decoys,
|
||||||
|
ringct::hash_to_point,
|
||||||
|
serialize::*
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(feature = "multisig")]
|
#[cfg(feature = "multisig")]
|
||||||
mod multisig;
|
mod multisig;
|
||||||
#[cfg(feature = "multisig")]
|
#[cfg(feature = "multisig")]
|
||||||
pub use multisig::{ClsagDetails, ClsagAddendum, ClsagMultisig};
|
pub use multisig::{ClsagDetails, ClsagMultisig};
|
||||||
#[cfg(feature = "multisig")]
|
|
||||||
pub(crate) use multisig::add_key_image_share;
|
|
||||||
|
|
||||||
/// Errors returned when CLSAG signing fails.
|
lazy_static! {
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
static ref INV_EIGHT: Scalar = Scalar::from(8u8).invert();
|
||||||
#[cfg_attr(feature = "std", derive(thiserror::Error))]
|
|
||||||
pub enum ClsagError {
|
|
||||||
#[cfg_attr(feature = "std", error("internal error ({0})"))]
|
|
||||||
InternalError(&'static str),
|
|
||||||
#[cfg_attr(feature = "std", error("invalid ring"))]
|
|
||||||
InvalidRing,
|
|
||||||
#[cfg_attr(feature = "std", error("invalid ring member (member {0}, ring size {1})"))]
|
|
||||||
InvalidRingMember(u8, u8),
|
|
||||||
#[cfg_attr(feature = "std", error("invalid commitment"))]
|
|
||||||
InvalidCommitment,
|
|
||||||
#[cfg_attr(feature = "std", error("invalid key image"))]
|
|
||||||
InvalidImage,
|
|
||||||
#[cfg_attr(feature = "std", error("invalid D"))]
|
|
||||||
InvalidD,
|
|
||||||
#[cfg_attr(feature = "std", error("invalid s"))]
|
|
||||||
InvalidS,
|
|
||||||
#[cfg_attr(feature = "std", error("invalid c1"))]
|
|
||||||
InvalidC1,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Input being signed for.
|
#[derive(Clone, Error, Debug)]
|
||||||
#[derive(Clone, PartialEq, Eq, Debug, Zeroize, ZeroizeOnDrop)]
|
pub enum ClsagError {
|
||||||
|
#[error("internal error ({0})")]
|
||||||
|
InternalError(String),
|
||||||
|
#[error("invalid ring member (member {0}, ring size {1})")]
|
||||||
|
InvalidRingMember(u8, u8),
|
||||||
|
#[error("invalid commitment")]
|
||||||
|
InvalidCommitment,
|
||||||
|
#[error("invalid D")]
|
||||||
|
InvalidD,
|
||||||
|
#[error("invalid s")]
|
||||||
|
InvalidS,
|
||||||
|
#[error("invalid c1")]
|
||||||
|
InvalidC1
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq, Debug)]
|
||||||
pub struct ClsagInput {
|
pub struct ClsagInput {
|
||||||
// The actual commitment for the true spend
|
// The actual commitment for the true spend
|
||||||
pub(crate) commitment: Commitment,
|
pub commitment: Commitment,
|
||||||
// True spend index, offsets, and ring
|
// True spend index, offsets, and ring
|
||||||
pub(crate) decoys: Decoys,
|
pub decoys: Decoys
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ClsagInput {
|
impl ClsagInput {
|
||||||
pub fn new(commitment: Commitment, decoys: Decoys) -> Result<ClsagInput, ClsagError> {
|
pub fn new(
|
||||||
|
commitment: Commitment,
|
||||||
|
decoys: Decoys
|
||||||
|
) -> Result<ClsagInput, ClsagError> {
|
||||||
let n = decoys.len();
|
let n = decoys.len();
|
||||||
if n > u8::MAX.into() {
|
if n > u8::MAX.into() {
|
||||||
Err(ClsagError::InternalError("max ring size in this library is u8 max"))?;
|
Err(ClsagError::InternalError("max ring size in this library is u8 max".to_string()))?;
|
||||||
}
|
}
|
||||||
let n = u8::try_from(n).unwrap();
|
let n = u8::try_from(n).unwrap();
|
||||||
if decoys.i >= n {
|
if decoys.i >= n {
|
||||||
@@ -81,10 +75,10 @@ impl ClsagInput {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::large_enum_variant)]
|
|
||||||
enum Mode {
|
enum Mode {
|
||||||
Sign(usize, EdwardsPoint, EdwardsPoint),
|
Sign(usize, EdwardsPoint, EdwardsPoint),
|
||||||
Verify(Scalar),
|
#[cfg(feature = "experimental")]
|
||||||
|
Verify(Scalar)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Core of the CLSAG algorithm, applicable to both sign and verify with minimal differences
|
// Core of the CLSAG algorithm, applicable to both sign and verify with minimal differences
|
||||||
@@ -96,26 +90,22 @@ fn core(
|
|||||||
msg: &[u8; 32],
|
msg: &[u8; 32],
|
||||||
D: &EdwardsPoint,
|
D: &EdwardsPoint,
|
||||||
s: &[Scalar],
|
s: &[Scalar],
|
||||||
A_c1: &Mode,
|
A_c1: Mode
|
||||||
) -> ((EdwardsPoint, Scalar, Scalar), Scalar) {
|
) -> ((EdwardsPoint, Scalar, Scalar), Scalar) {
|
||||||
let n = ring.len();
|
let n = ring.len();
|
||||||
|
|
||||||
let images_precomp = VartimeEdwardsPrecomputation::new([I, D]);
|
let images_precomp = VartimeEdwardsPrecomputation::new([I, D]);
|
||||||
let D = D * INV_EIGHT();
|
let D = D * *INV_EIGHT;
|
||||||
|
|
||||||
// Generate the transcript
|
// Generate the transcript
|
||||||
// Instead of generating multiple, a single transcript is created and then edited as needed
|
// Instead of generating multiple, a single transcript is created and then edited as needed
|
||||||
const PREFIX: &[u8] = b"CLSAG_";
|
let mut to_hash = vec![];
|
||||||
#[rustfmt::skip]
|
to_hash.reserve_exact(((2 * n) + 5) * 32);
|
||||||
const AGG_0: &[u8] = b"agg_0";
|
const PREFIX: &[u8] = "CLSAG_".as_bytes();
|
||||||
#[rustfmt::skip]
|
const AGG_0: &[u8] = "CLSAG_agg_0".as_bytes();
|
||||||
const ROUND: &[u8] = b"round";
|
const ROUND: &[u8] = "round".as_bytes();
|
||||||
const PREFIX_AGG_0_LEN: usize = PREFIX.len() + AGG_0.len();
|
|
||||||
|
|
||||||
let mut to_hash = Vec::with_capacity(((2 * n) + 5) * 32);
|
|
||||||
to_hash.extend(PREFIX);
|
|
||||||
to_hash.extend(AGG_0);
|
to_hash.extend(AGG_0);
|
||||||
to_hash.extend([0; 32 - PREFIX_AGG_0_LEN]);
|
to_hash.extend([0; 32 - AGG_0.len()]);
|
||||||
|
|
||||||
let mut P = Vec::with_capacity(n);
|
let mut P = Vec::with_capacity(n);
|
||||||
for member in ring {
|
for member in ring {
|
||||||
@@ -135,7 +125,7 @@ fn core(
|
|||||||
// mu_P with agg_0
|
// mu_P with agg_0
|
||||||
let mu_P = hash_to_scalar(&to_hash);
|
let mu_P = hash_to_scalar(&to_hash);
|
||||||
// mu_C with agg_1
|
// mu_C with agg_1
|
||||||
to_hash[PREFIX_AGG_0_LEN - 1] = b'1';
|
to_hash[AGG_0.len() - 1] = b'1';
|
||||||
let mu_C = hash_to_scalar(&to_hash);
|
let mu_C = hash_to_scalar(&to_hash);
|
||||||
|
|
||||||
// Truncate it for the round transcript, altering the DST as needed
|
// Truncate it for the round transcript, altering the DST as needed
|
||||||
@@ -159,30 +149,30 @@ fn core(
|
|||||||
to_hash.extend(A.compress().to_bytes());
|
to_hash.extend(A.compress().to_bytes());
|
||||||
to_hash.extend(AH.compress().to_bytes());
|
to_hash.extend(AH.compress().to_bytes());
|
||||||
c = hash_to_scalar(&to_hash);
|
c = hash_to_scalar(&to_hash);
|
||||||
}
|
},
|
||||||
|
|
||||||
|
#[cfg(feature = "experimental")]
|
||||||
Mode::Verify(c1) => {
|
Mode::Verify(c1) => {
|
||||||
start = 0;
|
start = 0;
|
||||||
end = n;
|
end = n;
|
||||||
c = *c1;
|
c = c1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Perform the core loop
|
// Perform the core loop
|
||||||
let mut c1 = CtOption::new(Scalar::ZERO, Choice::from(0));
|
let mut c1 = None;
|
||||||
for i in (start .. end).map(|i| i % n) {
|
for i in (start .. end).map(|i| i % n) {
|
||||||
// This will only execute once and shouldn't need to be constant time. Making it constant time
|
if i == 0 {
|
||||||
// removes the risk of branch prediction creating timing differences depending on ring index
|
c1 = Some(c);
|
||||||
// however
|
}
|
||||||
c1 = c1.or_else(|| CtOption::new(c, i.ct_eq(&0)));
|
|
||||||
|
|
||||||
let c_p = mu_P * c;
|
let c_p = mu_P * c;
|
||||||
let c_c = mu_C * c;
|
let c_c = mu_C * c;
|
||||||
|
|
||||||
let L = (&s[i] * ED25519_BASEPOINT_TABLE) + (c_p * P[i]) + (c_c * C[i]);
|
let L = (&s[i] * &ED25519_BASEPOINT_TABLE) + (c_p * P[i]) + (c_c * C[i]);
|
||||||
let PH = hash_to_point(&P[i]);
|
let PH = hash_to_point(P[i]);
|
||||||
// Shouldn't be an issue as all of the variables in this vartime statement are public
|
// Shouldn't be an issue as all of the variables in this vartime statement are public
|
||||||
let R = (s[i] * PH) + images_precomp.vartime_multiscalar_mul([c_p, c_c]);
|
let R = (s[i] * PH) + images_precomp.vartime_multiscalar_mul(&[c_p, c_c]);
|
||||||
|
|
||||||
to_hash.truncate(((2 * n) + 3) * 32);
|
to_hash.truncate(((2 * n) + 3) * 32);
|
||||||
to_hash.extend(L.compress().to_bytes());
|
to_hash.extend(L.compress().to_bytes());
|
||||||
@@ -194,12 +184,11 @@ fn core(
|
|||||||
((D, c * mu_P, c * mu_C), c1.unwrap_or(c))
|
((D, c * mu_P, c * mu_C), c1.unwrap_or(c))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// CLSAG signature, as used in Monero.
|
#[derive(Clone, PartialEq, Debug)]
|
||||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
|
||||||
pub struct Clsag {
|
pub struct Clsag {
|
||||||
pub D: EdwardsPoint,
|
pub D: EdwardsPoint,
|
||||||
pub s: Vec<Scalar>,
|
pub s: Vec<Scalar>,
|
||||||
pub c1: Scalar,
|
pub c1: Scalar
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Clsag {
|
impl Clsag {
|
||||||
@@ -212,36 +201,42 @@ impl Clsag {
|
|||||||
mask: Scalar,
|
mask: Scalar,
|
||||||
msg: &[u8; 32],
|
msg: &[u8; 32],
|
||||||
A: EdwardsPoint,
|
A: EdwardsPoint,
|
||||||
AH: EdwardsPoint,
|
AH: EdwardsPoint
|
||||||
) -> (Clsag, EdwardsPoint, Scalar, Scalar) {
|
) -> (Clsag, EdwardsPoint, Scalar, Scalar) {
|
||||||
let r: usize = input.decoys.i.into();
|
let r: usize = input.decoys.i.into();
|
||||||
|
|
||||||
let pseudo_out = Commitment::new(mask, input.commitment.amount).calculate();
|
let pseudo_out = Commitment::new(mask, input.commitment.amount).calculate();
|
||||||
let z = input.commitment.mask - mask;
|
let z = input.commitment.mask - mask;
|
||||||
|
|
||||||
let H = hash_to_point(&input.decoys.ring[r][0]);
|
let H = hash_to_point(input.decoys.ring[r][0]);
|
||||||
let D = H * z;
|
let D = H * z;
|
||||||
let mut s = Vec::with_capacity(input.decoys.ring.len());
|
let mut s = Vec::with_capacity(input.decoys.ring.len());
|
||||||
for _ in 0 .. input.decoys.ring.len() {
|
for _ in 0 .. input.decoys.ring.len() {
|
||||||
s.push(random_scalar(rng));
|
s.push(random_scalar(rng));
|
||||||
}
|
}
|
||||||
let ((D, p, c), c1) =
|
let ((D, p, c), c1) = core(&input.decoys.ring, I, &pseudo_out, msg, &D, &s, Mode::Sign(r, A, AH));
|
||||||
core(&input.decoys.ring, I, &pseudo_out, msg, &D, &s, &Mode::Sign(r, A, AH));
|
|
||||||
|
|
||||||
(Clsag { D, s, c1 }, pseudo_out, p, c * z)
|
(
|
||||||
|
Clsag { D, s, c1 },
|
||||||
|
pseudo_out,
|
||||||
|
p,
|
||||||
|
c * z
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generate CLSAG signatures for the given inputs.
|
// Single signer CLSAG
|
||||||
/// inputs is of the form (private key, key image, input).
|
|
||||||
/// sum_outputs is for the sum of the outputs' commitment masks.
|
|
||||||
pub fn sign<R: RngCore + CryptoRng>(
|
pub fn sign<R: RngCore + CryptoRng>(
|
||||||
rng: &mut R,
|
rng: &mut R,
|
||||||
mut inputs: Vec<(Zeroizing<Scalar>, EdwardsPoint, ClsagInput)>,
|
inputs: &[(Scalar, EdwardsPoint, ClsagInput)],
|
||||||
sum_outputs: Scalar,
|
sum_outputs: Scalar,
|
||||||
msg: [u8; 32],
|
msg: [u8; 32]
|
||||||
) -> Vec<(Clsag, EdwardsPoint)> {
|
) -> Vec<(Clsag, EdwardsPoint)> {
|
||||||
|
let nonce = random_scalar(rng);
|
||||||
|
let mut rand_source = [0; 64];
|
||||||
|
rng.fill_bytes(&mut rand_source);
|
||||||
|
|
||||||
let mut res = Vec::with_capacity(inputs.len());
|
let mut res = Vec::with_capacity(inputs.len());
|
||||||
let mut sum_pseudo_outs = Scalar::ZERO;
|
let mut sum_pseudo_outs = Scalar::zero();
|
||||||
for i in 0 .. inputs.len() {
|
for i in 0 .. inputs.len() {
|
||||||
let mut mask = random_scalar(rng);
|
let mut mask = random_scalar(rng);
|
||||||
if i == (inputs.len() - 1) {
|
if i == (inputs.len() - 1) {
|
||||||
@@ -250,25 +245,18 @@ impl Clsag {
|
|||||||
sum_pseudo_outs += mask;
|
sum_pseudo_outs += mask;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut nonce = Zeroizing::new(random_scalar(rng));
|
let mut rand_source = [0; 64];
|
||||||
|
rng.fill_bytes(&mut rand_source);
|
||||||
let (mut clsag, pseudo_out, p, c) = Clsag::sign_core(
|
let (mut clsag, pseudo_out, p, c) = Clsag::sign_core(
|
||||||
rng,
|
rng,
|
||||||
&inputs[i].1,
|
&inputs[i].1,
|
||||||
&inputs[i].2,
|
&inputs[i].2,
|
||||||
mask,
|
mask,
|
||||||
&msg,
|
&msg,
|
||||||
nonce.deref() * ED25519_BASEPOINT_TABLE,
|
&nonce * &ED25519_BASEPOINT_TABLE,
|
||||||
nonce.deref() *
|
nonce * hash_to_point(inputs[i].2.decoys.ring[usize::from(inputs[i].2.decoys.i)][0])
|
||||||
hash_to_point(&inputs[i].2.decoys.ring[usize::from(inputs[i].2.decoys.i)][0]),
|
|
||||||
);
|
);
|
||||||
clsag.s[usize::from(inputs[i].2.decoys.i)] =
|
clsag.s[usize::from(inputs[i].2.decoys.i)] = nonce - ((p * inputs[i].0) + c);
|
||||||
(-((p * inputs[i].0.deref()) + c)) + nonce.deref();
|
|
||||||
inputs[i].0.zeroize();
|
|
||||||
nonce.zeroize();
|
|
||||||
|
|
||||||
debug_assert!(clsag
|
|
||||||
.verify(&inputs[i].2.decoys.ring, &inputs[i].1, &pseudo_out, &msg)
|
|
||||||
.is_ok());
|
|
||||||
|
|
||||||
res.push((clsag, pseudo_out));
|
res.push((clsag, pseudo_out));
|
||||||
}
|
}
|
||||||
@@ -276,49 +264,97 @@ impl Clsag {
|
|||||||
res
|
res
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Verify the CLSAG signature against the given Transaction data.
|
// Not extensively tested nor guaranteed to have expected parity with Monero
|
||||||
pub fn verify(
|
#[cfg(feature = "experimental")]
|
||||||
|
pub fn rust_verify(
|
||||||
&self,
|
&self,
|
||||||
ring: &[[EdwardsPoint; 2]],
|
ring: &[[EdwardsPoint; 2]],
|
||||||
I: &EdwardsPoint,
|
I: &EdwardsPoint,
|
||||||
pseudo_out: &EdwardsPoint,
|
pseudo_out: &EdwardsPoint,
|
||||||
msg: &[u8; 32],
|
msg: &[u8; 32]
|
||||||
) -> Result<(), ClsagError> {
|
) -> Result<(), ClsagError> {
|
||||||
// Preliminary checks. s, c1, and points must also be encoded canonically, which isn't checked
|
let (_, c1) = core(
|
||||||
// here
|
ring,
|
||||||
if ring.is_empty() {
|
I,
|
||||||
Err(ClsagError::InvalidRing)?;
|
pseudo_out,
|
||||||
}
|
msg,
|
||||||
if ring.len() != self.s.len() {
|
&self.D.mul_by_cofactor(),
|
||||||
Err(ClsagError::InvalidS)?;
|
&self.s,
|
||||||
}
|
Mode::Verify(self.c1)
|
||||||
if I.is_identity() {
|
);
|
||||||
Err(ClsagError::InvalidImage)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let D = self.D.mul_by_cofactor();
|
|
||||||
if D.is_identity() {
|
|
||||||
Err(ClsagError::InvalidD)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let (_, c1) = core(ring, I, pseudo_out, msg, &D, &self.s, &Mode::Verify(self.c1));
|
|
||||||
if c1 != self.c1 {
|
if c1 != self.c1 {
|
||||||
Err(ClsagError::InvalidC1)?;
|
Err(ClsagError::InvalidC1)?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn fee_weight(ring_len: usize) -> usize {
|
pub(crate) fn fee_weight() -> usize {
|
||||||
(ring_len * 32) + 32 + 32
|
(RING_LEN * 32) + 32 + 32
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
pub fn serialize<W: std::io::Write>(&self, w: &mut W) -> std::io::Result<()> {
|
||||||
write_raw_vec(write_scalar, &self.s, w)?;
|
write_raw_vec(write_scalar, &self.s, w)?;
|
||||||
w.write_all(&self.c1.to_bytes())?;
|
w.write_all(&self.c1.to_bytes())?;
|
||||||
write_point(&self.D, w)
|
write_point(&self.D, w)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn read<R: Read>(decoys: usize, r: &mut R) -> io::Result<Clsag> {
|
pub fn deserialize<R: std::io::Read>(decoys: usize, r: &mut R) -> std::io::Result<Clsag> {
|
||||||
Ok(Clsag { s: read_raw_vec(read_scalar, decoys, r)?, c1: read_scalar(r)?, D: read_point(r)? })
|
Ok(
|
||||||
|
Clsag {
|
||||||
|
s: read_raw_vec(read_scalar, decoys, r)?,
|
||||||
|
c1: read_scalar(r)?,
|
||||||
|
D: read_point(r)?
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn verify(
|
||||||
|
&self,
|
||||||
|
ring: &[[EdwardsPoint; 2]],
|
||||||
|
I: &EdwardsPoint,
|
||||||
|
pseudo_out: &EdwardsPoint,
|
||||||
|
msg: &[u8; 32]
|
||||||
|
) -> Result<(), ClsagError> {
|
||||||
|
// Serialize it to pass the struct to Monero without extensive FFI
|
||||||
|
let mut serialized = Vec::with_capacity(1 + ((self.s.len() + 2) * 32));
|
||||||
|
write_varint(&self.s.len().try_into().unwrap(), &mut serialized).unwrap();
|
||||||
|
self.serialize(&mut serialized).unwrap();
|
||||||
|
|
||||||
|
let I_bytes = I.compress().to_bytes();
|
||||||
|
|
||||||
|
let mut ring_bytes = vec![];
|
||||||
|
for member in ring {
|
||||||
|
ring_bytes.extend(&member[0].compress().to_bytes());
|
||||||
|
ring_bytes.extend(&member[1].compress().to_bytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
let pseudo_out_bytes = pseudo_out.compress().to_bytes();
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
// Uses Monero's C verification function to ensure compatibility with Monero
|
||||||
|
#[link(name = "wrapper")]
|
||||||
|
extern "C" {
|
||||||
|
pub(crate) fn c_verify_clsag(
|
||||||
|
serialized_len: usize,
|
||||||
|
serialized: *const u8,
|
||||||
|
ring_size: u8,
|
||||||
|
ring: *const u8,
|
||||||
|
I: *const u8,
|
||||||
|
pseudo_out: *const u8,
|
||||||
|
msg: *const u8
|
||||||
|
) -> bool;
|
||||||
|
}
|
||||||
|
|
||||||
|
if c_verify_clsag(
|
||||||
|
serialized.len(), serialized.as_ptr(),
|
||||||
|
u8::try_from(ring.len()).map_err(|_| ClsagError::InternalError("too large ring".to_string()))?,
|
||||||
|
ring_bytes.as_ptr(),
|
||||||
|
I_bytes.as_ptr(), pseudo_out_bytes.as_ptr(), msg.as_ptr()
|
||||||
|
) {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(ClsagError::InvalidC1)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,51 +1,44 @@
|
|||||||
use core::{ops::Deref, fmt::Debug};
|
use core::fmt::Debug;
|
||||||
use std_shims::io::{self, Read, Write};
|
use std::{io::Read, sync::{Arc, RwLock}};
|
||||||
use std::sync::{Arc, RwLock};
|
|
||||||
|
|
||||||
use rand_core::{RngCore, CryptoRng, SeedableRng};
|
use rand_core::{RngCore, CryptoRng, SeedableRng};
|
||||||
use rand_chacha::ChaCha20Rng;
|
use rand_chacha::ChaCha12Rng;
|
||||||
|
|
||||||
use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing};
|
use curve25519_dalek::{
|
||||||
|
constants::ED25519_BASEPOINT_TABLE,
|
||||||
|
traits::{Identity, IsIdentity},
|
||||||
|
scalar::Scalar,
|
||||||
|
edwards::EdwardsPoint
|
||||||
|
};
|
||||||
|
|
||||||
use curve25519_dalek::{scalar::Scalar, edwards::EdwardsPoint};
|
use group::Group;
|
||||||
|
|
||||||
use group::{ff::Field, Group, GroupEncoding};
|
|
||||||
|
|
||||||
use transcript::{Transcript, RecommendedTranscript};
|
use transcript::{Transcript, RecommendedTranscript};
|
||||||
|
use frost::{curve::Ed25519, FrostError, FrostView, algorithm::Algorithm};
|
||||||
use dalek_ff_group as dfg;
|
use dalek_ff_group as dfg;
|
||||||
use dleq::DLEqProof;
|
|
||||||
use frost::{
|
|
||||||
dkg::lagrange,
|
|
||||||
curve::Ed25519,
|
|
||||||
Participant, FrostError, ThresholdKeys, ThresholdView,
|
|
||||||
algorithm::{WriteAddendum, Algorithm},
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::ringct::{
|
use crate::{
|
||||||
hash_to_point,
|
frost::{MultisigError, write_dleq, read_dleq},
|
||||||
clsag::{ClsagInput, Clsag},
|
ringct::{hash_to_point, clsag::{ClsagInput, Clsag}}
|
||||||
};
|
};
|
||||||
|
|
||||||
fn dleq_transcript() -> RecommendedTranscript {
|
|
||||||
RecommendedTranscript::new(b"monero_key_image_dleq")
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ClsagInput {
|
impl ClsagInput {
|
||||||
fn transcript<T: Transcript>(&self, transcript: &mut T) {
|
fn transcript<T: Transcript>(&self, transcript: &mut T) {
|
||||||
// Doesn't domain separate as this is considered part of the larger CLSAG proof
|
// Doesn't domain separate as this is considered part of the larger CLSAG proof
|
||||||
|
|
||||||
// Ring index
|
// Ring index
|
||||||
transcript.append_message(b"real_spend", [self.decoys.i]);
|
transcript.append_message(b"ring_index", &[self.decoys.i]);
|
||||||
|
|
||||||
// Ring
|
// Ring
|
||||||
for (i, pair) in self.decoys.ring.iter().enumerate() {
|
let mut ring = vec![];
|
||||||
|
for pair in &self.decoys.ring {
|
||||||
// Doesn't include global output indexes as CLSAG doesn't care and won't be affected by it
|
// Doesn't include global output indexes as CLSAG doesn't care and won't be affected by it
|
||||||
// They're just a unreliable reference to this data which will be included in the message
|
// They're just a unreliable reference to this data which will be included in the message
|
||||||
// if in use
|
// if in use
|
||||||
transcript.append_message(b"member", [u8::try_from(i).expect("ring size exceeded 255")]);
|
ring.extend(&pair[0].compress().to_bytes());
|
||||||
transcript.append_message(b"key", pair[0].compress().to_bytes());
|
ring.extend(&pair[1].compress().to_bytes());
|
||||||
transcript.append_message(b"commitment", pair[1].compress().to_bytes())
|
|
||||||
}
|
}
|
||||||
|
transcript.append_message(b"ring", &ring);
|
||||||
|
|
||||||
// Doesn't include the commitment's parts as the above ring + index includes the commitment
|
// Doesn't include the commitment's parts as the above ring + index includes the commitment
|
||||||
// The only potential malleability would be if the G/H relationship is known breaking the
|
// The only potential malleability would be if the G/H relationship is known breaking the
|
||||||
@@ -53,11 +46,10 @@ impl ClsagInput {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// CLSAG input and the mask to use for it.
|
#[derive(Clone, Debug)]
|
||||||
#[derive(Clone, Debug, Zeroize, ZeroizeOnDrop)]
|
|
||||||
pub struct ClsagDetails {
|
pub struct ClsagDetails {
|
||||||
input: ClsagInput,
|
input: ClsagInput,
|
||||||
mask: Scalar,
|
mask: Scalar
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ClsagDetails {
|
impl ClsagDetails {
|
||||||
@@ -66,64 +58,54 @@ impl ClsagDetails {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Addendum produced during the FROST signing process with relevant data.
|
|
||||||
#[derive(Clone, PartialEq, Eq, Zeroize, Debug)]
|
|
||||||
pub struct ClsagAddendum {
|
|
||||||
pub(crate) key_image: dfg::EdwardsPoint,
|
|
||||||
dleq: DLEqProof<dfg::EdwardsPoint>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WriteAddendum for ClsagAddendum {
|
|
||||||
fn write<W: Write>(&self, writer: &mut W) -> io::Result<()> {
|
|
||||||
writer.write_all(self.key_image.compress().to_bytes().as_ref())?;
|
|
||||||
self.dleq.write(writer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
#[derive(Clone, PartialEq, Debug)]
|
||||||
struct Interim {
|
struct Interim {
|
||||||
p: Scalar,
|
p: Scalar,
|
||||||
c: Scalar,
|
c: Scalar,
|
||||||
|
|
||||||
clsag: Clsag,
|
clsag: Clsag,
|
||||||
pseudo_out: EdwardsPoint,
|
pseudo_out: EdwardsPoint
|
||||||
}
|
}
|
||||||
|
|
||||||
/// FROST algorithm for producing a CLSAG signature.
|
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct ClsagMultisig {
|
pub struct ClsagMultisig {
|
||||||
transcript: RecommendedTranscript,
|
transcript: RecommendedTranscript,
|
||||||
|
|
||||||
pub(crate) H: EdwardsPoint,
|
H: EdwardsPoint,
|
||||||
// Merged here as CLSAG needs it, passing it would be a mess, yet having it beforehand requires
|
// Merged here as CLSAG needs it, passing it would be a mess, yet having it beforehand requires a round
|
||||||
// an extra round
|
|
||||||
image: EdwardsPoint,
|
image: EdwardsPoint,
|
||||||
|
|
||||||
details: Arc<RwLock<Option<ClsagDetails>>>,
|
details: Arc<RwLock<Option<ClsagDetails>>>,
|
||||||
|
|
||||||
msg: Option<[u8; 32]>,
|
msg: Option<[u8; 32]>,
|
||||||
interim: Option<Interim>,
|
interim: Option<Interim>
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ClsagMultisig {
|
impl ClsagMultisig {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
transcript: RecommendedTranscript,
|
transcript: RecommendedTranscript,
|
||||||
output_key: EdwardsPoint,
|
output_key: EdwardsPoint,
|
||||||
details: Arc<RwLock<Option<ClsagDetails>>>,
|
details: Arc<RwLock<Option<ClsagDetails>>>
|
||||||
) -> ClsagMultisig {
|
) -> Result<ClsagMultisig, MultisigError> {
|
||||||
ClsagMultisig {
|
Ok(
|
||||||
transcript,
|
ClsagMultisig {
|
||||||
|
transcript,
|
||||||
|
|
||||||
H: hash_to_point(&output_key),
|
H: hash_to_point(output_key),
|
||||||
image: EdwardsPoint::identity(),
|
image: EdwardsPoint::identity(),
|
||||||
|
|
||||||
details,
|
details,
|
||||||
|
|
||||||
msg: None,
|
msg: None,
|
||||||
interim: None,
|
interim: None
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn serialized_len() -> usize {
|
||||||
|
32 + (2 * 32)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn input(&self) -> ClsagInput {
|
fn input(&self) -> ClsagInput {
|
||||||
@@ -135,23 +117,8 @@ impl ClsagMultisig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn add_key_image_share(
|
|
||||||
image: &mut EdwardsPoint,
|
|
||||||
generator: EdwardsPoint,
|
|
||||||
offset: Scalar,
|
|
||||||
included: &[Participant],
|
|
||||||
participant: Participant,
|
|
||||||
share: EdwardsPoint,
|
|
||||||
) {
|
|
||||||
if image.is_identity().into() {
|
|
||||||
*image = generator * offset;
|
|
||||||
}
|
|
||||||
*image += share * lagrange::<dfg::Scalar>(participant, included).0;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Algorithm<Ed25519> for ClsagMultisig {
|
impl Algorithm<Ed25519> for ClsagMultisig {
|
||||||
type Transcript = RecommendedTranscript;
|
type Transcript = RecommendedTranscript;
|
||||||
type Addendum = ClsagAddendum;
|
|
||||||
type Signature = (Clsag, EdwardsPoint);
|
type Signature = (Clsag, EdwardsPoint);
|
||||||
|
|
||||||
fn nonces(&self) -> Vec<Vec<dfg::EdwardsPoint>> {
|
fn nonces(&self) -> Vec<Vec<dfg::EdwardsPoint>> {
|
||||||
@@ -161,70 +128,35 @@ impl Algorithm<Ed25519> for ClsagMultisig {
|
|||||||
fn preprocess_addendum<R: RngCore + CryptoRng>(
|
fn preprocess_addendum<R: RngCore + CryptoRng>(
|
||||||
&mut self,
|
&mut self,
|
||||||
rng: &mut R,
|
rng: &mut R,
|
||||||
keys: &ThresholdKeys<Ed25519>,
|
view: &FrostView<Ed25519>
|
||||||
) -> ClsagAddendum {
|
) -> Vec<u8> {
|
||||||
ClsagAddendum {
|
let mut serialized = Vec::with_capacity(Self::serialized_len());
|
||||||
key_image: dfg::EdwardsPoint(self.H) * keys.secret_share().deref(),
|
serialized.extend((view.secret_share().0 * self.H).compress().to_bytes());
|
||||||
dleq: DLEqProof::prove(
|
serialized.extend(write_dleq(rng, self.H, view.secret_share().0));
|
||||||
rng,
|
serialized
|
||||||
// Doesn't take in a larger transcript object due to the usage of this
|
|
||||||
// Every prover would immediately write their own DLEq proof, when they can only do so in
|
|
||||||
// the proper order if they want to reach consensus
|
|
||||||
// It'd be a poor API to have CLSAG define a new transcript solely to pass here, just to
|
|
||||||
// try to merge later in some form, when it should instead just merge xH (as it does)
|
|
||||||
&mut dleq_transcript(),
|
|
||||||
&[dfg::EdwardsPoint::generator(), dfg::EdwardsPoint(self.H)],
|
|
||||||
keys.secret_share(),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_addendum<R: Read>(&self, reader: &mut R) -> io::Result<ClsagAddendum> {
|
fn process_addendum<Re: Read>(
|
||||||
let mut bytes = [0; 32];
|
|
||||||
reader.read_exact(&mut bytes)?;
|
|
||||||
// dfg ensures the point is torsion free
|
|
||||||
let xH = Option::<dfg::EdwardsPoint>::from(dfg::EdwardsPoint::from_bytes(&bytes))
|
|
||||||
.ok_or_else(|| io::Error::other("invalid key image"))?;
|
|
||||||
// Ensure this is a canonical point
|
|
||||||
if xH.to_bytes() != bytes {
|
|
||||||
Err(io::Error::other("non-canonical key image"))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(ClsagAddendum { key_image: xH, dleq: DLEqProof::<dfg::EdwardsPoint>::read(reader)? })
|
|
||||||
}
|
|
||||||
|
|
||||||
fn process_addendum(
|
|
||||||
&mut self,
|
&mut self,
|
||||||
view: &ThresholdView<Ed25519>,
|
view: &FrostView<Ed25519>,
|
||||||
l: Participant,
|
l: u16,
|
||||||
addendum: ClsagAddendum,
|
serialized: &mut Re
|
||||||
) -> Result<(), FrostError> {
|
) -> Result<(), FrostError> {
|
||||||
if self.image.is_identity().into() {
|
if self.image.is_identity().into() {
|
||||||
self.transcript.domain_separate(b"CLSAG");
|
self.transcript.domain_separate(b"CLSAG");
|
||||||
self.input().transcript(&mut self.transcript);
|
self.input().transcript(&mut self.transcript);
|
||||||
self.transcript.append_message(b"mask", self.mask().to_bytes());
|
self.transcript.append_message(b"mask", &self.mask().to_bytes());
|
||||||
}
|
}
|
||||||
|
|
||||||
self.transcript.append_message(b"participant", l.to_bytes());
|
self.transcript.append_message(b"participant", &l.to_be_bytes());
|
||||||
|
let image = read_dleq(
|
||||||
addendum
|
serialized,
|
||||||
.dleq
|
|
||||||
.verify(
|
|
||||||
&mut dleq_transcript(),
|
|
||||||
&[dfg::EdwardsPoint::generator(), dfg::EdwardsPoint(self.H)],
|
|
||||||
&[view.original_verification_share(l), addendum.key_image],
|
|
||||||
)
|
|
||||||
.map_err(|_| FrostError::InvalidPreprocess(l))?;
|
|
||||||
|
|
||||||
self.transcript.append_message(b"key_image_share", addendum.key_image.compress().to_bytes());
|
|
||||||
add_key_image_share(
|
|
||||||
&mut self.image,
|
|
||||||
self.H,
|
self.H,
|
||||||
view.offset().0,
|
|
||||||
view.included(),
|
|
||||||
l,
|
l,
|
||||||
addendum.key_image.0,
|
view.verification_share(l)
|
||||||
);
|
).map_err(|_| FrostError::InvalidCommitment(l))?.0;
|
||||||
|
self.transcript.append_message(b"key_image_share", image.compress().to_bytes().as_ref());
|
||||||
|
self.image += image;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -235,17 +167,17 @@ impl Algorithm<Ed25519> for ClsagMultisig {
|
|||||||
|
|
||||||
fn sign_share(
|
fn sign_share(
|
||||||
&mut self,
|
&mut self,
|
||||||
view: &ThresholdView<Ed25519>,
|
view: &FrostView<Ed25519>,
|
||||||
nonce_sums: &[Vec<dfg::EdwardsPoint>],
|
nonce_sums: &[Vec<dfg::EdwardsPoint>],
|
||||||
nonces: Vec<Zeroizing<dfg::Scalar>>,
|
nonces: &[dfg::Scalar],
|
||||||
msg: &[u8],
|
msg: &[u8]
|
||||||
) -> dfg::Scalar {
|
) -> dfg::Scalar {
|
||||||
// Use the transcript to get a seeded random number generator
|
// Use the transcript to get a seeded random number generator
|
||||||
// The transcript contains private data, preventing passive adversaries from recreating this
|
// The transcript contains private data, preventing passive adversaries from recreating this
|
||||||
// process even if they have access to commitments (specifically, the ring index being signed
|
// process even if they have access to commitments (specifically, the ring index being signed
|
||||||
// for, along with the mask which should not only require knowing the shared keys yet also the
|
// for, along with the mask which should not only require knowing the shared keys yet also the
|
||||||
// input commitment masks)
|
// input commitment masks)
|
||||||
let mut rng = ChaCha20Rng::from_seed(self.transcript.rng_seed(b"decoy_responses"));
|
let mut rng = ChaCha12Rng::from_seed(self.transcript.rng_seed(b"decoy_responses"));
|
||||||
|
|
||||||
self.msg = Some(msg.try_into().expect("CLSAG message should be 32-bytes"));
|
self.msg = Some(msg.try_into().expect("CLSAG message should be 32-bytes"));
|
||||||
|
|
||||||
@@ -255,13 +187,15 @@ impl Algorithm<Ed25519> for ClsagMultisig {
|
|||||||
&self.image,
|
&self.image,
|
||||||
&self.input(),
|
&self.input(),
|
||||||
self.mask(),
|
self.mask(),
|
||||||
self.msg.as_ref().unwrap(),
|
&self.msg.as_ref().unwrap(),
|
||||||
nonce_sums[0][0].0,
|
nonce_sums[0][0].0,
|
||||||
nonce_sums[0][1].0,
|
nonce_sums[0][1].0
|
||||||
);
|
);
|
||||||
self.interim = Some(Interim { p, c, clsag, pseudo_out });
|
self.interim = Some(Interim { p, c, clsag, pseudo_out });
|
||||||
|
|
||||||
(-(dfg::Scalar(p) * view.secret_share().deref())) + nonces[0].deref()
|
let share = dfg::Scalar(nonces[0].0 - (p * view.secret_share().0));
|
||||||
|
|
||||||
|
share
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
@@ -269,36 +203,32 @@ impl Algorithm<Ed25519> for ClsagMultisig {
|
|||||||
&self,
|
&self,
|
||||||
_: dfg::EdwardsPoint,
|
_: dfg::EdwardsPoint,
|
||||||
_: &[Vec<dfg::EdwardsPoint>],
|
_: &[Vec<dfg::EdwardsPoint>],
|
||||||
sum: dfg::Scalar,
|
sum: dfg::Scalar
|
||||||
) -> Option<Self::Signature> {
|
) -> Option<Self::Signature> {
|
||||||
let interim = self.interim.as_ref().unwrap();
|
let interim = self.interim.as_ref().unwrap();
|
||||||
let mut clsag = interim.clsag.clone();
|
let mut clsag = interim.clsag.clone();
|
||||||
clsag.s[usize::from(self.input().decoys.i)] = sum.0 - interim.c;
|
clsag.s[usize::from(self.input().decoys.i)] = sum.0 - interim.c;
|
||||||
if clsag
|
if clsag.verify(
|
||||||
.verify(
|
&self.input().decoys.ring,
|
||||||
&self.input().decoys.ring,
|
&self.image,
|
||||||
&self.image,
|
&interim.pseudo_out,
|
||||||
&interim.pseudo_out,
|
&self.msg.as_ref().unwrap()
|
||||||
self.msg.as_ref().unwrap(),
|
).is_ok() {
|
||||||
)
|
|
||||||
.is_ok()
|
|
||||||
{
|
|
||||||
return Some((clsag, interim.pseudo_out));
|
return Some((clsag, interim.pseudo_out));
|
||||||
}
|
}
|
||||||
None
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
fn verify_share(
|
fn verify_share(
|
||||||
&self,
|
&self,
|
||||||
verification_share: dfg::EdwardsPoint,
|
verification_share: dfg::EdwardsPoint,
|
||||||
nonces: &[Vec<dfg::EdwardsPoint>],
|
nonces: &[Vec<dfg::EdwardsPoint>],
|
||||||
share: dfg::Scalar,
|
share: dfg::Scalar,
|
||||||
) -> Result<Vec<(dfg::Scalar, dfg::EdwardsPoint)>, ()> {
|
) -> bool {
|
||||||
let interim = self.interim.as_ref().unwrap();
|
let interim = self.interim.as_ref().unwrap();
|
||||||
Ok(vec![
|
return (&share.0 * &ED25519_BASEPOINT_TABLE) == (
|
||||||
(share, dfg::EdwardsPoint::generator()),
|
nonces[0][0].0 - (interim.p * verification_share.0)
|
||||||
(dfg::Scalar(interim.p), verification_share),
|
);
|
||||||
(-dfg::Scalar::ONE, nonces[0][0]),
|
|
||||||
])
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,67 @@
|
|||||||
use curve25519_dalek::edwards::EdwardsPoint;
|
use subtle::ConditionallySelectable;
|
||||||
|
|
||||||
pub use monero_generators::{hash_to_point as raw_hash_to_point};
|
use curve25519_dalek::edwards::{CompressedEdwardsY, EdwardsPoint};
|
||||||
|
|
||||||
/// Monero's hash to point function, as named `ge_fromfe_frombytes_vartime`.
|
use group::ff::{Field, PrimeField};
|
||||||
pub fn hash_to_point(key: &EdwardsPoint) -> EdwardsPoint {
|
use dalek_ff_group::field::FieldElement;
|
||||||
raw_hash_to_point(key.compress().to_bytes())
|
|
||||||
|
use crate::hash;
|
||||||
|
|
||||||
|
pub fn hash_to_point(point: EdwardsPoint) -> EdwardsPoint {
|
||||||
|
let mut bytes = point.compress().to_bytes();
|
||||||
|
unsafe {
|
||||||
|
#[link(name = "wrapper")]
|
||||||
|
extern "C" {
|
||||||
|
fn c_hash_to_point(point: *const u8);
|
||||||
|
}
|
||||||
|
|
||||||
|
c_hash_to_point(bytes.as_mut_ptr());
|
||||||
|
}
|
||||||
|
CompressedEdwardsY::from_slice(&bytes).decompress().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
// This works without issue. It's also 140 times slower (@ 3.5ms), and despite checking it passes
|
||||||
|
// for all branches, there still could be *some* discrepancy somewhere. There's no reason to use it
|
||||||
|
// unless we're trying to purge that section of the C static library, which we aren't right now
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub(crate) fn rust_hash_to_point(key: EdwardsPoint) -> EdwardsPoint {
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
let A = FieldElement::from(486662u64);
|
||||||
|
|
||||||
|
let v = FieldElement::from_square(hash(&key.compress().to_bytes())).double();
|
||||||
|
let w = v + FieldElement::one();
|
||||||
|
let x = w.square() + (-A.square() * v);
|
||||||
|
|
||||||
|
// This isn't the complete X, yet its initial value
|
||||||
|
// We don't calculate the full X, and instead solely calculate Y, letting dalek reconstruct X
|
||||||
|
// While inefficient, it solves API boundaries and reduces the amount of work done here
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
let X = {
|
||||||
|
let u = w;
|
||||||
|
let v = x;
|
||||||
|
let v3 = v * v * v;
|
||||||
|
let uv3 = u * v3;
|
||||||
|
let v7 = v3 * v3 * v;
|
||||||
|
let uv7 = u * v7;
|
||||||
|
uv3 * uv7.pow((-FieldElement::from(5u8)) * FieldElement::from(8u8).invert().unwrap())
|
||||||
|
};
|
||||||
|
let x = X.square() * x;
|
||||||
|
|
||||||
|
let y = w - x;
|
||||||
|
let non_zero_0 = !y.is_zero();
|
||||||
|
let y_if_non_zero_0 = w + x;
|
||||||
|
let sign = non_zero_0 & (!y_if_non_zero_0.is_zero());
|
||||||
|
|
||||||
|
let mut z = -A;
|
||||||
|
z *= FieldElement::conditional_select(&v, &FieldElement::from(1u8), sign);
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
let Z = z + w;
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
let mut Y = z - w;
|
||||||
|
|
||||||
|
Y = Y * Z.invert().unwrap();
|
||||||
|
let mut bytes = Y.to_repr();
|
||||||
|
bytes[31] |= sign.unwrap_u8() << 7;
|
||||||
|
|
||||||
|
CompressedEdwardsY(bytes).decompress().unwrap().mul_by_cofactor()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,213 +0,0 @@
|
|||||||
use std_shims::{
|
|
||||||
vec::Vec,
|
|
||||||
io::{self, Read, Write},
|
|
||||||
};
|
|
||||||
|
|
||||||
use zeroize::Zeroize;
|
|
||||||
|
|
||||||
use curve25519_dalek::{traits::IsIdentity, Scalar, EdwardsPoint};
|
|
||||||
|
|
||||||
use monero_generators::H;
|
|
||||||
|
|
||||||
use crate::{hash_to_scalar, ringct::hash_to_point, serialize::*};
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
|
||||||
#[cfg_attr(feature = "std", derive(thiserror::Error))]
|
|
||||||
pub enum MlsagError {
|
|
||||||
#[cfg_attr(feature = "std", error("invalid ring"))]
|
|
||||||
InvalidRing,
|
|
||||||
#[cfg_attr(feature = "std", error("invalid amount of key images"))]
|
|
||||||
InvalidAmountOfKeyImages,
|
|
||||||
#[cfg_attr(feature = "std", error("invalid ss"))]
|
|
||||||
InvalidSs,
|
|
||||||
#[cfg_attr(feature = "std", error("key image was identity"))]
|
|
||||||
IdentityKeyImage,
|
|
||||||
#[cfg_attr(feature = "std", error("invalid ci"))]
|
|
||||||
InvalidCi,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
|
|
||||||
pub struct RingMatrix {
|
|
||||||
matrix: Vec<Vec<EdwardsPoint>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RingMatrix {
|
|
||||||
pub fn new(matrix: Vec<Vec<EdwardsPoint>>) -> Result<Self, MlsagError> {
|
|
||||||
if matrix.is_empty() {
|
|
||||||
Err(MlsagError::InvalidRing)?;
|
|
||||||
}
|
|
||||||
for member in &matrix {
|
|
||||||
if member.is_empty() || (member.len() != matrix[0].len()) {
|
|
||||||
Err(MlsagError::InvalidRing)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(RingMatrix { matrix })
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Construct a ring matrix for an individual output.
|
|
||||||
pub fn individual(
|
|
||||||
ring: &[[EdwardsPoint; 2]],
|
|
||||||
pseudo_out: EdwardsPoint,
|
|
||||||
) -> Result<Self, MlsagError> {
|
|
||||||
let mut matrix = Vec::with_capacity(ring.len());
|
|
||||||
for ring_member in ring {
|
|
||||||
matrix.push(vec![ring_member[0], ring_member[1] - pseudo_out]);
|
|
||||||
}
|
|
||||||
RingMatrix::new(matrix)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn iter(&self) -> impl Iterator<Item = &[EdwardsPoint]> {
|
|
||||||
self.matrix.iter().map(AsRef::as_ref)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return the amount of members in the ring.
|
|
||||||
pub fn members(&self) -> usize {
|
|
||||||
self.matrix.len()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the length of a ring member.
|
|
||||||
///
|
|
||||||
/// A ring member is a vector of points for which the signer knows all of the discrete logarithms
|
|
||||||
/// of.
|
|
||||||
pub fn member_len(&self) -> usize {
|
|
||||||
// this is safe to do as the constructors don't allow empty rings
|
|
||||||
self.matrix[0].len()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
|
|
||||||
pub struct Mlsag {
|
|
||||||
pub ss: Vec<Vec<Scalar>>,
|
|
||||||
pub cc: Scalar,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Mlsag {
|
|
||||||
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
|
||||||
for ss in &self.ss {
|
|
||||||
write_raw_vec(write_scalar, ss, w)?;
|
|
||||||
}
|
|
||||||
write_scalar(&self.cc, w)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn read<R: Read>(mixins: usize, ss_2_elements: usize, r: &mut R) -> io::Result<Mlsag> {
|
|
||||||
Ok(Mlsag {
|
|
||||||
ss: (0 .. mixins)
|
|
||||||
.map(|_| read_raw_vec(read_scalar, ss_2_elements, r))
|
|
||||||
.collect::<Result<_, _>>()?,
|
|
||||||
cc: read_scalar(r)?,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn verify(
|
|
||||||
&self,
|
|
||||||
msg: &[u8; 32],
|
|
||||||
ring: &RingMatrix,
|
|
||||||
key_images: &[EdwardsPoint],
|
|
||||||
) -> Result<(), MlsagError> {
|
|
||||||
// Mlsag allows for layers to not need linkability, hence they don't need key images
|
|
||||||
// Monero requires that there is always only 1 non-linkable layer - the amount commitments.
|
|
||||||
if ring.member_len() != (key_images.len() + 1) {
|
|
||||||
Err(MlsagError::InvalidAmountOfKeyImages)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut buf = Vec::with_capacity(6 * 32);
|
|
||||||
buf.extend_from_slice(msg);
|
|
||||||
|
|
||||||
let mut ci = self.cc;
|
|
||||||
|
|
||||||
// This is an iterator over the key images as options with an added entry of `None` at the
|
|
||||||
// end for the non-linkable layer
|
|
||||||
let key_images_iter = key_images.iter().map(|ki| Some(*ki)).chain(core::iter::once(None));
|
|
||||||
|
|
||||||
if ring.matrix.len() != self.ss.len() {
|
|
||||||
Err(MlsagError::InvalidSs)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (ring_member, ss) in ring.iter().zip(&self.ss) {
|
|
||||||
if ring_member.len() != ss.len() {
|
|
||||||
Err(MlsagError::InvalidSs)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
for ((ring_member_entry, s), ki) in ring_member.iter().zip(ss).zip(key_images_iter.clone()) {
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
let L = EdwardsPoint::vartime_double_scalar_mul_basepoint(&ci, ring_member_entry, s);
|
|
||||||
|
|
||||||
buf.extend_from_slice(ring_member_entry.compress().as_bytes());
|
|
||||||
buf.extend_from_slice(L.compress().as_bytes());
|
|
||||||
|
|
||||||
// Not all dimensions need to be linkable, e.g. commitments, and only linkable layers need
|
|
||||||
// to have key images.
|
|
||||||
if let Some(ki) = ki {
|
|
||||||
if ki.is_identity() {
|
|
||||||
Err(MlsagError::IdentityKeyImage)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
let R = (s * hash_to_point(ring_member_entry)) + (ci * ki);
|
|
||||||
buf.extend_from_slice(R.compress().as_bytes());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ci = hash_to_scalar(&buf);
|
|
||||||
// keep the msg in the buffer.
|
|
||||||
buf.drain(msg.len() ..);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ci != self.cc {
|
|
||||||
Err(MlsagError::InvalidCi)?
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// An aggregate ring matrix builder, usable to set up the ring matrix to prove/verify an aggregate
|
|
||||||
/// MLSAG signature.
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug, Zeroize)]
|
|
||||||
pub struct AggregateRingMatrixBuilder {
|
|
||||||
key_ring: Vec<Vec<EdwardsPoint>>,
|
|
||||||
amounts_ring: Vec<EdwardsPoint>,
|
|
||||||
sum_out: EdwardsPoint,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AggregateRingMatrixBuilder {
|
|
||||||
/// Create a new AggregateRingMatrixBuilder.
|
|
||||||
///
|
|
||||||
/// Takes in the transaction's outputs; commitments and fee.
|
|
||||||
pub fn new(commitments: &[EdwardsPoint], fee: u64) -> Self {
|
|
||||||
AggregateRingMatrixBuilder {
|
|
||||||
key_ring: vec![],
|
|
||||||
amounts_ring: vec![],
|
|
||||||
sum_out: commitments.iter().sum::<EdwardsPoint>() + (H() * Scalar::from(fee)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Push a ring of [output key, commitment] to the matrix.
|
|
||||||
pub fn push_ring(&mut self, ring: &[[EdwardsPoint; 2]]) -> Result<(), MlsagError> {
|
|
||||||
if self.key_ring.is_empty() {
|
|
||||||
self.key_ring = vec![vec![]; ring.len()];
|
|
||||||
// Now that we know the length of the ring, fill the `amounts_ring`.
|
|
||||||
self.amounts_ring = vec![-self.sum_out; ring.len()];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (self.amounts_ring.len() != ring.len()) || ring.is_empty() {
|
|
||||||
// All the rings in an aggregate matrix must be the same length.
|
|
||||||
return Err(MlsagError::InvalidRing);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (i, ring_member) in ring.iter().enumerate() {
|
|
||||||
self.key_ring[i].push(ring_member[0]);
|
|
||||||
self.amounts_ring[i] += ring_member[1]
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Build and return the [`RingMatrix`]
|
|
||||||
pub fn build(mut self) -> Result<RingMatrix, MlsagError> {
|
|
||||||
for (i, amount_commitment) in self.amounts_ring.drain(..).enumerate() {
|
|
||||||
self.key_ring[i].push(amount_commitment);
|
|
||||||
}
|
|
||||||
RingMatrix::new(self.key_ring)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,389 +1,145 @@
|
|||||||
use core::ops::Deref;
|
|
||||||
use std_shims::{
|
|
||||||
vec::Vec,
|
|
||||||
io::{self, Read, Write},
|
|
||||||
};
|
|
||||||
|
|
||||||
use zeroize::{Zeroize, Zeroizing};
|
|
||||||
|
|
||||||
use curve25519_dalek::{constants::ED25519_BASEPOINT_TABLE, scalar::Scalar, edwards::EdwardsPoint};
|
use curve25519_dalek::{constants::ED25519_BASEPOINT_TABLE, scalar::Scalar, edwards::EdwardsPoint};
|
||||||
|
|
||||||
pub(crate) mod hash_to_point;
|
pub(crate) mod hash_to_point;
|
||||||
pub use hash_to_point::{raw_hash_to_point, hash_to_point};
|
pub use hash_to_point::hash_to_point;
|
||||||
|
|
||||||
/// MLSAG struct, along with verifying functionality.
|
|
||||||
pub mod mlsag;
|
|
||||||
/// CLSAG struct, along with signing and verifying functionality.
|
|
||||||
pub mod clsag;
|
pub mod clsag;
|
||||||
/// BorromeanRange struct, along with verifying functionality.
|
|
||||||
pub mod borromean;
|
|
||||||
/// Bulletproofs(+) structs, along with proving and verifying functionality.
|
|
||||||
pub mod bulletproofs;
|
pub mod bulletproofs;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
Protocol,
|
|
||||||
serialize::*,
|
serialize::*,
|
||||||
ringct::{mlsag::Mlsag, clsag::Clsag, borromean::BorromeanRange, bulletproofs::Bulletproofs},
|
ringct::{clsag::Clsag, bulletproofs::Bulletproofs}
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Generate a key image for a given key. Defined as `x * hash_to_point(xG)`.
|
pub fn generate_key_image(secret: Scalar) -> EdwardsPoint {
|
||||||
pub fn generate_key_image(secret: &Zeroizing<Scalar>) -> EdwardsPoint {
|
secret * hash_to_point(&secret * &ED25519_BASEPOINT_TABLE)
|
||||||
hash_to_point(&(ED25519_BASEPOINT_TABLE * secret.deref())) * secret.deref()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
#[derive(Clone, PartialEq, Debug)]
|
||||||
pub enum EncryptedAmount {
|
|
||||||
Original { mask: [u8; 32], amount: [u8; 32] },
|
|
||||||
Compact { amount: [u8; 8] },
|
|
||||||
}
|
|
||||||
|
|
||||||
impl EncryptedAmount {
|
|
||||||
pub fn read<R: Read>(compact: bool, r: &mut R) -> io::Result<EncryptedAmount> {
|
|
||||||
Ok(if !compact {
|
|
||||||
EncryptedAmount::Original { mask: read_bytes(r)?, amount: read_bytes(r)? }
|
|
||||||
} else {
|
|
||||||
EncryptedAmount::Compact { amount: read_bytes(r)? }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
|
||||||
match self {
|
|
||||||
EncryptedAmount::Original { mask, amount } => {
|
|
||||||
w.write_all(mask)?;
|
|
||||||
w.write_all(amount)
|
|
||||||
}
|
|
||||||
EncryptedAmount::Compact { amount } => w.write_all(amount),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
|
|
||||||
pub enum RctType {
|
|
||||||
/// No RCT proofs.
|
|
||||||
Null,
|
|
||||||
/// One MLSAG for multiple inputs and Borromean range proofs (RCTTypeFull).
|
|
||||||
MlsagAggregate,
|
|
||||||
// One MLSAG for each input and a Borromean range proof (RCTTypeSimple).
|
|
||||||
MlsagIndividual,
|
|
||||||
// One MLSAG for each input and a Bulletproof (RCTTypeBulletproof).
|
|
||||||
Bulletproofs,
|
|
||||||
/// One MLSAG for each input and a Bulletproof, yet starting to use EncryptedAmount::Compact
|
|
||||||
/// (RCTTypeBulletproof2).
|
|
||||||
BulletproofsCompactAmount,
|
|
||||||
/// One CLSAG for each input and a Bulletproof (RCTTypeCLSAG).
|
|
||||||
Clsag,
|
|
||||||
/// One CLSAG for each input and a Bulletproof+ (RCTTypeBulletproofPlus).
|
|
||||||
BulletproofsPlus,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RctType {
|
|
||||||
pub fn to_byte(self) -> u8 {
|
|
||||||
match self {
|
|
||||||
RctType::Null => 0,
|
|
||||||
RctType::MlsagAggregate => 1,
|
|
||||||
RctType::MlsagIndividual => 2,
|
|
||||||
RctType::Bulletproofs => 3,
|
|
||||||
RctType::BulletproofsCompactAmount => 4,
|
|
||||||
RctType::Clsag => 5,
|
|
||||||
RctType::BulletproofsPlus => 6,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn from_byte(byte: u8) -> Option<Self> {
|
|
||||||
Some(match byte {
|
|
||||||
0 => RctType::Null,
|
|
||||||
1 => RctType::MlsagAggregate,
|
|
||||||
2 => RctType::MlsagIndividual,
|
|
||||||
3 => RctType::Bulletproofs,
|
|
||||||
4 => RctType::BulletproofsCompactAmount,
|
|
||||||
5 => RctType::Clsag,
|
|
||||||
6 => RctType::BulletproofsPlus,
|
|
||||||
_ => None?,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn compact_encrypted_amounts(&self) -> bool {
|
|
||||||
match self {
|
|
||||||
RctType::Null |
|
|
||||||
RctType::MlsagAggregate |
|
|
||||||
RctType::MlsagIndividual |
|
|
||||||
RctType::Bulletproofs => false,
|
|
||||||
RctType::BulletproofsCompactAmount | RctType::Clsag | RctType::BulletproofsPlus => true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
|
||||||
pub struct RctBase {
|
pub struct RctBase {
|
||||||
pub fee: u64,
|
pub fee: u64,
|
||||||
pub pseudo_outs: Vec<EdwardsPoint>,
|
pub ecdh_info: Vec<[u8; 8]>,
|
||||||
pub encrypted_amounts: Vec<EncryptedAmount>,
|
pub commitments: Vec<EdwardsPoint>
|
||||||
pub commitments: Vec<EdwardsPoint>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RctBase {
|
impl RctBase {
|
||||||
pub(crate) fn fee_weight(outputs: usize, fee: u64) -> usize {
|
pub(crate) fn fee_weight(outputs: usize) -> usize {
|
||||||
// 1 byte for the RCT signature type
|
1 + 8 + (outputs * (8 + 32))
|
||||||
1 + (outputs * (8 + 32)) + varint_len(fee)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn write<W: Write>(&self, w: &mut W, rct_type: RctType) -> io::Result<()> {
|
pub fn serialize<W: std::io::Write>(&self, w: &mut W, rct_type: u8) -> std::io::Result<()> {
|
||||||
w.write_all(&[rct_type.to_byte()])?;
|
w.write_all(&[rct_type])?;
|
||||||
match rct_type {
|
match rct_type {
|
||||||
RctType::Null => Ok(()),
|
0 => Ok(()),
|
||||||
_ => {
|
5 => {
|
||||||
write_varint(&self.fee, w)?;
|
write_varint(&self.fee, w)?;
|
||||||
if rct_type == RctType::MlsagIndividual {
|
for ecdh in &self.ecdh_info {
|
||||||
write_raw_vec(write_point, &self.pseudo_outs, w)?;
|
w.write_all(ecdh)?;
|
||||||
}
|
|
||||||
for encrypted_amount in &self.encrypted_amounts {
|
|
||||||
encrypted_amount.write(w)?;
|
|
||||||
}
|
}
|
||||||
write_raw_vec(write_point, &self.commitments, w)
|
write_raw_vec(write_point, &self.commitments, w)
|
||||||
}
|
},
|
||||||
|
_ => panic!("Serializing unknown RctType's Base")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn read<R: Read>(inputs: usize, outputs: usize, r: &mut R) -> io::Result<(RctBase, RctType)> {
|
pub fn deserialize<R: std::io::Read>(outputs: usize, r: &mut R) -> std::io::Result<(RctBase, u8)> {
|
||||||
let rct_type =
|
let mut rct_type = [0];
|
||||||
RctType::from_byte(read_byte(r)?).ok_or_else(|| io::Error::other("invalid RCT type"))?;
|
r.read_exact(&mut rct_type)?;
|
||||||
|
|
||||||
match rct_type {
|
|
||||||
RctType::Null | RctType::MlsagAggregate | RctType::MlsagIndividual => {}
|
|
||||||
RctType::Bulletproofs |
|
|
||||||
RctType::BulletproofsCompactAmount |
|
|
||||||
RctType::Clsag |
|
|
||||||
RctType::BulletproofsPlus => {
|
|
||||||
if outputs == 0 {
|
|
||||||
// Because the Bulletproofs(+) layout must be canonical, there must be 1 Bulletproof if
|
|
||||||
// Bulletproofs are in use
|
|
||||||
// If there are Bulletproofs, there must be a matching amount of outputs, implicitly
|
|
||||||
// banning 0 outputs
|
|
||||||
// Since HF 12 (CLSAG being 13), a 2-output minimum has also been enforced
|
|
||||||
Err(io::Error::other("RCT with Bulletproofs(+) had 0 outputs"))?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok((
|
Ok((
|
||||||
if rct_type == RctType::Null {
|
if rct_type[0] == 0 {
|
||||||
RctBase { fee: 0, pseudo_outs: vec![], encrypted_amounts: vec![], commitments: vec![] }
|
RctBase { fee: 0, ecdh_info: vec![], commitments: vec![] }
|
||||||
} else {
|
} else {
|
||||||
RctBase {
|
RctBase {
|
||||||
fee: read_varint(r)?,
|
fee: read_varint(r)?,
|
||||||
pseudo_outs: if rct_type == RctType::MlsagIndividual {
|
ecdh_info: (0 .. outputs).map(
|
||||||
read_raw_vec(read_point, inputs, r)?
|
|_| { let mut ecdh = [0; 8]; r.read_exact(&mut ecdh).map(|_| ecdh) }
|
||||||
} else {
|
).collect::<Result<_, _>>()?,
|
||||||
vec![]
|
commitments: read_raw_vec(read_point, outputs, r)?
|
||||||
},
|
|
||||||
encrypted_amounts: (0 .. outputs)
|
|
||||||
.map(|_| EncryptedAmount::read(rct_type.compact_encrypted_amounts(), r))
|
|
||||||
.collect::<Result<_, _>>()?,
|
|
||||||
commitments: read_raw_vec(read_point, outputs, r)?,
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
rct_type,
|
rct_type[0]
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
#[derive(Clone, PartialEq, Debug)]
|
||||||
pub enum RctPrunable {
|
pub enum RctPrunable {
|
||||||
Null,
|
Null,
|
||||||
AggregateMlsagBorromean {
|
|
||||||
borromean: Vec<BorromeanRange>,
|
|
||||||
mlsag: Mlsag,
|
|
||||||
},
|
|
||||||
MlsagBorromean {
|
|
||||||
borromean: Vec<BorromeanRange>,
|
|
||||||
mlsags: Vec<Mlsag>,
|
|
||||||
},
|
|
||||||
MlsagBulletproofs {
|
|
||||||
bulletproofs: Bulletproofs,
|
|
||||||
mlsags: Vec<Mlsag>,
|
|
||||||
pseudo_outs: Vec<EdwardsPoint>,
|
|
||||||
},
|
|
||||||
Clsag {
|
Clsag {
|
||||||
bulletproofs: Bulletproofs,
|
bulletproofs: Vec<Bulletproofs>,
|
||||||
clsags: Vec<Clsag>,
|
clsags: Vec<Clsag>,
|
||||||
pseudo_outs: Vec<EdwardsPoint>,
|
pseudo_outs: Vec<EdwardsPoint>
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RctPrunable {
|
impl RctPrunable {
|
||||||
pub(crate) fn fee_weight(protocol: Protocol, inputs: usize, outputs: usize) -> usize {
|
pub fn rct_type(&self) -> u8 {
|
||||||
// 1 byte for number of BPs (technically a VarInt, yet there's always just zero or one)
|
match self {
|
||||||
1 + Bulletproofs::fee_weight(protocol.bp_plus(), outputs) +
|
RctPrunable::Null => 0,
|
||||||
(inputs * (Clsag::fee_weight(protocol.ring_len()) + 32))
|
RctPrunable::Clsag { .. } => 5
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn write<W: Write>(&self, w: &mut W, rct_type: RctType) -> io::Result<()> {
|
pub(crate) fn fee_weight(inputs: usize, outputs: usize) -> usize {
|
||||||
|
1 + Bulletproofs::fee_weight(outputs) + (inputs * (Clsag::fee_weight() + 32))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn serialize<W: std::io::Write>(&self, w: &mut W) -> std::io::Result<()> {
|
||||||
match self {
|
match self {
|
||||||
RctPrunable::Null => Ok(()),
|
RctPrunable::Null => Ok(()),
|
||||||
RctPrunable::AggregateMlsagBorromean { borromean, mlsag } => {
|
|
||||||
write_raw_vec(BorromeanRange::write, borromean, w)?;
|
|
||||||
mlsag.write(w)
|
|
||||||
}
|
|
||||||
RctPrunable::MlsagBorromean { borromean, mlsags } => {
|
|
||||||
write_raw_vec(BorromeanRange::write, borromean, w)?;
|
|
||||||
write_raw_vec(Mlsag::write, mlsags, w)
|
|
||||||
}
|
|
||||||
RctPrunable::MlsagBulletproofs { bulletproofs, mlsags, pseudo_outs } => {
|
|
||||||
if rct_type == RctType::Bulletproofs {
|
|
||||||
w.write_all(&1u32.to_le_bytes())?;
|
|
||||||
} else {
|
|
||||||
w.write_all(&[1])?;
|
|
||||||
}
|
|
||||||
bulletproofs.write(w)?;
|
|
||||||
|
|
||||||
write_raw_vec(Mlsag::write, mlsags, w)?;
|
|
||||||
write_raw_vec(write_point, pseudo_outs, w)
|
|
||||||
}
|
|
||||||
RctPrunable::Clsag { bulletproofs, clsags, pseudo_outs } => {
|
RctPrunable::Clsag { bulletproofs, clsags, pseudo_outs } => {
|
||||||
w.write_all(&[1])?;
|
write_vec(Bulletproofs::serialize, &bulletproofs, w)?;
|
||||||
bulletproofs.write(w)?;
|
write_raw_vec(Clsag::serialize, &clsags, w)?;
|
||||||
|
write_raw_vec(write_point, &pseudo_outs, w)
|
||||||
write_raw_vec(Clsag::write, clsags, w)?;
|
|
||||||
write_raw_vec(write_point, pseudo_outs, w)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn serialize(&self, rct_type: RctType) -> Vec<u8> {
|
pub fn deserialize<R: std::io::Read>(
|
||||||
let mut serialized = vec![];
|
rct_type: u8,
|
||||||
self.write(&mut serialized, rct_type).unwrap();
|
|
||||||
serialized
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn read<R: Read>(
|
|
||||||
rct_type: RctType,
|
|
||||||
decoys: &[usize],
|
decoys: &[usize],
|
||||||
outputs: usize,
|
r: &mut R
|
||||||
r: &mut R,
|
) -> std::io::Result<RctPrunable> {
|
||||||
) -> io::Result<RctPrunable> {
|
Ok(
|
||||||
// While we generally don't bother with misc consensus checks, this affects the safety of
|
match rct_type {
|
||||||
// the below defined rct_type function
|
0 => RctPrunable::Null,
|
||||||
// The exact line preventing zero-input transactions is:
|
5 => RctPrunable::Clsag {
|
||||||
// https://github.com/monero-project/monero/blob/00fd416a99686f0956361d1cd0337fe56e58d4a7/
|
// TODO: Can the amount of outputs be calculated from the BPs for any validly formed TX?
|
||||||
// src/ringct/rctSigs.cpp#L609
|
bulletproofs: read_vec(Bulletproofs::deserialize, r)?,
|
||||||
// And then for RctNull, that's only allowed for miner TXs which require one input of
|
clsags: (0 .. decoys.len()).map(|o| Clsag::deserialize(decoys[o], r)).collect::<Result<_, _>>()?,
|
||||||
// Input::Gen
|
pseudo_outs: read_raw_vec(read_point, decoys.len(), r)?
|
||||||
if decoys.is_empty() {
|
|
||||||
Err(io::Error::other("transaction had no inputs"))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(match rct_type {
|
|
||||||
RctType::Null => RctPrunable::Null,
|
|
||||||
RctType::MlsagAggregate => RctPrunable::AggregateMlsagBorromean {
|
|
||||||
borromean: read_raw_vec(BorromeanRange::read, outputs, r)?,
|
|
||||||
mlsag: Mlsag::read(decoys[0], decoys.len() + 1, r)?,
|
|
||||||
},
|
|
||||||
RctType::MlsagIndividual => RctPrunable::MlsagBorromean {
|
|
||||||
borromean: read_raw_vec(BorromeanRange::read, outputs, r)?,
|
|
||||||
mlsags: decoys.iter().map(|d| Mlsag::read(*d, 2, r)).collect::<Result<_, _>>()?,
|
|
||||||
},
|
|
||||||
RctType::Bulletproofs | RctType::BulletproofsCompactAmount => {
|
|
||||||
RctPrunable::MlsagBulletproofs {
|
|
||||||
bulletproofs: {
|
|
||||||
if (if rct_type == RctType::Bulletproofs {
|
|
||||||
u64::from(read_u32(r)?)
|
|
||||||
} else {
|
|
||||||
read_varint(r)?
|
|
||||||
}) != 1
|
|
||||||
{
|
|
||||||
Err(io::Error::other("n bulletproofs instead of one"))?;
|
|
||||||
}
|
|
||||||
Bulletproofs::read(r)?
|
|
||||||
},
|
|
||||||
mlsags: decoys.iter().map(|d| Mlsag::read(*d, 2, r)).collect::<Result<_, _>>()?,
|
|
||||||
pseudo_outs: read_raw_vec(read_point, decoys.len(), r)?,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
RctType::Clsag | RctType::BulletproofsPlus => RctPrunable::Clsag {
|
|
||||||
bulletproofs: {
|
|
||||||
if read_varint::<_, u64>(r)? != 1 {
|
|
||||||
Err(io::Error::other("n bulletproofs instead of one"))?;
|
|
||||||
}
|
|
||||||
(if rct_type == RctType::Clsag { Bulletproofs::read } else { Bulletproofs::read_plus })(
|
|
||||||
r,
|
|
||||||
)?
|
|
||||||
},
|
},
|
||||||
clsags: (0 .. decoys.len()).map(|o| Clsag::read(decoys[o], r)).collect::<Result<_, _>>()?,
|
_ => Err(std::io::Error::new(std::io::ErrorKind::Other, "Tried to deserialize unknown RCT type"))?
|
||||||
pseudo_outs: read_raw_vec(read_point, decoys.len(), r)?,
|
}
|
||||||
},
|
)
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn signature_write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
pub fn signature_serialize<W: std::io::Write>(&self, w: &mut W) -> std::io::Result<()> {
|
||||||
match self {
|
match self {
|
||||||
RctPrunable::Null => panic!("Serializing RctPrunable::Null for a signature"),
|
RctPrunable::Null => panic!("Serializing RctPrunable::Null for a signature"),
|
||||||
RctPrunable::AggregateMlsagBorromean { borromean, .. } |
|
RctPrunable::Clsag { bulletproofs, .. } => bulletproofs.iter().map(|bp| bp.signature_serialize(w)).collect(),
|
||||||
RctPrunable::MlsagBorromean { borromean, .. } => {
|
|
||||||
borromean.iter().try_for_each(|rs| rs.write(w))
|
|
||||||
}
|
|
||||||
RctPrunable::MlsagBulletproofs { bulletproofs, .. } |
|
|
||||||
RctPrunable::Clsag { bulletproofs, .. } => bulletproofs.signature_write(w),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
#[derive(Clone, PartialEq, Debug)]
|
||||||
pub struct RctSignatures {
|
pub struct RctSignatures {
|
||||||
pub base: RctBase,
|
pub base: RctBase,
|
||||||
pub prunable: RctPrunable,
|
pub prunable: RctPrunable
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RctSignatures {
|
impl RctSignatures {
|
||||||
/// RctType for a given RctSignatures struct.
|
pub(crate) fn fee_weight(inputs: usize, outputs: usize) -> usize {
|
||||||
pub fn rct_type(&self) -> RctType {
|
RctBase::fee_weight(outputs) + RctPrunable::fee_weight(inputs, outputs)
|
||||||
match &self.prunable {
|
|
||||||
RctPrunable::Null => RctType::Null,
|
|
||||||
RctPrunable::AggregateMlsagBorromean { .. } => RctType::MlsagAggregate,
|
|
||||||
RctPrunable::MlsagBorromean { .. } => RctType::MlsagIndividual,
|
|
||||||
// RctBase ensures there's at least one output, making the following
|
|
||||||
// inferences guaranteed/expects impossible on any valid RctSignatures
|
|
||||||
RctPrunable::MlsagBulletproofs { .. } => {
|
|
||||||
if matches!(
|
|
||||||
self
|
|
||||||
.base
|
|
||||||
.encrypted_amounts
|
|
||||||
.first()
|
|
||||||
.expect("MLSAG with Bulletproofs didn't have any outputs"),
|
|
||||||
EncryptedAmount::Original { .. }
|
|
||||||
) {
|
|
||||||
RctType::Bulletproofs
|
|
||||||
} else {
|
|
||||||
RctType::BulletproofsCompactAmount
|
|
||||||
}
|
|
||||||
}
|
|
||||||
RctPrunable::Clsag { bulletproofs, .. } => {
|
|
||||||
if matches!(bulletproofs, Bulletproofs::Original { .. }) {
|
|
||||||
RctType::Clsag
|
|
||||||
} else {
|
|
||||||
RctType::BulletproofsPlus
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn fee_weight(protocol: Protocol, inputs: usize, outputs: usize, fee: u64) -> usize {
|
pub fn serialize<W: std::io::Write>(&self, w: &mut W) -> std::io::Result<()> {
|
||||||
RctBase::fee_weight(outputs, fee) + RctPrunable::fee_weight(protocol, inputs, outputs)
|
self.base.serialize(w, self.prunable.rct_type())?;
|
||||||
|
self.prunable.serialize(w)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
pub fn deserialize<R: std::io::Read>(decoys: Vec<usize>, outputs: usize, r: &mut R) -> std::io::Result<RctSignatures> {
|
||||||
let rct_type = self.rct_type();
|
let base = RctBase::deserialize(outputs, r)?;
|
||||||
self.base.write(w, rct_type)?;
|
Ok(RctSignatures { base: base.0, prunable: RctPrunable::deserialize(base.1, &decoys, r)? })
|
||||||
self.prunable.write(w, rct_type)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn serialize(&self) -> Vec<u8> {
|
|
||||||
let mut serialized = vec![];
|
|
||||||
self.write(&mut serialized).unwrap();
|
|
||||||
serialized
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn read<R: Read>(decoys: &[usize], outputs: usize, r: &mut R) -> io::Result<RctSignatures> {
|
|
||||||
let base = RctBase::read(decoys.len(), outputs, r)?;
|
|
||||||
Ok(RctSignatures { base: base.0, prunable: RctPrunable::read(base.1, decoys, outputs, r)? })
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
353
coins/monero/src/rpc.rs
Normal file
353
coins/monero/src/rpc.rs
Normal file
@@ -0,0 +1,353 @@
|
|||||||
|
use std::fmt::Debug;
|
||||||
|
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use curve25519_dalek::edwards::{EdwardsPoint, CompressedEdwardsY};
|
||||||
|
|
||||||
|
use serde::{Serialize, Deserialize, de::DeserializeOwned};
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
use reqwest;
|
||||||
|
|
||||||
|
use crate::{transaction::{Input, Timelock, Transaction}, block::Block, wallet::Fee};
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
pub struct EmptyResponse {}
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
pub struct JsonRpcResponse<T> {
|
||||||
|
result: T
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Error, Debug)]
|
||||||
|
pub enum RpcError {
|
||||||
|
#[error("internal error ({0})")]
|
||||||
|
InternalError(String),
|
||||||
|
#[error("connection error")]
|
||||||
|
ConnectionError,
|
||||||
|
#[error("transactions not found")]
|
||||||
|
TransactionsNotFound(Vec<[u8; 32]>),
|
||||||
|
#[error("invalid point ({0})")]
|
||||||
|
InvalidPoint(String),
|
||||||
|
#[error("pruned transaction")]
|
||||||
|
PrunedTransaction,
|
||||||
|
#[error("invalid transaction ({0:?})")]
|
||||||
|
InvalidTransaction([u8; 32])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rpc_hex(value: &str) -> Result<Vec<u8>, RpcError> {
|
||||||
|
hex::decode(value).map_err(|_| RpcError::InternalError("Monero returned invalid hex".to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rpc_point(point: &str) -> Result<EdwardsPoint, RpcError> {
|
||||||
|
CompressedEdwardsY(
|
||||||
|
rpc_hex(point)?.try_into().map_err(|_| RpcError::InvalidPoint(point.to_string()))?
|
||||||
|
).decompress().ok_or(RpcError::InvalidPoint(point.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Rpc(String);
|
||||||
|
|
||||||
|
impl Rpc {
|
||||||
|
pub fn new(daemon: String) -> Rpc {
|
||||||
|
Rpc(daemon)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn rpc_call<
|
||||||
|
Params: Serialize + Debug,
|
||||||
|
Response: DeserializeOwned + Debug
|
||||||
|
>(&self, method: &str, params: Option<Params>) -> Result<Response, RpcError> {
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
let mut builder = client.post(&(self.0.clone() + "/" + method));
|
||||||
|
if let Some(params) = params.as_ref() {
|
||||||
|
builder = builder.json(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.call_tail(method, builder).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn bin_call<
|
||||||
|
Response: DeserializeOwned + Debug
|
||||||
|
>(&self, method: &str, params: Vec<u8>) -> Result<Response, RpcError> {
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
let builder = client.post(&(self.0.clone() + "/" + method)).body(params);
|
||||||
|
self.call_tail(method, builder.header("Content-Type", "application/octet-stream")).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn call_tail<
|
||||||
|
Response: DeserializeOwned + Debug
|
||||||
|
>(&self, method: &str, builder: reqwest::RequestBuilder) -> Result<Response, RpcError> {
|
||||||
|
let res = builder
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(|_| RpcError::ConnectionError)?;
|
||||||
|
|
||||||
|
Ok(
|
||||||
|
if !method.ends_with(".bin") {
|
||||||
|
serde_json::from_str(&res.text().await.map_err(|_| RpcError::ConnectionError)?)
|
||||||
|
.map_err(|_| RpcError::InternalError("Failed to parse JSON response".to_string()))?
|
||||||
|
} else {
|
||||||
|
monero_epee_bin_serde::from_bytes(&res.bytes().await.map_err(|_| RpcError::ConnectionError)?)
|
||||||
|
.map_err(|_| RpcError::InternalError("Failed to parse binary response".to_string()))?
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_height(&self) -> Result<usize, RpcError> {
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
struct HeightResponse {
|
||||||
|
height: usize
|
||||||
|
}
|
||||||
|
Ok(self.rpc_call::<Option<()>, HeightResponse>("get_height", None).await?.height)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_transactions_core(
|
||||||
|
&self,
|
||||||
|
hashes: &[[u8; 32]]
|
||||||
|
) -> Result<(Vec<Result<Transaction, RpcError>>, Vec<[u8; 32]>), RpcError> {
|
||||||
|
if hashes.len() == 0 {
|
||||||
|
return Ok((vec![], vec![]));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
struct TransactionResponse {
|
||||||
|
tx_hash: String,
|
||||||
|
as_hex: String,
|
||||||
|
pruned_as_hex: String
|
||||||
|
}
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
struct TransactionsResponse {
|
||||||
|
#[serde(default)]
|
||||||
|
missed_tx: Vec<String>,
|
||||||
|
txs: Vec<TransactionResponse>
|
||||||
|
}
|
||||||
|
|
||||||
|
let txs: TransactionsResponse = self.rpc_call("get_transactions", Some(json!({
|
||||||
|
"txs_hashes": hashes.iter().map(|hash| hex::encode(&hash)).collect::<Vec<_>>()
|
||||||
|
}))).await?;
|
||||||
|
|
||||||
|
Ok((
|
||||||
|
txs.txs.iter().map(|res| {
|
||||||
|
let tx = Transaction::deserialize(
|
||||||
|
&mut std::io::Cursor::new(
|
||||||
|
rpc_hex(if res.as_hex.len() != 0 { &res.as_hex } else { &res.pruned_as_hex }).unwrap()
|
||||||
|
)
|
||||||
|
).map_err(|_| RpcError::InvalidTransaction(hex::decode(&res.tx_hash).unwrap().try_into().unwrap()))?;
|
||||||
|
|
||||||
|
// https://github.com/monero-project/monero/issues/8311
|
||||||
|
if res.as_hex.len() == 0 {
|
||||||
|
match tx.prefix.inputs.get(0) {
|
||||||
|
Some(Input::Gen { .. }) => (),
|
||||||
|
_ => Err(RpcError::PrunedTransaction)?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(tx)
|
||||||
|
}).collect(),
|
||||||
|
|
||||||
|
txs.missed_tx.iter().map(|hash| hex::decode(&hash).unwrap().try_into().unwrap()).collect()
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_transactions(&self, hashes: &[[u8; 32]]) -> Result<Vec<Transaction>, RpcError> {
|
||||||
|
let (txs, missed) = self.get_transactions_core(hashes).await?;
|
||||||
|
if missed.len() != 0 {
|
||||||
|
Err(RpcError::TransactionsNotFound(missed))?;
|
||||||
|
}
|
||||||
|
// This will clone several KB and is accordingly inefficient
|
||||||
|
// TODO: Optimize
|
||||||
|
txs.iter().cloned().collect::<Result<_, _>>()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_transactions_possible(&self, hashes: &[[u8; 32]]) -> Result<Vec<Transaction>, RpcError> {
|
||||||
|
let (txs, _) = self.get_transactions_core(hashes).await?;
|
||||||
|
Ok(txs.iter().cloned().filter_map(|tx| tx.ok()).collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_block(&self, height: usize) -> Result<Block, RpcError> {
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
struct BlockResponse {
|
||||||
|
blob: String
|
||||||
|
}
|
||||||
|
|
||||||
|
let block: JsonRpcResponse<BlockResponse> = self.rpc_call("json_rpc", Some(json!({
|
||||||
|
"method": "get_block",
|
||||||
|
"params": {
|
||||||
|
"height": height
|
||||||
|
}
|
||||||
|
}))).await?;
|
||||||
|
|
||||||
|
Ok(
|
||||||
|
Block::deserialize(
|
||||||
|
&mut std::io::Cursor::new(rpc_hex(&block.result.blob)?)
|
||||||
|
).expect("Monero returned a block we couldn't deserialize")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_block_transactions_core(
|
||||||
|
&self,
|
||||||
|
height: usize,
|
||||||
|
possible: bool
|
||||||
|
) -> Result<Vec<Transaction>, RpcError> {
|
||||||
|
let block = self.get_block(height).await?;
|
||||||
|
let mut res = vec![block.miner_tx];
|
||||||
|
res.extend(
|
||||||
|
if possible {
|
||||||
|
self.get_transactions_possible(&block.txs).await?
|
||||||
|
} else {
|
||||||
|
self.get_transactions(&block.txs).await?
|
||||||
|
}
|
||||||
|
);
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_block_transactions(&self, height: usize) -> Result<Vec<Transaction>, RpcError> {
|
||||||
|
self.get_block_transactions_core(height, false).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_block_transactions_possible(&self, height: usize) -> Result<Vec<Transaction>, RpcError> {
|
||||||
|
self.get_block_transactions_core(height, true).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_o_indexes(&self, hash: [u8; 32]) -> Result<Vec<u64>, RpcError> {
|
||||||
|
#[derive(Serialize, Debug)]
|
||||||
|
struct Request {
|
||||||
|
txid: [u8; 32]
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
struct OIndexes {
|
||||||
|
o_indexes: Vec<u64>,
|
||||||
|
status: String,
|
||||||
|
untrusted: bool,
|
||||||
|
credits: usize,
|
||||||
|
top_hash: String
|
||||||
|
}
|
||||||
|
|
||||||
|
let indexes: OIndexes = self.bin_call("get_o_indexes.bin", monero_epee_bin_serde::to_bytes(
|
||||||
|
&Request {
|
||||||
|
txid: hash
|
||||||
|
}).unwrap()
|
||||||
|
).await?;
|
||||||
|
|
||||||
|
Ok(indexes.o_indexes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// from and to are inclusive
|
||||||
|
pub async fn get_output_distribution(&self, from: usize, to: usize) -> Result<Vec<u64>, RpcError> {
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
pub struct Distribution {
|
||||||
|
distribution: Vec<u64>
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
struct Distributions {
|
||||||
|
distributions: Vec<Distribution>
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut distributions: JsonRpcResponse<Distributions> = self.rpc_call("json_rpc", Some(json!({
|
||||||
|
"method": "get_output_distribution",
|
||||||
|
"params": {
|
||||||
|
"binary": false,
|
||||||
|
"amounts": [0],
|
||||||
|
"cumulative": true,
|
||||||
|
"from_height": from,
|
||||||
|
"to_height": to
|
||||||
|
}
|
||||||
|
}))).await?;
|
||||||
|
|
||||||
|
Ok(distributions.result.distributions.swap_remove(0).distribution)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_outputs(
|
||||||
|
&self,
|
||||||
|
indexes: &[u64],
|
||||||
|
height: usize
|
||||||
|
) -> Result<Vec<Option<[EdwardsPoint; 2]>>, RpcError> {
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
pub struct Out {
|
||||||
|
key: String,
|
||||||
|
mask: String,
|
||||||
|
txid: String
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
struct Outs {
|
||||||
|
outs: Vec<Out>
|
||||||
|
}
|
||||||
|
|
||||||
|
let outs: Outs = self.rpc_call("get_outs", Some(json!({
|
||||||
|
"get_txid": true,
|
||||||
|
"outputs": indexes.iter().map(|o| json!({
|
||||||
|
"amount": 0,
|
||||||
|
"index": o
|
||||||
|
})).collect::<Vec<_>>()
|
||||||
|
}))).await?;
|
||||||
|
|
||||||
|
let txs = self.get_transactions(
|
||||||
|
&outs.outs.iter().map(|out|
|
||||||
|
rpc_hex(&out.txid).expect("Monero returned an invalidly encoded hash")
|
||||||
|
.try_into().expect("Monero returned an invalid sized hash")
|
||||||
|
).collect::<Vec<_>>()
|
||||||
|
).await?;
|
||||||
|
// TODO: Support time based lock times. These shouldn't be needed, and it may be painful to
|
||||||
|
// get the median time for the given height, yet we do need to in order to be complete
|
||||||
|
outs.outs.iter().enumerate().map(
|
||||||
|
|(i, out)| Ok(
|
||||||
|
Some([rpc_point(&out.key)?, rpc_point(&out.mask)?]).filter(|_| {
|
||||||
|
match txs[i].prefix.timelock {
|
||||||
|
Timelock::Block(t_height) => (t_height <= height),
|
||||||
|
_ => false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_fee(&self) -> Result<Fee, RpcError> {
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
struct FeeResponse {
|
||||||
|
fee: u64,
|
||||||
|
quantization_mask: u64
|
||||||
|
}
|
||||||
|
|
||||||
|
let res: JsonRpcResponse<FeeResponse> = self.rpc_call("json_rpc", Some(json!({
|
||||||
|
"method": "get_fee_estimate"
|
||||||
|
}))).await?;
|
||||||
|
|
||||||
|
Ok(Fee { per_weight: res.result.fee, mask: res.result.quantization_mask })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn publish_transaction(&self, tx: &Transaction) -> Result<(), RpcError> {
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
struct SendRawResponse {
|
||||||
|
status: String,
|
||||||
|
double_spend: bool,
|
||||||
|
fee_too_low: bool,
|
||||||
|
invalid_input: bool,
|
||||||
|
invalid_output: bool,
|
||||||
|
low_mixin: bool,
|
||||||
|
not_relayed: bool,
|
||||||
|
overspend: bool,
|
||||||
|
too_big: bool,
|
||||||
|
too_few_outputs: bool,
|
||||||
|
reason: String
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut buf = Vec::with_capacity(2048);
|
||||||
|
tx.serialize(&mut buf).unwrap();
|
||||||
|
let res: SendRawResponse = self.rpc_call("send_raw_transaction", Some(json!({
|
||||||
|
"tx_as_hex": hex::encode(&buf)
|
||||||
|
}))).await?;
|
||||||
|
|
||||||
|
if res.status != "OK" {
|
||||||
|
Err(RpcError::InvalidTransaction(tx.hash()))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,286 +0,0 @@
|
|||||||
use std::{sync::Arc, io::Read, time::Duration};
|
|
||||||
|
|
||||||
use async_trait::async_trait;
|
|
||||||
|
|
||||||
use tokio::sync::Mutex;
|
|
||||||
|
|
||||||
use digest_auth::{WwwAuthenticateHeader, AuthContext};
|
|
||||||
use simple_request::{
|
|
||||||
hyper::{StatusCode, header::HeaderValue, Request},
|
|
||||||
Response, Client,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::rpc::{RpcError, RpcConnection, Rpc};
|
|
||||||
|
|
||||||
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
enum Authentication {
|
|
||||||
// If unauthenticated, use a single client
|
|
||||||
Unauthenticated(Client),
|
|
||||||
// If authenticated, use a single client which supports being locked and tracks its nonce
|
|
||||||
// This ensures that if a nonce is requested, another caller doesn't make a request invalidating
|
|
||||||
// it
|
|
||||||
Authenticated {
|
|
||||||
username: String,
|
|
||||||
password: String,
|
|
||||||
#[allow(clippy::type_complexity)]
|
|
||||||
connection: Arc<Mutex<(Option<(WwwAuthenticateHeader, u64)>, Client)>>,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
/// An HTTP(S) transport for the RPC.
|
|
||||||
///
|
|
||||||
/// Requires tokio.
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct HttpRpc {
|
|
||||||
authentication: Authentication,
|
|
||||||
url: String,
|
|
||||||
request_timeout: Duration,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl HttpRpc {
|
|
||||||
fn digest_auth_challenge(
|
|
||||||
response: &Response,
|
|
||||||
) -> Result<Option<(WwwAuthenticateHeader, u64)>, RpcError> {
|
|
||||||
Ok(if let Some(header) = response.headers().get("www-authenticate") {
|
|
||||||
Some((
|
|
||||||
digest_auth::parse(header.to_str().map_err(|_| {
|
|
||||||
RpcError::InvalidNode("www-authenticate header wasn't a string".to_string())
|
|
||||||
})?)
|
|
||||||
.map_err(|_| RpcError::InvalidNode("invalid digest-auth response".to_string()))?,
|
|
||||||
0,
|
|
||||||
))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a new HTTP(S) RPC connection.
|
|
||||||
///
|
|
||||||
/// A daemon requiring authentication can be used via including the username and password in the
|
|
||||||
/// URL.
|
|
||||||
pub async fn new(url: String) -> Result<Rpc<HttpRpc>, RpcError> {
|
|
||||||
Self::with_custom_timeout(url, DEFAULT_TIMEOUT).await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a new HTTP(S) RPC connection with a custom timeout.
|
|
||||||
///
|
|
||||||
/// A daemon requiring authentication can be used via including the username and password in the
|
|
||||||
/// URL.
|
|
||||||
pub async fn with_custom_timeout(
|
|
||||||
mut url: String,
|
|
||||||
request_timeout: Duration,
|
|
||||||
) -> Result<Rpc<HttpRpc>, RpcError> {
|
|
||||||
let authentication = if url.contains('@') {
|
|
||||||
// Parse out the username and password
|
|
||||||
let url_clone = url;
|
|
||||||
let split_url = url_clone.split('@').collect::<Vec<_>>();
|
|
||||||
if split_url.len() != 2 {
|
|
||||||
Err(RpcError::ConnectionError("invalid amount of login specifications".to_string()))?;
|
|
||||||
}
|
|
||||||
let mut userpass = split_url[0];
|
|
||||||
url = split_url[1].to_string();
|
|
||||||
|
|
||||||
// If there was additionally a protocol string, restore that to the daemon URL
|
|
||||||
if userpass.contains("://") {
|
|
||||||
let split_userpass = userpass.split("://").collect::<Vec<_>>();
|
|
||||||
if split_userpass.len() != 2 {
|
|
||||||
Err(RpcError::ConnectionError("invalid amount of protocol specifications".to_string()))?;
|
|
||||||
}
|
|
||||||
url = split_userpass[0].to_string() + "://" + &url;
|
|
||||||
userpass = split_userpass[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
let split_userpass = userpass.split(':').collect::<Vec<_>>();
|
|
||||||
if split_userpass.len() > 2 {
|
|
||||||
Err(RpcError::ConnectionError("invalid amount of passwords".to_string()))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let client = Client::without_connection_pool(&url)
|
|
||||||
.map_err(|_| RpcError::ConnectionError("invalid URL".to_string()))?;
|
|
||||||
// Obtain the initial challenge, which also somewhat validates this connection
|
|
||||||
let challenge = Self::digest_auth_challenge(
|
|
||||||
&client
|
|
||||||
.request(
|
|
||||||
Request::post(url.clone())
|
|
||||||
.body(vec![].into())
|
|
||||||
.map_err(|e| RpcError::ConnectionError(format!("couldn't make request: {e:?}")))?,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map_err(|e| RpcError::ConnectionError(format!("{e:?}")))?,
|
|
||||||
)?;
|
|
||||||
Authentication::Authenticated {
|
|
||||||
username: split_userpass[0].to_string(),
|
|
||||||
password: (*split_userpass.get(1).unwrap_or(&"")).to_string(),
|
|
||||||
connection: Arc::new(Mutex::new((challenge, client))),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Authentication::Unauthenticated(Client::with_connection_pool())
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Rpc(HttpRpc { authentication, url, request_timeout }))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl HttpRpc {
|
|
||||||
async fn inner_post(&self, route: &str, body: Vec<u8>) -> Result<Vec<u8>, RpcError> {
|
|
||||||
let request_fn = |uri| {
|
|
||||||
Request::post(uri)
|
|
||||||
.body(body.clone().into())
|
|
||||||
.map_err(|e| RpcError::ConnectionError(format!("couldn't make request: {e:?}")))
|
|
||||||
};
|
|
||||||
|
|
||||||
async fn body_from_response(response: Response<'_>) -> Result<Vec<u8>, RpcError> {
|
|
||||||
/*
|
|
||||||
let length = usize::try_from(
|
|
||||||
response
|
|
||||||
.headers()
|
|
||||||
.get("content-length")
|
|
||||||
.ok_or(RpcError::InvalidNode("no content-length header"))?
|
|
||||||
.to_str()
|
|
||||||
.map_err(|_| RpcError::InvalidNode("non-ascii content-length value"))?
|
|
||||||
.parse::<u32>()
|
|
||||||
.map_err(|_| RpcError::InvalidNode("non-u32 content-length value"))?,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
// Only pre-allocate 1 MB so a malicious node which claims a content-length of 1 GB actually
|
|
||||||
// has to send 1 GB of data to cause a 1 GB allocation
|
|
||||||
let mut res = Vec::with_capacity(length.max(1024 * 1024));
|
|
||||||
let mut body = response.into_body();
|
|
||||||
while res.len() < length {
|
|
||||||
let Some(data) = body.data().await else { break };
|
|
||||||
res.extend(data.map_err(|e| RpcError::ConnectionError(format!("{e:?}")))?.as_ref());
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
let mut res = Vec::with_capacity(128);
|
|
||||||
response
|
|
||||||
.body()
|
|
||||||
.await
|
|
||||||
.map_err(|e| RpcError::ConnectionError(format!("{e:?}")))?
|
|
||||||
.read_to_end(&mut res)
|
|
||||||
.unwrap();
|
|
||||||
Ok(res)
|
|
||||||
}
|
|
||||||
|
|
||||||
for attempt in 0 .. 2 {
|
|
||||||
return Ok(match &self.authentication {
|
|
||||||
Authentication::Unauthenticated(client) => {
|
|
||||||
body_from_response(
|
|
||||||
client
|
|
||||||
.request(request_fn(self.url.clone() + "/" + route)?)
|
|
||||||
.await
|
|
||||||
.map_err(|e| RpcError::ConnectionError(format!("{e:?}")))?,
|
|
||||||
)
|
|
||||||
.await?
|
|
||||||
}
|
|
||||||
Authentication::Authenticated { username, password, connection } => {
|
|
||||||
let mut connection_lock = connection.lock().await;
|
|
||||||
|
|
||||||
let mut request = request_fn("/".to_string() + route)?;
|
|
||||||
|
|
||||||
// If we don't have an auth challenge, obtain one
|
|
||||||
if connection_lock.0.is_none() {
|
|
||||||
connection_lock.0 = Self::digest_auth_challenge(
|
|
||||||
&connection_lock
|
|
||||||
.1
|
|
||||||
.request(request)
|
|
||||||
.await
|
|
||||||
.map_err(|e| RpcError::ConnectionError(format!("{e:?}")))?,
|
|
||||||
)?;
|
|
||||||
request = request_fn("/".to_string() + route)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insert the challenge response, if we have a challenge
|
|
||||||
if let Some((challenge, cnonce)) = connection_lock.0.as_mut() {
|
|
||||||
// Update the cnonce
|
|
||||||
// Overflow isn't a concern as this is a u64
|
|
||||||
*cnonce += 1;
|
|
||||||
|
|
||||||
let mut context = AuthContext::new_post::<_, _, _, &[u8]>(
|
|
||||||
username,
|
|
||||||
password,
|
|
||||||
"/".to_string() + route,
|
|
||||||
None,
|
|
||||||
);
|
|
||||||
context.set_custom_cnonce(hex::encode(cnonce.to_le_bytes()));
|
|
||||||
|
|
||||||
request.headers_mut().insert(
|
|
||||||
"Authorization",
|
|
||||||
HeaderValue::from_str(
|
|
||||||
&challenge
|
|
||||||
.respond(&context)
|
|
||||||
.map_err(|_| {
|
|
||||||
RpcError::InvalidNode("couldn't respond to digest-auth challenge".to_string())
|
|
||||||
})?
|
|
||||||
.to_header_string(),
|
|
||||||
)
|
|
||||||
.unwrap(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let response = connection_lock
|
|
||||||
.1
|
|
||||||
.request(request)
|
|
||||||
.await
|
|
||||||
.map_err(|e| RpcError::ConnectionError(format!("{e:?}")));
|
|
||||||
|
|
||||||
let (error, is_stale) = match &response {
|
|
||||||
Err(e) => (Some(e.clone()), false),
|
|
||||||
Ok(response) => (
|
|
||||||
None,
|
|
||||||
if response.status() == StatusCode::UNAUTHORIZED {
|
|
||||||
if let Some(header) = response.headers().get("www-authenticate") {
|
|
||||||
header
|
|
||||||
.to_str()
|
|
||||||
.map_err(|_| {
|
|
||||||
RpcError::InvalidNode("www-authenticate header wasn't a string".to_string())
|
|
||||||
})?
|
|
||||||
.contains("stale")
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
},
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
// If the connection entered an error state, drop the cached challenge as challenges are
|
|
||||||
// per-connection
|
|
||||||
// We don't need to create a new connection as simple-request will for us
|
|
||||||
if error.is_some() || is_stale {
|
|
||||||
connection_lock.0 = None;
|
|
||||||
// If we're not already on our second attempt, move to the next loop iteration
|
|
||||||
// (retrying all of this once)
|
|
||||||
if attempt == 0 {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if let Some(e) = error {
|
|
||||||
Err(e)?
|
|
||||||
} else {
|
|
||||||
debug_assert!(is_stale);
|
|
||||||
Err(RpcError::InvalidNode(
|
|
||||||
"node claimed fresh connection had stale authentication".to_string(),
|
|
||||||
))?
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
body_from_response(response.unwrap()).await?
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
unreachable!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl RpcConnection for HttpRpc {
|
|
||||||
async fn post(&self, route: &str, body: Vec<u8>) -> Result<Vec<u8>, RpcError> {
|
|
||||||
tokio::time::timeout(self.request_timeout, self.inner_post(route, body))
|
|
||||||
.await
|
|
||||||
.map_err(|e| RpcError::ConnectionError(format!("{e:?}")))?
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user