mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-08 12:19:24 +00:00
Compare commits
6 Commits
2ffdd2a01d
...
aggressive
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7685cc305f | ||
|
|
3ca76c51e4 | ||
|
|
286e96ccd8 | ||
|
|
f93106af6b | ||
|
|
dd5fb0df47 | ||
|
|
3a626cc51e |
2
.github/LICENSE → .github/actions/LICENSE
vendored
2
.github/LICENSE → .github/actions/LICENSE
vendored
@@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2022-2025 Luke Parker
|
Copyright (c) 2022-2023 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
|
||||||
13
.github/actions/bitcoin/action.yml
vendored
13
.github/actions/bitcoin/action.yml
vendored
@@ -5,14 +5,14 @@ inputs:
|
|||||||
version:
|
version:
|
||||||
description: "Version to download and run"
|
description: "Version to download and run"
|
||||||
required: false
|
required: false
|
||||||
default: "30.0"
|
default: 24.0.1
|
||||||
|
|
||||||
runs:
|
runs:
|
||||||
using: "composite"
|
using: "composite"
|
||||||
steps:
|
steps:
|
||||||
- name: Bitcoin Daemon Cache
|
- name: Bitcoin Daemon Cache
|
||||||
id: cache-bitcoind
|
id: cache-bitcoind
|
||||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809
|
uses: actions/cache@v3
|
||||||
with:
|
with:
|
||||||
path: bitcoin.tar.gz
|
path: bitcoin.tar.gz
|
||||||
key: bitcoind-${{ runner.os }}-${{ runner.arch }}-${{ inputs.version }}
|
key: bitcoind-${{ runner.os }}-${{ runner.arch }}-${{ inputs.version }}
|
||||||
@@ -37,4 +37,11 @@ runs:
|
|||||||
|
|
||||||
- name: Bitcoin Regtest Daemon
|
- name: Bitcoin Regtest Daemon
|
||||||
shell: bash
|
shell: bash
|
||||||
run: PATH=$PATH:/usr/bin ./orchestration/dev/networks/bitcoin/run.sh -txindex -daemon
|
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
|
||||||
|
|||||||
100
.github/actions/build-dependencies/action.yml
vendored
100
.github/actions/build-dependencies/action.yml
vendored
@@ -1,85 +1,43 @@
|
|||||||
name: build-dependencies
|
name: build-dependencies
|
||||||
description: Installs build dependencies for Serai
|
description: Installs build dependencies for Serai
|
||||||
|
|
||||||
|
inputs:
|
||||||
|
github-token:
|
||||||
|
description: "GitHub token to install Protobuf with"
|
||||||
|
require: true
|
||||||
|
default:
|
||||||
|
|
||||||
|
rust-toolchain:
|
||||||
|
description: "Rust toolchain to install"
|
||||||
|
required: false
|
||||||
|
default: stable
|
||||||
|
|
||||||
|
rust-components:
|
||||||
|
description: "Rust components to install"
|
||||||
|
required: false
|
||||||
|
default:
|
||||||
|
|
||||||
runs:
|
runs:
|
||||||
using: "composite"
|
using: "composite"
|
||||||
steps:
|
steps:
|
||||||
- name: Remove unused packages
|
- name: Install Protobuf
|
||||||
shell: bash
|
uses: arduino/setup-protoc@v2.0.0
|
||||||
run: |
|
with:
|
||||||
# Ensure the repositories are synced
|
repo-token: ${{ inputs.github-token }}
|
||||||
sudo apt update -y
|
|
||||||
|
|
||||||
# Actually perform the removals
|
|
||||||
sudo apt remove -y "*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 --allow-remove-essential -f shim-signed *python3*
|
|
||||||
# This removal command requires the prior removals due to unmet dependencies otherwise
|
|
||||||
sudo apt remove -y "*qemu*" "*sql*" "*texinfo*" "*imagemagick*"
|
|
||||||
|
|
||||||
# Reinstall python3 as a general dependency of a functional operating system
|
|
||||||
sudo apt install -y python3 --fix-missing
|
|
||||||
if: runner.os == 'Linux'
|
|
||||||
|
|
||||||
- name: Remove unused packages
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
(gem uninstall -aIx) || (exit 0)
|
|
||||||
brew uninstall --force "*msbuild*" "*powershell*" "*nuget*" "*bazel*" "*ansible*" "*terraform*" "*heroku*" "*aws*" azure-cli
|
|
||||||
brew uninstall --force "*nodejs*" "*npm*" "*yarn*" "*java*" "*kotlin*" "*golang*" "*swift*" "*julia*" "*fortran*" "*android*"
|
|
||||||
brew uninstall --force "*apache2*" "*nginx*" "*firefox*" "*chromium*" "*chrome*" "*edge*"
|
|
||||||
brew uninstall --force "*qemu*" "*sql*" "*texinfo*" "*imagemagick*"
|
|
||||||
brew cleanup
|
|
||||||
if: runner.os == 'macOS'
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
if [ "$RUNNER_OS" == "Linux" ]; then
|
|
||||||
sudo apt install -y ca-certificates protobuf-compiler libclang-dev
|
|
||||||
elif [ "$RUNNER_OS" == "Windows" ]; then
|
|
||||||
choco install protoc
|
|
||||||
elif [ "$RUNNER_OS" == "macOS" ]; then
|
|
||||||
brew install protobuf llvm
|
|
||||||
HOMEBREW_ROOT_PATH=/opt/homebrew # Apple Silicon
|
|
||||||
if [ $(uname -m) = "x86_64" ]; then HOMEBREW_ROOT_PATH=/usr/local; fi # Intel
|
|
||||||
ls $HOMEBREW_ROOT_PATH/opt/llvm/lib | grep "libclang.dylib" # Make sure this installed `libclang`
|
|
||||||
echo "DYLD_LIBRARY_PATH=$HOMEBREW_ROOT_PATH/opt/llvm/lib:$DYLD_LIBRARY_PATH" >> "$GITHUB_ENV"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Install solc
|
- name: Install solc
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
cargo +1.91 install svm-rs --version =0.5.19
|
pip3 install solc-select==0.2.1
|
||||||
svm install 0.8.29
|
solc-select install 0.8.16
|
||||||
svm use 0.8.29
|
solc-select use 0.8.16
|
||||||
|
|
||||||
- name: Remove preinstalled Docker
|
- name: Install Rust
|
||||||
shell: bash
|
uses: dtolnay/rust-toolchain@master
|
||||||
run: |
|
|
||||||
docker system prune -a --volumes
|
|
||||||
sudo apt remove -y *docker*
|
|
||||||
# Install uidmap which will be required for the explicitly installed Docker
|
|
||||||
sudo apt install uidmap
|
|
||||||
if: runner.os == 'Linux'
|
|
||||||
|
|
||||||
- name: Update system dependencies
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
sudo apt update -y
|
|
||||||
sudo apt upgrade -y
|
|
||||||
sudo apt autoremove -y
|
|
||||||
sudo apt clean
|
|
||||||
if: runner.os == 'Linux'
|
|
||||||
|
|
||||||
- name: Install rootless Docker
|
|
||||||
uses: docker/setup-docker-action@b60f85385d03ac8acfca6d9996982511d8620a19
|
|
||||||
with:
|
with:
|
||||||
rootless: true
|
toolchain: ${{ inputs.rust-toolchain }}
|
||||||
set-host: true
|
components: ${{ inputs.rust-components }}
|
||||||
if: runner.os == 'Linux'
|
targets: wasm32-unknown-unknown, riscv32imac-unknown-none-elf
|
||||||
|
|
||||||
# - name: Cache Rust
|
# - name: Cache Rust
|
||||||
# uses: Swatinem/rust-cache@a95ba195448af2da9b00fb742d14ffaaf3c21f43
|
# uses: Swatinem/rust-cache@v2
|
||||||
|
|||||||
11
.github/actions/monero-wallet-rpc/action.yml
vendored
11
.github/actions/monero-wallet-rpc/action.yml
vendored
@@ -5,14 +5,14 @@ inputs:
|
|||||||
version:
|
version:
|
||||||
description: "Version to download and run"
|
description: "Version to download and run"
|
||||||
required: false
|
required: false
|
||||||
default: v0.18.4.3
|
default: v0.18.2.0
|
||||||
|
|
||||||
runs:
|
runs:
|
||||||
using: "composite"
|
using: "composite"
|
||||||
steps:
|
steps:
|
||||||
- name: Monero Wallet RPC Cache
|
- name: Monero Wallet RPC Cache
|
||||||
id: cache-monero-wallet-rpc
|
id: cache-monero-wallet-rpc
|
||||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809
|
uses: actions/cache@v3
|
||||||
with:
|
with:
|
||||||
path: monero-wallet-rpc
|
path: monero-wallet-rpc
|
||||||
key: monero-wallet-rpc-${{ runner.os }}-${{ runner.arch }}-${{ inputs.version }}
|
key: monero-wallet-rpc-${{ runner.os }}-${{ runner.arch }}-${{ inputs.version }}
|
||||||
@@ -41,9 +41,4 @@ runs:
|
|||||||
|
|
||||||
- name: Monero Wallet RPC
|
- name: Monero Wallet RPC
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: ./monero-wallet-rpc --disable-rpc-login --rpc-bind-port 6061 --allow-mismatched-daemon-version --wallet-dir ./ --detach
|
||||||
./monero-wallet-rpc --allow-mismatched-daemon-version \
|
|
||||||
--daemon-address 0.0.0.0:18081 --daemon-login serai:seraidex \
|
|
||||||
--disable-rpc-login --rpc-bind-port 18082 \
|
|
||||||
--wallet-dir ./ \
|
|
||||||
--detach
|
|
||||||
|
|||||||
12
.github/actions/monero/action.yml
vendored
12
.github/actions/monero/action.yml
vendored
@@ -5,16 +5,16 @@ inputs:
|
|||||||
version:
|
version:
|
||||||
description: "Version to download and run"
|
description: "Version to download and run"
|
||||||
required: false
|
required: false
|
||||||
default: v0.18.4.3
|
default: v0.18.2.0
|
||||||
|
|
||||||
runs:
|
runs:
|
||||||
using: "composite"
|
using: "composite"
|
||||||
steps:
|
steps:
|
||||||
- name: Monero Daemon Cache
|
- name: Monero Daemon Cache
|
||||||
id: cache-monerod
|
id: cache-monerod
|
||||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809
|
uses: actions/cache@v3
|
||||||
with:
|
with:
|
||||||
path: /usr/bin/monerod
|
path: monerod
|
||||||
key: monerod-${{ runner.os }}-${{ runner.arch }}-${{ inputs.version }}
|
key: monerod-${{ runner.os }}-${{ runner.arch }}-${{ inputs.version }}
|
||||||
|
|
||||||
- name: Download the Monero Daemon
|
- name: Download the Monero Daemon
|
||||||
@@ -37,10 +37,8 @@ runs:
|
|||||||
wget https://downloads.getmonero.org/cli/$FILE
|
wget https://downloads.getmonero.org/cli/$FILE
|
||||||
tar -xvf $FILE
|
tar -xvf $FILE
|
||||||
|
|
||||||
sudo mv monero-x86_64-linux-gnu-${{ inputs.version }}/monerod /usr/bin/monerod
|
mv monero-x86_64-linux-gnu-${{ inputs.version }}/monerod monerod
|
||||||
sudo chmod 777 /usr/bin/monerod
|
|
||||||
sudo chmod +x /usr/bin/monerod
|
|
||||||
|
|
||||||
- name: Monero Regtest Daemon
|
- name: Monero Regtest Daemon
|
||||||
shell: bash
|
shell: bash
|
||||||
run: PATH=$PATH:/usr/bin ./orchestration/dev/networks/monero/run.sh --detach
|
run: ./monerod --regtest --offline --fixed-difficulty=1 --detach
|
||||||
|
|||||||
16
.github/actions/test-dependencies/action.yml
vendored
16
.github/actions/test-dependencies/action.yml
vendored
@@ -2,27 +2,33 @@ name: test-dependencies
|
|||||||
description: Installs test dependencies for Serai
|
description: Installs test dependencies for Serai
|
||||||
|
|
||||||
inputs:
|
inputs:
|
||||||
|
github-token:
|
||||||
|
description: "GitHub token to install Protobuf with"
|
||||||
|
require: true
|
||||||
|
default:
|
||||||
|
|
||||||
monero-version:
|
monero-version:
|
||||||
description: "Monero version to download and run as a regtest node"
|
description: "Monero version to download and run as a regtest node"
|
||||||
required: false
|
required: false
|
||||||
default: v0.18.4.3
|
default: v0.18.2.0
|
||||||
|
|
||||||
bitcoin-version:
|
bitcoin-version:
|
||||||
description: "Bitcoin version to download and run as a regtest node"
|
description: "Bitcoin version to download and run as a regtest node"
|
||||||
required: false
|
required: false
|
||||||
default: "30.0"
|
default: 24.0.1
|
||||||
|
|
||||||
runs:
|
runs:
|
||||||
using: "composite"
|
using: "composite"
|
||||||
steps:
|
steps:
|
||||||
- name: Install Build Dependencies
|
- name: Install Build Dependencies
|
||||||
uses: ./.github/actions/build-dependencies
|
uses: ./.github/actions/build-dependencies
|
||||||
|
with:
|
||||||
|
github-token: ${{ inputs.github-token }}
|
||||||
|
|
||||||
- name: Install Foundry
|
- name: Install Foundry
|
||||||
uses: foundry-rs/foundry-toolchain@8f1998e9878d786675189ef566a2e4bf24869773
|
uses: foundry-rs/foundry-toolchain@v1
|
||||||
with:
|
with:
|
||||||
version: nightly-f625d0fa7c51e65b4bf1e8f7931cd1c6e2e285e9
|
version: nightly
|
||||||
cache: false
|
|
||||||
|
|
||||||
- name: Run a Monero Regtest Node
|
- name: Run a Monero Regtest Node
|
||||||
uses: ./.github/actions/monero
|
uses: ./.github/actions/monero
|
||||||
|
|||||||
2
.github/nightly-version
vendored
2
.github/nightly-version
vendored
@@ -1 +1 @@
|
|||||||
nightly-2025-11-11
|
nightly-2023-07-01
|
||||||
|
|||||||
34
.github/workflows/common-tests.yml
vendored
34
.github/workflows/common-tests.yml
vendored
@@ -1,34 +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
|
|
||||||
|
|
||||||
- name: Run Tests
|
|
||||||
run: |
|
|
||||||
GITHUB_CI=true RUST_BACKTRACE=1 cargo test --all-features \
|
|
||||||
-p std-shims \
|
|
||||||
-p zalloc \
|
|
||||||
-p patchable-async-sleep \
|
|
||||||
-p serai-db \
|
|
||||||
-p serai-env \
|
|
||||||
-p serai-task \
|
|
||||||
-p simple-request
|
|
||||||
40
.github/workflows/coordinator-tests.yml
vendored
40
.github/workflows/coordinator-tests.yml
vendored
@@ -1,40 +0,0 @@
|
|||||||
name: Coordinator Tests
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- develop
|
|
||||||
paths:
|
|
||||||
- "common/**"
|
|
||||||
- "crypto/**"
|
|
||||||
- "networks/**"
|
|
||||||
- "message-queue/**"
|
|
||||||
- "coordinator/**"
|
|
||||||
- "orchestration/**"
|
|
||||||
- "tests/docker/**"
|
|
||||||
- "tests/coordinator/**"
|
|
||||||
|
|
||||||
pull_request:
|
|
||||||
paths:
|
|
||||||
- "common/**"
|
|
||||||
- "crypto/**"
|
|
||||||
- "networks/**"
|
|
||||||
- "message-queue/**"
|
|
||||||
- "coordinator/**"
|
|
||||||
- "orchestration/**"
|
|
||||||
- "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
|
|
||||||
|
|
||||||
- name: Run coordinator Docker tests
|
|
||||||
run: GITHUB_CI=true RUST_BACKTRACE=1 cargo test --all-features -p serai-coordinator-tests
|
|
||||||
48
.github/workflows/crypto-tests.yml
vendored
48
.github/workflows/crypto-tests.yml
vendored
@@ -1,48 +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
|
|
||||||
|
|
||||||
- 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 ciphersuite-kp256 \
|
|
||||||
-p multiexp \
|
|
||||||
-p schnorr-signatures \
|
|
||||||
-p prime-field \
|
|
||||||
-p short-weierstrass \
|
|
||||||
-p secq256k1 \
|
|
||||||
-p embedwards25519 \
|
|
||||||
-p dkg \
|
|
||||||
-p dkg-recovery \
|
|
||||||
-p dkg-dealer \
|
|
||||||
-p dkg-musig \
|
|
||||||
-p dkg-evrf \
|
|
||||||
-p modular-frost \
|
|
||||||
-p frost-schnorrkel
|
|
||||||
11
.github/workflows/daily-deny.yml
vendored
11
.github/workflows/daily-deny.yml
vendored
@@ -9,16 +9,19 @@ jobs:
|
|||||||
name: Run cargo deny
|
name: Run cargo deny
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Advisory Cache
|
- name: Advisory Cache
|
||||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809
|
uses: actions/cache@v3
|
||||||
with:
|
with:
|
||||||
path: ~/.cargo/advisory-db
|
path: ~/.cargo/advisory-db
|
||||||
key: rust-advisory-db
|
key: rust-advisory-db
|
||||||
|
|
||||||
|
- name: Install cargo
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
|
||||||
- name: Install cargo deny
|
- name: Install cargo deny
|
||||||
run: cargo +1.91 install cargo-deny --version =0.18.5
|
run: cargo install --locked cargo-deny
|
||||||
|
|
||||||
- name: Run cargo deny
|
- name: Run cargo deny
|
||||||
run: cargo deny -L error --all-features check --hide-inclusion-graph
|
run: cargo deny -L error --all-features check
|
||||||
|
|||||||
22
.github/workflows/full-stack-tests.yml
vendored
22
.github/workflows/full-stack-tests.yml
vendored
@@ -1,22 +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
|
|
||||||
|
|
||||||
- name: Run Full Stack Docker tests
|
|
||||||
run: GITHUB_CI=true RUST_BACKTRACE=1 cargo test --all-features -p serai-full-stack-tests
|
|
||||||
209
.github/workflows/lint.yml
vendored
209
.github/workflows/lint.yml
vendored
@@ -1,209 +0,0 @@
|
|||||||
name: Lint
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- develop
|
|
||||||
pull_request:
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
clippy:
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
os: [ubuntu-latest, macos-15-intel, macos-latest, windows-latest]
|
|
||||||
runs-on: ${{ matrix.os }}
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac
|
|
||||||
|
|
||||||
- name: Get nightly version to use
|
|
||||||
id: nightly
|
|
||||||
shell: bash
|
|
||||||
run: echo "version=$(cat .github/nightly-version)" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Build Dependencies
|
|
||||||
uses: ./.github/actions/build-dependencies
|
|
||||||
|
|
||||||
- name: Install nightly rust
|
|
||||||
run: rustup toolchain install ${{ steps.nightly.outputs.version }} --profile minimal -t wasm32v1-none -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
|
|
||||||
shell: bash
|
|
||||||
run: git diff | wc -l | LC_ALL="en_US.utf8" grep -x -e "^[ ]*0"
|
|
||||||
|
|
||||||
deny:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac
|
|
||||||
|
|
||||||
- name: Advisory Cache
|
|
||||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809
|
|
||||||
with:
|
|
||||||
path: ~/.cargo/advisory-db
|
|
||||||
key: rust-advisory-db
|
|
||||||
|
|
||||||
- name: Install cargo deny
|
|
||||||
run: cargo +1.91 install cargo-deny --version =0.18.5
|
|
||||||
|
|
||||||
- name: Run cargo deny
|
|
||||||
run: cargo deny -L error --all-features check --hide-inclusion-graph
|
|
||||||
|
|
||||||
fmt:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac
|
|
||||||
|
|
||||||
- name: Get nightly version to use
|
|
||||||
id: nightly
|
|
||||||
shell: bash
|
|
||||||
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
|
|
||||||
|
|
||||||
- name: Install foundry
|
|
||||||
uses: foundry-rs/foundry-toolchain@8f1998e9878d786675189ef566a2e4bf24869773
|
|
||||||
with:
|
|
||||||
version: nightly-41d4e5437107f6f42c7711123890147bc736a609
|
|
||||||
cache: false
|
|
||||||
|
|
||||||
- name: Run forge fmt
|
|
||||||
run: FOUNDRY_FMT_SORT_INPUTS=false FOUNDRY_FMT_LINE_LENGTH=100 FOUNDRY_FMT_TAB_WIDTH=2 FOUNDRY_FMT_BRACKET_SPACING=true FOUNDRY_FMT_INT_TYPES=preserve forge fmt --check $(find . -iname "*.sol")
|
|
||||||
|
|
||||||
machete:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac
|
|
||||||
- name: Verify all dependencies are in use
|
|
||||||
run: |
|
|
||||||
cargo +1.91 install cargo-machete --version =0.9.1
|
|
||||||
cargo +1.91 machete
|
|
||||||
|
|
||||||
msrv:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac
|
|
||||||
- name: Verify claimed `rust-version`
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
cargo +1.91 install cargo-msrv --version =0.18.4
|
|
||||||
|
|
||||||
function check_msrv {
|
|
||||||
# We `cd` into the directory passed as the first argument, but will return to the
|
|
||||||
# directory called from.
|
|
||||||
return_to=$(pwd)
|
|
||||||
echo "Checking $1"
|
|
||||||
cd $1
|
|
||||||
|
|
||||||
# We then find the existing `rust-version` using `grep` (for the right line) and then a
|
|
||||||
# regex (to strip to just the major and minor version).
|
|
||||||
existing=$(cat ./Cargo.toml | grep "rust-version" | grep -Eo "[0-9]+\.[0-9]+")
|
|
||||||
|
|
||||||
# We then backup the `Cargo.toml`, allowing us to restore it after, saving time on future
|
|
||||||
# MSRV checks (as they'll benefit from immediately exiting if the queried version is less
|
|
||||||
# than the declared MSRV).
|
|
||||||
mv ./Cargo.toml ./Cargo.toml.bak
|
|
||||||
|
|
||||||
# We then use an inverted (`-v`) grep to remove the existing `rust-version` from the
|
|
||||||
# `Cargo.toml`, as required because else earlier versions of Rust won't even attempt to
|
|
||||||
# compile this crate.
|
|
||||||
cat ./Cargo.toml.bak | grep -v "rust-version" > Cargo.toml
|
|
||||||
|
|
||||||
# We then find the actual `rust-version` using `cargo-msrv` (again stripping to just the
|
|
||||||
# major and minor version).
|
|
||||||
actual=$(cargo msrv find --output-format minimal | grep -Eo "^[0-9]+\.[0-9]+")
|
|
||||||
|
|
||||||
# Finally, we compare the two.
|
|
||||||
echo "Declared rust-version: $existing"
|
|
||||||
echo "Actual rust-version: $actual"
|
|
||||||
[ $existing == $actual ]
|
|
||||||
result=$?
|
|
||||||
|
|
||||||
# Restore the original `Cargo.toml`.
|
|
||||||
rm Cargo.toml
|
|
||||||
mv ./Cargo.toml.bak ./Cargo.toml
|
|
||||||
|
|
||||||
# Return to the directory called from and return the result.
|
|
||||||
cd $return_to
|
|
||||||
return $result
|
|
||||||
}
|
|
||||||
|
|
||||||
# Check each member of the workspace
|
|
||||||
function check_workspace {
|
|
||||||
# Get the members array from the workspace's `Cargo.toml`
|
|
||||||
cargo_toml_lines=$(cat ./Cargo.toml | wc -l)
|
|
||||||
# Keep all lines after the start of the array, then keep all lines before the next "]"
|
|
||||||
members=$(cat Cargo.toml | grep "members\ \=\ \[" -m1 -A$cargo_toml_lines | grep "]" -m1 -B$cargo_toml_lines)
|
|
||||||
|
|
||||||
# Parse out any comments, whitespace, including comments post-fixed on the same line as an entry
|
|
||||||
# We accomplish the latter by pruning all characters after the entry's ","
|
|
||||||
members=$(echo "$members" | grep -Ev "^[[:space:]]*(#|$)" | awk -F',' '{print $1","}')
|
|
||||||
# Replace the first line, which was "members = [" and is now "members = [,", with "["
|
|
||||||
members=$(echo "$members" | sed "1s/.*/\[/")
|
|
||||||
# Correct the last line, which was malleated to "],"
|
|
||||||
members=$(echo "$members" | sed "$(echo "$members" | wc -l)s/\]\,/\]/")
|
|
||||||
|
|
||||||
# Don't check the following
|
|
||||||
# Most of these are binaries, with the exception of the Substrate runtime which has a
|
|
||||||
# bespoke build pipeline
|
|
||||||
members=$(echo "$members" | grep -v "networks/ethereum/relayer\"")
|
|
||||||
members=$(echo "$members" | grep -v "message-queue\"")
|
|
||||||
members=$(echo "$members" | grep -v "processor/bin\"")
|
|
||||||
members=$(echo "$members" | grep -v "processor/bitcoin\"")
|
|
||||||
members=$(echo "$members" | grep -v "processor/ethereum\"")
|
|
||||||
members=$(echo "$members" | grep -v "processor/monero\"")
|
|
||||||
members=$(echo "$members" | grep -v "coordinator\"")
|
|
||||||
members=$(echo "$members" | grep -v "substrate/runtime\"")
|
|
||||||
members=$(echo "$members" | grep -v "substrate/node\"")
|
|
||||||
members=$(echo "$members" | grep -v "orchestration\"")
|
|
||||||
|
|
||||||
# Don't check the tests
|
|
||||||
members=$(echo "$members" | grep -v "mini\"")
|
|
||||||
members=$(echo "$members" | grep -v "tests/")
|
|
||||||
|
|
||||||
# Remove the trailing comma by replacing the last line's "," with ""
|
|
||||||
members=$(echo "$members" | sed "$(($(echo "$members" | wc -l) - 1))s/\,//")
|
|
||||||
|
|
||||||
echo $members | jq -r ".[]" | while read -r member; do
|
|
||||||
check_msrv $member
|
|
||||||
correct=$?
|
|
||||||
if [ $correct -ne 0 ]; then
|
|
||||||
return $correct
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
}
|
|
||||||
check_workspace
|
|
||||||
|
|
||||||
slither:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac
|
|
||||||
|
|
||||||
- name: Build Dependencies
|
|
||||||
uses: ./.github/actions/build-dependencies
|
|
||||||
|
|
||||||
- name: Slither
|
|
||||||
run: |
|
|
||||||
python3 -m pip install slither-analyzer
|
|
||||||
|
|
||||||
slither --include-paths ./networks/ethereum/schnorr/contracts/Schnorr.sol
|
|
||||||
slither --include-paths ./networks/ethereum/schnorr/contracts ./networks/ethereum/schnorr/contracts/tests/Schnorr.sol
|
|
||||||
slither processor/ethereum/deployer/contracts/Deployer.sol
|
|
||||||
slither processor/ethereum/erc20/contracts/IERC20.sol
|
|
||||||
|
|
||||||
cp networks/ethereum/schnorr/contracts/Schnorr.sol processor/ethereum/router/contracts/
|
|
||||||
cp processor/ethereum/erc20/contracts/IERC20.sol processor/ethereum/router/contracts/
|
|
||||||
cd processor/ethereum/router/contracts
|
|
||||||
slither Router.sol
|
|
||||||
36
.github/workflows/message-queue-tests.yml
vendored
36
.github/workflows/message-queue-tests.yml
vendored
@@ -1,36 +0,0 @@
|
|||||||
name: Message Queue Tests
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- develop
|
|
||||||
paths:
|
|
||||||
- "common/**"
|
|
||||||
- "crypto/**"
|
|
||||||
- "message-queue/**"
|
|
||||||
- "orchestration/**"
|
|
||||||
- "tests/docker/**"
|
|
||||||
- "tests/message-queue/**"
|
|
||||||
|
|
||||||
pull_request:
|
|
||||||
paths:
|
|
||||||
- "common/**"
|
|
||||||
- "crypto/**"
|
|
||||||
- "message-queue/**"
|
|
||||||
- "orchestration/**"
|
|
||||||
- "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
|
|
||||||
|
|
||||||
- name: Run message-queue Docker tests
|
|
||||||
run: GITHUB_CI=true RUST_BACKTRACE=1 cargo test --all-features -p serai-message-queue-tests
|
|
||||||
26
.github/workflows/mini-tests.yml
vendored
26
.github/workflows/mini-tests.yml
vendored
@@ -1,26 +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
|
|
||||||
|
|
||||||
- name: Run Tests
|
|
||||||
run: GITHUB_CI=true RUST_BACKTRACE=1 cargo test --all-features -p mini-serai
|
|
||||||
59
.github/workflows/monero-tests.yaml
vendored
Normal file
59
.github/workflows/monero-tests.yaml
vendored
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
name: Monero Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
paths:
|
||||||
|
- "coins/monero/**"
|
||||||
|
- "processor/**"
|
||||||
|
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- "coins/monero/**"
|
||||||
|
- "processor/**"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# Only run these once since they will be consistent regardless of any node
|
||||||
|
unit-tests:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Test Dependencies
|
||||||
|
uses: ./.github/actions/test-dependencies
|
||||||
|
with:
|
||||||
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Run Unit Tests Without Features
|
||||||
|
run: 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@v3
|
||||||
|
|
||||||
|
- 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: 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: |
|
||||||
|
cargo test --package monero-serai --all-features --test '*'
|
||||||
|
cargo test --package serai-processor --all-features monero
|
||||||
4
.github/workflows/monthly-nightly-update.yml
vendored
4
.github/workflows/monthly-nightly-update.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
|||||||
name: Update nightly
|
name: Update nightly
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac
|
- uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
submodules: "recursive"
|
submodules: "recursive"
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@ jobs:
|
|||||||
git push -u origin $(date +"nightly-%Y-%m")
|
git push -u origin $(date +"nightly-%Y-%m")
|
||||||
|
|
||||||
- name: Pull Request
|
- name: Pull Request
|
||||||
uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410
|
uses: actions/github-script@v6
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const { repo, owner } = context.repo;
|
const { repo, owner } = context.repo;
|
||||||
|
|||||||
36
.github/workflows/networks-tests.yml
vendored
36
.github/workflows/networks-tests.yml
vendored
@@ -1,36 +0,0 @@
|
|||||||
name: networks/ Tests
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- develop
|
|
||||||
paths:
|
|
||||||
- "common/**"
|
|
||||||
- "crypto/**"
|
|
||||||
- "networks/**"
|
|
||||||
|
|
||||||
pull_request:
|
|
||||||
paths:
|
|
||||||
- "common/**"
|
|
||||||
- "crypto/**"
|
|
||||||
- "networks/**"
|
|
||||||
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
test-networks:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac
|
|
||||||
|
|
||||||
- name: Test Dependencies
|
|
||||||
uses: ./.github/actions/test-dependencies
|
|
||||||
|
|
||||||
- name: Run Tests
|
|
||||||
run: |
|
|
||||||
GITHUB_CI=true RUST_BACKTRACE=1 cargo test --all-features \
|
|
||||||
-p bitcoin-serai \
|
|
||||||
-p build-solidity-contracts \
|
|
||||||
-p ethereum-schnorr-contract \
|
|
||||||
-p alloy-simple-request-transport \
|
|
||||||
-p serai-ethereum-relayer \
|
|
||||||
32
.github/workflows/no-std.yml
vendored
32
.github/workflows/no-std.yml
vendored
@@ -4,42 +4,18 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- develop
|
- develop
|
||||||
paths:
|
|
||||||
- "common/**"
|
|
||||||
- "crypto/**"
|
|
||||||
- "networks/**"
|
|
||||||
- "tests/no-std/**"
|
|
||||||
|
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
|
||||||
- "common/**"
|
|
||||||
- "crypto/**"
|
|
||||||
- "networks/**"
|
|
||||||
- "tests/no-std/**"
|
|
||||||
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Install Build Dependencies
|
- name: Install Build Dependencies
|
||||||
uses: ./.github/actions/build-dependencies
|
uses: ./.github/actions/build-dependencies
|
||||||
|
with:
|
||||||
- name: Get nightly version to use
|
github-token: ${{ inputs.github-token }}
|
||||||
id: nightly
|
|
||||||
shell: bash
|
|
||||||
run: echo "version=$(cat .github/nightly-version)" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Install RISC-V Toolchain
|
|
||||||
run: |
|
|
||||||
sudo apt update
|
|
||||||
sudo apt install -y gcc-riscv64-unknown-elf gcc-multilib
|
|
||||||
rustup toolchain install ${{ steps.nightly.outputs.version }} --profile minimal --component rust-src --target riscv32imac-unknown-none-elf
|
|
||||||
|
|
||||||
- name: Verify no-std builds
|
- name: Verify no-std builds
|
||||||
run: |
|
run: cd tests/no-std && cargo build --target riscv32imac-unknown-none-elf
|
||||||
CFLAGS=-I/usr/include cargo +${{ steps.nightly.outputs.version }} build --target riscv32imac-unknown-none-elf -Z build-std=core -p serai-no-std-tests
|
|
||||||
CFLAGS=-I/usr/include cargo +${{ steps.nightly.outputs.version }} build --target riscv32imac-unknown-none-elf -Z build-std=core,alloc -p serai-no-std-tests --features "alloc"
|
|
||||||
|
|||||||
91
.github/workflows/pages.yml
vendored
91
.github/workflows/pages.yml
vendored
@@ -1,91 +0,0 @@
|
|||||||
# MIT License
|
|
||||||
#
|
|
||||||
# Copyright (c) 2022 just-the-docs
|
|
||||||
# Copyright (c) 2022-2024 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.
|
|
||||||
|
|
||||||
name: Deploy Rust docs and Jekyll site to Pages
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- "develop"
|
|
||||||
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
pages: write
|
|
||||||
id-token: write
|
|
||||||
|
|
||||||
# Only allow one concurrent deployment
|
|
||||||
concurrency:
|
|
||||||
group: "pages"
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
# Build job
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac
|
|
||||||
- name: Setup Ruby
|
|
||||||
uses: ruby/setup-ruby@44511735964dcb71245e7e55f72539531f7bc0eb
|
|
||||||
with:
|
|
||||||
bundler-cache: true
|
|
||||||
cache-version: 0
|
|
||||||
working-directory: "${{ github.workspace }}/docs"
|
|
||||||
- name: Setup Pages
|
|
||||||
id: pages
|
|
||||||
uses: actions/configure-pages@983d7736d9b0ae728b81ab479565c72886d7745b
|
|
||||||
- name: Build with Jekyll
|
|
||||||
run: cd ${{ github.workspace }}/docs && bundle exec jekyll build --baseurl "${{ steps.pages.outputs.base_path }}"
|
|
||||||
env:
|
|
||||||
JEKYLL_ENV: production
|
|
||||||
|
|
||||||
- name: Get nightly version to use
|
|
||||||
id: nightly
|
|
||||||
shell: bash
|
|
||||||
run: echo "version=$(cat .github/nightly-version)" >> $GITHUB_OUTPUT
|
|
||||||
- name: Build Dependencies
|
|
||||||
uses: ./.github/actions/build-dependencies
|
|
||||||
- name: Buld Rust docs
|
|
||||||
run: |
|
|
||||||
rustup toolchain install ${{ steps.nightly.outputs.version }} --profile minimal -t wasm32v1-none -c rust-docs
|
|
||||||
RUSTDOCFLAGS="--cfg docsrs" cargo +${{ steps.nightly.outputs.version }} doc --workspace --no-deps --all-features
|
|
||||||
mv target/doc docs/_site/rust
|
|
||||||
|
|
||||||
- name: Upload artifact
|
|
||||||
uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b
|
|
||||||
with:
|
|
||||||
path: "docs/_site/"
|
|
||||||
|
|
||||||
# Deployment job
|
|
||||||
deploy:
|
|
||||||
environment:
|
|
||||||
name: github-pages
|
|
||||||
url: ${{ steps.deployment.outputs.page_url }}
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: build
|
|
||||||
steps:
|
|
||||||
- name: Deploy to GitHub Pages
|
|
||||||
id: deployment
|
|
||||||
uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e
|
|
||||||
40
.github/workflows/processor-tests.yml
vendored
40
.github/workflows/processor-tests.yml
vendored
@@ -1,40 +0,0 @@
|
|||||||
name: Processor Tests
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- develop
|
|
||||||
paths:
|
|
||||||
- "common/**"
|
|
||||||
- "crypto/**"
|
|
||||||
- "networks/**"
|
|
||||||
- "message-queue/**"
|
|
||||||
- "processor/**"
|
|
||||||
- "orchestration/**"
|
|
||||||
- "tests/docker/**"
|
|
||||||
- "tests/processor/**"
|
|
||||||
|
|
||||||
pull_request:
|
|
||||||
paths:
|
|
||||||
- "common/**"
|
|
||||||
- "crypto/**"
|
|
||||||
- "networks/**"
|
|
||||||
- "message-queue/**"
|
|
||||||
- "processor/**"
|
|
||||||
- "orchestration/**"
|
|
||||||
- "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
|
|
||||||
|
|
||||||
- name: Run processor Docker tests
|
|
||||||
run: GITHUB_CI=true RUST_BACKTRACE=1 cargo test --all-features -p serai-processor-tests
|
|
||||||
36
.github/workflows/reproducible-runtime.yml
vendored
36
.github/workflows/reproducible-runtime.yml
vendored
@@ -1,36 +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
|
|
||||||
|
|
||||||
- name: Run Reproducible Runtime tests
|
|
||||||
run: GITHUB_CI=true RUST_BACKTRACE=1 cargo test --all-features -p serai-reproducible-runtime-tests
|
|
||||||
145
.github/workflows/tests.yml
vendored
145
.github/workflows/tests.yml
vendored
@@ -4,111 +4,82 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- develop
|
- develop
|
||||||
paths:
|
|
||||||
- "common/**"
|
|
||||||
- "crypto/**"
|
|
||||||
- "networks/**"
|
|
||||||
- "message-queue/**"
|
|
||||||
- "processor/**"
|
|
||||||
- "coordinator/**"
|
|
||||||
- "substrate/**"
|
|
||||||
|
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
|
||||||
- "common/**"
|
|
||||||
- "crypto/**"
|
|
||||||
- "networks/**"
|
|
||||||
- "message-queue/**"
|
|
||||||
- "processor/**"
|
|
||||||
- "coordinator/**"
|
|
||||||
- "substrate/**"
|
|
||||||
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test-infra:
|
clippy:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Get nightly version to use
|
||||||
|
id: nightly
|
||||||
|
run: echo "version=$(cat .github/nightly-version)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Build Dependencies
|
- name: Build Dependencies
|
||||||
uses: ./.github/actions/build-dependencies
|
uses: ./.github/actions/build-dependencies
|
||||||
|
with:
|
||||||
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
rust-toolchain: ${{ steps.nightly.outputs.version }}
|
||||||
|
rust-components: clippy
|
||||||
|
|
||||||
- name: Run Tests
|
- name: Run Clippy
|
||||||
run: |
|
# Allow dbg_macro when run locally, yet not when pushed
|
||||||
GITHUB_CI=true RUST_BACKTRACE=1 cargo test --all-features \
|
run: cargo clippy --all-features --all-targets -- -D clippy::dbg_macro $(grep "\S" ../../clippy-config | grep -v "#")
|
||||||
-p serai-message-queue \
|
|
||||||
-p serai-processor-messages \
|
|
||||||
-p serai-processor-key-gen \
|
|
||||||
-p serai-processor-view-keys \
|
|
||||||
-p serai-processor-frost-attempt-manager \
|
|
||||||
-p serai-processor-primitives \
|
|
||||||
-p serai-processor-scanner \
|
|
||||||
-p serai-processor-scheduler-primitives \
|
|
||||||
-p serai-processor-utxo-scheduler-primitives \
|
|
||||||
-p serai-processor-utxo-scheduler \
|
|
||||||
-p serai-processor-transaction-chaining-scheduler \
|
|
||||||
-p serai-processor-smart-contract-scheduler \
|
|
||||||
-p serai-processor-signers \
|
|
||||||
-p serai-processor-bin \
|
|
||||||
-p serai-bitcoin-processor \
|
|
||||||
-p serai-processor-ethereum-primitives \
|
|
||||||
-p serai-processor-ethereum-test-primitives \
|
|
||||||
-p serai-processor-ethereum-deployer \
|
|
||||||
-p serai-processor-ethereum-router \
|
|
||||||
-p serai-processor-ethereum-erc20 \
|
|
||||||
-p serai-ethereum-processor \
|
|
||||||
-p serai-monero-processor \
|
|
||||||
-p tendermint-machine \
|
|
||||||
-p tributary-sdk \
|
|
||||||
-p serai-cosign-types \
|
|
||||||
-p serai-cosign \
|
|
||||||
-p serai-coordinator-substrate \
|
|
||||||
-p serai-coordinator-tributary \
|
|
||||||
-p serai-coordinator-p2p \
|
|
||||||
-p serai-coordinator-libp2p-p2p \
|
|
||||||
-p serai-coordinator \
|
|
||||||
-p serai-orchestrator \
|
|
||||||
-p serai-docker-tests
|
|
||||||
|
|
||||||
test-substrate:
|
deny:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Build Dependencies
|
- name: Advisory Cache
|
||||||
uses: ./.github/actions/build-dependencies
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: ~/.cargo/advisory-db
|
||||||
|
key: rust-advisory-db
|
||||||
|
|
||||||
- name: Run Tests
|
- name: Install cargo
|
||||||
run: |
|
uses: dtolnay/rust-toolchain@stable
|
||||||
GITHUB_CI=true RUST_BACKTRACE=1 cargo test --all-features \
|
|
||||||
-p serai-primitives \
|
|
||||||
-p serai-abi \
|
|
||||||
-p serai-core-pallet \
|
|
||||||
-p serai-coins-pallet \
|
|
||||||
-p serai-validator-sets-pallet \
|
|
||||||
-p serai-signals-pallet \
|
|
||||||
-p serai-dex-pallet \
|
|
||||||
-p serai-genesis-liquidity-pallet \
|
|
||||||
-p serai-economic-security-pallet \
|
|
||||||
-p serai-emissions-pallet \
|
|
||||||
-p serai-in-instructions-pallet \
|
|
||||||
-p serai-runtime \
|
|
||||||
-p serai-node
|
|
||||||
-p serai-substrate-tests
|
|
||||||
|
|
||||||
test-serai-client:
|
- name: Install cargo deny
|
||||||
|
run: cargo install --locked cargo-deny
|
||||||
|
|
||||||
|
- name: Run cargo deny
|
||||||
|
run: cargo deny -L error --all-features check
|
||||||
|
|
||||||
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Build Dependencies
|
- name: Test Dependencies
|
||||||
uses: ./.github/actions/build-dependencies
|
uses: ./.github/actions/test-dependencies
|
||||||
|
with:
|
||||||
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build node
|
||||||
|
run: |
|
||||||
|
cd substrate/node
|
||||||
|
cargo build
|
||||||
|
|
||||||
- name: Run Tests
|
- name: Run Tests
|
||||||
run: |
|
run: GITHUB_CI=true cargo test --all-features
|
||||||
GITHUB_CI=true RUST_BACKTRACE=1 cargo test --all-features -p serai-client-serai
|
|
||||||
GITHUB_CI=true RUST_BACKTRACE=1 cargo test --all-features -p serai-client-bitcoin
|
fmt:
|
||||||
GITHUB_CI=true RUST_BACKTRACE=1 cargo test --all-features -p serai-client-ethereum
|
runs-on: ubuntu-latest
|
||||||
GITHUB_CI=true RUST_BACKTRACE=1 cargo test --all-features -p serai-client-monero
|
steps:
|
||||||
GITHUB_CI=true RUST_BACKTRACE=1 cargo test --all-features -p serai-client
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Get nightly version to use
|
||||||
|
id: nightly
|
||||||
|
run: echo "version=$(cat .github/nightly-version)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Install rustfmt
|
||||||
|
uses: dtolnay/rust-toolchain@master
|
||||||
|
with:
|
||||||
|
toolchain: ${{ steps.nightly.outputs.version }}
|
||||||
|
components: rustfmt
|
||||||
|
|
||||||
|
- name: Run rustfmt
|
||||||
|
run: cargo +${{ steps.nightly.outputs.version }} fmt -- --check
|
||||||
|
|||||||
11
.gitignore
vendored
11
.gitignore
vendored
@@ -1,13 +1,2 @@
|
|||||||
target
|
target
|
||||||
|
|
||||||
# Don't commit any `Cargo.lock` which aren't the workspace's
|
|
||||||
Cargo.lock
|
|
||||||
!/Cargo.lock
|
|
||||||
|
|
||||||
# Don't commit any `Dockerfile`, as they're auto-generated, except the only one which isn't
|
|
||||||
Dockerfile
|
|
||||||
!orchestration/runtime/Dockerfile
|
|
||||||
|
|
||||||
.test-logs
|
|
||||||
|
|
||||||
.vscode
|
.vscode
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
edition = "2021"
|
|
||||||
tab_spaces = 2
|
tab_spaces = 2
|
||||||
|
|
||||||
max_width = 100
|
max_width = 100
|
||||||
|
|||||||
12099
Cargo.lock
generated
12099
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
238
Cargo.toml
238
Cargo.toml
@@ -1,13 +1,8 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
resolver = "2"
|
|
||||||
members = [
|
members = [
|
||||||
"common/std-shims",
|
"common/std-shims",
|
||||||
"common/zalloc",
|
"common/zalloc",
|
||||||
"common/patchable-async-sleep",
|
|
||||||
"common/db",
|
"common/db",
|
||||||
"common/env",
|
|
||||||
"common/task",
|
|
||||||
"common/request",
|
|
||||||
|
|
||||||
"crypto/transcript",
|
"crypto/transcript",
|
||||||
|
|
||||||
@@ -15,258 +10,63 @@ members = [
|
|||||||
"crypto/dalek-ff-group",
|
"crypto/dalek-ff-group",
|
||||||
"crypto/ed448",
|
"crypto/ed448",
|
||||||
"crypto/ciphersuite",
|
"crypto/ciphersuite",
|
||||||
"crypto/ciphersuite/kp256",
|
|
||||||
|
|
||||||
"crypto/multiexp",
|
"crypto/multiexp",
|
||||||
|
|
||||||
"crypto/schnorr",
|
"crypto/schnorr",
|
||||||
|
"crypto/dleq",
|
||||||
"crypto/prime-field",
|
|
||||||
"crypto/short-weierstrass",
|
|
||||||
"crypto/secq256k1",
|
|
||||||
"crypto/embedwards25519",
|
|
||||||
|
|
||||||
"crypto/dkg",
|
"crypto/dkg",
|
||||||
"crypto/dkg/recovery",
|
|
||||||
"crypto/dkg/dealer",
|
|
||||||
"crypto/dkg/musig",
|
|
||||||
"crypto/dkg/evrf",
|
|
||||||
"crypto/frost",
|
"crypto/frost",
|
||||||
"crypto/schnorrkel",
|
"crypto/schnorrkel",
|
||||||
|
|
||||||
"networks/bitcoin",
|
"coins/ethereum",
|
||||||
|
"coins/monero/generators",
|
||||||
"networks/ethereum/build-contracts",
|
"coins/monero",
|
||||||
"networks/ethereum/schnorr",
|
|
||||||
"networks/ethereum/alloy-simple-request-transport",
|
|
||||||
"networks/ethereum/relayer",
|
|
||||||
|
|
||||||
"message-queue",
|
"message-queue",
|
||||||
|
|
||||||
"processor/messages",
|
"processor/messages",
|
||||||
|
"processor",
|
||||||
|
|
||||||
"processor/key-gen",
|
"coordinator/tributary/tendermint",
|
||||||
"processor/view-keys",
|
|
||||||
"processor/frost-attempt-manager",
|
|
||||||
|
|
||||||
"processor/primitives",
|
|
||||||
"processor/scanner",
|
|
||||||
"processor/scheduler/primitives",
|
|
||||||
"processor/scheduler/utxo/primitives",
|
|
||||||
"processor/scheduler/utxo/standard",
|
|
||||||
"processor/scheduler/utxo/transaction-chaining",
|
|
||||||
"processor/scheduler/smart-contract",
|
|
||||||
"processor/signers",
|
|
||||||
|
|
||||||
"processor/bin",
|
|
||||||
"processor/bitcoin",
|
|
||||||
"processor/ethereum/primitives",
|
|
||||||
"processor/ethereum/test-primitives",
|
|
||||||
"processor/ethereum/deployer",
|
|
||||||
"processor/ethereum/erc20",
|
|
||||||
"processor/ethereum/router",
|
|
||||||
"processor/ethereum",
|
|
||||||
"processor/monero",
|
|
||||||
|
|
||||||
"coordinator/tributary-sdk/tendermint",
|
|
||||||
"coordinator/tributary-sdk",
|
|
||||||
"coordinator/cosign/types",
|
|
||||||
"coordinator/cosign",
|
|
||||||
"coordinator/substrate",
|
|
||||||
"coordinator/tributary",
|
"coordinator/tributary",
|
||||||
"coordinator/p2p",
|
|
||||||
"coordinator/p2p/libp2p",
|
|
||||||
"coordinator",
|
"coordinator",
|
||||||
|
|
||||||
"substrate/primitives",
|
"substrate/primitives",
|
||||||
"substrate/abi",
|
|
||||||
|
|
||||||
"substrate/core",
|
"substrate/tokens/primitives",
|
||||||
"substrate/coins",
|
"substrate/tokens/pallet",
|
||||||
"substrate/validator-sets",
|
|
||||||
"substrate/signals",
|
"substrate/in-instructions/primitives",
|
||||||
"substrate/dex",
|
"substrate/in-instructions/pallet",
|
||||||
"substrate/genesis-liquidity",
|
|
||||||
"substrate/economic-security",
|
"substrate/validator-sets/primitives",
|
||||||
"substrate/emissions",
|
"substrate/validator-sets/pallet",
|
||||||
"substrate/in-instructions",
|
|
||||||
|
|
||||||
"substrate/runtime",
|
"substrate/runtime",
|
||||||
"substrate/node",
|
"substrate/node",
|
||||||
|
|
||||||
"substrate/client/serai",
|
|
||||||
"substrate/client/bitcoin",
|
|
||||||
"substrate/client/ethereum",
|
|
||||||
"substrate/client/monero",
|
|
||||||
"substrate/client",
|
"substrate/client",
|
||||||
|
|
||||||
"orchestration",
|
|
||||||
|
|
||||||
"mini",
|
|
||||||
|
|
||||||
"tests/no-std",
|
"tests/no-std",
|
||||||
|
|
||||||
"tests/docker",
|
|
||||||
"tests/message-queue",
|
|
||||||
# TODO "tests/processor",
|
|
||||||
# TODO "tests/coordinator",
|
|
||||||
"tests/substrate",
|
|
||||||
# TODO "tests/full-stack",
|
|
||||||
"tests/reproducible-runtime",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[profile.dev.package]
|
|
||||||
# Always compile Monero (and a variety of dependencies) with optimizations due
|
# Always compile Monero (and a variety of dependencies) with optimizations due
|
||||||
# to the extensive operations required for Bulletproofs
|
# to the extensive operations required for Bulletproofs
|
||||||
|
[profile.dev.package]
|
||||||
subtle = { opt-level = 3 }
|
subtle = { opt-level = 3 }
|
||||||
|
curve25519-dalek = { opt-level = 3 }
|
||||||
sha3 = { opt-level = 3 }
|
|
||||||
blake2 = { opt-level = 3 }
|
|
||||||
|
|
||||||
ff = { opt-level = 3 }
|
ff = { opt-level = 3 }
|
||||||
group = { opt-level = 3 }
|
group = { opt-level = 3 }
|
||||||
|
|
||||||
crypto-bigint = { opt-level = 3 }
|
crypto-bigint = { opt-level = 3 }
|
||||||
curve25519-dalek = { opt-level = 3 }
|
|
||||||
dalek-ff-group = { opt-level = 3 }
|
dalek-ff-group = { opt-level = 3 }
|
||||||
|
minimal-ed448 = { opt-level = 3 }
|
||||||
|
|
||||||
multiexp = { opt-level = 3 }
|
multiexp = { opt-level = 3 }
|
||||||
|
|
||||||
monero-io = { opt-level = 3 }
|
monero-serai = { opt-level = 3 }
|
||||||
monero-primitives = { opt-level = 3 }
|
|
||||||
monero-ed25519 = { opt-level = 3 }
|
|
||||||
monero-mlsag = { opt-level = 3 }
|
|
||||||
monero-clsag = { opt-level = 3 }
|
|
||||||
monero-borromean = { opt-level = 3 }
|
|
||||||
monero-bulletproofs-generators = { opt-level = 3 }
|
|
||||||
monero-bulletproofs = {opt-level = 3 }
|
|
||||||
monero-oxide = { opt-level = 3 }
|
|
||||||
|
|
||||||
# Always compile the eVRF DKG tree with optimizations as well
|
|
||||||
secp256k1 = { opt-level = 3 }
|
|
||||||
secq256k1 = { opt-level = 3 }
|
|
||||||
embedwards25519 = { opt-level = 3 }
|
|
||||||
generalized-bulletproofs = { opt-level = 3 }
|
|
||||||
generalized-bulletproofs-circuit-abstraction = { opt-level = 3 }
|
|
||||||
generalized-bulletproofs-ec-gadgets = { opt-level = 3 }
|
|
||||||
|
|
||||||
# revm also effectively requires being built with optimizations
|
|
||||||
revm = { opt-level = 3 }
|
|
||||||
revm-bytecode = { opt-level = 3 }
|
|
||||||
revm-context = { opt-level = 3 }
|
|
||||||
revm-context-interface = { opt-level = 3 }
|
|
||||||
revm-database = { opt-level = 3 }
|
|
||||||
revm-database-interface = { opt-level = 3 }
|
|
||||||
revm-handler = { opt-level = 3 }
|
|
||||||
revm-inspector = { opt-level = 3 }
|
|
||||||
revm-interpreter = { opt-level = 3 }
|
|
||||||
revm-precompile = { opt-level = 3 }
|
|
||||||
revm-primitives = { opt-level = 3 }
|
|
||||||
revm-state = { opt-level = 3 }
|
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
panic = "unwind"
|
panic = "unwind"
|
||||||
overflow-checks = true
|
|
||||||
|
|
||||||
[patch.crates-io]
|
|
||||||
# Point to empty crates for crates unused within in our tree
|
|
||||||
ark-ff-3 = { package = "ark-ff", path = "patches/ethereum/ark-ff-0.3" }
|
|
||||||
ark-ff-4 = { package = "ark-ff", path = "patches/ethereum/ark-ff-0.4" }
|
|
||||||
c-kzg = { path = "patches/ethereum/c-kzg" }
|
|
||||||
secp256k1-30 = { package = "secp256k1", path = "patches/ethereum/secp256k1-30" }
|
|
||||||
|
|
||||||
# Dependencies from monero-oxide which originate from within our own tree, potentially shimmed to account for deviations since publishing
|
|
||||||
std-shims = { path = "patches/std-shims" }
|
|
||||||
simple-request = { path = "patches/simple-request" }
|
|
||||||
multiexp = { path = "crypto/multiexp" }
|
|
||||||
flexible-transcript = { path = "crypto/transcript" }
|
|
||||||
ciphersuite = { path = "patches/ciphersuite" }
|
|
||||||
dalek-ff-group = { path = "patches/dalek-ff-group" }
|
|
||||||
minimal-ed448 = { path = "crypto/ed448" }
|
|
||||||
modular-frost = { path = "crypto/frost" }
|
|
||||||
|
|
||||||
# Patch due to `std` now including the required functionality
|
|
||||||
is_terminal_polyfill = { path = "./patches/is_terminal_polyfill" }
|
|
||||||
# This has a non-deprecated `std` alternative since Rust's 2024 edition
|
|
||||||
home = { path = "patches/home" }
|
|
||||||
|
|
||||||
# Updates to the latest version
|
|
||||||
darling = { path = "patches/darling" }
|
|
||||||
thiserror = { path = "patches/thiserror" }
|
|
||||||
|
|
||||||
# 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" }
|
|
||||||
|
|
||||||
# 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
|
|
||||||
# The following two patches resolve everything
|
|
||||||
option-ext = { path = "patches/option-ext" }
|
|
||||||
directories-next = { path = "patches/directories-next" }
|
|
||||||
|
|
||||||
# Patch from a fork back to upstream
|
|
||||||
parity-bip39 = { path = "patches/parity-bip39" }
|
|
||||||
|
|
||||||
# Patch to include `FromUniformBytes<64>` over `Scalar`
|
|
||||||
k256 = { git = "https://github.com/kayabaNerve/elliptic-curves", rev = "4994c9ab163781a88cd4a49beae812a89a44e8c3" }
|
|
||||||
p256 = { git = "https://github.com/kayabaNerve/elliptic-curves", rev = "4994c9ab163781a88cd4a49beae812a89a44e8c3" }
|
|
||||||
|
|
||||||
# `jemalloc` conflicts with `mimalloc`, so patch to a `rocksdb` which never uses `jemalloc`
|
|
||||||
librocksdb-sys = { path = "patches/librocksdb-sys" }
|
|
||||||
|
|
||||||
[workspace.lints.clippy]
|
|
||||||
incompatible_msrv = "allow" # Manually verified with a GitHub workflow
|
|
||||||
manual_is_multiple_of = "allow"
|
|
||||||
unwrap_or_default = "allow"
|
|
||||||
map_unwrap_or = "allow"
|
|
||||||
needless_continue = "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"
|
|
||||||
match_bool = "deny"
|
|
||||||
match_same_arms = "deny"
|
|
||||||
missing_fields_in_debug = "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"
|
|
||||||
string_slice = "deny"
|
|
||||||
unchecked_time_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"
|
|
||||||
|
|
||||||
[workspace.lints.rust]
|
|
||||||
unused = "allow" # TODO: https://github.com/rust-lang/rust/issues/147648
|
|
||||||
|
|||||||
2
LICENSE
2
LICENSE
@@ -5,4 +5,4 @@ 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
|
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.
|
crate licensed under the AGPL-3.0, as per its terms.
|
||||||
|
|
||||||
The GitHub actions/workflows (`.github`) are licensed under the MIT license.
|
The GitHub actions (`.github/actions`) are licensed under the MIT license.
|
||||||
|
|||||||
41
README.md
41
README.md
@@ -5,32 +5,26 @@ Bitcoin, Ethereum, DAI, and Monero, offering a liquidity-pool-based 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](spec/Getting%20Started.md)
|
[Getting Started](docs/Getting%20Started.md)
|
||||||
|
|
||||||
### Layout
|
### Layout
|
||||||
|
|
||||||
- `audits`: Audits for various parts of Serai.
|
- `audits`: Audits for various parts of Serai.
|
||||||
|
|
||||||
- `spec`: The specification of the Serai protocol, both internally and as
|
- `docs`: Documentation on the Serai protocol.
|
||||||
networked.
|
|
||||||
|
|
||||||
- `docs`: User-facing documentation on the Serai protocol.
|
|
||||||
|
|
||||||
- `common`: Crates containing utilities common to a variety of areas under
|
- `common`: Crates containing utilities common to a variety of areas under
|
||||||
Serai, none neatly fitting under another category.
|
Serai, none neatly fitting under another category.
|
||||||
|
|
||||||
- `crypto`: A series of composable cryptographic libraries built around the
|
- `crypto`: A series of composable cryptographic libraries built around the
|
||||||
`ff`/`group` APIs, achieving a variety of tasks. These range from generic
|
`ff`/`group` APIs achieving a variety of tasks. These range from generic
|
||||||
infrastructure, to our IETF-compliant FROST implementation, to a DLEq proof as
|
infrastructure, to our IETF-compliant FROST implementation, to a DLEq proof as
|
||||||
needed for Bitcoin-Monero atomic swaps.
|
needed for Bitcoin-Monero atomic swaps.
|
||||||
|
|
||||||
- `networks`: Various libraries intended for usage in Serai yet also by the
|
- `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,
|
|
||||||
even when the other is offline.
|
|
||||||
|
|
||||||
- `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.
|
||||||
|
|
||||||
@@ -39,27 +33,12 @@ wallet.
|
|||||||
|
|
||||||
- `substrate`: Substrate crates used to instantiate the Serai network.
|
- `substrate`: Substrate crates used to instantiate the Serai network.
|
||||||
|
|
||||||
- `orchestration`: Dockerfiles and scripts to deploy a Serai node/test
|
- `deploy`: Scripts to deploy a Serai node/test environment.
|
||||||
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
|
### Links
|
||||||
|
|
||||||
- [Website](https://serai.exchange/): https://serai.exchange/
|
- [Twitter](https://twitter.com/SeraiDEX): https://twitter.com/SeraiDEX
|
||||||
- [Immunefi](https://immunefi.com/bounty/serai/): https://immunefi.com/bounty/serai/
|
- [Mastodon](https://cryptodon.lol/@serai): https://cryptodon.lol/@serai
|
||||||
- [Twitter](https://twitter.com/SeraiDEX): https://twitter.com/SeraiDEX
|
- [Discord](https://discord.gg/mpEUtJR3vz): https://discord.gg/mpEUtJR3vz
|
||||||
- [Discord](https://discord.gg/mpEUtJR3vz): https://discord.gg/mpEUtJR3vz
|
- [Matrix](https://matrix.to/#/#serai:matrix.org):
|
||||||
- [Matrix](https://matrix.to/#/#serai:matrix.org): 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,7 +0,0 @@
|
|||||||
# Cypher Stack /networks/bitcoin Audit, August 2023
|
|
||||||
|
|
||||||
This audit was over the `/networks/bitcoin` folder (at the time located at
|
|
||||||
`/coins/bitcoin`). It is encompassing up to commit
|
|
||||||
5121ca75199dff7bd34230880a1fdd793012068c.
|
|
||||||
|
|
||||||
Please see https://github.com/cypherstack/serai-btc-audit for provenance.
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
# Trail of Bits Ethereum Contracts Audit, June 2025
|
|
||||||
|
|
||||||
This audit included:
|
|
||||||
- Our Schnorr contract and associated library (/networks/ethereum/schnorr)
|
|
||||||
- Our Ethereum primitives library (/processor/ethereum/primitives)
|
|
||||||
- Our Deployer contract and associated library (/processor/ethereum/deployer)
|
|
||||||
- Our ERC20 library (/processor/ethereum/erc20)
|
|
||||||
- Our Router contract and associated library (/processor/ethereum/router)
|
|
||||||
|
|
||||||
It is encompassing up to commit 4e0c58464fc4673623938335f06e2e9ea96ca8dd.
|
|
||||||
|
|
||||||
Please see
|
|
||||||
https://github.com/trailofbits/publications/blob/30c4fa3ebf39ff8e4d23ba9567344ec9691697b5/reviews/2025-04-serai-dex-security-review.pdf
|
|
||||||
for the actual report.
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
# eVRF DKG
|
|
||||||
|
|
||||||
In 2024, the [eVRF paper](https://eprint.iacr.org/2024/397) was published to
|
|
||||||
the IACR preprint server. Within it was a one-round unbiased DKG and a
|
|
||||||
one-round unbiased threshold DKG. Unfortunately, both simply describe
|
|
||||||
communication of the secret shares as 'Alice sends $s_b$ to Bob'. This causes,
|
|
||||||
in practice, the need for an additional round of communication to occur where
|
|
||||||
all participants confirm they received their secret shares.
|
|
||||||
|
|
||||||
Within Serai, it was posited to use the same premises as the DDH eVRF itself to
|
|
||||||
achieve a verifiable encryption scheme. This allows the secret shares to be
|
|
||||||
posted to any 'bulletin board' (such as a blockchain) and for all observers to
|
|
||||||
confirm:
|
|
||||||
|
|
||||||
- A participant participated
|
|
||||||
- The secret shares sent can be received by the intended recipient so long as
|
|
||||||
they can access the bulletin board
|
|
||||||
|
|
||||||
Additionally, Serai desired a robust scheme (albeit with an biased key as the
|
|
||||||
output, which is fine for our purposes). Accordingly, our implementation
|
|
||||||
instantiates the threshold eVRF DKG from the eVRF paper, with our own proposal
|
|
||||||
for verifiable encryption, with the caller allowed to decide the set of
|
|
||||||
participants. They may:
|
|
||||||
|
|
||||||
- Select everyone, collapsing to the non-threshold unbiased DKG from the eVRF
|
|
||||||
paper
|
|
||||||
- Select a pre-determined set, collapsing to the threshold unbaised DKG from
|
|
||||||
the eVRF paper
|
|
||||||
- Select a post-determined set (with any solution for the Common Subset
|
|
||||||
problem), allowing achieving a robust threshold biased DKG
|
|
||||||
|
|
||||||
Note that the eVRF paper proposes using the eVRF to sample coefficients yet
|
|
||||||
this is unnecessary when the resulting key will be biased. Any proof of
|
|
||||||
knowledge for the coefficients, as necessary for their extraction within the
|
|
||||||
security proofs, would be sufficient.
|
|
||||||
|
|
||||||
MAGIC Grants contracted HashCloak to formalize Serai's proposal for a DKG and
|
|
||||||
provide proofs for its security. This resulted in
|
|
||||||
[this paper](<./Security Proofs.pdf>).
|
|
||||||
|
|
||||||
Our implementation itself is then built on top of the audited
|
|
||||||
[`generalized-bulletproofs`](https://github.com/kayabaNerve/monero-oxide/tree/generalized-bulletproofs/audits/crypto/generalized-bulletproofs)
|
|
||||||
and
|
|
||||||
[`generalized-bulletproofs-ec-gadgets`](https://github.com/monero-oxide/monero-oxide/tree/fcmp%2B%2B/audits/fcmps).
|
|
||||||
|
|
||||||
Note we do not use the originally premised DDH eVRF yet the one premised on
|
|
||||||
elliptic curve divisors, the methodology of which is commented on
|
|
||||||
[here](https://github.com/monero-oxide/monero-oxide/tree/fcmp%2B%2B/audits/divisors).
|
|
||||||
|
|
||||||
Our implementation itself is unaudited at this time however.
|
|
||||||
Binary file not shown.
51
clippy-config
Normal file
51
clippy-config
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
# No warnings allowed
|
||||||
|
-D warnings
|
||||||
|
|
||||||
|
# nursery
|
||||||
|
-D clippy::nursery
|
||||||
|
# Erratic and unhelpful
|
||||||
|
-A clippy::missing_const_for_fn
|
||||||
|
# Too many false/irrelevant positives
|
||||||
|
-A clippy::redundant_pub_crate
|
||||||
|
# Flags on any debug_assert using an RNG
|
||||||
|
-A clippy::debug_assert_with_mut_call
|
||||||
|
# Stylistic preference
|
||||||
|
-A clippy::option_if_let_else
|
||||||
|
|
||||||
|
# pedantic
|
||||||
|
-D clippy::unnecessary_wraps
|
||||||
|
-D clippy::unused_async
|
||||||
|
-D clippy::unused_self
|
||||||
|
|
||||||
|
# restrictions
|
||||||
|
|
||||||
|
# Safety
|
||||||
|
-D clippy::as_conversions
|
||||||
|
-D clippy::disallowed_script_idents
|
||||||
|
-D clippy::wildcard_enum_match_arm
|
||||||
|
|
||||||
|
# Clarity
|
||||||
|
-D clippy::assertions_on_result_states
|
||||||
|
-D clippy::deref_by_slicing
|
||||||
|
-D clippy::empty_structs_with_brackets
|
||||||
|
-D clippy::get_unwrap
|
||||||
|
-D clippy::rest_pat_in_fully_bound_structs
|
||||||
|
-D clippy::semicolon_inside_block
|
||||||
|
-D clippy::tests_outside_test_module
|
||||||
|
|
||||||
|
# Quality
|
||||||
|
-D clippy::format_push_string
|
||||||
|
-D clippy::string_to_string
|
||||||
|
|
||||||
|
# These potentially should be enabled in the future
|
||||||
|
# -D clippy::missing_errors_doc
|
||||||
|
# -D clippy::missing_panics_doc
|
||||||
|
# -D clippy::doc_markdown
|
||||||
|
|
||||||
|
# TODO: Enable this
|
||||||
|
# -D clippy::cargo
|
||||||
|
|
||||||
|
# Not in nightly yet
|
||||||
|
# -D clippy::redundant_type_annotations
|
||||||
|
# -D clippy::big_endian_bytes
|
||||||
|
# -D clippy::host_endian_bytes
|
||||||
37
coins/bitcoin/Cargo.toml
Normal file
37
coins/bitcoin/Cargo.toml
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
[package]
|
||||||
|
name = "bitcoin-serai"
|
||||||
|
version = "0.2.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"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
lazy_static = "1"
|
||||||
|
thiserror = "1"
|
||||||
|
|
||||||
|
zeroize = "^1.5"
|
||||||
|
rand_core = "0.6"
|
||||||
|
|
||||||
|
sha2 = "0.10"
|
||||||
|
|
||||||
|
secp256k1 = { version = "0.27", features = ["global-context"] }
|
||||||
|
bitcoin = { version = "0.30", features = ["serde"] }
|
||||||
|
|
||||||
|
k256 = { version = "^0.13.1", default-features = false, features = ["std", "arithmetic", "bits"] }
|
||||||
|
transcript = { package = "flexible-transcript", path = "../../crypto/transcript", version = "0.3", features = ["recommended"] }
|
||||||
|
frost = { package = "modular-frost", path = "../../crypto/frost", version = "0.7", features = ["secp256k1"] }
|
||||||
|
|
||||||
|
hex = "0.4"
|
||||||
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
serde_json = "1"
|
||||||
|
reqwest = { version = "0.11", features = ["json"] }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
frost = { package = "modular-frost", path = "../../crypto/frost", version = "0.7", features = ["tests"] }
|
||||||
|
|
||||||
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
|
||||||
|
[features]
|
||||||
|
hazmat = []
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2023-2025 Luke Parker
|
Copyright (c) 2022-2023 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
|
||||||
160
coins/bitcoin/src/crypto.rs
Normal file
160
coins/bitcoin/src/crypto.rs
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
use core::fmt::Debug;
|
||||||
|
use std::io;
|
||||||
|
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
|
||||||
|
use zeroize::Zeroizing;
|
||||||
|
use rand_core::{RngCore, CryptoRng};
|
||||||
|
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
use transcript::Transcript;
|
||||||
|
|
||||||
|
use secp256k1::schnorr::Signature;
|
||||||
|
use k256::{
|
||||||
|
elliptic_curve::{
|
||||||
|
ops::Reduce,
|
||||||
|
sec1::{Tag, ToEncodedPoint},
|
||||||
|
},
|
||||||
|
U256, Scalar, ProjectivePoint,
|
||||||
|
};
|
||||||
|
use frost::{
|
||||||
|
curve::{Ciphersuite, Secp256k1},
|
||||||
|
Participant, ThresholdKeys, ThresholdView, FrostError,
|
||||||
|
algorithm::{Hram as HramTrait, Algorithm, Schnorr as FrostSchnorr},
|
||||||
|
};
|
||||||
|
|
||||||
|
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-infinite even point to a XOnlyPublicKey. Panics on invalid input.
|
||||||
|
pub fn x_only(key: &ProjectivePoint) -> XOnlyPublicKey {
|
||||||
|
XOnlyPublicKey::from_slice(&x(key)).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Make a point even by adding the generator until it is even. Returns the even point and the
|
||||||
|
/// amount of additions required.
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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.
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub struct Hram {}
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
static ref TAG_HASH: [u8; 32] = Sha256::digest(b"BIP0340/challenge").into();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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);
|
||||||
|
|
||||||
|
let mut data = Sha256::new();
|
||||||
|
data.update(*TAG_HASH);
|
||||||
|
data.update(*TAG_HASH);
|
||||||
|
data.update(x(&R));
|
||||||
|
data.update(x(A));
|
||||||
|
data.update(m);
|
||||||
|
|
||||||
|
Scalar::reduce(U256::from_be_slice(&data.finalize()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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 = Signature;
|
||||||
|
|
||||||
|
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 secp256k1 signature
|
||||||
|
Signature::from_slice(&sig.serialize()[1 ..]).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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||||
#![doc = include_str!("../README.md")]
|
#![doc = include_str!("../README.md")]
|
||||||
#![cfg_attr(not(feature = "std"), no_std)]
|
|
||||||
|
|
||||||
/// The bitcoin Rust library.
|
/// The bitcoin Rust library.
|
||||||
pub use bitcoin;
|
pub use bitcoin;
|
||||||
@@ -14,7 +13,6 @@ pub(crate) mod crypto;
|
|||||||
/// Wallet functionality to create transactions.
|
/// Wallet functionality to create transactions.
|
||||||
pub mod wallet;
|
pub mod wallet;
|
||||||
/// A minimal asynchronous Bitcoin RPC client.
|
/// A minimal asynchronous Bitcoin RPC client.
|
||||||
#[cfg(feature = "rpc")]
|
|
||||||
pub mod rpc;
|
pub mod rpc;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
145
coins/bitcoin/src/rpc.rs
Normal file
145
coins/bitcoin/src/rpc.rs
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
use core::fmt::Debug;
|
||||||
|
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use serde::{Deserialize, de::DeserializeOwned};
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
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(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 sent an invalid response")]
|
||||||
|
InvalidResponse,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Rpc {
|
||||||
|
pub async fn new(url: String) -> Result<Rpc, RpcError> {
|
||||||
|
let rpc = Rpc(url);
|
||||||
|
// Make an RPC request to verify the node is reachable and sane
|
||||||
|
rpc.get_latest_block_number().await?;
|
||||||
|
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 client = reqwest::Client::new();
|
||||||
|
let res = client
|
||||||
|
.post(&self.0)
|
||||||
|
.json(&json!({ "jsonrpc": "2.0", "method": method, "params": params }))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(|_| RpcError::ConnectionError)?
|
||||||
|
.text()
|
||||||
|
.await
|
||||||
|
.map_err(|_| RpcError::ConnectionError)?;
|
||||||
|
|
||||||
|
let res: RpcResponse<Response> =
|
||||||
|
serde_json::from_str(&res).map_err(|_| RpcError::InvalidResponse)?;
|
||||||
|
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()
|
||||||
|
.as_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)?;
|
||||||
|
let block: Block = encode::deserialize(&bytes).map_err(|_| RpcError::InvalidResponse)?;
|
||||||
|
|
||||||
|
let mut block_hash = *block.block_hash().as_raw_hash().as_byte_array();
|
||||||
|
block_hash.reverse();
|
||||||
|
if hash != &block_hash {
|
||||||
|
Err(RpcError::InvalidResponse)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(block)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Publish a transaction.
|
||||||
|
pub async fn send_raw_transaction(&self, tx: &Transaction) -> Result<Txid, RpcError> {
|
||||||
|
let txid = self.rpc_call("sendrawtransaction", json!([encode::serialize_hex(tx)])).await?;
|
||||||
|
if txid != tx.txid() {
|
||||||
|
Err(RpcError::InvalidResponse)?;
|
||||||
|
}
|
||||||
|
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)?;
|
||||||
|
let tx: Transaction = encode::deserialize(&bytes).map_err(|_| RpcError::InvalidResponse)?;
|
||||||
|
|
||||||
|
let mut tx_hash = *tx.txid().as_raw_hash().as_byte_array();
|
||||||
|
tx_hash.reverse();
|
||||||
|
if hash != &tx_hash {
|
||||||
|
Err(RpcError::InvalidResponse)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(tx)
|
||||||
|
}
|
||||||
|
}
|
||||||
47
coins/bitcoin/src/tests/crypto.rs
Normal file
47
coins/bitcoin/src/tests/crypto.rs
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
use rand_core::OsRng;
|
||||||
|
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
|
||||||
|
use secp256k1::{SECP256K1, Message};
|
||||||
|
|
||||||
|
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.iter_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.clone(),
|
||||||
|
keys.clone(),
|
||||||
|
algorithm_machines(&mut OsRng, algo, &keys),
|
||||||
|
&Sha256::digest(MESSAGE),
|
||||||
|
);
|
||||||
|
|
||||||
|
SECP256K1
|
||||||
|
.verify_schnorr(
|
||||||
|
&sig,
|
||||||
|
&Message::from(Hash::hash(MESSAGE)),
|
||||||
|
&x_only(&keys[&Participant::new(1).unwrap()].group_key()),
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
160
coins/bitcoin/src/wallet/mod.rs
Normal file
160
coins/bitcoin/src/wallet/mod.rs
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
use std::{
|
||||||
|
io::{self, Read, Write},
|
||||||
|
collections::HashMap,
|
||||||
|
};
|
||||||
|
|
||||||
|
use k256::{
|
||||||
|
elliptic_curve::sec1::{Tag, ToEncodedPoint},
|
||||||
|
Scalar, ProjectivePoint,
|
||||||
|
};
|
||||||
|
use frost::{
|
||||||
|
curve::{Ciphersuite, Secp256k1},
|
||||||
|
ThresholdKeys,
|
||||||
|
};
|
||||||
|
|
||||||
|
use bitcoin::{
|
||||||
|
consensus::encode::{Decodable, serialize},
|
||||||
|
key::TweakedPublicKey,
|
||||||
|
OutPoint, ScriptBuf, TxOut, Transaction, Block, Network, Address,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::crypto::{x_only, make_even};
|
||||||
|
|
||||||
|
mod send;
|
||||||
|
pub use send::*;
|
||||||
|
|
||||||
|
/// Tweak keys to ensure they're usable with Bitcoin.
|
||||||
|
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 for a public key.
|
||||||
|
pub fn address(network: Network, key: ProjectivePoint) -> Option<Address> {
|
||||||
|
if key.to_encoded_point(true).tag() != Tag::CompressedEvenY {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(Address::p2tr_tweaked(TweakedPublicKey::dangerous_assume_tweaked(x_only(&key)), network))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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 outpoint for this output.
|
||||||
|
pub fn outpoint(&self) -> &OutPoint {
|
||||||
|
&self.outpoint
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The value of this output.
|
||||||
|
pub fn value(&self) -> u64 {
|
||||||
|
self.output.value
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read a ReceivedOutput from a generic satisfying Read.
|
||||||
|
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::new(io::ErrorKind::Other, "invalid TxOut"))?,
|
||||||
|
outpoint: OutPoint::consensus_decode(r)
|
||||||
|
.map_err(|_| io::Error::new(io::ErrorKind::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![];
|
||||||
|
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();
|
||||||
|
// Uses Network::Bitcoin since network is irrelevant here
|
||||||
|
scripts.insert(address(Network::Bitcoin, 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.
|
||||||
|
pub fn register_offset(&mut self, mut offset: Scalar) -> Option<Scalar> {
|
||||||
|
loop {
|
||||||
|
match address(Network::Bitcoin, 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![];
|
||||||
|
for (vout, output) in tx.output.iter().enumerate() {
|
||||||
|
if let Some(offset) = self.scripts.get(&output.script_pubkey) {
|
||||||
|
res.push(ReceivedOutput {
|
||||||
|
offset: *offset,
|
||||||
|
output: output.clone(),
|
||||||
|
outpoint: OutPoint::new(tx.txid(), u32::try_from(vout).unwrap()),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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![];
|
||||||
|
for tx in &block.txdata {
|
||||||
|
res.extend(self.scan_transaction(tx));
|
||||||
|
}
|
||||||
|
res
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
#[allow(unused_imports)]
|
use std::{
|
||||||
use std_shims::prelude::*;
|
|
||||||
use std_shims::{
|
|
||||||
io::{self, Read},
|
io::{self, Read},
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
};
|
};
|
||||||
@@ -9,29 +7,30 @@ use thiserror::Error;
|
|||||||
|
|
||||||
use rand_core::{RngCore, CryptoRng};
|
use rand_core::{RngCore, CryptoRng};
|
||||||
|
|
||||||
use k256::Scalar;
|
use transcript::{Transcript, RecommendedTranscript};
|
||||||
|
|
||||||
|
use k256::{elliptic_curve::sec1::ToEncodedPoint, Scalar};
|
||||||
use frost::{curve::Secp256k1, Participant, ThresholdKeys, FrostError, sign::*};
|
use frost::{curve::Secp256k1, Participant, ThresholdKeys, FrostError, sign::*};
|
||||||
|
|
||||||
use bitcoin::{
|
use bitcoin::{
|
||||||
hashes::Hash,
|
|
||||||
sighash::{TapSighashType, SighashCache, Prevouts},
|
sighash::{TapSighashType, SighashCache, Prevouts},
|
||||||
absolute::LockTime,
|
absolute::LockTime,
|
||||||
script::{PushBytesBuf, ScriptBuf},
|
script::{PushBytesBuf, ScriptBuf},
|
||||||
transaction::{Version, Transaction},
|
OutPoint, Sequence, Witness, TxIn, TxOut, Transaction, Network, Address,
|
||||||
OutPoint, Sequence, Witness, TxIn, Amount, TxOut,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
crypto::Schnorr,
|
crypto::Schnorr,
|
||||||
wallet::{ReceivedOutput, p2tr_script_buf},
|
wallet::{address, ReceivedOutput},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[rustfmt::skip]
|
#[rustfmt::skip]
|
||||||
// https://github.com/bitcoin/bitcoin/blob/306ccd4927a2efe325c8d84be1bdb79edeb29b04/src/policy/policy.cpp#L26-L63
|
// https://github.com/bitcoin/bitcoin/blob/306ccd4927a2efe325c8d84be1bdb79edeb29b04/src/policy/policy.h#L27
|
||||||
// As the above notes, a lower amount may not be considered dust if contained in a SegWit output
|
const MAX_STANDARD_TX_WEIGHT: u64 = 400_000;
|
||||||
// This doesn't bother with delineation due to how marginal these values are, and because it isn't
|
|
||||||
// worth the complexity to implement differentation
|
#[rustfmt::skip]
|
||||||
pub const DUST: u64 = 546;
|
//https://github.com/bitcoin/bitcoin/blob/a245429d680eb95cf4c0c78e58e63e3f0f5d979a/src/test/transaction_tests.cpp#L815-L816
|
||||||
|
const DUST: u64 = 674;
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug, Error)]
|
#[derive(Clone, PartialEq, Eq, Debug, Error)]
|
||||||
pub enum TransactionError {
|
pub enum TransactionError {
|
||||||
@@ -43,10 +42,8 @@ pub enum TransactionError {
|
|||||||
DustPayment,
|
DustPayment,
|
||||||
#[error("too much data was specified")]
|
#[error("too much data was specified")]
|
||||||
TooMuchData,
|
TooMuchData,
|
||||||
#[error("fee was too low to pass the default minimum fee rate")]
|
|
||||||
TooLowFee,
|
|
||||||
#[error("not enough funds for these payments")]
|
#[error("not enough funds for these payments")]
|
||||||
NotEnoughFunds { inputs: u64, payments: u64, fee: u64 },
|
NotEnoughFunds,
|
||||||
#[error("transaction was too large")]
|
#[error("transaction was too large")]
|
||||||
TooLargeTransaction,
|
TooLargeTransaction,
|
||||||
}
|
}
|
||||||
@@ -61,14 +58,10 @@ pub struct SignableTransaction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl SignableTransaction {
|
impl SignableTransaction {
|
||||||
fn calculate_weight_vbytes(
|
fn calculate_weight(inputs: usize, payments: &[(Address, u64)], change: Option<&Address>) -> u64 {
|
||||||
inputs: usize,
|
|
||||||
payments: &[(ScriptBuf, u64)],
|
|
||||||
change: Option<&ScriptBuf>,
|
|
||||||
) -> (u64, u64) {
|
|
||||||
// Expand this a full transaction in order to use the bitcoin library's weight function
|
// Expand this a full transaction in order to use the bitcoin library's weight function
|
||||||
let mut tx = Transaction {
|
let mut tx = Transaction {
|
||||||
version: Version(2),
|
version: 2,
|
||||||
lock_time: LockTime::ZERO,
|
lock_time: LockTime::ZERO,
|
||||||
input: vec![
|
input: vec![
|
||||||
TxIn {
|
TxIn {
|
||||||
@@ -88,44 +81,15 @@ impl SignableTransaction {
|
|||||||
.iter()
|
.iter()
|
||||||
// The payment is a fixed size so we don't have to use it here
|
// 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
|
// The script pub key is not of a fixed size and does have to be used here
|
||||||
.map(|payment| TxOut {
|
.map(|payment| TxOut { value: payment.1, script_pubkey: payment.0.script_pubkey() })
|
||||||
value: Amount::from_sat(payment.1),
|
|
||||||
script_pubkey: payment.0.clone(),
|
|
||||||
})
|
|
||||||
.collect(),
|
.collect(),
|
||||||
};
|
};
|
||||||
if let Some(change) = change {
|
if let Some(change) = change {
|
||||||
// Use a 0 value since we're currently unsure what the change amount will be, and since
|
// 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)
|
// the value is fixed size (so any value could be used here)
|
||||||
tx.output.push(TxOut { value: Amount::ZERO, script_pubkey: change.clone() });
|
tx.output.push(TxOut { value: 0, script_pubkey: change.script_pubkey() });
|
||||||
}
|
}
|
||||||
|
u64::try_from(tx.weight()).unwrap()
|
||||||
let weight = tx.weight();
|
|
||||||
|
|
||||||
// Now calculate the size in vbytes
|
|
||||||
|
|
||||||
/*
|
|
||||||
"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 almost as expected, with an additional consideration to signature operations
|
|
||||||
|
|
||||||
Signature operations (the second argument of the following call) do not count Taproot
|
|
||||||
signatures per https://github.com/bitcoin/bips/blob/master/bip-0342.mediawiki#cite_ref-11-0
|
|
||||||
|
|
||||||
We don't risk running afoul of the Taproot signature limit as it allows at least one per
|
|
||||||
input, which is all we use
|
|
||||||
*/
|
|
||||||
(
|
|
||||||
weight.to_wu(),
|
|
||||||
u64::try_from(bitcoin::policy::get_virtual_tx_size(
|
|
||||||
i64::try_from(weight.to_wu()).unwrap(),
|
|
||||||
0i64,
|
|
||||||
))
|
|
||||||
.unwrap(),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the fee necessary for this transaction to achieve the fee rate specified at
|
/// Returns the fee necessary for this transaction to achieve the fee rate specified at
|
||||||
@@ -136,12 +100,6 @@ impl SignableTransaction {
|
|||||||
self.needed_fee
|
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.
|
/// Create a new SignableTransaction.
|
||||||
///
|
///
|
||||||
/// If a change address is specified, any leftover funds will be sent to it if the leftover funds
|
/// If a change address is specified, any leftover funds will be sent to it if the leftover funds
|
||||||
@@ -151,10 +109,10 @@ impl SignableTransaction {
|
|||||||
/// If data is specified, an OP_RETURN output will be added with it.
|
/// If data is specified, an OP_RETURN output will be added with it.
|
||||||
pub fn new(
|
pub fn new(
|
||||||
mut inputs: Vec<ReceivedOutput>,
|
mut inputs: Vec<ReceivedOutput>,
|
||||||
payments: &[(ScriptBuf, u64)],
|
payments: &[(Address, u64)],
|
||||||
change: Option<ScriptBuf>,
|
change: Option<Address>,
|
||||||
data: Option<Vec<u8>>,
|
data: Option<Vec<u8>>,
|
||||||
fee_per_vbyte: u64,
|
fee_per_weight: u64,
|
||||||
) -> Result<SignableTransaction, TransactionError> {
|
) -> Result<SignableTransaction, TransactionError> {
|
||||||
if inputs.is_empty() {
|
if inputs.is_empty() {
|
||||||
Err(TransactionError::NoInputs)?;
|
Err(TransactionError::NoInputs)?;
|
||||||
@@ -170,11 +128,11 @@ impl SignableTransaction {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if data.as_ref().map_or(0, Vec::len) > 80 {
|
if data.as_ref().map(|data| data.len()).unwrap_or(0) > 80 {
|
||||||
Err(TransactionError::TooMuchData)?;
|
Err(TransactionError::TooMuchData)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let input_sat = inputs.iter().map(|input| input.output.value.to_sat()).sum::<u64>();
|
let input_sat = inputs.iter().map(|input| input.output.value).sum::<u64>();
|
||||||
let offsets = inputs.iter().map(|input| input.offset).collect();
|
let offsets = inputs.iter().map(|input| input.offset).collect();
|
||||||
let tx_ins = inputs
|
let tx_ins = inputs
|
||||||
.iter()
|
.iter()
|
||||||
@@ -189,47 +147,33 @@ impl SignableTransaction {
|
|||||||
let payment_sat = payments.iter().map(|payment| payment.1).sum::<u64>();
|
let payment_sat = payments.iter().map(|payment| payment.1).sum::<u64>();
|
||||||
let mut tx_outs = payments
|
let mut tx_outs = payments
|
||||||
.iter()
|
.iter()
|
||||||
.map(|payment| TxOut { value: Amount::from_sat(payment.1), script_pubkey: payment.0.clone() })
|
.map(|payment| TxOut { value: payment.1, script_pubkey: payment.0.script_pubkey() })
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
// Add the OP_RETURN output
|
// Add the OP_RETURN output
|
||||||
if let Some(data) = data {
|
if let Some(data) = data {
|
||||||
tx_outs.push(TxOut {
|
tx_outs.push(TxOut {
|
||||||
value: Amount::ZERO,
|
value: 0,
|
||||||
script_pubkey: ScriptBuf::new_op_return(
|
script_pubkey: ScriptBuf::new_op_return(
|
||||||
PushBytesBuf::try_from(data)
|
&PushBytesBuf::try_from(data)
|
||||||
.expect("data didn't fit into PushBytes depsite being checked"),
|
.expect("data didn't fit into PushBytes depsite being checked"),
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
let (mut weight, vbytes) = Self::calculate_weight_vbytes(tx_ins.len(), payments, None);
|
let mut weight = Self::calculate_weight(tx_ins.len(), payments, None);
|
||||||
|
let mut needed_fee = fee_per_weight * weight;
|
||||||
let mut needed_fee = fee_per_vbyte * vbytes;
|
|
||||||
// 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) * vbytes) / 1000) {
|
|
||||||
Err(TransactionError::TooLowFee)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if input_sat < (payment_sat + needed_fee) {
|
if input_sat < (payment_sat + needed_fee) {
|
||||||
Err(TransactionError::NotEnoughFunds {
|
Err(TransactionError::NotEnoughFunds)?;
|
||||||
inputs: input_sat,
|
|
||||||
payments: payment_sat,
|
|
||||||
fee: needed_fee,
|
|
||||||
})?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there's a change address, check if there's change to give it
|
// If there's a change address, check if there's change to give it
|
||||||
if let Some(change) = change {
|
if let Some(change) = change.as_ref() {
|
||||||
let (weight_with_change, vbytes_with_change) =
|
let weight_with_change = Self::calculate_weight(tx_ins.len(), payments, Some(change));
|
||||||
Self::calculate_weight_vbytes(tx_ins.len(), payments, Some(&change));
|
let fee_with_change = fee_per_weight * weight_with_change;
|
||||||
let fee_with_change = fee_per_vbyte * vbytes_with_change;
|
|
||||||
if let Some(value) = input_sat.checked_sub(payment_sat + fee_with_change) {
|
if let Some(value) = input_sat.checked_sub(payment_sat + fee_with_change) {
|
||||||
if value >= DUST {
|
if value >= DUST {
|
||||||
tx_outs.push(TxOut { value: Amount::from_sat(value), script_pubkey: change });
|
tx_outs.push(TxOut { value, script_pubkey: change.script_pubkey() });
|
||||||
weight = weight_with_change;
|
weight = weight_with_change;
|
||||||
needed_fee = fee_with_change;
|
needed_fee = fee_with_change;
|
||||||
}
|
}
|
||||||
@@ -240,47 +184,56 @@ impl SignableTransaction {
|
|||||||
Err(TransactionError::NoOutputs)?;
|
Err(TransactionError::NoOutputs)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if weight > u64::from(bitcoin::policy::MAX_STANDARD_TX_WEIGHT) {
|
if weight > MAX_STANDARD_TX_WEIGHT {
|
||||||
Err(TransactionError::TooLargeTransaction)?;
|
Err(TransactionError::TooLargeTransaction)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(SignableTransaction {
|
Ok(SignableTransaction {
|
||||||
tx: Transaction {
|
tx: Transaction { version: 2, lock_time: LockTime::ZERO, input: tx_ins, output: tx_outs },
|
||||||
version: Version(2),
|
|
||||||
lock_time: LockTime::ZERO,
|
|
||||||
input: tx_ins,
|
|
||||||
output: tx_outs,
|
|
||||||
},
|
|
||||||
offsets,
|
offsets,
|
||||||
prevouts: inputs.drain(..).map(|input| input.output).collect(),
|
prevouts: inputs.drain(..).map(|input| input.output).collect(),
|
||||||
needed_fee,
|
needed_fee,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the TX ID of the transaction this will create.
|
|
||||||
pub fn txid(&self) -> [u8; 32] {
|
|
||||||
let mut res = self.tx.compute_txid().to_byte_array();
|
|
||||||
res.reverse();
|
|
||||||
res
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the transaction, sans witness, this will create if signed.
|
|
||||||
pub fn transaction(&self) -> &Transaction {
|
|
||||||
&self.tx
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a multisig machine for this transaction.
|
/// Create a multisig machine for this transaction.
|
||||||
///
|
///
|
||||||
/// Returns None if the wrong keys are used.
|
/// Returns None if the wrong keys are used.
|
||||||
pub fn multisig(self, keys: &ThresholdKeys<Secp256k1>) -> Option<TransactionMachine> {
|
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_le_bytes());
|
||||||
|
}
|
||||||
|
|
||||||
let mut sigs = vec![];
|
let mut sigs = vec![];
|
||||||
for i in 0 .. self.tx.input.len() {
|
for i in 0 .. tx.input.len() {
|
||||||
|
let mut transcript = transcript.clone();
|
||||||
|
transcript.append_message(b"signing_input", u32::try_from(i).unwrap().to_le_bytes());
|
||||||
|
|
||||||
let offset = keys.clone().offset(self.offsets[i]);
|
let offset = keys.clone().offset(self.offsets[i]);
|
||||||
if p2tr_script_buf(offset.group_key())? != self.prevouts[i].script_pubkey {
|
if address(Network::Bitcoin, offset.group_key())?.script_pubkey() !=
|
||||||
|
self.prevouts[i].script_pubkey
|
||||||
|
{
|
||||||
None?;
|
None?;
|
||||||
}
|
}
|
||||||
|
|
||||||
sigs.push(AlgorithmMachine::new(Schnorr::new(), keys.clone().offset(self.offsets[i])));
|
sigs.push(AlgorithmMachine::new(
|
||||||
|
Schnorr::new(transcript),
|
||||||
|
keys.clone().offset(self.offsets[i]),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(TransactionMachine { tx: self, sigs })
|
Some(TransactionMachine { tx: self, sigs })
|
||||||
@@ -290,10 +243,10 @@ impl SignableTransaction {
|
|||||||
/// A FROST signing machine to produce a Bitcoin transaction.
|
/// 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 does not support caching its preprocess. When sign is called, the message must be empty.
|
||||||
/// This will panic if either `cache`, `from_cache` is called or the message isn't empty.
|
/// This will panic if it isn't.
|
||||||
pub struct TransactionMachine {
|
pub struct TransactionMachine {
|
||||||
tx: SignableTransaction,
|
tx: SignableTransaction,
|
||||||
sigs: Vec<AlgorithmMachine<Secp256k1, Schnorr>>,
|
sigs: Vec<AlgorithmMachine<Secp256k1, Schnorr<RecommendedTranscript>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PreprocessMachine for TransactionMachine {
|
impl PreprocessMachine for TransactionMachine {
|
||||||
@@ -322,7 +275,7 @@ impl PreprocessMachine for TransactionMachine {
|
|||||||
|
|
||||||
pub struct TransactionSignMachine {
|
pub struct TransactionSignMachine {
|
||||||
tx: SignableTransaction,
|
tx: SignableTransaction,
|
||||||
sigs: Vec<AlgorithmSignMachine<Secp256k1, Schnorr>>,
|
sigs: Vec<AlgorithmSignMachine<Secp256k1, Schnorr<RecommendedTranscript>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SignMachine<Transaction> for TransactionSignMachine {
|
impl SignMachine<Transaction> for TransactionSignMachine {
|
||||||
@@ -340,10 +293,10 @@ impl SignMachine<Transaction> for TransactionSignMachine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn from_cache(
|
fn from_cache(
|
||||||
(): (),
|
_: (),
|
||||||
_: ThresholdKeys<Secp256k1>,
|
_: ThresholdKeys<Secp256k1>,
|
||||||
_: CachedPreprocess,
|
_: CachedPreprocess,
|
||||||
) -> (Self, Self::Preprocess) {
|
) -> Result<Self, FrostError> {
|
||||||
unimplemented!(
|
unimplemented!(
|
||||||
"Bitcoin transactions don't support caching their preprocesses due to {}",
|
"Bitcoin transactions don't support caching their preprocesses due to {}",
|
||||||
"being already bound to a specific transaction"
|
"being already bound to a specific transaction"
|
||||||
@@ -360,7 +313,7 @@ impl SignMachine<Transaction> for TransactionSignMachine {
|
|||||||
msg: &[u8],
|
msg: &[u8],
|
||||||
) -> Result<(TransactionSignatureMachine, Self::SignatureShare), FrostError> {
|
) -> Result<(TransactionSignatureMachine, Self::SignatureShare), FrostError> {
|
||||||
if !msg.is_empty() {
|
if !msg.is_empty() {
|
||||||
panic!("message was passed to the TransactionSignMachine when it generates its own");
|
panic!("message was passed to the TransactionMachine when it generates its own");
|
||||||
}
|
}
|
||||||
|
|
||||||
let commitments = (0 .. self.sigs.len())
|
let commitments = (0 .. self.sigs.len())
|
||||||
@@ -386,9 +339,7 @@ impl SignMachine<Transaction> for TransactionSignMachine {
|
|||||||
commitments[i].clone(),
|
commitments[i].clone(),
|
||||||
cache
|
cache
|
||||||
.taproot_key_spend_signature_hash(i, &prevouts, TapSighashType::Default)
|
.taproot_key_spend_signature_hash(i, &prevouts, TapSighashType::Default)
|
||||||
// This should never happen since the inputs align with the TX the cache was
|
.unwrap()
|
||||||
// constructed with, and because i is always < prevouts.len()
|
|
||||||
.expect("taproot_key_spend_signature_hash failed to return a hash")
|
|
||||||
.as_ref(),
|
.as_ref(),
|
||||||
)?;
|
)?;
|
||||||
shares.push(share);
|
shares.push(share);
|
||||||
@@ -402,7 +353,7 @@ impl SignMachine<Transaction> for TransactionSignMachine {
|
|||||||
|
|
||||||
pub struct TransactionSignatureMachine {
|
pub struct TransactionSignatureMachine {
|
||||||
tx: Transaction,
|
tx: Transaction,
|
||||||
sigs: Vec<AlgorithmSignatureMachine<Secp256k1, Schnorr>>,
|
sigs: Vec<AlgorithmSignatureMachine<Secp256k1, Schnorr<RecommendedTranscript>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SignatureMachine<Transaction> for TransactionSignatureMachine {
|
impl SignatureMachine<Transaction> for TransactionSignatureMachine {
|
||||||
@@ -422,7 +373,7 @@ impl SignatureMachine<Transaction> for TransactionSignatureMachine {
|
|||||||
)?;
|
)?;
|
||||||
|
|
||||||
let mut witness = Witness::new();
|
let mut witness = Witness::new();
|
||||||
witness.push(sig);
|
witness.push(sig.as_ref());
|
||||||
input.witness = witness;
|
input.witness = witness;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,22 +1,21 @@
|
|||||||
use std::sync::LazyLock;
|
|
||||||
|
|
||||||
use bitcoin_serai::rpc::Rpc;
|
use bitcoin_serai::rpc::Rpc;
|
||||||
|
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
#[allow(dead_code)]
|
lazy_static::lazy_static! {
|
||||||
pub(crate) static SEQUENTIAL: LazyLock<Mutex<()>> = LazyLock::new(|| Mutex::new(()));
|
pub static ref SEQUENTIAL: Mutex<()> = Mutex::new(());
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub(crate) async fn rpc() -> Rpc {
|
pub(crate) async fn rpc() -> Rpc {
|
||||||
let rpc = Rpc::new("http://serai:seraidex@127.0.0.1:8332".to_string()).await.unwrap();
|
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 this node has already been interacted with, clear its chain
|
||||||
if rpc.get_latest_block_number().await.unwrap() > 0 {
|
if rpc.get_latest_block_number().await.unwrap() > 0 {
|
||||||
rpc
|
rpc
|
||||||
.call(
|
.rpc_call(
|
||||||
"invalidateblock",
|
"invalidateblock",
|
||||||
&format!(r#"["{}"]"#, hex::encode(rpc.get_block_hash(1).await.unwrap())),
|
serde_json::json!([hex::encode(rpc.get_block_hash(1).await.unwrap())]),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
@@ -2,6 +2,8 @@ use std::collections::HashMap;
|
|||||||
|
|
||||||
use rand_core::{RngCore, OsRng};
|
use rand_core::{RngCore, OsRng};
|
||||||
|
|
||||||
|
use transcript::{Transcript, RecommendedTranscript};
|
||||||
|
|
||||||
use k256::{
|
use k256::{
|
||||||
elliptic_curve::{
|
elliptic_curve::{
|
||||||
group::{ff::Field, Group},
|
group::{ff::Field, Group},
|
||||||
@@ -20,11 +22,9 @@ use bitcoin_serai::{
|
|||||||
hashes::Hash as HashTrait,
|
hashes::Hash as HashTrait,
|
||||||
blockdata::opcodes::all::OP_RETURN,
|
blockdata::opcodes::all::OP_RETURN,
|
||||||
script::{PushBytesBuf, Instruction, Instructions, Script},
|
script::{PushBytesBuf, Instruction, Instructions, Script},
|
||||||
OutPoint, Amount, TxOut, Transaction, Network, Address,
|
OutPoint, TxOut, Transaction, Network, Address,
|
||||||
},
|
|
||||||
wallet::{
|
|
||||||
tweak_keys, p2tr_script_buf, ReceivedOutput, Scanner, TransactionError, SignableTransaction,
|
|
||||||
},
|
},
|
||||||
|
wallet::{tweak_keys, address, ReceivedOutput, Scanner, TransactionError, SignableTransaction},
|
||||||
rpc::Rpc,
|
rpc::Rpc,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -41,21 +41,18 @@ async fn send_and_get_output(rpc: &Rpc, scanner: &Scanner, key: ProjectivePoint)
|
|||||||
let block_number = rpc.get_latest_block_number().await.unwrap() + 1;
|
let block_number = rpc.get_latest_block_number().await.unwrap() + 1;
|
||||||
|
|
||||||
rpc
|
rpc
|
||||||
.call::<Vec<String>>(
|
.rpc_call::<Vec<String>>(
|
||||||
"generatetoaddress",
|
"generatetoaddress",
|
||||||
&format!(
|
serde_json::json!([1, address(Network::Regtest, key).unwrap()]),
|
||||||
r#"[1, "{}"]"#,
|
|
||||||
Address::from_script(&p2tr_script_buf(key).unwrap(), Network::Regtest).unwrap()
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// Mine until maturity
|
// Mine until maturity
|
||||||
rpc
|
rpc
|
||||||
.call::<Vec<String>>(
|
.rpc_call::<Vec<String>>(
|
||||||
"generatetoaddress",
|
"generatetoaddress",
|
||||||
&format!(r#"[100, "{}"]"#, Address::p2sh(Script::new(), Network::Regtest).unwrap()),
|
serde_json::json!([100, Address::p2sh(Script::empty(), Network::Regtest).unwrap()]),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -66,8 +63,8 @@ async fn send_and_get_output(rpc: &Rpc, scanner: &Scanner, key: ProjectivePoint)
|
|||||||
assert_eq!(outputs, scanner.scan_transaction(&block.txdata[0]));
|
assert_eq!(outputs, scanner.scan_transaction(&block.txdata[0]));
|
||||||
|
|
||||||
assert_eq!(outputs.len(), 1);
|
assert_eq!(outputs.len(), 1);
|
||||||
assert_eq!(outputs[0].outpoint(), &OutPoint::new(block.txdata[0].compute_txid(), 0));
|
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!(outputs[0].value(), block.txdata[0].output[0].value);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ReceivedOutput::read::<&[u8]>(&mut outputs[0].serialize().as_ref()).unwrap(),
|
ReceivedOutput::read::<&[u8]>(&mut outputs[0].serialize().as_ref()).unwrap(),
|
||||||
@@ -79,8 +76,8 @@ async fn send_and_get_output(rpc: &Rpc, scanner: &Scanner, key: ProjectivePoint)
|
|||||||
|
|
||||||
fn keys() -> (HashMap<Participant, ThresholdKeys<Secp256k1>>, ProjectivePoint) {
|
fn keys() -> (HashMap<Participant, ThresholdKeys<Secp256k1>>, ProjectivePoint) {
|
||||||
let mut keys = key_gen(&mut OsRng);
|
let mut keys = key_gen(&mut OsRng);
|
||||||
for keys in keys.values_mut() {
|
for (_, keys) in keys.iter_mut() {
|
||||||
*keys = tweak_keys(keys.clone());
|
*keys = tweak_keys(keys);
|
||||||
}
|
}
|
||||||
let key = keys.values().next().unwrap().group_key();
|
let key = keys.values().next().unwrap().group_key();
|
||||||
(keys, key)
|
(keys, key)
|
||||||
@@ -88,15 +85,50 @@ fn keys() -> (HashMap<Participant, ThresholdKeys<Secp256k1>>, ProjectivePoint) {
|
|||||||
|
|
||||||
fn sign(
|
fn sign(
|
||||||
keys: &HashMap<Participant, ThresholdKeys<Secp256k1>>,
|
keys: &HashMap<Participant, ThresholdKeys<Secp256k1>>,
|
||||||
tx: &SignableTransaction,
|
tx: SignableTransaction,
|
||||||
) -> Transaction {
|
) -> Transaction {
|
||||||
let mut machines = HashMap::new();
|
let mut machines = HashMap::new();
|
||||||
for i in (1 ..= THRESHOLD).map(|i| Participant::new(i).unwrap()) {
|
for i in (1 ..= THRESHOLD).map(|i| Participant::new(i).unwrap()) {
|
||||||
machines.insert(i, tx.clone().multisig(&keys[&i].clone()).unwrap());
|
machines.insert(
|
||||||
|
i,
|
||||||
|
tx.clone()
|
||||||
|
.multisig(keys[&i].clone(), RecommendedTranscript::new(b"bitcoin-serai Test Transaction"))
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
sign_without_caching(&mut OsRng, machines, &[])
|
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_sequential! {
|
||||||
async fn test_scanner() {
|
async fn test_scanner() {
|
||||||
// Test Scanners are creatable for even keys.
|
// Test Scanners are creatable for even keys.
|
||||||
@@ -155,7 +187,7 @@ async_sequential! {
|
|||||||
assert_eq!(output.offset(), Scalar::ZERO);
|
assert_eq!(output.offset(), Scalar::ZERO);
|
||||||
|
|
||||||
let inputs = vec![output];
|
let inputs = vec![output];
|
||||||
let addr = || p2tr_script_buf(key).unwrap();
|
let addr = || address(Network::Regtest, key).unwrap();
|
||||||
let payments = vec![(addr(), 1000)];
|
let payments = vec![(addr(), 1000)];
|
||||||
|
|
||||||
assert!(SignableTransaction::new(inputs.clone(), &payments, None, None, FEE).is_ok());
|
assert!(SignableTransaction::new(inputs.clone(), &payments, None, None, FEE).is_ok());
|
||||||
@@ -191,17 +223,12 @@ async_sequential! {
|
|||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
SignableTransaction::new(inputs.clone(), &[], Some(addr()), None, 0),
|
SignableTransaction::new(inputs.clone(), &[(addr(), inputs[0].value() * 2)], None, None, FEE),
|
||||||
Err(TransactionError::TooLowFee),
|
Err(TransactionError::NotEnoughFunds),
|
||||||
);
|
);
|
||||||
|
|
||||||
assert!(matches!(
|
|
||||||
SignableTransaction::new(inputs.clone(), &[(addr(), inputs[0].value() * 2)], None, None, FEE),
|
|
||||||
Err(TransactionError::NotEnoughFunds { .. }),
|
|
||||||
));
|
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
SignableTransaction::new(inputs, &vec![(addr(), 1000); 10000], None, None, FEE),
|
SignableTransaction::new(inputs, &vec![(addr(), 1000); 10000], None, None, 0),
|
||||||
Err(TransactionError::TooLargeTransaction),
|
Err(TransactionError::TooLargeTransaction),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -223,13 +250,13 @@ async_sequential! {
|
|||||||
|
|
||||||
// Declare payments, change, fee
|
// Declare payments, change, fee
|
||||||
let payments = [
|
let payments = [
|
||||||
(p2tr_script_buf(key).unwrap(), 1005),
|
(address(Network::Regtest, key).unwrap(), 1005),
|
||||||
(p2tr_script_buf(offset_key).unwrap(), 1007)
|
(address(Network::Regtest, offset_key).unwrap(), 1007)
|
||||||
];
|
];
|
||||||
|
|
||||||
let change_offset = scanner.register_offset(Scalar::random(&mut OsRng)).unwrap();
|
let change_offset = scanner.register_offset(Scalar::random(&mut OsRng)).unwrap();
|
||||||
let change_key = key + (ProjectivePoint::GENERATOR * change_offset);
|
let change_key = key + (ProjectivePoint::GENERATOR * change_offset);
|
||||||
let change_addr = p2tr_script_buf(change_key).unwrap();
|
let change_addr = address(Network::Regtest, change_key).unwrap();
|
||||||
|
|
||||||
// Create and sign the TX
|
// Create and sign the TX
|
||||||
let tx = SignableTransaction::new(
|
let tx = SignableTransaction::new(
|
||||||
@@ -240,15 +267,14 @@ async_sequential! {
|
|||||||
FEE
|
FEE
|
||||||
).unwrap();
|
).unwrap();
|
||||||
let needed_fee = tx.needed_fee();
|
let needed_fee = tx.needed_fee();
|
||||||
let expected_id = tx.txid();
|
let tx = sign(&keys, tx);
|
||||||
let tx = sign(&keys, &tx);
|
|
||||||
|
|
||||||
assert_eq!(tx.output.len(), 3);
|
assert_eq!(tx.output.len(), 3);
|
||||||
|
|
||||||
// Ensure we can scan it
|
// Ensure we can scan it
|
||||||
let outputs = scanner.scan_transaction(&tx);
|
let outputs = scanner.scan_transaction(&tx);
|
||||||
for (o, output) in outputs.iter().enumerate() {
|
for (o, output) in outputs.iter().enumerate() {
|
||||||
assert_eq!(output.outpoint(), &OutPoint::new(tx.compute_txid(), u32::try_from(o).unwrap()));
|
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!(&ReceivedOutput::read::<&[u8]>(&mut output.serialize().as_ref()).unwrap(), output);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -258,33 +284,29 @@ async_sequential! {
|
|||||||
|
|
||||||
// Make sure the payments were properly created
|
// Make sure the payments were properly created
|
||||||
for ((output, scanned), payment) in tx.output.iter().zip(outputs.iter()).zip(payments.iter()) {
|
for ((output, scanned), payment) in tx.output.iter().zip(outputs.iter()).zip(payments.iter()) {
|
||||||
assert_eq!(
|
assert_eq!(output, &TxOut { script_pubkey: payment.0.script_pubkey(), value: payment.1 });
|
||||||
output,
|
|
||||||
&TxOut { script_pubkey: payment.0.clone(), value: Amount::from_sat(payment.1) },
|
|
||||||
);
|
|
||||||
assert_eq!(scanned.value(), payment.1 );
|
assert_eq!(scanned.value(), payment.1 );
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure the change is correct
|
// Make sure the change is correct
|
||||||
assert_eq!(needed_fee, u64::try_from(tx.vsize()).unwrap() * FEE);
|
assert_eq!(needed_fee, u64::try_from(tx.weight()).unwrap() * FEE);
|
||||||
let input_value = output.value() + offset_output.value();
|
let input_value = output.value() + offset_output.value();
|
||||||
let output_value = tx.output.iter().map(|output| output.value.to_sat()).sum::<u64>();
|
let output_value = tx.output.iter().map(|output| output.value).sum::<u64>();
|
||||||
assert_eq!(input_value - output_value, needed_fee);
|
assert_eq!(input_value - output_value, needed_fee);
|
||||||
|
|
||||||
let change_amount =
|
let change_amount =
|
||||||
input_value - payments.iter().map(|payment| payment.1).sum::<u64>() - needed_fee;
|
input_value - payments.iter().map(|payment| payment.1).sum::<u64>() - needed_fee;
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
tx.output[2],
|
tx.output[2],
|
||||||
TxOut { script_pubkey: change_addr, value: Amount::from_sat(change_amount) },
|
TxOut { script_pubkey: change_addr.script_pubkey(), value: change_amount },
|
||||||
);
|
);
|
||||||
|
|
||||||
// This also tests send_raw_transaction and get_transaction, which the RPC test can't
|
// This also tests send_raw_transaction and get_transaction, which the RPC test can't
|
||||||
// effectively test
|
// effectively test
|
||||||
rpc.send_raw_transaction(&tx).await.unwrap();
|
rpc.send_raw_transaction(&tx).await.unwrap();
|
||||||
let mut hash = *tx.compute_txid().as_raw_hash().as_byte_array();
|
let mut hash = *tx.txid().as_raw_hash().as_byte_array();
|
||||||
hash.reverse();
|
hash.reverse();
|
||||||
assert_eq!(tx, rpc.get_transaction(&hash).await.unwrap());
|
assert_eq!(tx, rpc.get_transaction(&hash).await.unwrap());
|
||||||
assert_eq!(expected_id, hash);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn test_data() {
|
async fn test_data() {
|
||||||
@@ -302,10 +324,10 @@ async_sequential! {
|
|||||||
|
|
||||||
let tx = sign(
|
let tx = sign(
|
||||||
&keys,
|
&keys,
|
||||||
&SignableTransaction::new(
|
SignableTransaction::new(
|
||||||
vec![output],
|
vec![output],
|
||||||
&[],
|
&[],
|
||||||
Some(p2tr_script_buf(key).unwrap()),
|
address(Network::Regtest, key),
|
||||||
Some(data.clone()),
|
Some(data.clone()),
|
||||||
FEE
|
FEE
|
||||||
).unwrap()
|
).unwrap()
|
||||||
3
coins/ethereum/.gitignore
vendored
Normal file
3
coins/ethereum/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# solidity build outputs
|
||||||
|
cache
|
||||||
|
artifacts
|
||||||
37
coins/ethereum/Cargo.toml
Normal file
37
coins/ethereum/Cargo.toml
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
[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
|
||||||
|
|
||||||
|
[package.metadata.docs.rs]
|
||||||
|
all-features = true
|
||||||
|
rustdoc-args = ["--cfg", "docsrs"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
thiserror = "1"
|
||||||
|
rand_core = "0.6"
|
||||||
|
|
||||||
|
serde_json = "1"
|
||||||
|
serde = "1"
|
||||||
|
|
||||||
|
sha2 = "0.10"
|
||||||
|
sha3 = "0.10"
|
||||||
|
|
||||||
|
group = "0.13"
|
||||||
|
k256 = { version = "^0.13.1", default-features = false, features = ["std", "arithmetic", "bits", "ecdsa"] }
|
||||||
|
frost = { package = "modular-frost", path = "../../crypto/frost", features = ["secp256k1", "tests"] }
|
||||||
|
|
||||||
|
eyre = "0.6"
|
||||||
|
|
||||||
|
ethers = { version = "2", default-features = false, features = ["abigen", "ethers-solc"] }
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
ethers-solc = "2"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tokio = { version = "1", features = ["macros"] }
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
AGPL-3.0-only license
|
AGPL-3.0-only license
|
||||||
|
|
||||||
Copyright (c) 2023-2025 Luke Parker
|
Copyright (c) 2022-2023 Luke Parker
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
it under the terms of the GNU Affero General Public License Version 3 as
|
||||||
9
coins/ethereum/README.md
Normal file
9
coins/ethereum/README.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# Ethereum
|
||||||
|
|
||||||
|
This package contains Ethereum-related functionality, specifically deploying and
|
||||||
|
interacting with Serai contracts.
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
|
||||||
|
- solc
|
||||||
|
- [Foundry](https://github.com/foundry-rs/foundry)
|
||||||
16
coins/ethereum/build.rs
Normal file
16
coins/ethereum/build.rs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
use ethers_solc::{Project, ProjectPathsConfig};
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
println!("cargo:rerun-if-changed=contracts");
|
||||||
|
println!("cargo:rerun-if-changed=artifacts");
|
||||||
|
|
||||||
|
// configure the project with all its paths, solc, cache etc.
|
||||||
|
let project = Project::builder()
|
||||||
|
.paths(ProjectPathsConfig::hardhat(env!("CARGO_MANIFEST_DIR")).unwrap())
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
project.compile().unwrap();
|
||||||
|
|
||||||
|
// Tell Cargo that if a source file changes, to rerun this build script.
|
||||||
|
project.rerun_if_sources_changed();
|
||||||
|
}
|
||||||
36
coins/ethereum/contracts/Schnorr.sol
Normal file
36
coins/ethereum/contracts/Schnorr.sol
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
//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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
52
coins/ethereum/src/contract.rs
Normal file
52
coins/ethereum/src/contract.rs
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
use crate::crypto::ProcessedSignature;
|
||||||
|
use ethers::{contract::ContractFactory, prelude::*, solc::artifacts::contract::ContractBytecode};
|
||||||
|
use eyre::{eyre, Result};
|
||||||
|
use std::fs::File;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum EthereumError {
|
||||||
|
#[error("failed to verify Schnorr signature")]
|
||||||
|
VerificationError,
|
||||||
|
}
|
||||||
|
|
||||||
|
abigen!(
|
||||||
|
Schnorr,
|
||||||
|
"./artifacts/Schnorr.sol/Schnorr.json",
|
||||||
|
event_derives(serde::Deserialize, serde::Serialize),
|
||||||
|
);
|
||||||
|
|
||||||
|
pub async fn deploy_schnorr_verifier_contract(
|
||||||
|
client: Arc<SignerMiddleware<Provider<Http>, LocalWallet>>,
|
||||||
|
) -> Result<Schnorr<SignerMiddleware<Provider<Http>, LocalWallet>>> {
|
||||||
|
let path = "./artifacts/Schnorr.sol/Schnorr.json";
|
||||||
|
let artifact: ContractBytecode = serde_json::from_reader(File::open(path).unwrap()).unwrap();
|
||||||
|
let abi = artifact.abi.unwrap();
|
||||||
|
let bin = artifact.bytecode.unwrap().object;
|
||||||
|
let factory = ContractFactory::new(abi, bin.into_bytes().unwrap(), client.clone());
|
||||||
|
let contract = factory.deploy(())?.send().await?;
|
||||||
|
let contract = Schnorr::new(contract.address(), client);
|
||||||
|
Ok(contract)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn call_verify(
|
||||||
|
contract: &Schnorr<SignerMiddleware<Provider<Http>, LocalWallet>>,
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
107
coins/ethereum/src/crypto.rs
Normal file
107
coins/ethereum/src/crypto.rs
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
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).try_into().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
2
coins/ethereum/src/lib.rs
Normal file
2
coins/ethereum/src/lib.rs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
pub mod contract;
|
||||||
|
pub mod crypto;
|
||||||
71
coins/ethereum/tests/contract.rs
Normal file
71
coins/ethereum/tests/contract.rs
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
use std::{convert::TryFrom, sync::Arc, time::Duration};
|
||||||
|
|
||||||
|
use rand_core::OsRng;
|
||||||
|
|
||||||
|
use ::k256::{elliptic_curve::bigint::ArrayEncoding, U256};
|
||||||
|
|
||||||
|
use ethers::{
|
||||||
|
prelude::*,
|
||||||
|
utils::{keccak256, Anvil, AnvilInstance},
|
||||||
|
};
|
||||||
|
|
||||||
|
use frost::{
|
||||||
|
curve::Secp256k1,
|
||||||
|
Participant,
|
||||||
|
algorithm::IetfSchnorr,
|
||||||
|
tests::{key_gen, algorithm_machines, sign},
|
||||||
|
};
|
||||||
|
|
||||||
|
use ethereum_serai::{
|
||||||
|
crypto,
|
||||||
|
contract::{Schnorr, call_verify, deploy_schnorr_verifier_contract},
|
||||||
|
};
|
||||||
|
|
||||||
|
async fn deploy_test_contract(
|
||||||
|
) -> (u32, AnvilInstance, Schnorr<SignerMiddleware<Provider<Http>, LocalWallet>>) {
|
||||||
|
let anvil = Anvil::new().spawn();
|
||||||
|
|
||||||
|
let wallet: LocalWallet = anvil.keys()[0].clone().into();
|
||||||
|
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 client = Arc::new(SignerMiddleware::new_with_provider_chain(provider, wallet).await.unwrap());
|
||||||
|
|
||||||
|
(chain_id, anvil, deploy_schnorr_verifier_contract(client).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.clone(),
|
||||||
|
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());
|
||||||
|
}
|
||||||
92
coins/ethereum/tests/crypto.rs
Normal file
92
coins/ethereum/tests/crypto.rs
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
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, IetfSchnorr::<Secp256k1, EthereumHram>::ietf(), &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.clone(),
|
||||||
|
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));
|
||||||
|
}
|
||||||
2
coins/ethereum/tests/mod.rs
Normal file
2
coins/ethereum/tests/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
mod contract;
|
||||||
|
mod crypto;
|
||||||
107
coins/monero/Cargo.toml
Normal file
107
coins/monero/Cargo.toml
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
[package]
|
||||||
|
name = "monero-serai"
|
||||||
|
version = "0.1.4-alpha"
|
||||||
|
description = "A modern Monero transaction library"
|
||||||
|
license = "MIT"
|
||||||
|
repository = "https://github.com/serai-dex/serai/tree/develop/coins/monero"
|
||||||
|
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[package.metadata.docs.rs]
|
||||||
|
all-features = true
|
||||||
|
rustdoc-args = ["--cfg", "docsrs"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
std-shims = { path = "../../common/std-shims", version = "0.1", default-features = false }
|
||||||
|
|
||||||
|
async-trait = { version = "0.1", default-features = false }
|
||||||
|
thiserror = { version = "1", 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 }
|
||||||
|
|
||||||
|
crc = { version = "3", default-features = false }
|
||||||
|
sha3 = { version = "0.10", default-features = false }
|
||||||
|
|
||||||
|
curve25519-dalek = { version = "^3.2", default-features = false }
|
||||||
|
|
||||||
|
# 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.3", default-features = false }
|
||||||
|
multiexp = { path = "../../crypto/multiexp", version = "0.3", 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.3", features = ["serialize"], optional = true }
|
||||||
|
frost = { package = "modular-frost", path = "../../crypto/frost", version = "0.7", features = ["ed25519"], optional = true }
|
||||||
|
|
||||||
|
monero-generators = { path = "generators", version = "0.3", default-features = false }
|
||||||
|
|
||||||
|
futures = { version = "0.3", default-features = false, features = ["alloc"], optional = true }
|
||||||
|
|
||||||
|
hex-literal = "0.4"
|
||||||
|
hex = { version = "0.4", default-features = false, features = ["alloc"] }
|
||||||
|
serde = { version = "1", default-features = false, features = ["derive"] }
|
||||||
|
serde_json = { version = "1", default-features = false, features = ["alloc"] }
|
||||||
|
|
||||||
|
base58-monero = { version = "1", git = "https://github.com/monero-rs/base58-monero", rev = "5045e8d2b817b3b6c1190661f504e879bc769c29", default-features = false, features = ["check"] }
|
||||||
|
|
||||||
|
# Used for the provided RPC
|
||||||
|
digest_auth = { version = "0.3", optional = true }
|
||||||
|
reqwest = { version = "0.11", features = ["json"], optional = true }
|
||||||
|
|
||||||
|
# Used for the binaries
|
||||||
|
tokio = { version = "1", features = ["full"], optional = true }
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
dalek-ff-group = { path = "../../crypto/dalek-ff-group", version = "0.3", default-features = false }
|
||||||
|
monero-generators = { path = "generators", version = "0.3", default-features = false }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
monero-rpc = "0.3"
|
||||||
|
|
||||||
|
frost = { package = "modular-frost", path = "../../crypto/frost", version = "0.7", features = ["tests"] }
|
||||||
|
|
||||||
|
[features]
|
||||||
|
std = [
|
||||||
|
"std-shims/std",
|
||||||
|
|
||||||
|
"thiserror",
|
||||||
|
|
||||||
|
"zeroize/std",
|
||||||
|
"subtle/std",
|
||||||
|
|
||||||
|
"rand_core/std",
|
||||||
|
"rand_chacha/std",
|
||||||
|
"rand/std",
|
||||||
|
"rand_distr/std",
|
||||||
|
|
||||||
|
"sha3/std",
|
||||||
|
|
||||||
|
"curve25519-dalek/std",
|
||||||
|
|
||||||
|
"multiexp/std",
|
||||||
|
|
||||||
|
"monero-generators/std",
|
||||||
|
|
||||||
|
"futures/std",
|
||||||
|
|
||||||
|
"hex/std",
|
||||||
|
"serde/std",
|
||||||
|
"serde_json/std",
|
||||||
|
]
|
||||||
|
|
||||||
|
http_rpc = ["digest_auth", "reqwest"]
|
||||||
|
multisig = ["transcript", "frost", "dleq", "std"]
|
||||||
|
binaries = ["tokio"]
|
||||||
|
experimental = []
|
||||||
|
|
||||||
|
default = ["std", "http_rpc"]
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2024-2025 Luke Parker
|
Copyright (c) 2022-2023 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
|
||||||
49
coins/monero/README.md
Normal file
49
coins/monero/README.md
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
# monero-serai
|
||||||
|
|
||||||
|
A modern Monero transaction library intended for usage in wallets. It prides
|
||||||
|
itself on accuracy, correctness, and removing common pit falls developers may
|
||||||
|
face.
|
||||||
|
|
||||||
|
monero-serai also offers the following features:
|
||||||
|
|
||||||
|
- 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.
|
||||||
67
coins/monero/build.rs
Normal file
67
coins/monero/build.rs
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
use std::{
|
||||||
|
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 static GENERATORS_CELL: OnceLock<Generators> = OnceLock::new();
|
||||||
|
pub fn GENERATORS() -> &'static Generators {{
|
||||||
|
GENERATORS_CELL.get_or_init(|| Generators {{
|
||||||
|
G: [
|
||||||
|
{G_str}
|
||||||
|
],
|
||||||
|
H: [
|
||||||
|
{H_str}
|
||||||
|
],
|
||||||
|
}})
|
||||||
|
}}
|
||||||
|
",
|
||||||
|
)
|
||||||
|
.as_bytes(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
println!("cargo:rerun-if-changed=build.rs");
|
||||||
|
|
||||||
|
generators("bulletproof", "generators.rs");
|
||||||
|
generators("bulletproof_plus", "generators_plus.rs");
|
||||||
|
}
|
||||||
28
coins/monero/generators/Cargo.toml
Normal file
28
coins/monero/generators/Cargo.toml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
[package]
|
||||||
|
name = "monero-generators"
|
||||||
|
version = "0.3.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"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
std-shims = { path = "../../../common/std-shims", version = "0.1", default-features = false }
|
||||||
|
|
||||||
|
subtle = { version = "^2.4", default-features = false }
|
||||||
|
|
||||||
|
sha3 = { version = "0.10", default-features = false }
|
||||||
|
|
||||||
|
curve25519-dalek = { version = "3", default-features = false }
|
||||||
|
|
||||||
|
group = { version = "0.13", default-features = false }
|
||||||
|
dalek-ff-group = { path = "../../../crypto/dalek-ff-group", version = "0.3" }
|
||||||
|
|
||||||
|
[features]
|
||||||
|
std = ["std-shims/std"]
|
||||||
|
default = ["std"]
|
||||||
21
coins/monero/generators/LICENSE
Normal file
21
coins/monero/generators/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
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.
|
||||||
7
coins/monero/generators/README.md
Normal file
7
coins/monero/generators/README.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# 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 `alloc` feature is enabled.
|
||||||
52
coins/monero/generators/src/hash_to_point.rs
Normal file
52
coins/monero/generators/src/hash_to_point.rs
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
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`.
|
||||||
|
#[allow(clippy::many_single_char_names)]
|
||||||
|
pub fn hash_to_point(bytes: [u8; 32]) -> EdwardsPoint {
|
||||||
|
#[allow(non_snake_case, clippy::unreadable_literal)]
|
||||||
|
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()
|
||||||
|
}
|
||||||
80
coins/monero/generators/src/lib.rs
Normal file
80
coins/monero/generators/src/lib.rs
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
//! 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;
|
||||||
|
|
||||||
|
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: [EdwardsPoint; MAX_MN],
|
||||||
|
pub H: [EdwardsPoint; MAX_MN],
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate generators as needed for Bulletproofs(+), as Monero does.
|
||||||
|
pub fn bulletproofs_generators(dst: &'static [u8]) -> Generators {
|
||||||
|
let mut res =
|
||||||
|
Generators { G: [EdwardsPoint::identity(); MAX_MN], H: [EdwardsPoint::identity(); 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[i / 2] = EdwardsPoint(hash_to_point(hash(&even)));
|
||||||
|
res.G[i / 2] = EdwardsPoint(hash_to_point(hash(&odd)));
|
||||||
|
}
|
||||||
|
res
|
||||||
|
}
|
||||||
18
coins/monero/generators/src/varint.rs
Normal file
18
coins/monero/generators/src/varint.rs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
use std_shims::io::{self, Write};
|
||||||
|
|
||||||
|
const VARINT_CONTINUATION_MASK: u8 = 0b1000_0000;
|
||||||
|
|
||||||
|
#[allow(clippy::trivially_copy_pass_by_ref)] // &u64 is needed for API consistency
|
||||||
|
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(())
|
||||||
|
}
|
||||||
155
coins/monero/src/bin/reserialize_chain.rs
Normal file
155
coins/monero/src/bin/reserialize_chain.rs
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use serde::Deserialize;
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
use monero_serai::{
|
||||||
|
transaction::Transaction,
|
||||||
|
block::Block,
|
||||||
|
rpc::{Rpc, HttpRpc},
|
||||||
|
};
|
||||||
|
|
||||||
|
use tokio::task::JoinHandle;
|
||||||
|
|
||||||
|
async fn check_block(rpc: Arc<Rpc<HttpRpc>>, block_i: usize) {
|
||||||
|
let hash = rpc.get_block_hash(block_i).await.expect("couldn't get block {block_i}'s hash");
|
||||||
|
|
||||||
|
// TODO: Grab the JSON to also check it was deserialized correctly
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
struct BlockResponse {
|
||||||
|
blob: String,
|
||||||
|
}
|
||||||
|
let res: BlockResponse = rpc
|
||||||
|
.json_rpc_call("get_block", Some(json!({ "hash": hex::encode(hash) })))
|
||||||
|
.await
|
||||||
|
.expect("couldn't get block {block} via block.hash()");
|
||||||
|
|
||||||
|
let blob = hex::decode(res.blob).expect("node returned non-hex block");
|
||||||
|
let block = Block::read(&mut blob.as_slice()).expect("couldn't deserialize block {block_i}");
|
||||||
|
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 = rpc
|
||||||
|
.rpc_call(
|
||||||
|
"get_transactions",
|
||||||
|
Some(json!({
|
||||||
|
"txs_hashes": hashes_hex.drain(.. hashes_hex.len().min(100)).collect::<Vec<_>>(),
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("couldn't call get_transactions");
|
||||||
|
assert!(txs.missed_tx.is_empty());
|
||||||
|
all_txs.extend(txs.txs);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (tx_hash, tx_res) in block.txs.into_iter().zip(all_txs.into_iter()) {
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("Deserialized, hashed, and reserialized {block_i} with {} TXs", txs_len);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
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| {
|
||||||
|
HttpRpc::new(url.clone())
|
||||||
|
.unwrap_or_else(|_| panic!("couldn't create HttpRpc connected to {url}"))
|
||||||
|
};
|
||||||
|
let main_rpc = rpc(nodes[0].clone());
|
||||||
|
let mut rpcs = vec![];
|
||||||
|
for i in 0 .. async_parallelism {
|
||||||
|
rpcs.push(Arc::new(rpc(nodes[i % nodes.len()].clone())));
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
116
coins/monero/src/block.rs
Normal file
116
coins/monero/src/block.rs
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
use std_shims::{
|
||||||
|
vec::Vec,
|
||||||
|
io::{self, Read, Write},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
hash,
|
||||||
|
merkle::merkle_root,
|
||||||
|
serialize::*,
|
||||||
|
transaction::{Input, Transaction},
|
||||||
|
};
|
||||||
|
|
||||||
|
const CORRECT_BLOCK_HASH_202612: [u8; 32] =
|
||||||
|
hex_literal::hex!("426d16cff04c71f8b16340b722dc4010a2dd3831c22041431f772547ba6e331a");
|
||||||
|
const EXISTING_BLOCK_HASH_202612: [u8; 32] =
|
||||||
|
hex_literal::hex!("bbd604d2ba11ba27935e006ed39c9bfdd99b76bf4a50654bc1e1e61217962698");
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||||
|
pub struct BlockHeader {
|
||||||
|
pub major_version: u64,
|
||||||
|
pub minor_version: u64,
|
||||||
|
pub timestamp: u64,
|
||||||
|
pub previous: [u8; 32],
|
||||||
|
pub nonce: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BlockHeader {
|
||||||
|
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
||||||
|
write_varint(&self.major_version, w)?;
|
||||||
|
write_varint(&self.minor_version, w)?;
|
||||||
|
write_varint(&self.timestamp, w)?;
|
||||||
|
w.write_all(&self.previous)?;
|
||||||
|
w.write_all(&self.nonce.to_le_bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
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<Self> {
|
||||||
|
Ok(Self {
|
||||||
|
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)]
|
||||||
|
pub struct Block {
|
||||||
|
pub header: BlockHeader,
|
||||||
|
pub miner_tx: Transaction,
|
||||||
|
pub txs: Vec<[u8; 32]>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Block {
|
||||||
|
pub fn number(&self) -> usize {
|
||||||
|
match self.miner_tx.prefix.inputs.get(0) {
|
||||||
|
Some(Input::Gen(number)) => (*number).try_into().unwrap(),
|
||||||
|
_ => panic!("invalid block, miner TX didn't have a Input::Gen"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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().try_into().unwrap(), w)?;
|
||||||
|
for tx in &self.txs {
|
||||||
|
w.write_all(tx)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tx_merkle_root(&self) -> [u8; 32] {
|
||||||
|
merkle_root(self.miner_tx.hash(), &self.txs)
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
let mut out = Vec::with_capacity(8 + blob.len());
|
||||||
|
write_varint(&u64::try_from(blob.len()).unwrap(), &mut out).unwrap();
|
||||||
|
out.append(&mut blob);
|
||||||
|
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn hash(&self) -> [u8; 32] {
|
||||||
|
let hash = hash(&self.serialize_hashable());
|
||||||
|
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<Self> {
|
||||||
|
Ok(Self {
|
||||||
|
header: BlockHeader::read(r)?,
|
||||||
|
miner_tx: Transaction::read(r)?,
|
||||||
|
txs: (0 .. read_varint(r)?).map(|_| read_bytes(r)).collect::<Result<_, _>>()?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
178
coins/monero/src/lib.rs
Normal file
178
coins/monero/src/lib.rs
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||||
|
#![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 rand_core::{RngCore, CryptoRng};
|
||||||
|
|
||||||
|
use zeroize::{Zeroize, ZeroizeOnDrop};
|
||||||
|
|
||||||
|
use sha3::{Digest, Keccak256};
|
||||||
|
|
||||||
|
use curve25519_dalek::{constants::ED25519_BASEPOINT_TABLE, scalar::Scalar, edwards::EdwardsPoint};
|
||||||
|
|
||||||
|
pub use monero_generators::H;
|
||||||
|
|
||||||
|
mod merkle;
|
||||||
|
|
||||||
|
mod serialize;
|
||||||
|
use serialize::{read_byte, read_u16};
|
||||||
|
|
||||||
|
/// RingCT structs and functionality.
|
||||||
|
pub mod ringct;
|
||||||
|
use ringct::RctType;
|
||||||
|
|
||||||
|
/// Transaction structs.
|
||||||
|
pub mod transaction;
|
||||||
|
/// Block structs.
|
||||||
|
pub mod block;
|
||||||
|
|
||||||
|
/// Monero daemon RPC interface.
|
||||||
|
pub mod rpc;
|
||||||
|
/// Wallet functionality, enabling scanning and sending transactions.
|
||||||
|
pub mod wallet;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests;
|
||||||
|
|
||||||
|
static INV_EIGHT_CELL: OnceLock<Scalar> = OnceLock::new();
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub(crate) fn INV_EIGHT() -> Scalar {
|
||||||
|
*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 },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Protocol {
|
||||||
|
/// Amount of ring members under this protocol version.
|
||||||
|
pub const fn ring_len(&self) -> usize {
|
||||||
|
match self {
|
||||||
|
Self::v14 => 11,
|
||||||
|
Self::v16 => 16,
|
||||||
|
Self::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 const fn bp_plus(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
Self::v14 => false,
|
||||||
|
Self::v16 => true,
|
||||||
|
Self::Custom { bp_plus, .. } => *bp_plus,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Make this an Option when we support pre-RCT protocols
|
||||||
|
pub const fn optimal_rct_type(&self) -> RctType {
|
||||||
|
match self {
|
||||||
|
Self::v14 => RctType::Clsag,
|
||||||
|
Self::v16 => RctType::BulletproofsPlus,
|
||||||
|
Self::Custom { optimal_rct_type, .. } => *optimal_rct_type,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn write<W: io::Write>(&self, w: &mut W) -> io::Result<()> {
|
||||||
|
match self {
|
||||||
|
Self::v14 => w.write_all(&[0, 14]),
|
||||||
|
Self::v16 => w.write_all(&[0, 16]),
|
||||||
|
Self::Custom { ring_len, bp_plus, optimal_rct_type } => {
|
||||||
|
// 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()])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn read<R: io::Read>(r: &mut R) -> io::Result<Self> {
|
||||||
|
Ok(match read_byte(r)? {
|
||||||
|
// Monero protocol
|
||||||
|
0 => match read_byte(r)? {
|
||||||
|
14 => Self::v14,
|
||||||
|
16 => Self::v16,
|
||||||
|
_ => Err(io::Error::new(io::ErrorKind::Other, "unrecognized monero protocol"))?,
|
||||||
|
},
|
||||||
|
// Custom
|
||||||
|
1 => match read_byte(r)? {
|
||||||
|
0 => Self::Custom {
|
||||||
|
ring_len: read_u16(r)?.into(),
|
||||||
|
bp_plus: match read_byte(r)? {
|
||||||
|
0 => false,
|
||||||
|
1 => true,
|
||||||
|
_ => Err(io::Error::new(io::ErrorKind::Other, "invalid bool serialization"))?,
|
||||||
|
},
|
||||||
|
optimal_rct_type: RctType::from_byte(read_byte(r)?)
|
||||||
|
.ok_or_else(|| io::Error::new(io::ErrorKind::Other, "invalid RctType serialization"))?,
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
Err(io::Error::new(io::ErrorKind::Other, "unrecognized custom protocol serialization"))?
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => Err(io::Error::new(io::ErrorKind::Other, "unrecognized protocol serialization"))?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Transparent structure representing a Pedersen commitment's contents.
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
#[derive(Clone, PartialEq, Eq, Debug, Zeroize, ZeroizeOnDrop)]
|
||||||
|
pub struct Commitment {
|
||||||
|
pub mask: Scalar,
|
||||||
|
pub amount: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Commitment {
|
||||||
|
/// A commitment to zero, defined with a mask of 1 (as to not be the identity).
|
||||||
|
pub fn zero() -> Self {
|
||||||
|
Self { mask: Scalar::one(), amount: 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new(mask: Scalar, amount: u64) -> Self {
|
||||||
|
Self { mask, amount }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculate a Pedersen commitment, as a point, from the transparent structure.
|
||||||
|
pub fn calculate(&self) -> EdwardsPoint {
|
||||||
|
(&self.mask * &ED25519_BASEPOINT_TABLE) + (Scalar::from(self.amount) * H())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Support generating a random scalar using a modern rand, as dalek's is notoriously dated.
|
||||||
|
pub fn random_scalar<R: RngCore + CryptoRng>(rng: &mut R) -> Scalar {
|
||||||
|
let mut r = [0; 64];
|
||||||
|
rng.fill_bytes(&mut r);
|
||||||
|
Scalar::from_bytes_mod_order_wide(&r)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn hash(data: &[u8]) -> [u8; 32] {
|
||||||
|
Keccak256::digest(data).into()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Hash the provided data to a scalar via keccak256(data) % l.
|
||||||
|
pub fn hash_to_scalar(data: &[u8]) -> Scalar {
|
||||||
|
let scalar = 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
|
||||||
|
}
|
||||||
55
coins/monero/src/merkle.rs
Normal file
55
coins/monero/src/merkle.rs
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
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]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
102
coins/monero/src/ringct/borromean.rs
Normal file
102
coins/monero/src/ringct/borromean.rs
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
use core::fmt::Debug;
|
||||||
|
use std_shims::io::{self, Read, Write};
|
||||||
|
|
||||||
|
use curve25519_dalek::edwards::EdwardsPoint;
|
||||||
|
#[cfg(feature = "experimental")]
|
||||||
|
use curve25519_dalek::{traits::Identity, scalar::Scalar};
|
||||||
|
|
||||||
|
#[cfg(feature = "experimental")]
|
||||||
|
use monero_generators::H_pow_2;
|
||||||
|
#[cfg(feature = "experimental")]
|
||||||
|
use crate::hash_to_scalar;
|
||||||
|
use crate::serialize::*;
|
||||||
|
|
||||||
|
/// 64 Borromean ring signatures.
|
||||||
|
///
|
||||||
|
/// This type keeps the data as raw bytes as Monero has some transactions with unreduced scalars in
|
||||||
|
/// this field. While we could use `from_bytes_mod_order`, we'd then not be able to encode this
|
||||||
|
/// back into it's original form.
|
||||||
|
///
|
||||||
|
/// Those scalars also have a custom reduction algorithm...
|
||||||
|
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||||
|
pub struct BorromeanSignatures {
|
||||||
|
pub s0: [[u8; 32]; 64],
|
||||||
|
pub s1: [[u8; 32]; 64],
|
||||||
|
pub ee: [u8; 32],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BorromeanSignatures {
|
||||||
|
pub fn read<R: Read>(r: &mut R) -> io::Result<Self> {
|
||||||
|
Ok(Self { s0: read_array(read_bytes, r)?, s1: read_array(read_bytes, r)?, ee: read_bytes(r)? })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
||||||
|
for s0 in &self.s0 {
|
||||||
|
w.write_all(s0)?;
|
||||||
|
}
|
||||||
|
for s1 in &self.s1 {
|
||||||
|
w.write_all(s1)?;
|
||||||
|
}
|
||||||
|
w.write_all(&self.ee)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "experimental")]
|
||||||
|
fn verify(&self, keys_a: &[EdwardsPoint], keys_b: &[EdwardsPoint]) -> bool {
|
||||||
|
let mut transcript = [0; 2048];
|
||||||
|
for i in 0 .. 64 {
|
||||||
|
// TODO: These aren't the correct reduction
|
||||||
|
// TODO: Can either of these be tightened?
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
let LL = EdwardsPoint::vartime_double_scalar_mul_basepoint(
|
||||||
|
&Scalar::from_bytes_mod_order(self.ee),
|
||||||
|
&keys_a[i],
|
||||||
|
&Scalar::from_bytes_mod_order(self.s0[i]),
|
||||||
|
);
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
let LV = EdwardsPoint::vartime_double_scalar_mul_basepoint(
|
||||||
|
&hash_to_scalar(LL.compress().as_bytes()),
|
||||||
|
&keys_b[i],
|
||||||
|
&Scalar::from_bytes_mod_order(self.s1[i]),
|
||||||
|
);
|
||||||
|
transcript[i .. ((i + 1) * 32)].copy_from_slice(LV.compress().as_bytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: This isn't the correct reduction
|
||||||
|
// TODO: Can this be tightened to from_canonical_bytes?
|
||||||
|
hash_to_scalar(&transcript) == Scalar::from_bytes_mod_order(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<Self> {
|
||||||
|
Ok(Self { 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "experimental")]
|
||||||
|
#[must_use]
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
151
coins/monero/src/ringct/bulletproofs/core.rs
Normal file
151
coins/monero/src/ringct/bulletproofs/core.rs
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
179
coins/monero/src/ringct/bulletproofs/mod.rs
Normal file
179
coins/monero/src/ringct/bulletproofs/mod.rs
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
#![allow(non_snake_case)]
|
||||||
|
|
||||||
|
use std_shims::{
|
||||||
|
vec::Vec,
|
||||||
|
io::{self, Read, Write},
|
||||||
|
};
|
||||||
|
|
||||||
|
use rand_core::{RngCore, CryptoRng};
|
||||||
|
|
||||||
|
use zeroize::Zeroize;
|
||||||
|
|
||||||
|
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;
|
||||||
|
pub use original::GENERATORS as BULLETPROOFS_GENERATORS;
|
||||||
|
pub(crate) mod plus;
|
||||||
|
pub use plus::GENERATORS as BULLETPROOFS_PLUS_GENERATORS;
|
||||||
|
|
||||||
|
pub(crate) use self::original::OriginalStruct;
|
||||||
|
pub(crate) use self::plus::PlusStruct;
|
||||||
|
|
||||||
|
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(PlusStruct),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Bulletproofs {
|
||||||
|
pub(crate) fn fee_weight(plus: bool, outputs: usize) -> usize {
|
||||||
|
let fields = if plus { 6 } else { 9 };
|
||||||
|
|
||||||
|
// TODO: Shouldn't this use u32/u64?
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
let mut LR_len = usize::try_from(usize::BITS - (outputs - 1).leading_zeros()).unwrap();
|
||||||
|
let padded_outputs = 1 << LR_len;
|
||||||
|
LR_len += LOG_N;
|
||||||
|
|
||||||
|
let len = (fields + (2 * LR_len)) * 32;
|
||||||
|
len +
|
||||||
|
if padded_outputs <= 2 {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
let base = ((fields + (2 * (LOG_N + 1))) * 32) / 2;
|
||||||
|
let size = (fields + (2 * LR_len)) * 32;
|
||||||
|
((base * padded_outputs) - size) * 4 / 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Prove the list of commitments are within [0 .. 2^64).
|
||||||
|
pub fn prove<R: RngCore + CryptoRng>(
|
||||||
|
rng: &mut R,
|
||||||
|
outputs: &[Commitment],
|
||||||
|
plus: bool,
|
||||||
|
) -> Result<Self, TransactionError> {
|
||||||
|
if outputs.len() > MAX_OUTPUTS {
|
||||||
|
return Err(TransactionError::TooManyOutputs)?;
|
||||||
|
}
|
||||||
|
Ok(if !plus {
|
||||||
|
Self::Plus(PlusStruct::prove(rng, outputs))
|
||||||
|
} else {
|
||||||
|
Self::Original(OriginalStruct::prove(rng, outputs))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verify the given Bulletproofs.
|
||||||
|
#[must_use]
|
||||||
|
pub fn verify<R: RngCore + CryptoRng>(&self, rng: &mut R, commitments: &[EdwardsPoint]) -> bool {
|
||||||
|
match self {
|
||||||
|
Self::Original(bp) => bp.verify(rng, commitments),
|
||||||
|
Self::Plus(bp) => bp.verify(rng, commitments),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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 {
|
||||||
|
Self::Original(bp) => bp.batch_verify(rng, verifier, id, commitments),
|
||||||
|
Self::Plus(bp) => bp.batch_verify(rng, verifier, id, commitments),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_core<W: Write, F: Fn(&[EdwardsPoint], &mut W) -> io::Result<()>>(
|
||||||
|
&self,
|
||||||
|
w: &mut W,
|
||||||
|
specific_write_vec: F,
|
||||||
|
) -> io::Result<()> {
|
||||||
|
match self {
|
||||||
|
Self::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)
|
||||||
|
}
|
||||||
|
|
||||||
|
Self::Plus(bp) => {
|
||||||
|
write_point(&bp.A, w)?;
|
||||||
|
write_point(&bp.A1, w)?;
|
||||||
|
write_point(&bp.B, w)?;
|
||||||
|
write_scalar(&bp.r1, w)?;
|
||||||
|
write_scalar(&bp.s1, w)?;
|
||||||
|
write_scalar(&bp.d1, w)?;
|
||||||
|
specific_write_vec(&bp.L, w)?;
|
||||||
|
specific_write_vec(&bp.R, 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<Self> {
|
||||||
|
Ok(Self::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<Self> {
|
||||||
|
Ok(Self::Plus(PlusStruct {
|
||||||
|
A: read_point(r)?,
|
||||||
|
A1: read_point(r)?,
|
||||||
|
B: read_point(r)?,
|
||||||
|
r1: read_scalar(r)?,
|
||||||
|
s1: read_scalar(r)?,
|
||||||
|
d1: read_scalar(r)?,
|
||||||
|
L: read_vec(read_point, r)?,
|
||||||
|
R: read_vec(read_point, r)?,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
308
coins/monero/src/ringct/bulletproofs/original.rs
Normal file
308
coins/monero/src/ringct/bulletproofs/original.rs
Normal file
@@ -0,0 +1,308 @@
|
|||||||
|
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 {
|
||||||
|
#[allow(clippy::many_single_char_names)]
|
||||||
|
pub(crate) fn prove<R: RngCore + CryptoRng>(rng: &mut R, commitments: &[Commitment]) -> Self {
|
||||||
|
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 = Self {
|
||||||
|
A: *A,
|
||||||
|
S: *S,
|
||||||
|
T1: *T1,
|
||||||
|
T2: *T2,
|
||||||
|
taux: *taux,
|
||||||
|
mu: *mu,
|
||||||
|
L,
|
||||||
|
R,
|
||||||
|
a: *a[0],
|
||||||
|
b: *b[0],
|
||||||
|
t: *t,
|
||||||
|
};
|
||||||
|
debug_assert!(res.verify(rng, &commitments_points));
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::many_single_char_names)]
|
||||||
|
#[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)
|
||||||
|
}
|
||||||
|
}
|
||||||
300
coins/monero/src/ringct/bulletproofs/plus.rs
Normal file
300
coins/monero/src/ringct/bulletproofs/plus.rs
Normal file
@@ -0,0 +1,300 @@
|
|||||||
|
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;
|
||||||
|
use dalek_ff_group::{ED25519_BASEPOINT_POINT as G, Scalar, EdwardsPoint};
|
||||||
|
|
||||||
|
use multiexp::BatchVerifier;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
Commitment, hash,
|
||||||
|
ringct::{hash_to_point::raw_hash_to_point, bulletproofs::core::*},
|
||||||
|
};
|
||||||
|
|
||||||
|
include!(concat!(env!("OUT_DIR"), "/generators_plus.rs"));
|
||||||
|
|
||||||
|
static TRANSCRIPT_CELL: OnceLock<[u8; 32]> = OnceLock::new();
|
||||||
|
pub(crate) fn TRANSCRIPT() -> [u8; 32] {
|
||||||
|
*TRANSCRIPT_CELL.get_or_init(|| {
|
||||||
|
EdwardsPoint(raw_hash_to_point(hash(b"bulletproof_plus_transcript"))).compress().to_bytes()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TRANSCRIPT isn't a Scalar, so we need this alternative for the first hash
|
||||||
|
fn hash_plus<C: IntoIterator<Item = DalekPoint>>(commitments: C) -> (Scalar, Vec<EdwardsPoint>) {
|
||||||
|
let (cache, commitments) = hash_commitments(commitments);
|
||||||
|
(hash_to_scalar(&[TRANSCRIPT().as_ref(), &cache.to_bytes()].concat()), commitments)
|
||||||
|
}
|
||||||
|
|
||||||
|
// d[j*N+i] = z**(2*(j+1)) * 2**i
|
||||||
|
fn d(z: Scalar, M: usize, MN: usize) -> (ScalarVector, ScalarVector) {
|
||||||
|
let zpow = ScalarVector::even_powers(z, 2 * M);
|
||||||
|
let mut d = vec![Scalar::ZERO; MN];
|
||||||
|
for j in 0 .. M {
|
||||||
|
for i in 0 .. N {
|
||||||
|
d[(j * N) + i] = zpow[j] * TWO_N()[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(zpow, ScalarVector(d))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||||
|
pub struct PlusStruct {
|
||||||
|
pub(crate) A: DalekPoint,
|
||||||
|
pub(crate) A1: DalekPoint,
|
||||||
|
pub(crate) B: DalekPoint,
|
||||||
|
pub(crate) r1: DalekScalar,
|
||||||
|
pub(crate) s1: DalekScalar,
|
||||||
|
pub(crate) d1: DalekScalar,
|
||||||
|
pub(crate) L: Vec<DalekPoint>,
|
||||||
|
pub(crate) R: Vec<DalekPoint>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PlusStruct {
|
||||||
|
#[allow(clippy::many_single_char_names)]
|
||||||
|
pub(crate) fn prove<R: RngCore + CryptoRng>(rng: &mut R, commitments: &[Commitment]) -> Self {
|
||||||
|
let generators = GENERATORS();
|
||||||
|
|
||||||
|
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_plus(commitments_points.clone());
|
||||||
|
let (mut alpha1, A) = alpha_rho(&mut *rng, generators, &aL, &aR);
|
||||||
|
|
||||||
|
let y = hash_cache(&mut cache, &[A.compress().to_bytes()]);
|
||||||
|
let mut cache = hash_to_scalar(&y.to_bytes());
|
||||||
|
let z = cache;
|
||||||
|
|
||||||
|
let (zpow, d) = d(z, M, MN);
|
||||||
|
|
||||||
|
let aL1 = aL - z;
|
||||||
|
|
||||||
|
let ypow = ScalarVector::powers(y, MN + 2);
|
||||||
|
let mut y_for_d = ScalarVector(ypow.0[1 ..= MN].to_vec());
|
||||||
|
y_for_d.0.reverse();
|
||||||
|
let aR1 = (aR + z) + (y_for_d * d);
|
||||||
|
|
||||||
|
for (j, gamma) in commitments.iter().map(|c| Scalar(c.mask)).enumerate() {
|
||||||
|
alpha1 += zpow[j] * ypow[MN + 1] * gamma;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut a = aL1;
|
||||||
|
let mut b = aR1;
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
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 = weighted_inner_product(&aL, &bR, y);
|
||||||
|
let cR = weighted_inner_product(&(&aR * ypow[aR.len()]), &bL, y);
|
||||||
|
|
||||||
|
let (mut dL, mut dR) = (Scalar::random(&mut *rng), Scalar::random(&mut *rng));
|
||||||
|
|
||||||
|
let (G_L, G_R) = G_proof.split_at(aL.len());
|
||||||
|
let (H_L, H_R) = H_proof.split_at(aL.len());
|
||||||
|
|
||||||
|
let mut L_i = LR_statements(&(&aL * yinvpow[aL.len()]), G_R, &bR, H_L, cL, H());
|
||||||
|
L_i.push((dL, G));
|
||||||
|
let L_i = prove_multiexp(&L_i);
|
||||||
|
L.push(*L_i);
|
||||||
|
|
||||||
|
let mut R_i = LR_statements(&(&aR * ypow[aR.len()]), G_L, &bL, H_R, cR, H());
|
||||||
|
R_i.push((dR, G));
|
||||||
|
let R_i = prove_multiexp(&R_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();
|
||||||
|
|
||||||
|
G_proof = hadamard_fold(G_L, G_R, winv, w * yinvpow[aL.len()]);
|
||||||
|
H_proof = hadamard_fold(H_L, H_R, w, winv);
|
||||||
|
|
||||||
|
a = (&aL * w) + (aR * (winv * ypow[aL.len()]));
|
||||||
|
b = (bL * winv) + (bR * w);
|
||||||
|
|
||||||
|
alpha1 += (dL * (w * w)) + (dR * (winv * winv));
|
||||||
|
|
||||||
|
dL.zeroize();
|
||||||
|
dR.zeroize();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut r = Scalar::random(&mut *rng);
|
||||||
|
let mut s = Scalar::random(&mut *rng);
|
||||||
|
let mut d = Scalar::random(&mut *rng);
|
||||||
|
let mut eta = Scalar::random(&mut *rng);
|
||||||
|
|
||||||
|
let A1 = prove_multiexp(&[
|
||||||
|
(r, G_proof[0]),
|
||||||
|
(s, H_proof[0]),
|
||||||
|
(d, G),
|
||||||
|
((r * y * b[0]) + (s * y * a[0]), H()),
|
||||||
|
]);
|
||||||
|
let B = prove_multiexp(&[(r * y * s, H()), (eta, G)]);
|
||||||
|
let e = hash_cache(&mut cache, &[A1.compress().to_bytes(), B.compress().to_bytes()]);
|
||||||
|
|
||||||
|
let r1 = (a[0] * e) + r;
|
||||||
|
r.zeroize();
|
||||||
|
let s1 = (b[0] * e) + s;
|
||||||
|
s.zeroize();
|
||||||
|
let d1 = ((d * e) + eta) + (alpha1 * (e * e));
|
||||||
|
d.zeroize();
|
||||||
|
eta.zeroize();
|
||||||
|
alpha1.zeroize();
|
||||||
|
|
||||||
|
let res = Self { A: *A, A1: *A1, B: *B, r1: *r1, s1: *s1, d1: *d1, L, R };
|
||||||
|
debug_assert!(res.verify(rng, &commitments_points));
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::many_single_char_names)]
|
||||||
|
#[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_plus(commitments.iter().copied());
|
||||||
|
let y = hash_cache(&mut cache, &[self.A.compress().to_bytes()]);
|
||||||
|
let yinv = y.invert().unwrap();
|
||||||
|
let z = hash_to_scalar(&y.to_bytes());
|
||||||
|
cache = z;
|
||||||
|
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
|
||||||
|
let e = hash_cache(&mut cache, &[self.A1.compress().to_bytes(), self.B.compress().to_bytes()]);
|
||||||
|
|
||||||
|
// 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 A = normalize(&self.A);
|
||||||
|
let A1 = normalize(&self.A1);
|
||||||
|
let B = normalize(&self.B);
|
||||||
|
|
||||||
|
// Verify it
|
||||||
|
let mut proof = Vec::with_capacity(logMN + 5 + (2 * (MN + logMN)));
|
||||||
|
|
||||||
|
let mut yMN = y;
|
||||||
|
for _ in 0 .. logMN {
|
||||||
|
yMN *= yMN;
|
||||||
|
}
|
||||||
|
let yMNy = yMN * y;
|
||||||
|
|
||||||
|
let (zpow, d) = d(z, M, MN);
|
||||||
|
let zsq = zpow[0];
|
||||||
|
|
||||||
|
let esq = e * e;
|
||||||
|
let minus_esq = -esq;
|
||||||
|
let commitment_weight = minus_esq * yMNy;
|
||||||
|
for (i, commitment) in commitments.iter().map(EdwardsPoint::mul_by_cofactor).enumerate() {
|
||||||
|
proof.push((commitment_weight * zpow[i], commitment));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invert B, instead of the Scalar, as the latter is only 2x as expensive yet enables reduction
|
||||||
|
// to a single addition under vartime for the first BP verified in the batch, which is expected
|
||||||
|
// to be much more significant
|
||||||
|
proof.push((Scalar::ONE, -B));
|
||||||
|
proof.push((-e, A1));
|
||||||
|
proof.push((minus_esq, A));
|
||||||
|
proof.push((Scalar(self.d1), G));
|
||||||
|
|
||||||
|
let d_sum = zpow.sum() * Scalar::from(u64::MAX);
|
||||||
|
let y_sum = weighted_powers(y, MN).sum();
|
||||||
|
proof.push((
|
||||||
|
Scalar(self.r1 * y.0 * self.s1) + (esq * ((yMNy * z * d_sum) + ((zsq - z) * y_sum))),
|
||||||
|
H(),
|
||||||
|
));
|
||||||
|
|
||||||
|
let w_cache = challenge_products(&w, &winv);
|
||||||
|
|
||||||
|
let mut e_r1_y = e * Scalar(self.r1);
|
||||||
|
let e_s1 = e * Scalar(self.s1);
|
||||||
|
let esq_z = esq * z;
|
||||||
|
let minus_esq_z = -esq_z;
|
||||||
|
let mut minus_esq_y = minus_esq * yMN;
|
||||||
|
|
||||||
|
let generators = GENERATORS();
|
||||||
|
for i in 0 .. MN {
|
||||||
|
proof.push((e_r1_y * w_cache[i] + esq_z, generators.G[i]));
|
||||||
|
proof.push((
|
||||||
|
(e_s1 * w_cache[(!i) & (MN - 1)]) + minus_esq_z + (minus_esq_y * d[i]),
|
||||||
|
generators.H[i],
|
||||||
|
));
|
||||||
|
|
||||||
|
e_r1_y *= yinv;
|
||||||
|
minus_esq_y *= yinv;
|
||||||
|
}
|
||||||
|
|
||||||
|
for i in 0 .. logMN {
|
||||||
|
proof.push((minus_esq * w[i] * w[i], L[i]));
|
||||||
|
proof.push((minus_esq * 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
137
coins/monero/src/ringct/bulletproofs/scalar_vector.rs
Normal file
137
coins/monero/src/ringct/bulletproofs/scalar_vector.rs
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
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) => {
|
||||||
|
impl $Op<Scalar> for ScalarVector {
|
||||||
|
type Output = Self;
|
||||||
|
fn $op(self, b: Scalar) -> Self {
|
||||||
|
Self(self.0.iter().map(|a| $f((a, &b))).collect())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl $Op<Scalar> for &ScalarVector {
|
||||||
|
type Output = ScalarVector;
|
||||||
|
fn $op(self, b: Scalar) -> ScalarVector {
|
||||||
|
ScalarVector(self.0.iter().map(|a| $f((a, &b))).collect())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl $Op<ScalarVector> for ScalarVector {
|
||||||
|
type Output = Self;
|
||||||
|
fn $op(self, b: Self) -> Self {
|
||||||
|
debug_assert_eq!(self.len(), b.len());
|
||||||
|
Self(self.0.iter().zip(b.0.iter()).map($f).collect())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl $Op<Self> for &ScalarVector {
|
||||||
|
type Output = ScalarVector;
|
||||||
|
fn $op(self, b: Self) -> 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) -> Self {
|
||||||
|
Self(vec![Scalar::ZERO; len])
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn powers(x: Scalar, len: usize) -> Self {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
Self(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn even_powers(x: Scalar, pow: usize) -> Self {
|
||||||
|
debug_assert!(pow != 0);
|
||||||
|
// Verify pow is a power of two
|
||||||
|
debug_assert_eq!(((pow - 1) & pow), 0);
|
||||||
|
|
||||||
|
let xsq = x * x;
|
||||||
|
let mut res = Self(Vec::with_capacity(pow / 2));
|
||||||
|
res.0.push(xsq);
|
||||||
|
|
||||||
|
let mut prev = 2;
|
||||||
|
while prev < pow {
|
||||||
|
res.0.push(res[res.len() - 1] * xsq);
|
||||||
|
prev += 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
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) -> (Self, Self) {
|
||||||
|
let (l, r) = self.0.split_at(self.0.len() / 2);
|
||||||
|
(Self(l.to_vec()), Self(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()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn weighted_powers(x: Scalar, len: usize) -> ScalarVector {
|
||||||
|
ScalarVector(ScalarVector::powers(x, len + 1).0[1 ..].to_vec())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn weighted_inner_product(a: &ScalarVector, b: &ScalarVector, y: Scalar) -> Scalar {
|
||||||
|
// y ** 0 is not used as a power
|
||||||
|
(a * b * weighted_powers(y, a.len())).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
|
||||||
|
}
|
||||||
325
coins/monero/src/ringct/clsag/mod.rs
Normal file
325
coins/monero/src/ringct/clsag/mod.rs
Normal file
@@ -0,0 +1,325 @@
|
|||||||
|
#![allow(non_snake_case)]
|
||||||
|
|
||||||
|
use core::ops::Deref;
|
||||||
|
use std_shims::{
|
||||||
|
vec::Vec,
|
||||||
|
io::{self, Read, Write},
|
||||||
|
};
|
||||||
|
|
||||||
|
use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing};
|
||||||
|
use subtle::{ConstantTimeEq, Choice, CtOption};
|
||||||
|
use rand_core::{RngCore, CryptoRng};
|
||||||
|
|
||||||
|
use curve25519_dalek::{
|
||||||
|
constants::ED25519_BASEPOINT_TABLE,
|
||||||
|
scalar::Scalar,
|
||||||
|
traits::{IsIdentity, VartimePrecomputedMultiscalarMul},
|
||||||
|
edwards::{EdwardsPoint, VartimeEdwardsPrecomputation},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
INV_EIGHT, Commitment, random_scalar, hash_to_scalar, wallet::decoys::Decoys,
|
||||||
|
ringct::hash_to_point, serialize::*,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(feature = "multisig")]
|
||||||
|
mod multisig;
|
||||||
|
#[cfg(feature = "multisig")]
|
||||||
|
pub use multisig::{ClsagDetails, ClsagAddendum, ClsagMultisig};
|
||||||
|
#[cfg(feature = "multisig")]
|
||||||
|
pub(crate) use multisig::add_key_image_share;
|
||||||
|
|
||||||
|
/// Errors returned when CLSAG signing fails.
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||||
|
#[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, PartialEq, Eq, Debug, Zeroize, ZeroizeOnDrop)]
|
||||||
|
pub struct ClsagInput {
|
||||||
|
// The actual commitment for the true spend
|
||||||
|
pub(crate) commitment: Commitment,
|
||||||
|
// True spend index, offsets, and ring
|
||||||
|
pub(crate) decoys: Decoys,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ClsagInput {
|
||||||
|
pub fn new(commitment: Commitment, decoys: Decoys) -> Result<Self, ClsagError> {
|
||||||
|
let n = decoys.len();
|
||||||
|
if n > u8::MAX.into() {
|
||||||
|
Err(ClsagError::InternalError("max ring size in this library is u8 max"))?;
|
||||||
|
}
|
||||||
|
let n = u8::try_from(n).unwrap();
|
||||||
|
if decoys.i >= n {
|
||||||
|
Err(ClsagError::InvalidRingMember(decoys.i, n))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the commitment matches
|
||||||
|
if decoys.ring[usize::from(decoys.i)][1] != commitment.calculate() {
|
||||||
|
Err(ClsagError::InvalidCommitment)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self { commitment, decoys })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::large_enum_variant)]
|
||||||
|
enum Mode {
|
||||||
|
Sign(usize, EdwardsPoint, EdwardsPoint),
|
||||||
|
Verify(Scalar),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Core of the CLSAG algorithm, applicable to both sign and verify with minimal differences
|
||||||
|
// Said differences are covered via the above Mode
|
||||||
|
fn core(
|
||||||
|
ring: &[[EdwardsPoint; 2]],
|
||||||
|
I: &EdwardsPoint,
|
||||||
|
pseudo_out: &EdwardsPoint,
|
||||||
|
msg: &[u8; 32],
|
||||||
|
D: &EdwardsPoint,
|
||||||
|
s: &[Scalar],
|
||||||
|
A_c1: Mode,
|
||||||
|
) -> ((EdwardsPoint, Scalar, Scalar), Scalar) {
|
||||||
|
let n = ring.len();
|
||||||
|
|
||||||
|
let images_precomp = VartimeEdwardsPrecomputation::new([I, D]);
|
||||||
|
let D = D * INV_EIGHT();
|
||||||
|
|
||||||
|
// Generate the transcript
|
||||||
|
// Instead of generating multiple, a single transcript is created and then edited as needed
|
||||||
|
const PREFIX: &[u8] = b"CLSAG_";
|
||||||
|
#[rustfmt::skip]
|
||||||
|
const AGG_0: &[u8] = b"agg_0";
|
||||||
|
#[rustfmt::skip]
|
||||||
|
const ROUND: &[u8] = b"round";
|
||||||
|
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([0; 32 - PREFIX_AGG_0_LEN]);
|
||||||
|
|
||||||
|
let mut P = Vec::with_capacity(n);
|
||||||
|
for member in ring {
|
||||||
|
P.push(member[0]);
|
||||||
|
to_hash.extend(member[0].compress().to_bytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut C = Vec::with_capacity(n);
|
||||||
|
for member in ring {
|
||||||
|
C.push(member[1] - pseudo_out);
|
||||||
|
to_hash.extend(member[1].compress().to_bytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
to_hash.extend(I.compress().to_bytes());
|
||||||
|
to_hash.extend(D.compress().to_bytes());
|
||||||
|
to_hash.extend(pseudo_out.compress().to_bytes());
|
||||||
|
// mu_P with agg_0
|
||||||
|
let mu_P = hash_to_scalar(&to_hash);
|
||||||
|
// mu_C with agg_1
|
||||||
|
to_hash[PREFIX_AGG_0_LEN - 1] = b'1';
|
||||||
|
let mu_C = hash_to_scalar(&to_hash);
|
||||||
|
|
||||||
|
// Truncate it for the round transcript, altering the DST as needed
|
||||||
|
to_hash.truncate(((2 * n) + 1) * 32);
|
||||||
|
for i in 0 .. ROUND.len() {
|
||||||
|
to_hash[PREFIX.len() + i] = ROUND[i];
|
||||||
|
}
|
||||||
|
// Unfortunately, it's I D pseudo_out instead of pseudo_out I D, meaning this needs to be
|
||||||
|
// truncated just to add it back
|
||||||
|
to_hash.extend(pseudo_out.compress().to_bytes());
|
||||||
|
to_hash.extend(msg);
|
||||||
|
|
||||||
|
// Configure the loop based on if we're signing or verifying
|
||||||
|
let start;
|
||||||
|
let end;
|
||||||
|
let mut c;
|
||||||
|
match A_c1 {
|
||||||
|
Mode::Sign(r, A, AH) => {
|
||||||
|
start = r + 1;
|
||||||
|
end = r + n;
|
||||||
|
to_hash.extend(A.compress().to_bytes());
|
||||||
|
to_hash.extend(AH.compress().to_bytes());
|
||||||
|
c = hash_to_scalar(&to_hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
Mode::Verify(c1) => {
|
||||||
|
start = 0;
|
||||||
|
end = n;
|
||||||
|
c = c1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform the core loop
|
||||||
|
let mut c1 = CtOption::new(Scalar::zero(), Choice::from(0));
|
||||||
|
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
|
||||||
|
// removes the risk of branch prediction creating timing differences depending on ring index
|
||||||
|
// however
|
||||||
|
c1 = c1.or_else(|| CtOption::new(c, i.ct_eq(&0)));
|
||||||
|
|
||||||
|
let c_p = mu_P * c;
|
||||||
|
let c_c = mu_C * c;
|
||||||
|
|
||||||
|
let L = (&s[i] * &ED25519_BASEPOINT_TABLE) + (c_p * P[i]) + (c_c * C[i]);
|
||||||
|
let PH = hash_to_point(P[i]);
|
||||||
|
// 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]);
|
||||||
|
|
||||||
|
to_hash.truncate(((2 * n) + 3) * 32);
|
||||||
|
to_hash.extend(L.compress().to_bytes());
|
||||||
|
to_hash.extend(R.compress().to_bytes());
|
||||||
|
c = hash_to_scalar(&to_hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
// This first tuple is needed to continue signing, the latter is the c to be tested/worked with
|
||||||
|
((D, c * mu_P, c * mu_C), c1.unwrap_or(c))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// CLSAG signature, as used in Monero.
|
||||||
|
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||||
|
pub struct Clsag {
|
||||||
|
pub D: EdwardsPoint,
|
||||||
|
pub s: Vec<Scalar>,
|
||||||
|
pub c1: Scalar,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Clsag {
|
||||||
|
// Sign core is the extension of core as needed for signing, yet is shared between single signer
|
||||||
|
// and multisig, hence why it's still core
|
||||||
|
#[allow(clippy::many_single_char_names)]
|
||||||
|
pub(crate) fn sign_core<R: RngCore + CryptoRng>(
|
||||||
|
rng: &mut R,
|
||||||
|
I: &EdwardsPoint,
|
||||||
|
input: &ClsagInput,
|
||||||
|
mask: Scalar,
|
||||||
|
msg: &[u8; 32],
|
||||||
|
A: EdwardsPoint,
|
||||||
|
AH: EdwardsPoint,
|
||||||
|
) -> (Self, EdwardsPoint, Scalar, Scalar) {
|
||||||
|
let r: usize = input.decoys.i.into();
|
||||||
|
|
||||||
|
let pseudo_out = Commitment::new(mask, input.commitment.amount).calculate();
|
||||||
|
let z = input.commitment.mask - mask;
|
||||||
|
|
||||||
|
let H = hash_to_point(input.decoys.ring[r][0]);
|
||||||
|
let D = H * z;
|
||||||
|
let mut s = Vec::with_capacity(input.decoys.ring.len());
|
||||||
|
for _ in 0 .. input.decoys.ring.len() {
|
||||||
|
s.push(random_scalar(rng));
|
||||||
|
}
|
||||||
|
let ((D, p, c), c1) =
|
||||||
|
core(&input.decoys.ring, I, &pseudo_out, msg, &D, &s, Mode::Sign(r, A, AH));
|
||||||
|
|
||||||
|
(Self { D, s, c1 }, pseudo_out, p, c * z)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate CLSAG signatures for the given inputs.
|
||||||
|
/// 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>(
|
||||||
|
rng: &mut R,
|
||||||
|
mut inputs: Vec<(Zeroizing<Scalar>, EdwardsPoint, ClsagInput)>,
|
||||||
|
sum_outputs: Scalar,
|
||||||
|
msg: [u8; 32],
|
||||||
|
) -> Vec<(Self, EdwardsPoint)> {
|
||||||
|
let mut res = Vec::with_capacity(inputs.len());
|
||||||
|
let mut sum_pseudo_outs = Scalar::zero();
|
||||||
|
for i in 0 .. inputs.len() {
|
||||||
|
let mask = if i == (inputs.len() - 1) {
|
||||||
|
sum_outputs - sum_pseudo_outs
|
||||||
|
} else {
|
||||||
|
let mask = random_scalar(rng);
|
||||||
|
sum_pseudo_outs += mask;
|
||||||
|
mask
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut nonce = Zeroizing::new(random_scalar(rng));
|
||||||
|
let (mut clsag, pseudo_out, p, c) = Self::sign_core(
|
||||||
|
rng,
|
||||||
|
&inputs[i].1,
|
||||||
|
&inputs[i].2,
|
||||||
|
mask,
|
||||||
|
&msg,
|
||||||
|
nonce.deref() * &ED25519_BASEPOINT_TABLE,
|
||||||
|
nonce.deref() *
|
||||||
|
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)] =
|
||||||
|
(-((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
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Verify the CLSAG signature against the given Transaction data.
|
||||||
|
pub fn verify(
|
||||||
|
&self,
|
||||||
|
ring: &[[EdwardsPoint; 2]],
|
||||||
|
I: &EdwardsPoint,
|
||||||
|
pseudo_out: &EdwardsPoint,
|
||||||
|
msg: &[u8; 32],
|
||||||
|
) -> Result<(), ClsagError> {
|
||||||
|
// Preliminary checks. s, c1, and points must also be encoded canonically, which isn't checked
|
||||||
|
// here
|
||||||
|
if ring.is_empty() {
|
||||||
|
Err(ClsagError::InvalidRing)?;
|
||||||
|
}
|
||||||
|
if ring.len() != self.s.len() {
|
||||||
|
Err(ClsagError::InvalidS)?;
|
||||||
|
}
|
||||||
|
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 {
|
||||||
|
Err(ClsagError::InvalidC1)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn fee_weight(ring_len: usize) -> usize {
|
||||||
|
(ring_len * 32) + 32 + 32
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
||||||
|
write_raw_vec(write_scalar, &self.s, w)?;
|
||||||
|
w.write_all(&self.c1.to_bytes())?;
|
||||||
|
write_point(&self.D, w)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read<R: Read>(decoys: usize, r: &mut R) -> io::Result<Self> {
|
||||||
|
Ok(Self { s: read_raw_vec(read_scalar, decoys, r)?, c1: read_scalar(r)?, D: read_point(r)? })
|
||||||
|
}
|
||||||
|
}
|
||||||
311
coins/monero/src/ringct/clsag/multisig.rs
Normal file
311
coins/monero/src/ringct/clsag/multisig.rs
Normal file
@@ -0,0 +1,311 @@
|
|||||||
|
use core::{ops::Deref, fmt::Debug};
|
||||||
|
use std_shims::{
|
||||||
|
sync::Arc,
|
||||||
|
io::{self, Read, Write},
|
||||||
|
};
|
||||||
|
use std::sync::RwLock;
|
||||||
|
|
||||||
|
use rand_core::{RngCore, CryptoRng, SeedableRng};
|
||||||
|
use rand_chacha::ChaCha20Rng;
|
||||||
|
|
||||||
|
use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing};
|
||||||
|
|
||||||
|
use curve25519_dalek::{
|
||||||
|
traits::{Identity, IsIdentity},
|
||||||
|
scalar::Scalar,
|
||||||
|
edwards::EdwardsPoint,
|
||||||
|
};
|
||||||
|
|
||||||
|
use group::{ff::Field, Group, GroupEncoding};
|
||||||
|
|
||||||
|
use transcript::{Transcript, RecommendedTranscript};
|
||||||
|
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::{
|
||||||
|
hash_to_point,
|
||||||
|
clsag::{ClsagInput, Clsag},
|
||||||
|
};
|
||||||
|
|
||||||
|
fn dleq_transcript() -> RecommendedTranscript {
|
||||||
|
RecommendedTranscript::new(b"monero_key_image_dleq")
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ClsagInput {
|
||||||
|
fn transcript<T: Transcript>(&self, transcript: &mut T) {
|
||||||
|
// Doesn't domain separate as this is considered part of the larger CLSAG proof
|
||||||
|
|
||||||
|
// Ring index
|
||||||
|
transcript.append_message(b"real_spend", [self.decoys.i]);
|
||||||
|
|
||||||
|
// Ring
|
||||||
|
for (i, pair) in self.decoys.ring.iter().enumerate() {
|
||||||
|
// 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
|
||||||
|
// if in use
|
||||||
|
transcript.append_message(b"member", [u8::try_from(i).expect("ring size exceeded 255")]);
|
||||||
|
transcript.append_message(b"key", pair[0].compress().to_bytes());
|
||||||
|
transcript.append_message(b"commitment", pair[1].compress().to_bytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
// discrete log problem, which breaks everything already
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// CLSAG input and the mask to use for it.
|
||||||
|
#[derive(Clone, Debug, Zeroize, ZeroizeOnDrop)]
|
||||||
|
pub struct ClsagDetails {
|
||||||
|
input: ClsagInput,
|
||||||
|
mask: Scalar,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ClsagDetails {
|
||||||
|
pub fn new(input: ClsagInput, mask: Scalar) -> Self {
|
||||||
|
Self { input, mask }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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)]
|
||||||
|
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||||
|
struct Interim {
|
||||||
|
p: Scalar,
|
||||||
|
c: Scalar,
|
||||||
|
|
||||||
|
clsag: Clsag,
|
||||||
|
pseudo_out: EdwardsPoint,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// FROST algorithm for producing a CLSAG signature.
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct ClsagMultisig {
|
||||||
|
transcript: RecommendedTranscript,
|
||||||
|
|
||||||
|
pub(crate) H: EdwardsPoint,
|
||||||
|
// Merged here as CLSAG needs it, passing it would be a mess, yet having it beforehand requires
|
||||||
|
// an extra round
|
||||||
|
image: EdwardsPoint,
|
||||||
|
|
||||||
|
details: Arc<RwLock<Option<ClsagDetails>>>,
|
||||||
|
|
||||||
|
msg: Option<[u8; 32]>,
|
||||||
|
interim: Option<Interim>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ClsagMultisig {
|
||||||
|
pub fn new(
|
||||||
|
transcript: RecommendedTranscript,
|
||||||
|
output_key: EdwardsPoint,
|
||||||
|
details: Arc<RwLock<Option<ClsagDetails>>>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
transcript,
|
||||||
|
|
||||||
|
H: hash_to_point(output_key),
|
||||||
|
image: EdwardsPoint::identity(),
|
||||||
|
|
||||||
|
details,
|
||||||
|
|
||||||
|
msg: None,
|
||||||
|
interim: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn input(&self) -> ClsagInput {
|
||||||
|
(*self.details.read().unwrap()).as_ref().unwrap().input.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mask(&self) -> Scalar {
|
||||||
|
(*self.details.read().unwrap()).as_ref().unwrap().mask
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn add_key_image_share(
|
||||||
|
image: &mut EdwardsPoint,
|
||||||
|
generator: EdwardsPoint,
|
||||||
|
offset: Scalar,
|
||||||
|
included: &[Participant],
|
||||||
|
participant: Participant,
|
||||||
|
share: EdwardsPoint,
|
||||||
|
) {
|
||||||
|
if image.is_identity() {
|
||||||
|
*image = generator * offset;
|
||||||
|
}
|
||||||
|
*image += share * lagrange::<dfg::Scalar>(participant, included).0;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Algorithm<Ed25519> for ClsagMultisig {
|
||||||
|
type Transcript = RecommendedTranscript;
|
||||||
|
type Addendum = ClsagAddendum;
|
||||||
|
type Signature = (Clsag, EdwardsPoint);
|
||||||
|
|
||||||
|
fn nonces(&self) -> Vec<Vec<dfg::EdwardsPoint>> {
|
||||||
|
vec![vec![dfg::EdwardsPoint::generator(), dfg::EdwardsPoint(self.H)]]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn preprocess_addendum<R: RngCore + CryptoRng>(
|
||||||
|
&mut self,
|
||||||
|
rng: &mut R,
|
||||||
|
keys: &ThresholdKeys<Ed25519>,
|
||||||
|
) -> ClsagAddendum {
|
||||||
|
ClsagAddendum {
|
||||||
|
key_image: dfg::EdwardsPoint(self.H) * keys.secret_share().deref(),
|
||||||
|
dleq: 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 dleq_transcript(),
|
||||||
|
&[dfg::EdwardsPoint::generator(), dfg::EdwardsPoint(self.H)],
|
||||||
|
keys.secret_share(),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_addendum<R: Read>(&self, reader: &mut R) -> io::Result<ClsagAddendum> {
|
||||||
|
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::new(io::ErrorKind::Other, "invalid key image"))?;
|
||||||
|
// Ensure this is a canonical point
|
||||||
|
if xH.to_bytes() != bytes {
|
||||||
|
Err(io::Error::new(io::ErrorKind::Other, "non-canonical key image"))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(ClsagAddendum { key_image: xH, dleq: DLEqProof::<dfg::EdwardsPoint>::read(reader)? })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process_addendum(
|
||||||
|
&mut self,
|
||||||
|
view: &ThresholdView<Ed25519>,
|
||||||
|
l: Participant,
|
||||||
|
addendum: ClsagAddendum,
|
||||||
|
) -> Result<(), FrostError> {
|
||||||
|
if self.image.is_identity() {
|
||||||
|
self.transcript.domain_separate(b"CLSAG");
|
||||||
|
self.input().transcript(&mut self.transcript);
|
||||||
|
self.transcript.append_message(b"mask", self.mask().to_bytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
self.transcript.append_message(b"participant", l.to_bytes());
|
||||||
|
|
||||||
|
addendum
|
||||||
|
.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,
|
||||||
|
view.offset().0,
|
||||||
|
view.included(),
|
||||||
|
l,
|
||||||
|
addendum.key_image.0,
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn transcript(&mut self) -> &mut Self::Transcript {
|
||||||
|
&mut self.transcript
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sign_share(
|
||||||
|
&mut self,
|
||||||
|
view: &ThresholdView<Ed25519>,
|
||||||
|
nonce_sums: &[Vec<dfg::EdwardsPoint>],
|
||||||
|
nonces: Vec<Zeroizing<dfg::Scalar>>,
|
||||||
|
msg: &[u8],
|
||||||
|
) -> dfg::Scalar {
|
||||||
|
// Use the transcript to get a seeded random number generator
|
||||||
|
// 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
|
||||||
|
// for, along with the mask which should not only require knowing the shared keys yet also the
|
||||||
|
// input commitment masks)
|
||||||
|
let mut rng = ChaCha20Rng::from_seed(self.transcript.rng_seed(b"decoy_responses"));
|
||||||
|
|
||||||
|
self.msg = Some(msg.try_into().expect("CLSAG message should be 32-bytes"));
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
let (clsag, pseudo_out, p, c) = Clsag::sign_core(
|
||||||
|
&mut rng,
|
||||||
|
&self.image,
|
||||||
|
&self.input(),
|
||||||
|
self.mask(),
|
||||||
|
self.msg.as_ref().unwrap(),
|
||||||
|
nonce_sums[0][0].0,
|
||||||
|
nonce_sums[0][1].0,
|
||||||
|
);
|
||||||
|
self.interim = Some(Interim { p, c, clsag, pseudo_out });
|
||||||
|
|
||||||
|
(-(dfg::Scalar(p) * view.secret_share().deref())) + nonces[0].deref()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
fn verify(
|
||||||
|
&self,
|
||||||
|
_: dfg::EdwardsPoint,
|
||||||
|
_: &[Vec<dfg::EdwardsPoint>],
|
||||||
|
sum: dfg::Scalar,
|
||||||
|
) -> Option<Self::Signature> {
|
||||||
|
let interim = self.interim.as_ref().unwrap();
|
||||||
|
let mut clsag = interim.clsag.clone();
|
||||||
|
clsag.s[usize::from(self.input().decoys.i)] = sum.0 - interim.c;
|
||||||
|
if clsag
|
||||||
|
.verify(
|
||||||
|
&self.input().decoys.ring,
|
||||||
|
&self.image,
|
||||||
|
&interim.pseudo_out,
|
||||||
|
self.msg.as_ref().unwrap(),
|
||||||
|
)
|
||||||
|
.is_ok()
|
||||||
|
{
|
||||||
|
return Some((clsag, interim.pseudo_out));
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn verify_share(
|
||||||
|
&self,
|
||||||
|
verification_share: dfg::EdwardsPoint,
|
||||||
|
nonces: &[Vec<dfg::EdwardsPoint>],
|
||||||
|
share: dfg::Scalar,
|
||||||
|
) -> Result<Vec<(dfg::Scalar, dfg::EdwardsPoint)>, ()> {
|
||||||
|
let interim = self.interim.as_ref().unwrap();
|
||||||
|
Ok(vec![
|
||||||
|
(share, dfg::EdwardsPoint::generator()),
|
||||||
|
(dfg::Scalar(interim.p), verification_share),
|
||||||
|
(-dfg::Scalar::ONE, nonces[0][0]),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
8
coins/monero/src/ringct/hash_to_point.rs
Normal file
8
coins/monero/src/ringct/hash_to_point.rs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
use curve25519_dalek::edwards::EdwardsPoint;
|
||||||
|
|
||||||
|
pub use monero_generators::{hash_to_point as raw_hash_to_point};
|
||||||
|
|
||||||
|
/// Monero's hash to point function, as named `ge_fromfe_frombytes_vartime`.
|
||||||
|
pub fn hash_to_point(key: EdwardsPoint) -> EdwardsPoint {
|
||||||
|
raw_hash_to_point(key.compress().to_bytes())
|
||||||
|
}
|
||||||
72
coins/monero/src/ringct/mlsag.rs
Normal file
72
coins/monero/src/ringct/mlsag.rs
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
use std_shims::{
|
||||||
|
vec::Vec,
|
||||||
|
io::{self, Read, Write},
|
||||||
|
};
|
||||||
|
|
||||||
|
use curve25519_dalek::scalar::Scalar;
|
||||||
|
#[cfg(feature = "experimental")]
|
||||||
|
use curve25519_dalek::edwards::EdwardsPoint;
|
||||||
|
|
||||||
|
use crate::serialize::*;
|
||||||
|
#[cfg(feature = "experimental")]
|
||||||
|
use crate::{hash_to_scalar, ringct::hash_to_point};
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||||
|
pub struct Mlsag {
|
||||||
|
pub ss: Vec<[Scalar; 2]>,
|
||||||
|
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, r: &mut R) -> io::Result<Self> {
|
||||||
|
Ok(Self {
|
||||||
|
ss: (0 .. mixins).map(|_| read_array(read_scalar, r)).collect::<Result<_, _>>()?,
|
||||||
|
cc: read_scalar(r)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "experimental")]
|
||||||
|
#[must_use]
|
||||||
|
pub fn verify(
|
||||||
|
&self,
|
||||||
|
msg: &[u8; 32],
|
||||||
|
ring: &[[EdwardsPoint; 2]],
|
||||||
|
key_image: &EdwardsPoint,
|
||||||
|
) -> bool {
|
||||||
|
if ring.is_empty() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut buf = Vec::with_capacity(6 * 32);
|
||||||
|
let mut ci = self.cc;
|
||||||
|
for (i, ring_member) in ring.iter().enumerate() {
|
||||||
|
buf.extend_from_slice(msg);
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
let L =
|
||||||
|
|r| EdwardsPoint::vartime_double_scalar_mul_basepoint(&ci, &ring_member[r], &self.ss[i][r]);
|
||||||
|
|
||||||
|
buf.extend_from_slice(ring_member[0].compress().as_bytes());
|
||||||
|
buf.extend_from_slice(L(0).compress().as_bytes());
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
let R = (self.ss[i][0] * hash_to_point(ring_member[0])) + (ci * key_image);
|
||||||
|
buf.extend_from_slice(R.compress().as_bytes());
|
||||||
|
|
||||||
|
buf.extend_from_slice(ring_member[1].compress().as_bytes());
|
||||||
|
buf.extend_from_slice(L(1).compress().as_bytes());
|
||||||
|
|
||||||
|
ci = hash_to_scalar(&buf);
|
||||||
|
buf.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
ci == self.cc
|
||||||
|
}
|
||||||
|
}
|
||||||
383
coins/monero/src/ringct/mod.rs
Normal file
383
coins/monero/src/ringct/mod.rs
Normal file
@@ -0,0 +1,383 @@
|
|||||||
|
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};
|
||||||
|
|
||||||
|
pub(crate) mod hash_to_point;
|
||||||
|
pub use hash_to_point::{raw_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;
|
||||||
|
/// BorromeanRange struct, along with verifying functionality.
|
||||||
|
pub mod borromean;
|
||||||
|
/// Bulletproofs(+) structs, along with proving and verifying functionality.
|
||||||
|
pub mod bulletproofs;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
Protocol,
|
||||||
|
serialize::*,
|
||||||
|
ringct::{mlsag::Mlsag, clsag::Clsag, borromean::BorromeanRange, bulletproofs::Bulletproofs},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Generate a key image for a given key. Defined as `x * hash_to_point(xG)`.
|
||||||
|
pub fn generate_key_image(secret: &Zeroizing<Scalar>) -> EdwardsPoint {
|
||||||
|
hash_to_point(&ED25519_BASEPOINT_TABLE * secret.deref()) * secret.deref()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq, Eq, 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<Self> {
|
||||||
|
Ok(if compact {
|
||||||
|
Self::Compact { amount: read_bytes(r)? }
|
||||||
|
} else {
|
||||||
|
Self::Original { mask: read_bytes(r)?, amount: read_bytes(r)? }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
||||||
|
match self {
|
||||||
|
Self::Original { mask, amount } => {
|
||||||
|
w.write_all(mask)?;
|
||||||
|
w.write_all(amount)
|
||||||
|
}
|
||||||
|
Self::Compact { amount } => w.write_all(amount),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
|
||||||
|
pub enum RctType {
|
||||||
|
/// No RCT proofs.
|
||||||
|
Null,
|
||||||
|
/// One MLSAG for a single input and a Borromean range proof (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 {
|
||||||
|
Self::Null => 0,
|
||||||
|
Self::MlsagAggregate => 1,
|
||||||
|
Self::MlsagIndividual => 2,
|
||||||
|
Self::Bulletproofs => 3,
|
||||||
|
Self::BulletproofsCompactAmount => 4,
|
||||||
|
Self::Clsag => 5,
|
||||||
|
Self::BulletproofsPlus => 6,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_byte(byte: u8) -> Option<Self> {
|
||||||
|
Some(match byte {
|
||||||
|
0 => Self::Null,
|
||||||
|
1 => Self::MlsagAggregate,
|
||||||
|
2 => Self::MlsagIndividual,
|
||||||
|
3 => Self::Bulletproofs,
|
||||||
|
4 => Self::BulletproofsCompactAmount,
|
||||||
|
5 => Self::Clsag,
|
||||||
|
6 => Self::BulletproofsPlus,
|
||||||
|
_ => None?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn compact_encrypted_amounts(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
Self::Null | Self::MlsagAggregate | Self::MlsagIndividual | Self::Bulletproofs => false,
|
||||||
|
Self::BulletproofsCompactAmount | Self::Clsag | Self::BulletproofsPlus => true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||||
|
pub struct RctBase {
|
||||||
|
pub fee: u64,
|
||||||
|
pub pseudo_outs: Vec<EdwardsPoint>,
|
||||||
|
pub encrypted_amounts: Vec<EncryptedAmount>,
|
||||||
|
pub commitments: Vec<EdwardsPoint>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RctBase {
|
||||||
|
pub(crate) fn fee_weight(outputs: usize) -> usize {
|
||||||
|
1 + 8 + (outputs * (8 + 32))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write<W: Write>(&self, w: &mut W, rct_type: RctType) -> io::Result<()> {
|
||||||
|
w.write_all(&[rct_type.to_byte()])?;
|
||||||
|
match rct_type {
|
||||||
|
RctType::Null => Ok(()),
|
||||||
|
RctType::MlsagAggregate |
|
||||||
|
RctType::MlsagIndividual |
|
||||||
|
RctType::Bulletproofs |
|
||||||
|
RctType::BulletproofsCompactAmount |
|
||||||
|
RctType::Clsag |
|
||||||
|
RctType::BulletproofsPlus => {
|
||||||
|
write_varint(&self.fee, w)?;
|
||||||
|
if rct_type == RctType::MlsagIndividual {
|
||||||
|
write_raw_vec(write_point, &self.pseudo_outs, w)?;
|
||||||
|
}
|
||||||
|
for encrypted_amount in &self.encrypted_amounts {
|
||||||
|
encrypted_amount.write(w)?;
|
||||||
|
}
|
||||||
|
write_raw_vec(write_point, &self.commitments, w)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read<R: Read>(inputs: usize, outputs: usize, r: &mut R) -> io::Result<(Self, RctType)> {
|
||||||
|
let rct_type = RctType::from_byte(read_byte(r)?)
|
||||||
|
.ok_or_else(|| io::Error::new(io::ErrorKind::Other, "invalid 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::new(io::ErrorKind::Other, "RCT with Bulletproofs(+) had 0 outputs"))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((
|
||||||
|
if rct_type == RctType::Null {
|
||||||
|
Self { fee: 0, pseudo_outs: vec![], encrypted_amounts: vec![], commitments: vec![] }
|
||||||
|
} else {
|
||||||
|
Self {
|
||||||
|
fee: read_varint(r)?,
|
||||||
|
pseudo_outs: if rct_type == RctType::MlsagIndividual {
|
||||||
|
read_raw_vec(read_point, inputs, r)?
|
||||||
|
} else {
|
||||||
|
vec![]
|
||||||
|
},
|
||||||
|
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,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||||
|
pub enum RctPrunable {
|
||||||
|
Null,
|
||||||
|
MlsagBorromean {
|
||||||
|
borromean: Vec<BorromeanRange>,
|
||||||
|
mlsags: Vec<Mlsag>,
|
||||||
|
},
|
||||||
|
MlsagBulletproofs {
|
||||||
|
bulletproofs: Bulletproofs,
|
||||||
|
mlsags: Vec<Mlsag>,
|
||||||
|
pseudo_outs: Vec<EdwardsPoint>,
|
||||||
|
},
|
||||||
|
Clsag {
|
||||||
|
bulletproofs: Bulletproofs,
|
||||||
|
clsags: Vec<Clsag>,
|
||||||
|
pseudo_outs: Vec<EdwardsPoint>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RctPrunable {
|
||||||
|
pub(crate) fn fee_weight(protocol: Protocol, inputs: usize, outputs: usize) -> usize {
|
||||||
|
1 + Bulletproofs::fee_weight(protocol.bp_plus(), outputs) +
|
||||||
|
(inputs * (Clsag::fee_weight(protocol.ring_len()) + 32))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write<W: Write>(&self, w: &mut W, rct_type: RctType) -> io::Result<()> {
|
||||||
|
match self {
|
||||||
|
Self::Null => Ok(()),
|
||||||
|
Self::MlsagBorromean { borromean, mlsags } => {
|
||||||
|
write_raw_vec(BorromeanRange::write, borromean, w)?;
|
||||||
|
write_raw_vec(Mlsag::write, mlsags, w)
|
||||||
|
}
|
||||||
|
Self::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)
|
||||||
|
}
|
||||||
|
Self::Clsag { bulletproofs, clsags, pseudo_outs } => {
|
||||||
|
w.write_all(&[1])?;
|
||||||
|
bulletproofs.write(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> {
|
||||||
|
let mut serialized = vec![];
|
||||||
|
self.write(&mut serialized, rct_type).unwrap();
|
||||||
|
serialized
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read<R: Read>(
|
||||||
|
rct_type: RctType,
|
||||||
|
decoys: &[usize],
|
||||||
|
outputs: usize,
|
||||||
|
r: &mut R,
|
||||||
|
) -> io::Result<Self> {
|
||||||
|
Ok(match rct_type {
|
||||||
|
RctType::Null => Self::Null,
|
||||||
|
RctType::MlsagAggregate | RctType::MlsagIndividual => Self::MlsagBorromean {
|
||||||
|
borromean: read_raw_vec(BorromeanRange::read, outputs, r)?,
|
||||||
|
mlsags: decoys.iter().map(|d| Mlsag::read(*d, r)).collect::<Result<_, _>>()?,
|
||||||
|
},
|
||||||
|
RctType::Bulletproofs | RctType::BulletproofsCompactAmount => Self::MlsagBulletproofs {
|
||||||
|
bulletproofs: {
|
||||||
|
if (if rct_type == RctType::Bulletproofs {
|
||||||
|
u64::from(read_u32(r)?)
|
||||||
|
} else {
|
||||||
|
read_varint(r)?
|
||||||
|
}) != 1
|
||||||
|
{
|
||||||
|
Err(io::Error::new(io::ErrorKind::Other, "n bulletproofs instead of one"))?;
|
||||||
|
}
|
||||||
|
Bulletproofs::read(r)?
|
||||||
|
},
|
||||||
|
mlsags: decoys.iter().map(|d| Mlsag::read(*d, r)).collect::<Result<_, _>>()?,
|
||||||
|
pseudo_outs: read_raw_vec(read_point, decoys.len(), r)?,
|
||||||
|
},
|
||||||
|
RctType::Clsag | RctType::BulletproofsPlus => Self::Clsag {
|
||||||
|
bulletproofs: {
|
||||||
|
if read_varint(r)? != 1 {
|
||||||
|
Err(io::Error::new(io::ErrorKind::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<_, _>>()?,
|
||||||
|
pseudo_outs: read_raw_vec(read_point, decoys.len(), r)?,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn signature_write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
||||||
|
match self {
|
||||||
|
Self::Null => panic!("Serializing RctPrunable::Null for a signature"),
|
||||||
|
Self::MlsagBorromean { borromean, .. } => borromean.iter().try_for_each(|rs| rs.write(w)),
|
||||||
|
Self::MlsagBulletproofs { bulletproofs, .. } => bulletproofs.signature_write(w),
|
||||||
|
Self::Clsag { bulletproofs, .. } => bulletproofs.signature_write(w),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||||
|
pub struct RctSignatures {
|
||||||
|
pub base: RctBase,
|
||||||
|
pub prunable: RctPrunable,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RctSignatures {
|
||||||
|
/// RctType for a given RctSignatures struct.
|
||||||
|
pub fn rct_type(&self) -> RctType {
|
||||||
|
match &self.prunable {
|
||||||
|
RctPrunable::Null => RctType::Null,
|
||||||
|
RctPrunable::MlsagBorromean { .. } => {
|
||||||
|
/*
|
||||||
|
This type of RctPrunable may have no outputs, yet pseudo_outs are per input
|
||||||
|
This will only be a valid RctSignatures if it's for a TX with inputs
|
||||||
|
That makes this valid for any valid RctSignatures
|
||||||
|
|
||||||
|
While it will be invalid for any invalid RctSignatures, potentially letting an invalid
|
||||||
|
MlsagAggregate be interpreted as a valid MlsagIndividual (or vice versa), they have
|
||||||
|
incompatible deserializations
|
||||||
|
|
||||||
|
This means it's impossible to receive a MlsagAggregate over the wire and interpret it
|
||||||
|
as a MlsagIndividual (or vice versa)
|
||||||
|
|
||||||
|
That only makes manual manipulation unsafe, which will always be true since these fields
|
||||||
|
are all pub
|
||||||
|
|
||||||
|
TODO: Consider making them private with read-only accessors?
|
||||||
|
*/
|
||||||
|
if self.base.pseudo_outs.is_empty() {
|
||||||
|
RctType::MlsagAggregate
|
||||||
|
} else {
|
||||||
|
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
|
||||||
|
.get(0)
|
||||||
|
.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) -> usize {
|
||||||
|
RctBase::fee_weight(outputs) + RctPrunable::fee_weight(protocol, inputs, outputs)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
||||||
|
let rct_type = self.rct_type();
|
||||||
|
self.base.write(w, rct_type)?;
|
||||||
|
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: Vec<usize>, outputs: usize, r: &mut R) -> io::Result<Self> {
|
||||||
|
let base = RctBase::read(decoys.len(), outputs, r)?;
|
||||||
|
Ok(Self { base: base.0, prunable: RctPrunable::read(base.1, &decoys, outputs, r)? })
|
||||||
|
}
|
||||||
|
}
|
||||||
91
coins/monero/src/rpc/http.rs
Normal file
91
coins/monero/src/rpc/http.rs
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
use async_trait::async_trait;
|
||||||
|
|
||||||
|
use digest_auth::AuthContext;
|
||||||
|
use reqwest::Client;
|
||||||
|
|
||||||
|
use crate::rpc::{RpcError, RpcConnection, Rpc};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct HttpRpc {
|
||||||
|
client: Client,
|
||||||
|
userpass: Option<(String, String)>,
|
||||||
|
url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HttpRpc {
|
||||||
|
/// Create a new HTTP(S) RPC connection.
|
||||||
|
///
|
||||||
|
/// A daemon requiring authentication can be used via including the username and password in the
|
||||||
|
/// URL.
|
||||||
|
pub fn new(mut url: String) -> Result<Rpc<Self>, RpcError> {
|
||||||
|
// Parse out the username and password
|
||||||
|
let userpass = if url.contains('@') {
|
||||||
|
let url_clone = url;
|
||||||
|
let split_url = url_clone.split('@').collect::<Vec<_>>();
|
||||||
|
if split_url.len() != 2 {
|
||||||
|
Err(RpcError::InvalidNode)?;
|
||||||
|
}
|
||||||
|
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::InvalidNode)?;
|
||||||
|
}
|
||||||
|
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::InvalidNode)?;
|
||||||
|
}
|
||||||
|
Some((split_userpass[0].to_string(), split_userpass[1].to_string()))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Rpc(Self { client: Client::new(), userpass, url }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl RpcConnection for HttpRpc {
|
||||||
|
async fn post(&self, route: &str, body: Vec<u8>) -> Result<Vec<u8>, RpcError> {
|
||||||
|
let mut builder = self.client.post(self.url.clone() + "/" + route).body(body);
|
||||||
|
|
||||||
|
if let Some((user, pass)) = &self.userpass {
|
||||||
|
let req = self.client.post(&self.url).send().await.map_err(|_| RpcError::InvalidNode)?;
|
||||||
|
// Only provide authentication if this daemon actually expects it
|
||||||
|
if let Some(header) = req.headers().get("www-authenticate") {
|
||||||
|
builder = builder.header(
|
||||||
|
"Authorization",
|
||||||
|
digest_auth::parse(header.to_str().map_err(|_| RpcError::InvalidNode)?)
|
||||||
|
.map_err(|_| RpcError::InvalidNode)?
|
||||||
|
.respond(&AuthContext::new_post::<_, _, _, &[u8]>(
|
||||||
|
user,
|
||||||
|
pass,
|
||||||
|
"/".to_string() + route,
|
||||||
|
None,
|
||||||
|
))
|
||||||
|
.map_err(|_| RpcError::InvalidNode)?
|
||||||
|
.to_header_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(
|
||||||
|
builder
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(|_| RpcError::ConnectionError)?
|
||||||
|
.bytes()
|
||||||
|
.await
|
||||||
|
.map_err(|_| RpcError::ConnectionError)?
|
||||||
|
.slice(..)
|
||||||
|
.to_vec(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
617
coins/monero/src/rpc/mod.rs
Normal file
617
coins/monero/src/rpc/mod.rs
Normal file
@@ -0,0 +1,617 @@
|
|||||||
|
use core::fmt::Debug;
|
||||||
|
#[cfg(not(feature = "std"))]
|
||||||
|
use alloc::boxed::Box;
|
||||||
|
use std_shims::{
|
||||||
|
vec::Vec,
|
||||||
|
io,
|
||||||
|
string::{String, ToString},
|
||||||
|
};
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
|
||||||
|
use curve25519_dalek::edwards::{EdwardsPoint, CompressedEdwardsY};
|
||||||
|
|
||||||
|
use serde::{Serialize, Deserialize, de::DeserializeOwned};
|
||||||
|
use serde_json::{Value, json};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
Protocol,
|
||||||
|
serialize::*,
|
||||||
|
transaction::{Input, Timelock, Transaction},
|
||||||
|
block::Block,
|
||||||
|
wallet::Fee,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(feature = "http_rpc")]
|
||||||
|
mod http;
|
||||||
|
#[cfg(feature = "http_rpc")]
|
||||||
|
pub use http::*;
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
pub struct EmptyResponse;
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
pub struct JsonRpcResponse<T> {
|
||||||
|
result: T,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||||
|
#[cfg_attr(feature = "std", derive(thiserror::Error))]
|
||||||
|
pub enum RpcError {
|
||||||
|
#[cfg_attr(feature = "std", error("internal error ({0})"))]
|
||||||
|
InternalError(&'static str),
|
||||||
|
#[cfg_attr(feature = "std", error("connection error"))]
|
||||||
|
ConnectionError,
|
||||||
|
#[cfg_attr(feature = "std", error("invalid node"))]
|
||||||
|
InvalidNode,
|
||||||
|
#[cfg_attr(feature = "std", error("unsupported protocol version ({0})"))]
|
||||||
|
UnsupportedProtocol(usize),
|
||||||
|
#[cfg_attr(feature = "std", error("transactions not found"))]
|
||||||
|
TransactionsNotFound(Vec<[u8; 32]>),
|
||||||
|
#[cfg_attr(feature = "std", error("invalid point ({0})"))]
|
||||||
|
InvalidPoint(String),
|
||||||
|
#[cfg_attr(feature = "std", error("pruned transaction"))]
|
||||||
|
PrunedTransaction,
|
||||||
|
#[cfg_attr(feature = "std", error("invalid transaction ({0:?})"))]
|
||||||
|
InvalidTransaction([u8; 32]),
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rpc_hex(value: &str) -> Result<Vec<u8>, RpcError> {
|
||||||
|
hex::decode(value).map_err(|_| RpcError::InvalidNode)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hash_hex(hash: &str) -> Result<[u8; 32], RpcError> {
|
||||||
|
rpc_hex(hash)?.try_into().map_err(|_| RpcError::InvalidNode)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rpc_point(point: &str) -> Result<EdwardsPoint, RpcError> {
|
||||||
|
CompressedEdwardsY(
|
||||||
|
rpc_hex(point)?.try_into().map_err(|_| RpcError::InvalidPoint(point.to_string()))?,
|
||||||
|
)
|
||||||
|
.decompress()
|
||||||
|
.ok_or_else(|| RpcError::InvalidPoint(point.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read an EPEE VarInt, distinct from the VarInts used throughout the rest of the protocol
|
||||||
|
fn read_epee_vi<R: io::Read>(reader: &mut R) -> io::Result<u64> {
|
||||||
|
let vi_start = read_byte(reader)?;
|
||||||
|
let len = match vi_start & 0b11 {
|
||||||
|
0 => 1,
|
||||||
|
1 => 2,
|
||||||
|
2 => 4,
|
||||||
|
3 => 8,
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
let mut vi = u64::from(vi_start >> 2);
|
||||||
|
for i in 1 .. len {
|
||||||
|
vi |= u64::from(read_byte(reader)?) << (((i - 1) * 8) + 6);
|
||||||
|
}
|
||||||
|
Ok(vi)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait RpcConnection: Send + Sync + Clone + Debug {
|
||||||
|
/// Perform a POST request to the specified route with the specified body.
|
||||||
|
///
|
||||||
|
/// The implementor is left to handle anything such as authentication.
|
||||||
|
async fn post(&self, route: &str, body: Vec<u8>) -> Result<Vec<u8>, RpcError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Make this provided methods for RpcConnection?
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Rpc<R: RpcConnection>(R);
|
||||||
|
impl<R: RpcConnection> Rpc<R> {
|
||||||
|
/// Perform a RPC call to the specified route with the provided parameters.
|
||||||
|
///
|
||||||
|
/// This is NOT a JSON-RPC call. They use a route of "json_rpc" and are available via
|
||||||
|
/// `json_rpc_call`.
|
||||||
|
pub async fn rpc_call<Params: Send + Serialize + Debug, Response: DeserializeOwned + Debug>(
|
||||||
|
&self,
|
||||||
|
route: &str,
|
||||||
|
params: Option<Params>,
|
||||||
|
) -> Result<Response, RpcError> {
|
||||||
|
serde_json::from_str(
|
||||||
|
std_shims::str::from_utf8(
|
||||||
|
&self
|
||||||
|
.0
|
||||||
|
.post(
|
||||||
|
route,
|
||||||
|
if let Some(params) = params {
|
||||||
|
serde_json::to_string(¶ms).unwrap().into_bytes()
|
||||||
|
} else {
|
||||||
|
vec![]
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await?,
|
||||||
|
)
|
||||||
|
.map_err(|_| RpcError::InvalidNode)?,
|
||||||
|
)
|
||||||
|
.map_err(|_| RpcError::InvalidNode)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Perform a JSON-RPC call with the specified method with the provided parameters
|
||||||
|
pub async fn json_rpc_call<Response: DeserializeOwned + Debug>(
|
||||||
|
&self,
|
||||||
|
method: &str,
|
||||||
|
params: Option<Value>,
|
||||||
|
) -> Result<Response, RpcError> {
|
||||||
|
let mut req = json!({ "method": method });
|
||||||
|
if let Some(params) = params {
|
||||||
|
req.as_object_mut().unwrap().insert("params".into(), params);
|
||||||
|
}
|
||||||
|
Ok(self.rpc_call::<_, JsonRpcResponse<Response>>("json_rpc", Some(req)).await?.result)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Perform a binary call to the specified route with the provided parameters.
|
||||||
|
pub async fn bin_call(&self, route: &str, params: Vec<u8>) -> Result<Vec<u8>, RpcError> {
|
||||||
|
self.0.post(route, params).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the active blockchain protocol version.
|
||||||
|
pub async fn get_protocol(&self) -> Result<Protocol, RpcError> {
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
struct ProtocolResponse {
|
||||||
|
major_version: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
struct LastHeaderResponse {
|
||||||
|
block_header: ProtocolResponse,
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(
|
||||||
|
match self
|
||||||
|
.json_rpc_call::<LastHeaderResponse>("get_last_block_header", None)
|
||||||
|
.await?
|
||||||
|
.block_header
|
||||||
|
.major_version
|
||||||
|
{
|
||||||
|
13 | 14 => Protocol::v14,
|
||||||
|
15 | 16 => Protocol::v16,
|
||||||
|
protocol => Err(RpcError::UnsupportedProtocol(protocol))?,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_transactions(&self, hashes: &[[u8; 32]]) -> Result<Vec<Transaction>, RpcError> {
|
||||||
|
if hashes.is_empty() {
|
||||||
|
return Ok(vec![]);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut hashes_hex = hashes.iter().map(hex::encode).collect::<Vec<_>>();
|
||||||
|
let mut all_txs = Vec::with_capacity(hashes.len());
|
||||||
|
while !hashes_hex.is_empty() {
|
||||||
|
// Monero errors if more than 100 is requested unless using a non-restricted RPC
|
||||||
|
const TXS_PER_REQUEST: usize = 100;
|
||||||
|
let this_count = TXS_PER_REQUEST.min(hashes_hex.len());
|
||||||
|
|
||||||
|
let txs: TransactionsResponse = self
|
||||||
|
.rpc_call(
|
||||||
|
"get_transactions",
|
||||||
|
Some(json!({
|
||||||
|
"txs_hashes": hashes_hex.drain(.. this_count).collect::<Vec<_>>(),
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if !txs.missed_tx.is_empty() {
|
||||||
|
Err(RpcError::TransactionsNotFound(
|
||||||
|
txs.missed_tx.iter().map(|hash| hash_hex(hash)).collect::<Result<_, _>>()?,
|
||||||
|
))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
all_txs.extend(txs.txs);
|
||||||
|
}
|
||||||
|
|
||||||
|
all_txs
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, res)| {
|
||||||
|
let tx = Transaction::read::<&[u8]>(
|
||||||
|
&mut rpc_hex(if !res.as_hex.is_empty() { &res.as_hex } else { &res.pruned_as_hex })?
|
||||||
|
.as_ref(),
|
||||||
|
)
|
||||||
|
.map_err(|_| match hash_hex(&res.tx_hash) {
|
||||||
|
Ok(hash) => RpcError::InvalidTransaction(hash),
|
||||||
|
Err(err) => err,
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// https://github.com/monero-project/monero/issues/8311
|
||||||
|
if res.as_hex.is_empty() {
|
||||||
|
match tx.prefix.inputs.get(0) {
|
||||||
|
Some(Input::Gen { .. }) => (),
|
||||||
|
_ => Err(RpcError::PrunedTransaction)?,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This does run a few keccak256 hashes, which is pointless if the node is trusted
|
||||||
|
// In exchange, this provides resilience against invalid/malicious nodes
|
||||||
|
if tx.hash() != hashes[i] {
|
||||||
|
Err(RpcError::InvalidNode)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(tx)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_transaction(&self, tx: [u8; 32]) -> Result<Transaction, RpcError> {
|
||||||
|
self.get_transactions(&[tx]).await.map(|mut txs| txs.swap_remove(0))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the hash of a block from the node by the block's numbers.
|
||||||
|
/// This function does not verify the returned block hash is actually for the number in question.
|
||||||
|
pub async fn get_block_hash(&self, number: usize) -> Result<[u8; 32], RpcError> {
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
struct BlockHeaderResponse {
|
||||||
|
hash: String,
|
||||||
|
}
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
struct BlockHeaderByHeightResponse {
|
||||||
|
block_header: BlockHeaderResponse,
|
||||||
|
}
|
||||||
|
|
||||||
|
let header: BlockHeaderByHeightResponse =
|
||||||
|
self.json_rpc_call("get_block_header_by_height", Some(json!({ "height": number }))).await?;
|
||||||
|
rpc_hex(&header.block_header.hash)?.try_into().map_err(|_| RpcError::InvalidNode)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a block from the node by its hash.
|
||||||
|
/// This function does not verify the returned block actually has the hash in question.
|
||||||
|
pub async fn get_block(&self, hash: [u8; 32]) -> Result<Block, RpcError> {
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
struct BlockResponse {
|
||||||
|
blob: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
let res: BlockResponse =
|
||||||
|
self.json_rpc_call("get_block", Some(json!({ "hash": hex::encode(hash) }))).await?;
|
||||||
|
|
||||||
|
let block =
|
||||||
|
Block::read::<&[u8]>(&mut rpc_hex(&res.blob)?.as_ref()).map_err(|_| RpcError::InvalidNode)?;
|
||||||
|
if block.hash() != hash {
|
||||||
|
Err(RpcError::InvalidNode)?;
|
||||||
|
}
|
||||||
|
Ok(block)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_block_by_number(&self, number: usize) -> Result<Block, RpcError> {
|
||||||
|
match self.get_block(self.get_block_hash(number).await?).await {
|
||||||
|
Ok(block) => {
|
||||||
|
// Make sure this is actually the block for this number
|
||||||
|
match block.miner_tx.prefix.inputs.get(0) {
|
||||||
|
Some(Input::Gen(actual)) => {
|
||||||
|
if usize::try_from(*actual).unwrap() == number {
|
||||||
|
Ok(block)
|
||||||
|
} else {
|
||||||
|
Err(RpcError::InvalidNode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(Input::ToKey { .. }) | None => Err(RpcError::InvalidNode),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
e => e,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_block_transactions(&self, hash: [u8; 32]) -> Result<Vec<Transaction>, RpcError> {
|
||||||
|
let block = self.get_block(hash).await?;
|
||||||
|
let mut res = vec![block.miner_tx];
|
||||||
|
res.extend(self.get_transactions(&block.txs).await?);
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_block_transactions_by_number(
|
||||||
|
&self,
|
||||||
|
number: usize,
|
||||||
|
) -> Result<Vec<Transaction>, RpcError> {
|
||||||
|
self.get_block_transactions(self.get_block_hash(number).await?).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the output indexes of the specified transaction.
|
||||||
|
pub async fn get_o_indexes(&self, hash: [u8; 32]) -> Result<Vec<u64>, RpcError> {
|
||||||
|
/*
|
||||||
|
TODO: Use these when a suitable epee serde lib exists
|
||||||
|
|
||||||
|
#[derive(Serialize, Debug)]
|
||||||
|
struct Request {
|
||||||
|
txid: [u8; 32],
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
struct OIndexes {
|
||||||
|
o_indexes: Vec<u64>,
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Given the immaturity of Rust epee libraries, this is a homegrown one which is only validated
|
||||||
|
// to work against this specific function
|
||||||
|
|
||||||
|
// Header for EPEE, an 8-byte magic and a version
|
||||||
|
const EPEE_HEADER: &[u8] = b"\x01\x11\x01\x01\x01\x01\x02\x01\x01";
|
||||||
|
|
||||||
|
let mut request = EPEE_HEADER.to_vec();
|
||||||
|
// Number of fields (shifted over 2 bits as the 2 LSBs are reserved for metadata)
|
||||||
|
request.push(1 << 2);
|
||||||
|
// Length of field name
|
||||||
|
request.push(4);
|
||||||
|
// Field name
|
||||||
|
request.extend(b"txid");
|
||||||
|
// Type of field
|
||||||
|
request.push(10);
|
||||||
|
// Length of string, since this byte array is technically a string
|
||||||
|
request.push(32 << 2);
|
||||||
|
// The "string"
|
||||||
|
request.extend(hash);
|
||||||
|
|
||||||
|
let indexes_buf = self.bin_call("get_o_indexes.bin", request).await?;
|
||||||
|
let mut indexes: &[u8] = indexes_buf.as_ref();
|
||||||
|
|
||||||
|
(|| {
|
||||||
|
if read_bytes::<_, { EPEE_HEADER.len() }>(&mut indexes)? != EPEE_HEADER {
|
||||||
|
Err(io::Error::new(io::ErrorKind::Other, "invalid header"))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let read_object = |reader: &mut &[u8]| {
|
||||||
|
let fields = read_byte(reader)? >> 2;
|
||||||
|
|
||||||
|
for _ in 0 .. fields {
|
||||||
|
let name_len = read_byte(reader)?;
|
||||||
|
let name = read_raw_vec(read_byte, name_len.into(), reader)?;
|
||||||
|
|
||||||
|
let type_with_array_flag = read_byte(reader)?;
|
||||||
|
let kind = type_with_array_flag & (!0x80);
|
||||||
|
|
||||||
|
let iters = if type_with_array_flag != kind { read_epee_vi(reader)? } else { 1 };
|
||||||
|
|
||||||
|
if (&name == b"o_indexes") && (kind != 5) {
|
||||||
|
Err(io::Error::new(io::ErrorKind::Other, "o_indexes weren't u64s"))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let f = match kind {
|
||||||
|
// i64
|
||||||
|
1 => |reader: &mut &[u8]| read_raw_vec(read_byte, 8, reader),
|
||||||
|
// i32
|
||||||
|
2 => |reader: &mut &[u8]| read_raw_vec(read_byte, 4, reader),
|
||||||
|
// i16
|
||||||
|
3 => |reader: &mut &[u8]| read_raw_vec(read_byte, 2, reader),
|
||||||
|
// i8
|
||||||
|
4 => |reader: &mut &[u8]| read_raw_vec(read_byte, 1, reader),
|
||||||
|
// u64
|
||||||
|
5 => |reader: &mut &[u8]| read_raw_vec(read_byte, 8, reader),
|
||||||
|
// u32
|
||||||
|
6 => |reader: &mut &[u8]| read_raw_vec(read_byte, 4, reader),
|
||||||
|
// u16
|
||||||
|
7 => |reader: &mut &[u8]| read_raw_vec(read_byte, 2, reader),
|
||||||
|
// u8
|
||||||
|
8 => |reader: &mut &[u8]| read_raw_vec(read_byte, 1, reader),
|
||||||
|
// double
|
||||||
|
9 => |reader: &mut &[u8]| read_raw_vec(read_byte, 8, reader),
|
||||||
|
// string, or any collection of bytes
|
||||||
|
10 => |reader: &mut &[u8]| {
|
||||||
|
let len = read_epee_vi(reader)?;
|
||||||
|
read_raw_vec(
|
||||||
|
read_byte,
|
||||||
|
len
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| io::Error::new(io::ErrorKind::Other, "u64 length exceeded usize"))?,
|
||||||
|
reader,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
// bool
|
||||||
|
11 => |reader: &mut &[u8]| read_raw_vec(read_byte, 1, reader),
|
||||||
|
// object, errors here as it shouldn't be used on this call
|
||||||
|
12 => |_: &mut &[u8]| {
|
||||||
|
Err(io::Error::new(
|
||||||
|
io::ErrorKind::Other,
|
||||||
|
"node used object in reply to get_o_indexes",
|
||||||
|
))
|
||||||
|
},
|
||||||
|
// array, so far unused
|
||||||
|
13 => |_: &mut &[u8]| {
|
||||||
|
Err(io::Error::new(io::ErrorKind::Other, "node used the unused array type"))
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
|_: &mut &[u8]| Err(io::Error::new(io::ErrorKind::Other, "node used an invalid type"))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut res = vec![];
|
||||||
|
for _ in 0 .. iters {
|
||||||
|
res.push(f(reader)?);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut actual_res = Vec::with_capacity(res.len());
|
||||||
|
if &name == b"o_indexes" {
|
||||||
|
for o_index in res {
|
||||||
|
actual_res.push(u64::from_le_bytes(o_index.try_into().map_err(|_| {
|
||||||
|
io::Error::new(io::ErrorKind::Other, "node didn't provide 8 bytes for a u64")
|
||||||
|
})?));
|
||||||
|
}
|
||||||
|
return Ok(actual_res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Didn't return a response with o_indexes
|
||||||
|
// TODO: Check if this didn't have o_indexes because it's an error response
|
||||||
|
Err(io::Error::new(io::ErrorKind::Other, "response didn't contain o_indexes"))
|
||||||
|
};
|
||||||
|
|
||||||
|
read_object(&mut indexes)
|
||||||
|
})()
|
||||||
|
.map_err(|_| RpcError::InvalidNode)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the output distribution, from the specified height to the specified height (both
|
||||||
|
/// inclusive).
|
||||||
|
pub async fn get_output_distribution(
|
||||||
|
&self,
|
||||||
|
from: usize,
|
||||||
|
to: usize,
|
||||||
|
) -> Result<Vec<u64>, RpcError> {
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
struct Distribution {
|
||||||
|
distribution: Vec<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
struct Distributions {
|
||||||
|
distributions: Vec<Distribution>,
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut distributions: Distributions = self
|
||||||
|
.json_rpc_call(
|
||||||
|
"get_output_distribution",
|
||||||
|
Some(json!({
|
||||||
|
"binary": false,
|
||||||
|
"amounts": [0],
|
||||||
|
"cumulative": true,
|
||||||
|
"from_height": from,
|
||||||
|
"to_height": to,
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(distributions.distributions.swap_remove(0).distribution)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the specified outputs from the RingCT (zero-amount) pool, but only return them if their
|
||||||
|
/// timelock has been satisfied. This is distinct from being free of the 10-block lock applied to
|
||||||
|
/// all Monero transactions.
|
||||||
|
pub async fn get_unlocked_outputs(
|
||||||
|
&self,
|
||||||
|
indexes: &[u64],
|
||||||
|
height: usize,
|
||||||
|
) -> Result<Vec<Option<[EdwardsPoint; 2]>>, RpcError> {
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
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)?.try_into().map_err(|_| RpcError::InvalidNode))
|
||||||
|
.collect::<Result<Vec<_>, _>>()?,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// TODO: https://github.com/serai-dex/serai/issues/104
|
||||||
|
outs
|
||||||
|
.outs
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, out)| {
|
||||||
|
Ok(
|
||||||
|
Some([rpc_point(&out.key)?, rpc_point(&out.mask)?])
|
||||||
|
.filter(|_| Timelock::Block(height) >= txs[i].prefix.timelock),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the currently estimated fee from the node. This may be manipulated to unsafe levels and
|
||||||
|
/// MUST be sanity checked.
|
||||||
|
// TODO: Take a sanity check argument
|
||||||
|
pub async fn get_fee(&self) -> Result<Fee, RpcError> {
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
struct FeeResponse {
|
||||||
|
fee: u64,
|
||||||
|
quantization_mask: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
let res: FeeResponse = self.json_rpc_call("get_fee_estimate", None).await?;
|
||||||
|
Ok(Fee { per_weight: res.fee, mask: res.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 res: SendRawResponse = self
|
||||||
|
.rpc_call("send_raw_transaction", Some(json!({ "tx_as_hex": hex::encode(tx.serialize()) })))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if res.status != "OK" {
|
||||||
|
Err(RpcError::InvalidTransaction(tx.hash()))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn generate_blocks(&self, address: &str, block_count: usize) -> Result<(), RpcError> {
|
||||||
|
self
|
||||||
|
.rpc_call::<_, EmptyResponse>(
|
||||||
|
"json_rpc",
|
||||||
|
Some(json!({
|
||||||
|
"method": "generateblocks",
|
||||||
|
"params": {
|
||||||
|
"wallet_address": address,
|
||||||
|
"amount_of_blocks": block_count
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
156
coins/monero/src/serialize.rs
Normal file
156
coins/monero/src/serialize.rs
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
use core::fmt::Debug;
|
||||||
|
use std_shims::{
|
||||||
|
vec::Vec,
|
||||||
|
io::{self, Read, Write},
|
||||||
|
};
|
||||||
|
|
||||||
|
use curve25519_dalek::{
|
||||||
|
scalar::Scalar,
|
||||||
|
edwards::{EdwardsPoint, CompressedEdwardsY},
|
||||||
|
};
|
||||||
|
|
||||||
|
const VARINT_CONTINUATION_MASK: u8 = 0b1000_0000;
|
||||||
|
|
||||||
|
pub(crate) fn varint_len(varint: usize) -> usize {
|
||||||
|
((usize::try_from(usize::BITS - varint.leading_zeros()).unwrap().saturating_sub(1)) / 7) + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn write_byte<W: Write>(byte: &u8, w: &mut W) -> io::Result<()> {
|
||||||
|
w.write_all(&[*byte])
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
write_byte(&b, w)?;
|
||||||
|
varint != 0
|
||||||
|
} {}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn write_scalar<W: Write>(scalar: &Scalar, w: &mut W) -> io::Result<()> {
|
||||||
|
w.write_all(&scalar.to_bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn write_point<W: Write>(point: &EdwardsPoint, w: &mut W) -> io::Result<()> {
|
||||||
|
w.write_all(&point.compress().to_bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn write_raw_vec<T, W: Write, F: Fn(&T, &mut W) -> io::Result<()>>(
|
||||||
|
f: F,
|
||||||
|
values: &[T],
|
||||||
|
w: &mut W,
|
||||||
|
) -> io::Result<()> {
|
||||||
|
for value in values {
|
||||||
|
f(value, w)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn write_vec<T, W: Write, F: Fn(&T, &mut W) -> io::Result<()>>(
|
||||||
|
f: F,
|
||||||
|
values: &[T],
|
||||||
|
w: &mut W,
|
||||||
|
) -> io::Result<()> {
|
||||||
|
write_varint(&values.len().try_into().unwrap(), w)?;
|
||||||
|
write_raw_vec(f, values, w)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn read_bytes<R: Read, const N: usize>(r: &mut R) -> io::Result<[u8; N]> {
|
||||||
|
let mut res = [0; N];
|
||||||
|
r.read_exact(&mut res)?;
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn read_byte<R: Read>(r: &mut R) -> io::Result<u8> {
|
||||||
|
Ok(read_bytes::<_, 1>(r)?[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn read_u16<R: Read>(r: &mut R) -> io::Result<u16> {
|
||||||
|
read_bytes(r).map(u16::from_le_bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn read_u32<R: Read>(r: &mut R) -> io::Result<u32> {
|
||||||
|
read_bytes(r).map(u32::from_le_bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn read_u64<R: Read>(r: &mut R) -> io::Result<u64> {
|
||||||
|
read_bytes(r).map(u64::from_le_bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn read_varint<R: Read>(r: &mut R) -> io::Result<u64> {
|
||||||
|
let mut bits = 0;
|
||||||
|
let mut res = 0;
|
||||||
|
while {
|
||||||
|
let b = read_byte(r)?;
|
||||||
|
if (bits != 0) && (b == 0) {
|
||||||
|
Err(io::Error::new(io::ErrorKind::Other, "non-canonical varint"))?;
|
||||||
|
}
|
||||||
|
if ((bits + 7) > 64) && (b >= (1 << (64 - bits))) {
|
||||||
|
Err(io::Error::new(io::ErrorKind::Other, "varint overflow"))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
res += u64::from(b & (!VARINT_CONTINUATION_MASK)) << bits;
|
||||||
|
bits += 7;
|
||||||
|
b & VARINT_CONTINUATION_MASK == VARINT_CONTINUATION_MASK
|
||||||
|
} {}
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
// All scalar fields supported by monero-serai are checked to be canonical for valid transactions
|
||||||
|
// While from_bytes_mod_order would be more flexible, it's not currently needed and would be
|
||||||
|
// inaccurate to include now. While casting a wide net may be preferable, it'd also be inaccurate
|
||||||
|
// for now. There's also further edge cases as noted by
|
||||||
|
// https://github.com/monero-project/monero/issues/8438, where some scalars had an archaic
|
||||||
|
// reduction applied
|
||||||
|
pub(crate) fn read_scalar<R: Read>(r: &mut R) -> io::Result<Scalar> {
|
||||||
|
Scalar::from_canonical_bytes(read_bytes(r)?)
|
||||||
|
.ok_or_else(|| io::Error::new(io::ErrorKind::Other, "unreduced scalar"))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn read_point<R: Read>(r: &mut R) -> io::Result<EdwardsPoint> {
|
||||||
|
let bytes = read_bytes(r)?;
|
||||||
|
CompressedEdwardsY(bytes)
|
||||||
|
.decompress()
|
||||||
|
// Ban points which are either unreduced or -0
|
||||||
|
.filter(|point| point.compress().to_bytes() == bytes)
|
||||||
|
.ok_or_else(|| io::Error::new(io::ErrorKind::Other, "invalid point"))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn read_torsion_free_point<R: Read>(r: &mut R) -> io::Result<EdwardsPoint> {
|
||||||
|
read_point(r)
|
||||||
|
.ok()
|
||||||
|
.filter(EdwardsPoint::is_torsion_free)
|
||||||
|
.ok_or_else(|| io::Error::new(io::ErrorKind::Other, "invalid point"))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn read_raw_vec<R: Read, T, F: Fn(&mut R) -> io::Result<T>>(
|
||||||
|
f: F,
|
||||||
|
len: usize,
|
||||||
|
r: &mut R,
|
||||||
|
) -> io::Result<Vec<T>> {
|
||||||
|
let mut res = vec![];
|
||||||
|
for _ in 0 .. len {
|
||||||
|
res.push(f(r)?);
|
||||||
|
}
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn read_array<R: Read, T: Debug, F: Fn(&mut R) -> io::Result<T>, const N: usize>(
|
||||||
|
f: F,
|
||||||
|
r: &mut R,
|
||||||
|
) -> io::Result<[T; N]> {
|
||||||
|
read_raw_vec(f, N, r).map(|vec| vec.try_into().unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn read_vec<R: Read, T, F: Fn(&mut R) -> io::Result<T>>(
|
||||||
|
f: F,
|
||||||
|
r: &mut R,
|
||||||
|
) -> io::Result<Vec<T>> {
|
||||||
|
read_raw_vec(f, read_varint(r)?.try_into().unwrap(), r)
|
||||||
|
}
|
||||||
176
coins/monero/src/tests/address.rs
Normal file
176
coins/monero/src/tests/address.rs
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
use hex_literal::hex;
|
||||||
|
|
||||||
|
use rand_core::{RngCore, OsRng};
|
||||||
|
|
||||||
|
use curve25519_dalek::{constants::ED25519_BASEPOINT_TABLE, edwards::CompressedEdwardsY};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
random_scalar,
|
||||||
|
wallet::address::{Network, AddressType, AddressMeta, MoneroAddress},
|
||||||
|
};
|
||||||
|
|
||||||
|
const SPEND: [u8; 32] = hex!("f8631661f6ab4e6fda310c797330d86e23a682f20d5bc8cc27b18051191f16d7");
|
||||||
|
const VIEW: [u8; 32] = hex!("4a1535063ad1fee2dabbf909d4fd9a873e29541b401f0944754e17c9a41820ce");
|
||||||
|
|
||||||
|
const STANDARD: &str =
|
||||||
|
"4B33mFPMq6mKi7Eiyd5XuyKRVMGVZz1Rqb9ZTyGApXW5d1aT7UBDZ89ewmnWFkzJ5wPd2SFbn313vCT8a4E2Qf4KQH4pNey";
|
||||||
|
|
||||||
|
const PAYMENT_ID: [u8; 8] = hex!("b8963a57855cf73f");
|
||||||
|
const INTEGRATED: &str =
|
||||||
|
"4Ljin4CrSNHKi7Eiyd5XuyKRVMGVZz1Rqb9ZTyGApXW5d1aT7UBDZ89ewmnWFkzJ5wPd2SFbn313vCT8a4E2Qf4KbaTH6Mn\
|
||||||
|
pXSn88oBX35";
|
||||||
|
|
||||||
|
const SUB_SPEND: [u8; 32] =
|
||||||
|
hex!("fe358188b528335ad1cfdc24a22a23988d742c882b6f19a602892eaab3c1b62b");
|
||||||
|
const SUB_VIEW: [u8; 32] = hex!("9bc2b464de90d058468522098d5610c5019c45fd1711a9517db1eea7794f5470");
|
||||||
|
const SUBADDRESS: &str =
|
||||||
|
"8C5zHM5ud8nGC4hC2ULiBLSWx9infi8JUUmWEat4fcTf8J4H38iWYVdFmPCA9UmfLTZxD43RsyKnGEdZkoGij6csDeUnbEB";
|
||||||
|
|
||||||
|
const FEATURED_JSON: &str = include_str!("vectors/featured_addresses.json");
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn standard_address() {
|
||||||
|
let addr = MoneroAddress::from_str(Network::Mainnet, STANDARD).unwrap();
|
||||||
|
assert_eq!(addr.meta.network, Network::Mainnet);
|
||||||
|
assert_eq!(addr.meta.kind, AddressType::Standard);
|
||||||
|
assert!(!addr.meta.kind.is_subaddress());
|
||||||
|
assert_eq!(addr.meta.kind.payment_id(), None);
|
||||||
|
assert!(!addr.meta.kind.is_guaranteed());
|
||||||
|
assert_eq!(addr.spend.compress().to_bytes(), SPEND);
|
||||||
|
assert_eq!(addr.view.compress().to_bytes(), VIEW);
|
||||||
|
assert_eq!(addr.to_string(), STANDARD);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn integrated_address() {
|
||||||
|
let addr = MoneroAddress::from_str(Network::Mainnet, INTEGRATED).unwrap();
|
||||||
|
assert_eq!(addr.meta.network, Network::Mainnet);
|
||||||
|
assert_eq!(addr.meta.kind, AddressType::Integrated(PAYMENT_ID));
|
||||||
|
assert!(!addr.meta.kind.is_subaddress());
|
||||||
|
assert_eq!(addr.meta.kind.payment_id(), Some(PAYMENT_ID));
|
||||||
|
assert!(!addr.meta.kind.is_guaranteed());
|
||||||
|
assert_eq!(addr.spend.compress().to_bytes(), SPEND);
|
||||||
|
assert_eq!(addr.view.compress().to_bytes(), VIEW);
|
||||||
|
assert_eq!(addr.to_string(), INTEGRATED);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn subaddress() {
|
||||||
|
let addr = MoneroAddress::from_str(Network::Mainnet, SUBADDRESS).unwrap();
|
||||||
|
assert_eq!(addr.meta.network, Network::Mainnet);
|
||||||
|
assert_eq!(addr.meta.kind, AddressType::Subaddress);
|
||||||
|
assert!(addr.meta.kind.is_subaddress());
|
||||||
|
assert_eq!(addr.meta.kind.payment_id(), None);
|
||||||
|
assert!(!addr.meta.kind.is_guaranteed());
|
||||||
|
assert_eq!(addr.spend.compress().to_bytes(), SUB_SPEND);
|
||||||
|
assert_eq!(addr.view.compress().to_bytes(), SUB_VIEW);
|
||||||
|
assert_eq!(addr.to_string(), SUBADDRESS);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn featured() {
|
||||||
|
for (network, first) in
|
||||||
|
[(Network::Mainnet, 'C'), (Network::Testnet, 'K'), (Network::Stagenet, 'F')]
|
||||||
|
{
|
||||||
|
for _ in 0 .. 100 {
|
||||||
|
let spend = &random_scalar(&mut OsRng) * &ED25519_BASEPOINT_TABLE;
|
||||||
|
let view = &random_scalar(&mut OsRng) * &ED25519_BASEPOINT_TABLE;
|
||||||
|
|
||||||
|
for features in 0 .. (1 << 3) {
|
||||||
|
const SUBADDRESS_FEATURE_BIT: u8 = 1;
|
||||||
|
const INTEGRATED_FEATURE_BIT: u8 = 1 << 1;
|
||||||
|
const GUARANTEED_FEATURE_BIT: u8 = 1 << 2;
|
||||||
|
|
||||||
|
let subaddress = (features & SUBADDRESS_FEATURE_BIT) == SUBADDRESS_FEATURE_BIT;
|
||||||
|
|
||||||
|
let mut payment_id = [0; 8];
|
||||||
|
OsRng.fill_bytes(&mut payment_id);
|
||||||
|
let payment_id = Some(payment_id)
|
||||||
|
.filter(|_| (features & INTEGRATED_FEATURE_BIT) == INTEGRATED_FEATURE_BIT);
|
||||||
|
|
||||||
|
let guaranteed = (features & GUARANTEED_FEATURE_BIT) == GUARANTEED_FEATURE_BIT;
|
||||||
|
|
||||||
|
let kind = AddressType::Featured { subaddress, payment_id, guaranteed };
|
||||||
|
let meta = AddressMeta::new(network, kind);
|
||||||
|
let addr = MoneroAddress::new(meta, spend, view);
|
||||||
|
|
||||||
|
assert_eq!(addr.to_string().chars().next().unwrap(), first);
|
||||||
|
assert_eq!(MoneroAddress::from_str(network, &addr.to_string()).unwrap(), addr);
|
||||||
|
|
||||||
|
assert_eq!(addr.spend, spend);
|
||||||
|
assert_eq!(addr.view, view);
|
||||||
|
|
||||||
|
assert_eq!(addr.is_subaddress(), subaddress);
|
||||||
|
assert_eq!(addr.payment_id(), payment_id);
|
||||||
|
assert_eq!(addr.is_guaranteed(), guaranteed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn featured_vectors() {
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
struct Vector {
|
||||||
|
address: String,
|
||||||
|
|
||||||
|
network: String,
|
||||||
|
spend: String,
|
||||||
|
view: String,
|
||||||
|
|
||||||
|
subaddress: bool,
|
||||||
|
integrated: bool,
|
||||||
|
payment_id: Option<[u8; 8]>,
|
||||||
|
guaranteed: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
let vectors = serde_json::from_str::<Vec<Vector>>(FEATURED_JSON).unwrap();
|
||||||
|
for vector in vectors {
|
||||||
|
let first = vector.address.chars().next().unwrap();
|
||||||
|
let network = match vector.network.as_str() {
|
||||||
|
"Mainnet" => {
|
||||||
|
assert_eq!(first, 'C');
|
||||||
|
Network::Mainnet
|
||||||
|
}
|
||||||
|
"Testnet" => {
|
||||||
|
assert_eq!(first, 'K');
|
||||||
|
Network::Testnet
|
||||||
|
}
|
||||||
|
"Stagenet" => {
|
||||||
|
assert_eq!(first, 'F');
|
||||||
|
Network::Stagenet
|
||||||
|
}
|
||||||
|
_ => panic!("Unknown network"),
|
||||||
|
};
|
||||||
|
let spend =
|
||||||
|
CompressedEdwardsY::from_slice(&hex::decode(vector.spend).unwrap()).decompress().unwrap();
|
||||||
|
let view =
|
||||||
|
CompressedEdwardsY::from_slice(&hex::decode(vector.view).unwrap()).decompress().unwrap();
|
||||||
|
|
||||||
|
let addr = MoneroAddress::from_str(network, &vector.address).unwrap();
|
||||||
|
assert_eq!(addr.spend, spend);
|
||||||
|
assert_eq!(addr.view, view);
|
||||||
|
|
||||||
|
assert_eq!(addr.is_subaddress(), vector.subaddress);
|
||||||
|
assert_eq!(vector.integrated, vector.payment_id.is_some());
|
||||||
|
assert_eq!(addr.payment_id(), vector.payment_id);
|
||||||
|
assert_eq!(addr.is_guaranteed(), vector.guaranteed);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
MoneroAddress::new(
|
||||||
|
AddressMeta::new(
|
||||||
|
network,
|
||||||
|
AddressType::Featured {
|
||||||
|
subaddress: vector.subaddress,
|
||||||
|
payment_id: vector.payment_id,
|
||||||
|
guaranteed: vector.guaranteed
|
||||||
|
}
|
||||||
|
),
|
||||||
|
spend,
|
||||||
|
view
|
||||||
|
)
|
||||||
|
.to_string(),
|
||||||
|
vector.address
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
92
coins/monero/src/tests/bulletproofs.rs
Normal file
92
coins/monero/src/tests/bulletproofs.rs
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
use hex_literal::hex;
|
||||||
|
use rand_core::OsRng;
|
||||||
|
|
||||||
|
use curve25519_dalek::{scalar::Scalar, edwards::CompressedEdwardsY};
|
||||||
|
use multiexp::BatchVerifier;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
Commitment, random_scalar,
|
||||||
|
ringct::bulletproofs::{Bulletproofs, original::OriginalStruct},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bulletproofs_vector() {
|
||||||
|
let scalar = |scalar| Scalar::from_canonical_bytes(scalar).unwrap();
|
||||||
|
let point = |point| CompressedEdwardsY(point).decompress().unwrap();
|
||||||
|
|
||||||
|
// Generated from Monero
|
||||||
|
assert!(Bulletproofs::Original(OriginalStruct {
|
||||||
|
A: point(hex!("ef32c0b9551b804decdcb107eb22aa715b7ce259bf3c5cac20e24dfa6b28ac71")),
|
||||||
|
S: point(hex!("e1285960861783574ee2b689ae53622834eb0b035d6943103f960cd23e063fa0")),
|
||||||
|
T1: point(hex!("4ea07735f184ba159d0e0eb662bac8cde3eb7d39f31e567b0fbda3aa23fe5620")),
|
||||||
|
T2: point(hex!("b8390aa4b60b255630d40e592f55ec6b7ab5e3a96bfcdcd6f1cd1d2fc95f441e")),
|
||||||
|
taux: scalar(hex!("5957dba8ea9afb23d6e81cc048a92f2d502c10c749dc1b2bd148ae8d41ec7107")),
|
||||||
|
mu: scalar(hex!("923023b234c2e64774b820b4961f7181f6c1dc152c438643e5a25b0bf271bc02")),
|
||||||
|
L: vec![
|
||||||
|
point(hex!("c45f656316b9ebf9d357fb6a9f85b5f09e0b991dd50a6e0ae9b02de3946c9d99")),
|
||||||
|
point(hex!("9304d2bf0f27183a2acc58cc755a0348da11bd345485fda41b872fee89e72aac")),
|
||||||
|
point(hex!("1bb8b71925d155dd9569f64129ea049d6149fdc4e7a42a86d9478801d922129b")),
|
||||||
|
point(hex!("5756a7bf887aa72b9a952f92f47182122e7b19d89e5dd434c747492b00e1c6b7")),
|
||||||
|
point(hex!("6e497c910d102592830555356af5ff8340e8d141e3fb60ea24cfa587e964f07d")),
|
||||||
|
point(hex!("f4fa3898e7b08e039183d444f3d55040f3c790ed806cb314de49f3068bdbb218")),
|
||||||
|
point(hex!("0bbc37597c3ead517a3841e159c8b7b79a5ceaee24b2a9a20350127aab428713")),
|
||||||
|
],
|
||||||
|
R: vec![
|
||||||
|
point(hex!("609420ba1702781692e84accfd225adb3d077aedc3cf8125563400466b52dbd9")),
|
||||||
|
point(hex!("fb4e1d079e7a2b0ec14f7e2a3943bf50b6d60bc346a54fcf562fb234b342abf8")),
|
||||||
|
point(hex!("6ae3ac97289c48ce95b9c557289e82a34932055f7f5e32720139824fe81b12e5")),
|
||||||
|
point(hex!("d071cc2ffbdab2d840326ad15f68c01da6482271cae3cf644670d1632f29a15c")),
|
||||||
|
point(hex!("e52a1754b95e1060589ba7ce0c43d0060820ebfc0d49dc52884bc3c65ad18af5")),
|
||||||
|
point(hex!("41573b06140108539957df71aceb4b1816d2409ce896659aa5c86f037ca5e851")),
|
||||||
|
point(hex!("a65970b2cc3c7b08b2b5b739dbc8e71e646783c41c625e2a5b1535e3d2e0f742")),
|
||||||
|
],
|
||||||
|
a: scalar(hex!("0077c5383dea44d3cd1bc74849376bd60679612dc4b945255822457fa0c0a209")),
|
||||||
|
b: scalar(hex!("fe80cf5756473482581e1d38644007793ddc66fdeb9404ec1689a907e4863302")),
|
||||||
|
t: scalar(hex!("40dfb08e09249040df997851db311bd6827c26e87d6f0f332c55be8eef10e603"))
|
||||||
|
})
|
||||||
|
.verify(
|
||||||
|
&mut OsRng,
|
||||||
|
&[
|
||||||
|
// For some reason, these vectors are * INV_EIGHT
|
||||||
|
point(hex!("8e8f23f315edae4f6c2f948d9a861e0ae32d356b933cd11d2f0e031ac744c41f"))
|
||||||
|
.mul_by_cofactor(),
|
||||||
|
point(hex!("2829cbd025aa54cd6e1b59a032564f22f0b2e5627f7f2c4297f90da438b5510f"))
|
||||||
|
.mul_by_cofactor(),
|
||||||
|
]
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! bulletproofs_tests {
|
||||||
|
($name: ident, $max: ident, $plus: literal) => {
|
||||||
|
#[test]
|
||||||
|
fn $name() {
|
||||||
|
// Create Bulletproofs for all possible output quantities
|
||||||
|
let mut verifier = BatchVerifier::new(16);
|
||||||
|
for i in 1 .. 17 {
|
||||||
|
let commitments = (1 ..= i)
|
||||||
|
.map(|i| Commitment::new(random_scalar(&mut OsRng), u64::try_from(i).unwrap()))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let bp = Bulletproofs::prove(&mut OsRng, &commitments, $plus).unwrap();
|
||||||
|
|
||||||
|
let commitments = commitments.iter().map(Commitment::calculate).collect::<Vec<_>>();
|
||||||
|
assert!(bp.verify(&mut OsRng, &commitments));
|
||||||
|
assert!(bp.batch_verify(&mut OsRng, &mut verifier, i, &commitments));
|
||||||
|
}
|
||||||
|
assert!(verifier.verify_vartime());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn $max() {
|
||||||
|
// Check Bulletproofs errors if we try to prove for too many outputs
|
||||||
|
let mut commitments = vec![];
|
||||||
|
for _ in 0 .. 17 {
|
||||||
|
commitments.push(Commitment::new(Scalar::zero(), 0));
|
||||||
|
}
|
||||||
|
assert!(Bulletproofs::prove(&mut OsRng, &commitments, $plus).is_err());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
bulletproofs_tests!(bulletproofs, bulletproofs_max, false);
|
||||||
|
bulletproofs_tests!(bulletproofs_plus, bulletproofs_plus_max, true);
|
||||||
127
coins/monero/src/tests/clsag.rs
Normal file
127
coins/monero/src/tests/clsag.rs
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
use core::ops::Deref;
|
||||||
|
#[cfg(feature = "multisig")]
|
||||||
|
use std_shims::sync::Arc;
|
||||||
|
#[cfg(feature = "multisig")]
|
||||||
|
use std::sync::RwLock;
|
||||||
|
|
||||||
|
use zeroize::Zeroizing;
|
||||||
|
use rand_core::{RngCore, OsRng};
|
||||||
|
|
||||||
|
use curve25519_dalek::{constants::ED25519_BASEPOINT_TABLE, scalar::Scalar};
|
||||||
|
|
||||||
|
#[cfg(feature = "multisig")]
|
||||||
|
use transcript::{Transcript, RecommendedTranscript};
|
||||||
|
#[cfg(feature = "multisig")]
|
||||||
|
use frost::curve::Ed25519;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
Commitment, random_scalar,
|
||||||
|
wallet::Decoys,
|
||||||
|
ringct::{
|
||||||
|
generate_key_image,
|
||||||
|
clsag::{ClsagInput, Clsag},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
#[cfg(feature = "multisig")]
|
||||||
|
use crate::ringct::clsag::{ClsagDetails, ClsagMultisig};
|
||||||
|
|
||||||
|
#[cfg(feature = "multisig")]
|
||||||
|
use frost::{
|
||||||
|
Participant,
|
||||||
|
tests::{key_gen, algorithm_machines, sign},
|
||||||
|
};
|
||||||
|
|
||||||
|
const RING_LEN: u64 = 11;
|
||||||
|
const AMOUNT: u64 = 1337;
|
||||||
|
|
||||||
|
#[cfg(feature = "multisig")]
|
||||||
|
const RING_INDEX: u8 = 3;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn clsag() {
|
||||||
|
for real in 0 .. RING_LEN {
|
||||||
|
let msg = [1; 32];
|
||||||
|
|
||||||
|
let mut secrets = (Zeroizing::new(Scalar::zero()), Scalar::zero());
|
||||||
|
let mut ring = vec![];
|
||||||
|
for i in 0 .. RING_LEN {
|
||||||
|
let dest = Zeroizing::new(random_scalar(&mut OsRng));
|
||||||
|
let mask = random_scalar(&mut OsRng);
|
||||||
|
let amount = if i == real {
|
||||||
|
secrets = (dest.clone(), mask);
|
||||||
|
AMOUNT
|
||||||
|
} else {
|
||||||
|
OsRng.next_u64()
|
||||||
|
};
|
||||||
|
ring
|
||||||
|
.push([dest.deref() * &ED25519_BASEPOINT_TABLE, Commitment::new(mask, amount).calculate()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
let image = generate_key_image(&secrets.0);
|
||||||
|
let (clsag, pseudo_out) = Clsag::sign(
|
||||||
|
&mut OsRng,
|
||||||
|
vec![(
|
||||||
|
secrets.0,
|
||||||
|
image,
|
||||||
|
ClsagInput::new(
|
||||||
|
Commitment::new(secrets.1, AMOUNT),
|
||||||
|
Decoys {
|
||||||
|
i: u8::try_from(real).unwrap(),
|
||||||
|
offsets: (1 ..= RING_LEN).collect(),
|
||||||
|
ring: ring.clone(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
)],
|
||||||
|
random_scalar(&mut OsRng),
|
||||||
|
msg,
|
||||||
|
)
|
||||||
|
.swap_remove(0);
|
||||||
|
clsag.verify(&ring, &image, &pseudo_out, &msg).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "multisig")]
|
||||||
|
#[test]
|
||||||
|
fn clsag_multisig() {
|
||||||
|
let keys = key_gen::<_, Ed25519>(&mut OsRng);
|
||||||
|
|
||||||
|
let randomness = random_scalar(&mut OsRng);
|
||||||
|
let mut ring = vec![];
|
||||||
|
for i in 0 .. RING_LEN {
|
||||||
|
let dest;
|
||||||
|
let mask;
|
||||||
|
let amount = if i == u64::from(RING_INDEX) {
|
||||||
|
dest = keys[&Participant::new(1).unwrap()].group_key().0;
|
||||||
|
mask = randomness;
|
||||||
|
AMOUNT
|
||||||
|
} else {
|
||||||
|
dest = &random_scalar(&mut OsRng) * &ED25519_BASEPOINT_TABLE;
|
||||||
|
mask = random_scalar(&mut OsRng);
|
||||||
|
OsRng.next_u64()
|
||||||
|
};
|
||||||
|
ring.push([dest, Commitment::new(mask, amount).calculate()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mask_sum = random_scalar(&mut OsRng);
|
||||||
|
let algorithm = ClsagMultisig::new(
|
||||||
|
RecommendedTranscript::new(b"Monero Serai CLSAG Test"),
|
||||||
|
keys[&Participant::new(1).unwrap()].group_key().0,
|
||||||
|
Arc::new(RwLock::new(Some(ClsagDetails::new(
|
||||||
|
ClsagInput::new(
|
||||||
|
Commitment::new(randomness, AMOUNT),
|
||||||
|
Decoys { i: RING_INDEX, offsets: (1 ..= RING_LEN).collect(), ring: ring.clone() },
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
mask_sum,
|
||||||
|
)))),
|
||||||
|
);
|
||||||
|
|
||||||
|
sign(
|
||||||
|
&mut OsRng,
|
||||||
|
algorithm.clone(),
|
||||||
|
keys.clone(),
|
||||||
|
algorithm_machines(&mut OsRng, algorithm, &keys),
|
||||||
|
&[1; 32],
|
||||||
|
);
|
||||||
|
}
|
||||||
4
coins/monero/src/tests/mod.rs
Normal file
4
coins/monero/src/tests/mod.rs
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
mod clsag;
|
||||||
|
mod bulletproofs;
|
||||||
|
mod address;
|
||||||
|
mod seed;
|
||||||
177
coins/monero/src/tests/seed.rs
Normal file
177
coins/monero/src/tests/seed.rs
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
use zeroize::Zeroizing;
|
||||||
|
|
||||||
|
use rand_core::OsRng;
|
||||||
|
|
||||||
|
use curve25519_dalek::scalar::Scalar;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
hash,
|
||||||
|
wallet::seed::{Seed, Language, classic::trim_by_lang},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_classic_seed() {
|
||||||
|
struct Vector {
|
||||||
|
language: Language,
|
||||||
|
seed: String,
|
||||||
|
spend: String,
|
||||||
|
view: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
let vectors = [
|
||||||
|
Vector {
|
||||||
|
language: Language::Chinese,
|
||||||
|
seed: "摇 曲 艺 武 滴 然 效 似 赏 式 祥 歌 买 疑 小 碧 堆 博 键 房 鲜 悲 付 喷 武".into(),
|
||||||
|
spend: "a5e4fff1706ef9212993a69f246f5c95ad6d84371692d63e9bb0ea112a58340d".into(),
|
||||||
|
view: "1176c43ce541477ea2f3ef0b49b25112b084e26b8a843e1304ac4677b74cdf02".into(),
|
||||||
|
},
|
||||||
|
Vector {
|
||||||
|
language: Language::English,
|
||||||
|
seed: "washing thirsty occur lectures tuesday fainted toxic adapt \
|
||||||
|
abnormal memoir nylon mostly building shrugged online ember northern \
|
||||||
|
ruby woes dauntless boil family illness inroads northern"
|
||||||
|
.into(),
|
||||||
|
spend: "c0af65c0dd837e666b9d0dfed62745f4df35aed7ea619b2798a709f0fe545403".into(),
|
||||||
|
view: "513ba91c538a5a9069e0094de90e927c0cd147fa10428ce3ac1afd49f63e3b01".into(),
|
||||||
|
},
|
||||||
|
Vector {
|
||||||
|
language: Language::Dutch,
|
||||||
|
seed: "setwinst riphagen vimmetje extase blief tuitelig fuiven meifeest \
|
||||||
|
ponywagen zesmaal ripdeal matverf codetaal leut ivoor rotten \
|
||||||
|
wisgerhof winzucht typograaf atrium rein zilt traktaat verzaagd setwinst"
|
||||||
|
.into(),
|
||||||
|
spend: "e2d2873085c447c2bc7664222ac8f7d240df3aeac137f5ff2022eaa629e5b10a".into(),
|
||||||
|
view: "eac30b69477e3f68093d131c7fd961564458401b07f8c87ff8f6030c1a0c7301".into(),
|
||||||
|
},
|
||||||
|
Vector {
|
||||||
|
language: Language::French,
|
||||||
|
seed: "poids vaseux tarte bazar poivre effet entier nuance \
|
||||||
|
sensuel ennui pacte osselet poudre battre alibi mouton \
|
||||||
|
stade paquet pliage gibier type question position projet pliage"
|
||||||
|
.into(),
|
||||||
|
spend: "2dd39ff1a4628a94b5c2ec3e42fb3dfe15c2b2f010154dc3b3de6791e805b904".into(),
|
||||||
|
view: "6725b32230400a1032f31d622b44c3a227f88258939b14a7c72e00939e7bdf0e".into(),
|
||||||
|
},
|
||||||
|
Vector {
|
||||||
|
language: Language::Spanish,
|
||||||
|
seed: "minero ocupar mirar evadir octubre cal logro miope \
|
||||||
|
opaco disco ancla litio clase cuello nasal clase \
|
||||||
|
fiar avance deseo mente grumo negro cordón croqueta clase"
|
||||||
|
.into(),
|
||||||
|
spend: "ae2c9bebdddac067d73ec0180147fc92bdf9ac7337f1bcafbbe57dd13558eb02".into(),
|
||||||
|
view: "18deafb34d55b7a43cae2c1c1c206a3c80c12cc9d1f84640b484b95b7fec3e05".into(),
|
||||||
|
},
|
||||||
|
Vector {
|
||||||
|
language: Language::German,
|
||||||
|
seed: "Kaliber Gabelung Tapir Liveband Favorit Specht Enklave Nabel \
|
||||||
|
Jupiter Foliant Chronik nisten löten Vase Aussage Rekord \
|
||||||
|
Yeti Gesetz Eleganz Alraune Künstler Almweide Jahr Kastanie Almweide"
|
||||||
|
.into(),
|
||||||
|
spend: "79801b7a1b9796856e2397d862a113862e1fdc289a205e79d8d70995b276db06".into(),
|
||||||
|
view: "99f0ec556643bd9c038a4ed86edcb9c6c16032c4622ed2e000299d527a792701".into(),
|
||||||
|
},
|
||||||
|
Vector {
|
||||||
|
language: Language::Italian,
|
||||||
|
seed: "cavo pancetta auto fulmine alleanza filmato diavolo prato \
|
||||||
|
forzare meritare litigare lezione segreto evasione votare buio \
|
||||||
|
licenza cliente dorso natale crescere vento tutelare vetta evasione"
|
||||||
|
.into(),
|
||||||
|
spend: "5e7fd774eb00fa5877e2a8b4dc9c7ffe111008a3891220b56a6e49ac816d650a".into(),
|
||||||
|
view: "698a1dce6018aef5516e82ca0cb3e3ec7778d17dfb41a137567bfa2e55e63a03".into(),
|
||||||
|
},
|
||||||
|
Vector {
|
||||||
|
language: Language::Portuguese,
|
||||||
|
seed: "agito eventualidade onus itrio holograma sodomizar objetos dobro \
|
||||||
|
iugoslavo bcrepuscular odalisca abjeto iuane darwinista eczema acetona \
|
||||||
|
cibernetico hoquei gleba driver buffer azoto megera nogueira agito"
|
||||||
|
.into(),
|
||||||
|
spend: "13b3115f37e35c6aa1db97428b897e584698670c1b27854568d678e729200c0f".into(),
|
||||||
|
view: "ad1b4fd35270f5f36c4da7166672b347e75c3f4d41346ec2a06d1d0193632801".into(),
|
||||||
|
},
|
||||||
|
Vector {
|
||||||
|
language: Language::Japanese,
|
||||||
|
seed: "ぜんぶ どうぐ おたがい せんきょ おうじ そんちょう じゅしん いろえんぴつ \
|
||||||
|
かほう つかれる えらぶ にちじょう くのう にちようび ぬまえび さんきゃく \
|
||||||
|
おおや ちぬき うすめる いがく せつでん さうな すいえい せつだん おおや"
|
||||||
|
.into(),
|
||||||
|
spend: "c56e895cdb13007eda8399222974cdbab493640663804b93cbef3d8c3df80b0b".into(),
|
||||||
|
view: "6c3634a313ec2ee979d565c33888fd7c3502d696ce0134a8bc1a2698c7f2c508".into(),
|
||||||
|
},
|
||||||
|
Vector {
|
||||||
|
language: Language::Russian,
|
||||||
|
seed: "шатер икра нация ехать получать инерция доза реальный \
|
||||||
|
рыжий таможня лопата душа веселый клетка атлас лекция \
|
||||||
|
обгонять паек наивный лыжный дурак стать ежик задача паек"
|
||||||
|
.into(),
|
||||||
|
spend: "7cb5492df5eb2db4c84af20766391cd3e3662ab1a241c70fc881f3d02c381f05".into(),
|
||||||
|
view: "fcd53e41ec0df995ab43927f7c44bc3359c93523d5009fb3f5ba87431d545a03".into(),
|
||||||
|
},
|
||||||
|
Vector {
|
||||||
|
language: Language::Esperanto,
|
||||||
|
seed: "ukazo klini peco etikedo fabriko imitado onklino urino \
|
||||||
|
pudro incidento kumuluso ikono smirgi hirundo uretro krii \
|
||||||
|
sparkado super speciala pupo alpinisto cvana vokegi zombio fabriko"
|
||||||
|
.into(),
|
||||||
|
spend: "82ebf0336d3b152701964ed41df6b6e9a035e57fc98b84039ed0bd4611c58904".into(),
|
||||||
|
view: "cd4d120e1ea34360af528f6a3e6156063312d9cefc9aa6b5218d366c0ed6a201".into(),
|
||||||
|
},
|
||||||
|
Vector {
|
||||||
|
language: Language::Lojban,
|
||||||
|
seed: "jetnu vensa julne xrotu xamsi julne cutci dakli \
|
||||||
|
mlatu xedja muvgau palpi xindo sfubu ciste cinri \
|
||||||
|
blabi darno dembi janli blabi fenki bukpu burcu blabi"
|
||||||
|
.into(),
|
||||||
|
spend: "e4f8c6819ab6cf792cebb858caabac9307fd646901d72123e0367ebc0a79c200".into(),
|
||||||
|
view: "c806ce62bafaa7b2d597f1a1e2dbe4a2f96bfd804bf6f8420fc7f4a6bd700c00".into(),
|
||||||
|
},
|
||||||
|
Vector {
|
||||||
|
language: Language::EnglishOld,
|
||||||
|
seed: "glorious especially puff son moment add youth nowhere \
|
||||||
|
throw glide grip wrong rhythm consume very swear \
|
||||||
|
bitter heavy eventually begin reason flirt type unable"
|
||||||
|
.into(),
|
||||||
|
spend: "647f4765b66b636ff07170ab6280a9a6804dfbaf19db2ad37d23be024a18730b".into(),
|
||||||
|
view: "045da65316a906a8c30046053119c18020b07a7a3a6ef5c01ab2a8755416bd02".into(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for vector in vectors {
|
||||||
|
let trim_seed = |seed: &str| {
|
||||||
|
seed
|
||||||
|
.split_whitespace()
|
||||||
|
.map(|word| trim_by_lang(word, vector.language))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(" ")
|
||||||
|
};
|
||||||
|
|
||||||
|
// Test against Monero
|
||||||
|
{
|
||||||
|
let seed = Seed::from_string(Zeroizing::new(vector.seed.clone())).unwrap();
|
||||||
|
assert_eq!(seed, Seed::from_string(Zeroizing::new(trim_seed(&vector.seed))).unwrap());
|
||||||
|
|
||||||
|
let spend: [u8; 32] = hex::decode(vector.spend).unwrap().try_into().unwrap();
|
||||||
|
// For classical seeds, Monero directly uses the entropy as a spend key
|
||||||
|
assert_eq!(
|
||||||
|
Scalar::from_canonical_bytes(*seed.entropy()),
|
||||||
|
Scalar::from_canonical_bytes(spend)
|
||||||
|
);
|
||||||
|
|
||||||
|
let view: [u8; 32] = hex::decode(vector.view).unwrap().try_into().unwrap();
|
||||||
|
// Monero then derives the view key as H(spend)
|
||||||
|
assert_eq!(
|
||||||
|
Scalar::from_bytes_mod_order(hash(&spend)),
|
||||||
|
Scalar::from_canonical_bytes(view).unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(Seed::from_entropy(vector.language, Zeroizing::new(spend)).unwrap(), seed);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test against ourself
|
||||||
|
{
|
||||||
|
let seed = Seed::new(&mut OsRng, vector.language);
|
||||||
|
assert_eq!(seed, Seed::from_string(Zeroizing::new(trim_seed(&seed.to_string()))).unwrap());
|
||||||
|
assert_eq!(seed, Seed::from_entropy(vector.language, seed.entropy()).unwrap());
|
||||||
|
assert_eq!(seed, Seed::from_string(seed.to_string()).unwrap());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
230
coins/monero/src/tests/vectors/featured_addresses.json
Normal file
230
coins/monero/src/tests/vectors/featured_addresses.json
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"address": "CjWdTpuDaZ69nTGxzm9YarR82YDYFECi1WaaREZTMy5yDsjaRX5bC3cbC3JpcrBPd7YYpjoWKuBMidgGaKBK5Jye2v3pYyUDn",
|
||||||
|
"network": "Mainnet",
|
||||||
|
"spend": "258dfe7eef9be934839f3b8e0d40e79035fe85879c0a9eb0d7372ae2deb0004c",
|
||||||
|
"view": "f91382373045f3cc69233254ab0406bc9e008707569ff9db4718654812d839df",
|
||||||
|
"subaddress": false,
|
||||||
|
"integrated": false,
|
||||||
|
"guaranteed": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": "CjWdTpuDaZ69nTGxzm9YarR82YDYFECi1WaaREZTMy5yDsjaRX5bC3cbC3JpcrBPd7YYpjoWKuBMidgGaKBK5Jye2v3wfMHCy",
|
||||||
|
"network": "Mainnet",
|
||||||
|
"spend": "258dfe7eef9be934839f3b8e0d40e79035fe85879c0a9eb0d7372ae2deb0004c",
|
||||||
|
"view": "f91382373045f3cc69233254ab0406bc9e008707569ff9db4718654812d839df",
|
||||||
|
"subaddress": true,
|
||||||
|
"integrated": false,
|
||||||
|
"guaranteed": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": "CjWdTpuDaZ69nTGxzm9YarR82YDYFECi1WaaREZTMy5yDsjaRX5bC3cbC3JpcrBPd7YYpjoWKuBMidgGaKBK5JyeeJTo4p5ayvj36PStM5AX",
|
||||||
|
"network": "Mainnet",
|
||||||
|
"spend": "258dfe7eef9be934839f3b8e0d40e79035fe85879c0a9eb0d7372ae2deb0004c",
|
||||||
|
"view": "f91382373045f3cc69233254ab0406bc9e008707569ff9db4718654812d839df",
|
||||||
|
"subaddress": false,
|
||||||
|
"integrated": true,
|
||||||
|
"payment_id": [46, 48, 134, 34, 245, 148, 243, 195],
|
||||||
|
"guaranteed": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": "CjWdTpuDaZ69nTGxzm9YarR82YDYFECi1WaaREZTMy5yDsjaRX5bC3cbC3JpcrBPd7YYpjoWKuBMidgGaKBK5JyeeJWv5WqMCNE2hRs9rJfy",
|
||||||
|
"network": "Mainnet",
|
||||||
|
"spend": "258dfe7eef9be934839f3b8e0d40e79035fe85879c0a9eb0d7372ae2deb0004c",
|
||||||
|
"view": "f91382373045f3cc69233254ab0406bc9e008707569ff9db4718654812d839df",
|
||||||
|
"subaddress": true,
|
||||||
|
"integrated": true,
|
||||||
|
"payment_id": [153, 176, 98, 204, 151, 27, 197, 168],
|
||||||
|
"guaranteed": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": "CjWdTpuDaZ69nTGxzm9YarR82YDYFECi1WaaREZTMy5yDsjaRX5bC3cbC3JpcrBPd7YYpjoWKuBMidgGaKBK5Jye2v4DwqwH1",
|
||||||
|
"network": "Mainnet",
|
||||||
|
"spend": "258dfe7eef9be934839f3b8e0d40e79035fe85879c0a9eb0d7372ae2deb0004c",
|
||||||
|
"view": "f91382373045f3cc69233254ab0406bc9e008707569ff9db4718654812d839df",
|
||||||
|
"subaddress": false,
|
||||||
|
"integrated": false,
|
||||||
|
"guaranteed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": "CjWdTpuDaZ69nTGxzm9YarR82YDYFECi1WaaREZTMy5yDsjaRX5bC3cbC3JpcrBPd7YYpjoWKuBMidgGaKBK5Jye2v4Pyz8bD",
|
||||||
|
"network": "Mainnet",
|
||||||
|
"spend": "258dfe7eef9be934839f3b8e0d40e79035fe85879c0a9eb0d7372ae2deb0004c",
|
||||||
|
"view": "f91382373045f3cc69233254ab0406bc9e008707569ff9db4718654812d839df",
|
||||||
|
"subaddress": true,
|
||||||
|
"integrated": false,
|
||||||
|
"guaranteed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": "CjWdTpuDaZ69nTGxzm9YarR82YDYFECi1WaaREZTMy5yDsjaRX5bC3cbC3JpcrBPd7YYpjoWKuBMidgGaKBK5JyeeJcwt7hykou237MqZZDA",
|
||||||
|
"network": "Mainnet",
|
||||||
|
"spend": "258dfe7eef9be934839f3b8e0d40e79035fe85879c0a9eb0d7372ae2deb0004c",
|
||||||
|
"view": "f91382373045f3cc69233254ab0406bc9e008707569ff9db4718654812d839df",
|
||||||
|
"subaddress": false,
|
||||||
|
"integrated": true,
|
||||||
|
"payment_id": [88, 37, 149, 111, 171, 108, 120, 181],
|
||||||
|
"guaranteed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": "CjWdTpuDaZ69nTGxzm9YarR82YDYFECi1WaaREZTMy5yDsjaRX5bC3cbC3JpcrBPd7YYpjoWKuBMidgGaKBK5JyeeJfTrFAp69u2MYbf5YeN",
|
||||||
|
"network": "Mainnet",
|
||||||
|
"spend": "258dfe7eef9be934839f3b8e0d40e79035fe85879c0a9eb0d7372ae2deb0004c",
|
||||||
|
"view": "f91382373045f3cc69233254ab0406bc9e008707569ff9db4718654812d839df",
|
||||||
|
"subaddress": true,
|
||||||
|
"integrated": true,
|
||||||
|
"payment_id": [125, 69, 155, 152, 140, 160, 157, 186],
|
||||||
|
"guaranteed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": "Kgx5uCVsMSEVm7seL8tjyRGmmVXjWfEowKpKjgaXUGVyMViBYMh13VQ4mfqpB7zEVVcJx3E8FFgAuQ8cq6mg5x712U9w7ScYA",
|
||||||
|
"network": "Testnet",
|
||||||
|
"spend": "bba3a8a5bb47f7abf2e2dffeaf43385e4b308fd63a9ff6707e355f3b0a6c247a",
|
||||||
|
"view": "881713a4fa9777168a54bbdcb75290d319fb92fdf1026a8a4b125a8e341de8ab",
|
||||||
|
"subaddress": false,
|
||||||
|
"integrated": false,
|
||||||
|
"guaranteed": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": "Kgx5uCVsMSEVm7seL8tjyRGmmVXjWfEowKpKjgaXUGVyMViBYMh13VQ4mfqpB7zEVVcJx3E8FFgAuQ8cq6mg5x712UA2gCrT1",
|
||||||
|
"network": "Testnet",
|
||||||
|
"spend": "bba3a8a5bb47f7abf2e2dffeaf43385e4b308fd63a9ff6707e355f3b0a6c247a",
|
||||||
|
"view": "881713a4fa9777168a54bbdcb75290d319fb92fdf1026a8a4b125a8e341de8ab",
|
||||||
|
"subaddress": true,
|
||||||
|
"integrated": false,
|
||||||
|
"guaranteed": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": "Kgx5uCVsMSEVm7seL8tjyRGmmVXjWfEowKpKjgaXUGVyMViBYMh13VQ4mfqpB7zEVVcJx3E8FFgAuQ8cq6mg5x71Vc1DbPKwJu81cxJjqBkS",
|
||||||
|
"network": "Testnet",
|
||||||
|
"spend": "bba3a8a5bb47f7abf2e2dffeaf43385e4b308fd63a9ff6707e355f3b0a6c247a",
|
||||||
|
"view": "881713a4fa9777168a54bbdcb75290d319fb92fdf1026a8a4b125a8e341de8ab",
|
||||||
|
"subaddress": false,
|
||||||
|
"integrated": true,
|
||||||
|
"payment_id": [92, 225, 118, 220, 39, 3, 72, 51],
|
||||||
|
"guaranteed": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": "Kgx5uCVsMSEVm7seL8tjyRGmmVXjWfEowKpKjgaXUGVyMViBYMh13VQ4mfqpB7zEVVcJx3E8FFgAuQ8cq6mg5x71Vc2o1rPMaXN31Fe5J6dn",
|
||||||
|
"network": "Testnet",
|
||||||
|
"spend": "bba3a8a5bb47f7abf2e2dffeaf43385e4b308fd63a9ff6707e355f3b0a6c247a",
|
||||||
|
"view": "881713a4fa9777168a54bbdcb75290d319fb92fdf1026a8a4b125a8e341de8ab",
|
||||||
|
"subaddress": true,
|
||||||
|
"integrated": true,
|
||||||
|
"payment_id": [20, 120, 47, 89, 72, 165, 233, 115],
|
||||||
|
"guaranteed": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": "Kgx5uCVsMSEVm7seL8tjyRGmmVXjWfEowKpKjgaXUGVyMViBYMh13VQ4mfqpB7zEVVcJx3E8FFgAuQ8cq6mg5x712UAQHCRZ4",
|
||||||
|
"network": "Testnet",
|
||||||
|
"spend": "bba3a8a5bb47f7abf2e2dffeaf43385e4b308fd63a9ff6707e355f3b0a6c247a",
|
||||||
|
"view": "881713a4fa9777168a54bbdcb75290d319fb92fdf1026a8a4b125a8e341de8ab",
|
||||||
|
"subaddress": false,
|
||||||
|
"integrated": false,
|
||||||
|
"guaranteed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": "Kgx5uCVsMSEVm7seL8tjyRGmmVXjWfEowKpKjgaXUGVyMViBYMh13VQ4mfqpB7zEVVcJx3E8FFgAuQ8cq6mg5x712UAUzqaii",
|
||||||
|
"network": "Testnet",
|
||||||
|
"spend": "bba3a8a5bb47f7abf2e2dffeaf43385e4b308fd63a9ff6707e355f3b0a6c247a",
|
||||||
|
"view": "881713a4fa9777168a54bbdcb75290d319fb92fdf1026a8a4b125a8e341de8ab",
|
||||||
|
"subaddress": true,
|
||||||
|
"integrated": false,
|
||||||
|
"guaranteed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": "Kgx5uCVsMSEVm7seL8tjyRGmmVXjWfEowKpKjgaXUGVyMViBYMh13VQ4mfqpB7zEVVcJx3E8FFgAuQ8cq6mg5x71VcAsfQc3gJQ2gHLd5DiQ",
|
||||||
|
"network": "Testnet",
|
||||||
|
"spend": "bba3a8a5bb47f7abf2e2dffeaf43385e4b308fd63a9ff6707e355f3b0a6c247a",
|
||||||
|
"view": "881713a4fa9777168a54bbdcb75290d319fb92fdf1026a8a4b125a8e341de8ab",
|
||||||
|
"subaddress": false,
|
||||||
|
"integrated": true,
|
||||||
|
"payment_id": [193, 149, 123, 214, 180, 205, 195, 91],
|
||||||
|
"guaranteed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": "Kgx5uCVsMSEVm7seL8tjyRGmmVXjWfEowKpKjgaXUGVyMViBYMh13VQ4mfqpB7zEVVcJx3E8FFgAuQ8cq6mg5x71VcDBAD5jbZQ3AMHFyvQB",
|
||||||
|
"network": "Testnet",
|
||||||
|
"spend": "bba3a8a5bb47f7abf2e2dffeaf43385e4b308fd63a9ff6707e355f3b0a6c247a",
|
||||||
|
"view": "881713a4fa9777168a54bbdcb75290d319fb92fdf1026a8a4b125a8e341de8ab",
|
||||||
|
"subaddress": true,
|
||||||
|
"integrated": true,
|
||||||
|
"payment_id": [205, 170, 65, 0, 51, 175, 251, 184],
|
||||||
|
"guaranteed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": "FSDinqdKK54PbjF73GgW3nUpf7bF8QbyxFCUurENmUyeEfSxSLL2hxwANBLzq1A8gTSAzzEn65hKjetA8o5BvjV61VPJnBtTP",
|
||||||
|
"network": "Stagenet",
|
||||||
|
"spend": "4cd503040f5e43871bf37d8ca7177da655bda410859af754e24e7b44437f3151",
|
||||||
|
"view": "af60d42b6c6e4437fd93eb32657a14967efa393630d7aee27b5973c8e1c5ad39",
|
||||||
|
"subaddress": false,
|
||||||
|
"integrated": false,
|
||||||
|
"guaranteed": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": "FSDinqdKK54PbjF73GgW3nUpf7bF8QbyxFCUurENmUyeEfSxSLL2hxwANBLzq1A8gTSAzzEn65hKjetA8o5BvjV61VPUrwMvP",
|
||||||
|
"network": "Stagenet",
|
||||||
|
"spend": "4cd503040f5e43871bf37d8ca7177da655bda410859af754e24e7b44437f3151",
|
||||||
|
"view": "af60d42b6c6e4437fd93eb32657a14967efa393630d7aee27b5973c8e1c5ad39",
|
||||||
|
"subaddress": true,
|
||||||
|
"integrated": false,
|
||||||
|
"guaranteed": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": "FSDinqdKK54PbjF73GgW3nUpf7bF8QbyxFCUurENmUyeEfSxSLL2hxwANBLzq1A8gTSAzzEn65hKjetA8o5BvjV6AY5ECEhP5Nr1aCRPXdxk",
|
||||||
|
"network": "Stagenet",
|
||||||
|
"spend": "4cd503040f5e43871bf37d8ca7177da655bda410859af754e24e7b44437f3151",
|
||||||
|
"view": "af60d42b6c6e4437fd93eb32657a14967efa393630d7aee27b5973c8e1c5ad39",
|
||||||
|
"subaddress": false,
|
||||||
|
"integrated": true,
|
||||||
|
"payment_id": [173, 149, 78, 64, 215, 211, 66, 170],
|
||||||
|
"guaranteed": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": "FSDinqdKK54PbjF73GgW3nUpf7bF8QbyxFCUurENmUyeEfSxSLL2hxwANBLzq1A8gTSAzzEn65hKjetA8o5BvjV6AY882kTUS1D2LttnPvTR",
|
||||||
|
"network": "Stagenet",
|
||||||
|
"spend": "4cd503040f5e43871bf37d8ca7177da655bda410859af754e24e7b44437f3151",
|
||||||
|
"view": "af60d42b6c6e4437fd93eb32657a14967efa393630d7aee27b5973c8e1c5ad39",
|
||||||
|
"subaddress": true,
|
||||||
|
"integrated": true,
|
||||||
|
"payment_id": [254, 159, 186, 162, 1, 8, 156, 108],
|
||||||
|
"guaranteed": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": "FSDinqdKK54PbjF73GgW3nUpf7bF8QbyxFCUurENmUyeEfSxSLL2hxwANBLzq1A8gTSAzzEn65hKjetA8o5BvjV61VPpBBo8F",
|
||||||
|
"network": "Stagenet",
|
||||||
|
"spend": "4cd503040f5e43871bf37d8ca7177da655bda410859af754e24e7b44437f3151",
|
||||||
|
"view": "af60d42b6c6e4437fd93eb32657a14967efa393630d7aee27b5973c8e1c5ad39",
|
||||||
|
"subaddress": false,
|
||||||
|
"integrated": false,
|
||||||
|
"guaranteed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": "FSDinqdKK54PbjF73GgW3nUpf7bF8QbyxFCUurENmUyeEfSxSLL2hxwANBLzq1A8gTSAzzEn65hKjetA8o5BvjV61VPuUJX3b",
|
||||||
|
"network": "Stagenet",
|
||||||
|
"spend": "4cd503040f5e43871bf37d8ca7177da655bda410859af754e24e7b44437f3151",
|
||||||
|
"view": "af60d42b6c6e4437fd93eb32657a14967efa393630d7aee27b5973c8e1c5ad39",
|
||||||
|
"subaddress": true,
|
||||||
|
"integrated": false,
|
||||||
|
"guaranteed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": "FSDinqdKK54PbjF73GgW3nUpf7bF8QbyxFCUurENmUyeEfSxSLL2hxwANBLzq1A8gTSAzzEn65hKjetA8o5BvjV6AYCZPxVAoDu21DryMoto",
|
||||||
|
"network": "Stagenet",
|
||||||
|
"spend": "4cd503040f5e43871bf37d8ca7177da655bda410859af754e24e7b44437f3151",
|
||||||
|
"view": "af60d42b6c6e4437fd93eb32657a14967efa393630d7aee27b5973c8e1c5ad39",
|
||||||
|
"subaddress": false,
|
||||||
|
"integrated": true,
|
||||||
|
"payment_id": [3, 115, 230, 129, 172, 108, 116, 235],
|
||||||
|
"guaranteed": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"address": "FSDinqdKK54PbjF73GgW3nUpf7bF8QbyxFCUurENmUyeEfSxSLL2hxwANBLzq1A8gTSAzzEn65hKjetA8o5BvjV6AYFYCqKQAWL18KkpBQ8R",
|
||||||
|
"network": "Stagenet",
|
||||||
|
"spend": "4cd503040f5e43871bf37d8ca7177da655bda410859af754e24e7b44437f3151",
|
||||||
|
"view": "af60d42b6c6e4437fd93eb32657a14967efa393630d7aee27b5973c8e1c5ad39",
|
||||||
|
"subaddress": true,
|
||||||
|
"integrated": true,
|
||||||
|
"payment_id": [94, 122, 63, 167, 209, 225, 14, 180],
|
||||||
|
"guaranteed": true
|
||||||
|
}
|
||||||
|
]
|
||||||
378
coins/monero/src/transaction.rs
Normal file
378
coins/monero/src/transaction.rs
Normal file
@@ -0,0 +1,378 @@
|
|||||||
|
use core::cmp::Ordering;
|
||||||
|
use std_shims::{
|
||||||
|
vec::Vec,
|
||||||
|
io::{self, Read, Write},
|
||||||
|
};
|
||||||
|
|
||||||
|
use zeroize::Zeroize;
|
||||||
|
|
||||||
|
use curve25519_dalek::{
|
||||||
|
scalar::Scalar,
|
||||||
|
edwards::{EdwardsPoint, CompressedEdwardsY},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
Protocol, hash,
|
||||||
|
serialize::*,
|
||||||
|
ringct::{RctBase, RctPrunable, RctSignatures},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||||
|
pub enum Input {
|
||||||
|
Gen(u64),
|
||||||
|
ToKey { amount: Option<u64>, key_offsets: Vec<u64>, key_image: EdwardsPoint },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Input {
|
||||||
|
// Worst-case predictive len
|
||||||
|
pub(crate) fn fee_weight(ring_len: usize) -> usize {
|
||||||
|
// Uses 1 byte for the VarInt amount due to amount being 0
|
||||||
|
// Uses 1 byte for the VarInt encoding of the length of the ring as well
|
||||||
|
1 + 1 + 1 + (8 * ring_len) + 32
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
||||||
|
match self {
|
||||||
|
Self::Gen(height) => {
|
||||||
|
w.write_all(&[255])?;
|
||||||
|
write_varint(height, w)
|
||||||
|
}
|
||||||
|
|
||||||
|
Self::ToKey { amount, key_offsets, key_image } => {
|
||||||
|
w.write_all(&[2])?;
|
||||||
|
write_varint(&amount.unwrap_or(0), w)?;
|
||||||
|
write_vec(write_varint, key_offsets, w)?;
|
||||||
|
write_point(key_image, w)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn serialize(&self) -> Vec<u8> {
|
||||||
|
let mut res = vec![];
|
||||||
|
self.write(&mut res).unwrap();
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read<R: Read>(interpret_as_rct: bool, r: &mut R) -> io::Result<Self> {
|
||||||
|
Ok(match read_byte(r)? {
|
||||||
|
255 => Self::Gen(read_varint(r)?),
|
||||||
|
2 => {
|
||||||
|
let amount = read_varint(r)?;
|
||||||
|
let amount = if (amount == 0) && interpret_as_rct { None } else { Some(amount) };
|
||||||
|
Self::ToKey {
|
||||||
|
amount,
|
||||||
|
key_offsets: read_vec(read_varint, r)?,
|
||||||
|
key_image: read_torsion_free_point(r)?,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
Err(io::Error::new(io::ErrorKind::Other, "Tried to deserialize unknown/unused input type"))?
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Doesn't bother moving to an enum for the unused Script classes
|
||||||
|
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||||
|
pub struct Output {
|
||||||
|
pub amount: Option<u64>,
|
||||||
|
pub key: CompressedEdwardsY,
|
||||||
|
pub view_tag: Option<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Output {
|
||||||
|
pub(crate) fn fee_weight() -> usize {
|
||||||
|
1 + 1 + 32 + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
||||||
|
write_varint(&self.amount.unwrap_or(0), w)?;
|
||||||
|
w.write_all(&[2 + u8::from(self.view_tag.is_some())])?;
|
||||||
|
w.write_all(&self.key.to_bytes())?;
|
||||||
|
if let Some(view_tag) = self.view_tag {
|
||||||
|
w.write_all(&[view_tag])?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn serialize(&self) -> Vec<u8> {
|
||||||
|
let mut res = Vec::with_capacity(8 + 1 + 32);
|
||||||
|
self.write(&mut res).unwrap();
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read<R: Read>(interpret_as_rct: bool, r: &mut R) -> io::Result<Self> {
|
||||||
|
let amount = read_varint(r)?;
|
||||||
|
let amount = if interpret_as_rct {
|
||||||
|
if amount != 0 {
|
||||||
|
Err(io::Error::new(io::ErrorKind::Other, "RCT TX output wasn't 0"))?;
|
||||||
|
}
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(amount)
|
||||||
|
};
|
||||||
|
|
||||||
|
let view_tag = match read_byte(r)? {
|
||||||
|
2 => false,
|
||||||
|
3 => true,
|
||||||
|
_ => Err(io::Error::new(
|
||||||
|
io::ErrorKind::Other,
|
||||||
|
"Tried to deserialize unknown/unused output type",
|
||||||
|
))?,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
amount,
|
||||||
|
key: CompressedEdwardsY(read_bytes(r)?),
|
||||||
|
view_tag: if view_tag { Some(read_byte(r)?) } else { None },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
|
||||||
|
pub enum Timelock {
|
||||||
|
None,
|
||||||
|
Block(usize),
|
||||||
|
Time(u64),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Timelock {
|
||||||
|
fn from_raw(raw: u64) -> Self {
|
||||||
|
if raw == 0 {
|
||||||
|
Self::None
|
||||||
|
} else if raw < 500_000_000 {
|
||||||
|
Self::Block(usize::try_from(raw).unwrap())
|
||||||
|
} else {
|
||||||
|
Self::Time(raw)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
||||||
|
write_varint(
|
||||||
|
&match self {
|
||||||
|
Self::None => 0,
|
||||||
|
Self::Block(block) => (*block).try_into().unwrap(),
|
||||||
|
Self::Time(time) => *time,
|
||||||
|
},
|
||||||
|
w,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialOrd for Timelock {
|
||||||
|
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||||
|
match (self, other) {
|
||||||
|
(Self::None, _) => Some(Ordering::Less),
|
||||||
|
(Self::Block(a), Self::Block(b)) => a.partial_cmp(b),
|
||||||
|
(Self::Time(a), Self::Time(b)) => a.partial_cmp(b),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||||
|
pub struct TransactionPrefix {
|
||||||
|
pub version: u64,
|
||||||
|
pub timelock: Timelock,
|
||||||
|
pub inputs: Vec<Input>,
|
||||||
|
pub outputs: Vec<Output>,
|
||||||
|
pub extra: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TransactionPrefix {
|
||||||
|
pub(crate) fn fee_weight(ring_len: usize, inputs: usize, outputs: usize, extra: usize) -> usize {
|
||||||
|
// Assumes Timelock::None since this library won't let you create a TX with a timelock
|
||||||
|
1 + 1 +
|
||||||
|
varint_len(inputs) +
|
||||||
|
(inputs * Input::fee_weight(ring_len)) +
|
||||||
|
1 +
|
||||||
|
(outputs * Output::fee_weight()) +
|
||||||
|
varint_len(extra) +
|
||||||
|
extra
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
||||||
|
write_varint(&self.version, w)?;
|
||||||
|
self.timelock.write(w)?;
|
||||||
|
write_vec(Input::write, &self.inputs, w)?;
|
||||||
|
write_vec(Output::write, &self.outputs, w)?;
|
||||||
|
write_varint(&self.extra.len().try_into().unwrap(), w)?;
|
||||||
|
w.write_all(&self.extra)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn serialize(&self) -> Vec<u8> {
|
||||||
|
let mut res = vec![];
|
||||||
|
self.write(&mut res).unwrap();
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read<R: Read>(r: &mut R) -> io::Result<Self> {
|
||||||
|
let version = read_varint(r)?;
|
||||||
|
// TODO: Create an enum out of version
|
||||||
|
if (version == 0) || (version > 2) {
|
||||||
|
Err(io::Error::new(io::ErrorKind::Other, "unrecognized transaction version"))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let timelock = Timelock::from_raw(read_varint(r)?);
|
||||||
|
|
||||||
|
let inputs = read_vec(|r| Input::read(version == 2, r), r)?;
|
||||||
|
if inputs.is_empty() {
|
||||||
|
Err(io::Error::new(io::ErrorKind::Other, "transaction had no inputs"))?;
|
||||||
|
}
|
||||||
|
let is_miner_tx = matches!(inputs[0], Input::Gen { .. });
|
||||||
|
|
||||||
|
let mut prefix = Self {
|
||||||
|
version,
|
||||||
|
timelock,
|
||||||
|
inputs,
|
||||||
|
outputs: read_vec(|r| Output::read((!is_miner_tx) && (version == 2), r), r)?,
|
||||||
|
extra: vec![],
|
||||||
|
};
|
||||||
|
prefix.extra = read_vec(read_byte, r)?;
|
||||||
|
Ok(prefix)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn hash(&self) -> [u8; 32] {
|
||||||
|
hash(&self.serialize())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Monero transaction. For version 1, rct_signatures still contains an accurate fee value.
|
||||||
|
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||||
|
pub struct Transaction {
|
||||||
|
pub prefix: TransactionPrefix,
|
||||||
|
pub signatures: Vec<Vec<(Scalar, Scalar)>>,
|
||||||
|
pub rct_signatures: RctSignatures,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Transaction {
|
||||||
|
pub(crate) fn fee_weight(
|
||||||
|
protocol: Protocol,
|
||||||
|
inputs: usize,
|
||||||
|
outputs: usize,
|
||||||
|
extra: usize,
|
||||||
|
) -> usize {
|
||||||
|
TransactionPrefix::fee_weight(protocol.ring_len(), inputs, outputs, extra) +
|
||||||
|
RctSignatures::fee_weight(protocol, inputs, outputs)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
||||||
|
self.prefix.write(w)?;
|
||||||
|
if self.prefix.version == 1 {
|
||||||
|
for sigs in &self.signatures {
|
||||||
|
for sig in sigs {
|
||||||
|
write_scalar(&sig.0, w)?;
|
||||||
|
write_scalar(&sig.1, w)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
} else if self.prefix.version == 2 {
|
||||||
|
self.rct_signatures.write(w)
|
||||||
|
} else {
|
||||||
|
panic!("Serializing a transaction with an unknown version");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn serialize(&self) -> Vec<u8> {
|
||||||
|
let mut res = Vec::with_capacity(2048);
|
||||||
|
self.write(&mut res).unwrap();
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read<R: Read>(r: &mut R) -> io::Result<Self> {
|
||||||
|
let prefix = TransactionPrefix::read(r)?;
|
||||||
|
let mut signatures = vec![];
|
||||||
|
let mut rct_signatures = RctSignatures {
|
||||||
|
base: RctBase { fee: 0, encrypted_amounts: vec![], pseudo_outs: vec![], commitments: vec![] },
|
||||||
|
prunable: RctPrunable::Null,
|
||||||
|
};
|
||||||
|
|
||||||
|
if prefix.version == 1 {
|
||||||
|
signatures = prefix
|
||||||
|
.inputs
|
||||||
|
.iter()
|
||||||
|
.filter_map(|input| match input {
|
||||||
|
Input::ToKey { key_offsets, .. } => Some(
|
||||||
|
key_offsets
|
||||||
|
.iter()
|
||||||
|
.map(|_| Ok((read_scalar(r)?, read_scalar(r)?)))
|
||||||
|
.collect::<Result<_, io::Error>>(),
|
||||||
|
),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.collect::<Result<_, _>>()?;
|
||||||
|
|
||||||
|
rct_signatures.base.fee = prefix
|
||||||
|
.inputs
|
||||||
|
.iter()
|
||||||
|
.map(|input| match input {
|
||||||
|
Input::Gen(..) => 0,
|
||||||
|
Input::ToKey { amount, .. } => amount.unwrap(),
|
||||||
|
})
|
||||||
|
.sum::<u64>()
|
||||||
|
.saturating_sub(prefix.outputs.iter().map(|output| output.amount.unwrap()).sum());
|
||||||
|
} else if prefix.version == 2 {
|
||||||
|
rct_signatures = RctSignatures::read(
|
||||||
|
prefix
|
||||||
|
.inputs
|
||||||
|
.iter()
|
||||||
|
.map(|input| match input {
|
||||||
|
Input::Gen(_) => 0,
|
||||||
|
Input::ToKey { key_offsets, .. } => key_offsets.len(),
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
prefix.outputs.len(),
|
||||||
|
r,
|
||||||
|
)?;
|
||||||
|
} else {
|
||||||
|
Err(io::Error::new(io::ErrorKind::Other, "Tried to deserialize unknown version"))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self { prefix, signatures, rct_signatures })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn hash(&self) -> [u8; 32] {
|
||||||
|
let mut buf = Vec::with_capacity(2048);
|
||||||
|
if self.prefix.version == 1 {
|
||||||
|
self.write(&mut buf).unwrap();
|
||||||
|
hash(&buf)
|
||||||
|
} else {
|
||||||
|
let mut hashes = Vec::with_capacity(96);
|
||||||
|
|
||||||
|
hashes.extend(self.prefix.hash());
|
||||||
|
|
||||||
|
self.rct_signatures.base.write(&mut buf, self.rct_signatures.rct_type()).unwrap();
|
||||||
|
hashes.extend(hash(&buf));
|
||||||
|
buf.clear();
|
||||||
|
|
||||||
|
hashes.extend(&match self.rct_signatures.prunable {
|
||||||
|
RctPrunable::Null => [0; 32],
|
||||||
|
RctPrunable::MlsagBorromean { .. } |
|
||||||
|
RctPrunable::MlsagBulletproofs { .. } |
|
||||||
|
RctPrunable::Clsag { .. } => {
|
||||||
|
self.rct_signatures.prunable.write(&mut buf, self.rct_signatures.rct_type()).unwrap();
|
||||||
|
hash(&buf)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
hash(&hashes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculate the hash of this transaction as needed for signing it.
|
||||||
|
pub fn signature_hash(&self) -> [u8; 32] {
|
||||||
|
let mut buf = Vec::with_capacity(2048);
|
||||||
|
let mut sig_hash = Vec::with_capacity(96);
|
||||||
|
|
||||||
|
sig_hash.extend(self.prefix.hash());
|
||||||
|
|
||||||
|
self.rct_signatures.base.write(&mut buf, self.rct_signatures.rct_type()).unwrap();
|
||||||
|
sig_hash.extend(hash(&buf));
|
||||||
|
buf.clear();
|
||||||
|
|
||||||
|
self.rct_signatures.prunable.signature_write(&mut buf).unwrap();
|
||||||
|
sig_hash.extend(hash(&buf));
|
||||||
|
|
||||||
|
hash(&sig_hash)
|
||||||
|
}
|
||||||
|
}
|
||||||
311
coins/monero/src/wallet/address.rs
Normal file
311
coins/monero/src/wallet/address.rs
Normal file
@@ -0,0 +1,311 @@
|
|||||||
|
use core::{marker::PhantomData, fmt::Debug};
|
||||||
|
use std_shims::string::{String, ToString};
|
||||||
|
|
||||||
|
use zeroize::Zeroize;
|
||||||
|
|
||||||
|
use curve25519_dalek::edwards::{EdwardsPoint, CompressedEdwardsY};
|
||||||
|
|
||||||
|
use base58_monero::base58::{encode_check, decode_check};
|
||||||
|
|
||||||
|
/// The network this address is for.
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
|
||||||
|
pub enum Network {
|
||||||
|
Mainnet,
|
||||||
|
Testnet,
|
||||||
|
Stagenet,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The address type, supporting the officially documented addresses, along with
|
||||||
|
/// [Featured Addresses](https://gist.github.com/kayabaNerve/01c50bbc35441e0bbdcee63a9d823789).
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
|
||||||
|
pub enum AddressType {
|
||||||
|
Standard,
|
||||||
|
Integrated([u8; 8]),
|
||||||
|
Subaddress,
|
||||||
|
Featured { subaddress: bool, payment_id: Option<[u8; 8]>, guaranteed: bool },
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
|
||||||
|
pub struct SubaddressIndex {
|
||||||
|
pub(crate) account: u32,
|
||||||
|
pub(crate) address: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SubaddressIndex {
|
||||||
|
pub const fn new(account: u32, address: u32) -> Option<Self> {
|
||||||
|
if (account == 0) && (address == 0) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(Self { account, address })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn account(&self) -> u32 {
|
||||||
|
self.account
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn address(&self) -> u32 {
|
||||||
|
self.address
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Address specification. Used internally to create addresses.
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)]
|
||||||
|
pub enum AddressSpec {
|
||||||
|
Standard,
|
||||||
|
Integrated([u8; 8]),
|
||||||
|
Subaddress(SubaddressIndex),
|
||||||
|
Featured { subaddress: Option<SubaddressIndex>, payment_id: Option<[u8; 8]>, guaranteed: bool },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AddressType {
|
||||||
|
pub const fn is_subaddress(&self) -> bool {
|
||||||
|
matches!(self, Self::Subaddress) || matches!(self, Self::Featured { subaddress: true, .. })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn payment_id(&self) -> Option<[u8; 8]> {
|
||||||
|
if let Self::Integrated(id) = self {
|
||||||
|
Some(*id)
|
||||||
|
} else if let Self::Featured { payment_id, .. } = self {
|
||||||
|
*payment_id
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn is_guaranteed(&self) -> bool {
|
||||||
|
matches!(self, Self::Featured { guaranteed: true, .. })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A type which returns the byte for a given address.
|
||||||
|
pub trait AddressBytes: Clone + Copy + PartialEq + Eq + Debug {
|
||||||
|
fn network_bytes(network: Network) -> (u8, u8, u8, u8);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Address bytes for Monero.
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||||
|
pub struct MoneroAddressBytes;
|
||||||
|
impl AddressBytes for MoneroAddressBytes {
|
||||||
|
fn network_bytes(network: Network) -> (u8, u8, u8, u8) {
|
||||||
|
match network {
|
||||||
|
Network::Mainnet => (18, 19, 42, 70),
|
||||||
|
Network::Testnet => (53, 54, 63, 111),
|
||||||
|
Network::Stagenet => (24, 25, 36, 86),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Address metadata.
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||||
|
pub struct AddressMeta<B: AddressBytes> {
|
||||||
|
_bytes: PhantomData<B>,
|
||||||
|
pub network: Network,
|
||||||
|
pub kind: AddressType,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<B: AddressBytes> Zeroize for AddressMeta<B> {
|
||||||
|
fn zeroize(&mut self) {
|
||||||
|
self.network.zeroize();
|
||||||
|
self.kind.zeroize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Error when decoding an address.
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||||
|
#[cfg_attr(feature = "std", derive(thiserror::Error))]
|
||||||
|
pub enum AddressError {
|
||||||
|
#[cfg_attr(feature = "std", error("invalid address byte"))]
|
||||||
|
InvalidByte,
|
||||||
|
#[cfg_attr(feature = "std", error("invalid address encoding"))]
|
||||||
|
InvalidEncoding,
|
||||||
|
#[cfg_attr(feature = "std", error("invalid length"))]
|
||||||
|
InvalidLength,
|
||||||
|
#[cfg_attr(feature = "std", error("invalid key"))]
|
||||||
|
InvalidKey,
|
||||||
|
#[cfg_attr(feature = "std", error("unknown features"))]
|
||||||
|
UnknownFeatures,
|
||||||
|
#[cfg_attr(feature = "std", error("different network than expected"))]
|
||||||
|
DifferentNetwork,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<B: AddressBytes> AddressMeta<B> {
|
||||||
|
#[allow(clippy::wrong_self_convention)]
|
||||||
|
fn to_byte(&self) -> u8 {
|
||||||
|
let bytes = B::network_bytes(self.network);
|
||||||
|
match self.kind {
|
||||||
|
AddressType::Standard => bytes.0,
|
||||||
|
AddressType::Integrated(_) => bytes.1,
|
||||||
|
AddressType::Subaddress => bytes.2,
|
||||||
|
AddressType::Featured { .. } => bytes.3,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create an address's metadata.
|
||||||
|
pub const fn new(network: Network, kind: AddressType) -> Self {
|
||||||
|
Self { _bytes: PhantomData, network, kind }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns an incomplete instantiation in the case of Integrated/Featured addresses
|
||||||
|
fn from_byte(byte: u8) -> Result<Self, AddressError> {
|
||||||
|
let mut meta = None;
|
||||||
|
for network in [Network::Mainnet, Network::Testnet, Network::Stagenet] {
|
||||||
|
let (standard, integrated, subaddress, featured) = B::network_bytes(network);
|
||||||
|
if let Some(kind) = match byte {
|
||||||
|
_ if byte == standard => Some(AddressType::Standard),
|
||||||
|
_ if byte == integrated => Some(AddressType::Integrated([0; 8])),
|
||||||
|
_ if byte == subaddress => Some(AddressType::Subaddress),
|
||||||
|
_ if byte == featured => {
|
||||||
|
Some(AddressType::Featured { subaddress: false, payment_id: None, guaranteed: false })
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
} {
|
||||||
|
meta = Some(Self::new(network, kind));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
meta.ok_or(AddressError::InvalidByte)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn is_subaddress(&self) -> bool {
|
||||||
|
self.kind.is_subaddress()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn payment_id(&self) -> Option<[u8; 8]> {
|
||||||
|
self.kind.payment_id()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn is_guaranteed(&self) -> bool {
|
||||||
|
self.kind.is_guaranteed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A Monero address, composed of metadata and a spend/view key.
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||||
|
pub struct Address<B: AddressBytes> {
|
||||||
|
pub meta: AddressMeta<B>,
|
||||||
|
pub spend: EdwardsPoint,
|
||||||
|
pub view: EdwardsPoint,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<B: AddressBytes> Zeroize for Address<B> {
|
||||||
|
fn zeroize(&mut self) {
|
||||||
|
self.meta.zeroize();
|
||||||
|
self.spend.zeroize();
|
||||||
|
self.view.zeroize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<B: AddressBytes> ToString for Address<B> {
|
||||||
|
fn to_string(&self) -> String {
|
||||||
|
let mut data = vec![self.meta.to_byte()];
|
||||||
|
data.extend(self.spend.compress().to_bytes());
|
||||||
|
data.extend(self.view.compress().to_bytes());
|
||||||
|
if let AddressType::Featured { subaddress, payment_id, guaranteed } = self.meta.kind {
|
||||||
|
// Technically should be a VarInt, yet we don't have enough features it's needed
|
||||||
|
data.push(
|
||||||
|
u8::from(subaddress) + (u8::from(payment_id.is_some()) << 1) + (u8::from(guaranteed) << 2),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if let Some(id) = self.meta.kind.payment_id() {
|
||||||
|
data.extend(id);
|
||||||
|
}
|
||||||
|
encode_check(&data).unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<B: AddressBytes> Address<B> {
|
||||||
|
pub const fn new(meta: AddressMeta<B>, spend: EdwardsPoint, view: EdwardsPoint) -> Self {
|
||||||
|
Self { meta, spend, view }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_str_raw(s: &str) -> Result<Self, AddressError> {
|
||||||
|
let raw = decode_check(s).map_err(|_| AddressError::InvalidEncoding)?;
|
||||||
|
if raw.len() < (1 + 32 + 32) {
|
||||||
|
Err(AddressError::InvalidLength)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut meta = AddressMeta::from_byte(raw[0])?;
|
||||||
|
let spend = CompressedEdwardsY(raw[1 .. 33].try_into().unwrap())
|
||||||
|
.decompress()
|
||||||
|
.ok_or(AddressError::InvalidKey)?;
|
||||||
|
let view = CompressedEdwardsY(raw[33 .. 65].try_into().unwrap())
|
||||||
|
.decompress()
|
||||||
|
.ok_or(AddressError::InvalidKey)?;
|
||||||
|
let mut read = 65;
|
||||||
|
|
||||||
|
if matches!(meta.kind, AddressType::Featured { .. }) {
|
||||||
|
if raw[read] >= (2 << 3) {
|
||||||
|
Err(AddressError::UnknownFeatures)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let subaddress = (raw[read] & 1) == 1;
|
||||||
|
let integrated = ((raw[read] >> 1) & 1) == 1;
|
||||||
|
let guaranteed = ((raw[read] >> 2) & 1) == 1;
|
||||||
|
|
||||||
|
meta.kind = AddressType::Featured {
|
||||||
|
subaddress,
|
||||||
|
payment_id: Some([0; 8]).filter(|_| integrated),
|
||||||
|
guaranteed,
|
||||||
|
};
|
||||||
|
read += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update read early so we can verify the length
|
||||||
|
if meta.kind.payment_id().is_some() {
|
||||||
|
read += 8;
|
||||||
|
}
|
||||||
|
if raw.len() != read {
|
||||||
|
Err(AddressError::InvalidLength)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let AddressType::Integrated(ref mut id) = meta.kind {
|
||||||
|
id.copy_from_slice(&raw[(read - 8) .. read]);
|
||||||
|
}
|
||||||
|
if let AddressType::Featured { payment_id: Some(ref mut id), .. } = meta.kind {
|
||||||
|
id.copy_from_slice(&raw[(read - 8) .. read]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self { meta, spend, view })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_str(network: Network, s: &str) -> Result<Self, AddressError> {
|
||||||
|
Self::from_str_raw(s).and_then(|addr| {
|
||||||
|
if addr.meta.network == network {
|
||||||
|
Ok(addr)
|
||||||
|
} else {
|
||||||
|
Err(AddressError::DifferentNetwork)?
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn network(&self) -> Network {
|
||||||
|
self.meta.network
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn is_subaddress(&self) -> bool {
|
||||||
|
self.meta.is_subaddress()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn payment_id(&self) -> Option<[u8; 8]> {
|
||||||
|
self.meta.payment_id()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn is_guaranteed(&self) -> bool {
|
||||||
|
self.meta.is_guaranteed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Instantiation of the Address type with Monero's network bytes.
|
||||||
|
pub type MoneroAddress = Address<MoneroAddressBytes>;
|
||||||
|
// Allow re-interpreting of an arbitrary address as a Monero address so it can be used with the
|
||||||
|
// rest of this library. Doesn't use From as it was conflicting with From<T> for T.
|
||||||
|
impl MoneroAddress {
|
||||||
|
pub const fn from<B: AddressBytes>(address: Address<B>) -> Self {
|
||||||
|
Self::new(
|
||||||
|
AddressMeta::new(address.meta.network, address.meta.kind),
|
||||||
|
address.spend,
|
||||||
|
address.view,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
281
coins/monero/src/wallet/decoys.rs
Normal file
281
coins/monero/src/wallet/decoys.rs
Normal file
@@ -0,0 +1,281 @@
|
|||||||
|
use std_shims::{sync::OnceLock, vec::Vec, collections::HashSet};
|
||||||
|
|
||||||
|
#[cfg(not(feature = "std"))]
|
||||||
|
use std_shims::sync::Mutex;
|
||||||
|
#[cfg(feature = "std")]
|
||||||
|
use futures::lock::Mutex;
|
||||||
|
|
||||||
|
use zeroize::{Zeroize, ZeroizeOnDrop};
|
||||||
|
|
||||||
|
use rand_core::{RngCore, CryptoRng};
|
||||||
|
use rand_distr::{Distribution, Gamma};
|
||||||
|
#[cfg(not(feature = "std"))]
|
||||||
|
use rand_distr::num_traits::Float;
|
||||||
|
|
||||||
|
use curve25519_dalek::edwards::EdwardsPoint;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
wallet::SpendableOutput,
|
||||||
|
rpc::{RpcError, RpcConnection, Rpc},
|
||||||
|
};
|
||||||
|
|
||||||
|
const LOCK_WINDOW: usize = 10;
|
||||||
|
const MATURITY: u64 = 60;
|
||||||
|
const RECENT_WINDOW: usize = 15;
|
||||||
|
const BLOCK_TIME: usize = 120;
|
||||||
|
const BLOCKS_PER_YEAR: usize = 365 * 24 * 60 * 60 / BLOCK_TIME;
|
||||||
|
#[allow(clippy::as_conversions)]
|
||||||
|
const TIP_APPLICATION: f64 = (LOCK_WINDOW * BLOCK_TIME) as f64;
|
||||||
|
|
||||||
|
// TODO: Expose an API to reset this in case a reorg occurs/the RPC fails/returns garbage
|
||||||
|
// TODO: Update this when scanning a block, as possible
|
||||||
|
static DISTRIBUTION_CELL: OnceLock<Mutex<Vec<u64>>> = OnceLock::new();
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
fn DISTRIBUTION() -> &'static Mutex<Vec<u64>> {
|
||||||
|
DISTRIBUTION_CELL.get_or_init(|| Mutex::new(Vec::with_capacity(3_000_000)))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments, clippy::as_conversions)]
|
||||||
|
async fn select_n<'a, R: Send + RngCore + CryptoRng, RPC: RpcConnection>(
|
||||||
|
rng: &mut R,
|
||||||
|
rpc: &Rpc<RPC>,
|
||||||
|
distribution: &[u64],
|
||||||
|
height: usize,
|
||||||
|
high: u64,
|
||||||
|
per_second: f64,
|
||||||
|
real: &[u64],
|
||||||
|
used: &mut HashSet<u64>,
|
||||||
|
count: usize,
|
||||||
|
) -> Result<Vec<(u64, [EdwardsPoint; 2])>, RpcError> {
|
||||||
|
if height >= rpc.get_height().await? {
|
||||||
|
// TODO: Don't use InternalError for the caller's failure
|
||||||
|
Err(RpcError::InternalError("decoys being requested from too young blocks"))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
let mut iters = 0;
|
||||||
|
let mut confirmed = Vec::with_capacity(count);
|
||||||
|
// Retries on failure. Retries are obvious as decoys, yet should be minimal
|
||||||
|
while confirmed.len() != count {
|
||||||
|
let remaining = count - confirmed.len();
|
||||||
|
let mut candidates = Vec::with_capacity(remaining);
|
||||||
|
while candidates.len() != remaining {
|
||||||
|
#[cfg(test)]
|
||||||
|
{
|
||||||
|
iters += 1;
|
||||||
|
// This is cheap and on fresh chains, a lot of rounds may be needed
|
||||||
|
if iters == 100 {
|
||||||
|
Err(RpcError::InternalError("hit decoy selection round limit"))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use a gamma distribution
|
||||||
|
let mut age = Gamma::<f64>::new(19.28, 1.0 / 1.61).unwrap().sample(rng).exp();
|
||||||
|
if age > TIP_APPLICATION {
|
||||||
|
age -= TIP_APPLICATION;
|
||||||
|
} else {
|
||||||
|
// f64 does not have try_from available, which is why these are written with `as`
|
||||||
|
age = (rng.next_u64() % u64::try_from(RECENT_WINDOW * BLOCK_TIME).unwrap()) as f64;
|
||||||
|
}
|
||||||
|
|
||||||
|
let o = (age * per_second) as u64;
|
||||||
|
if o < high {
|
||||||
|
let i = distribution.partition_point(|s| *s < (high - 1 - o));
|
||||||
|
let prev = i.saturating_sub(1);
|
||||||
|
let n = distribution[i] - distribution[prev];
|
||||||
|
if n != 0 {
|
||||||
|
let o = distribution[prev] + (rng.next_u64() % n);
|
||||||
|
if !used.contains(&o) {
|
||||||
|
// It will either actually be used, or is unusable and this prevents trying it again
|
||||||
|
used.insert(o);
|
||||||
|
candidates.push(o);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this is the first time we're requesting these outputs, include the real one as well
|
||||||
|
// Prevents the node we're connected to from having a list of known decoys and then seeing a
|
||||||
|
// TX which uses all of them, with one additional output (the true spend)
|
||||||
|
let mut real_indexes = HashSet::with_capacity(real.len());
|
||||||
|
if confirmed.is_empty() {
|
||||||
|
for real in real {
|
||||||
|
candidates.push(*real);
|
||||||
|
}
|
||||||
|
// Sort candidates so the real spends aren't the ones at the end
|
||||||
|
candidates.sort();
|
||||||
|
for real in real {
|
||||||
|
real_indexes.insert(candidates.binary_search(real).unwrap());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i, output) in rpc.get_unlocked_outputs(&candidates, height).await?.iter_mut().enumerate() {
|
||||||
|
// Don't include the real spend as a decoy, despite requesting it
|
||||||
|
if real_indexes.contains(&i) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(output) = output.take() {
|
||||||
|
confirmed.push((candidates[i], output));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(confirmed)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn offset(ring: &[u64]) -> Vec<u64> {
|
||||||
|
let mut res = vec![ring[0]];
|
||||||
|
res.resize(ring.len(), 0);
|
||||||
|
for m in (1 .. ring.len()).rev() {
|
||||||
|
res[m] = ring[m] - ring[m - 1];
|
||||||
|
}
|
||||||
|
res
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decoy data, containing the actual member as well (at index `i`).
|
||||||
|
#[derive(Clone, PartialEq, Eq, Debug, Zeroize, ZeroizeOnDrop)]
|
||||||
|
pub struct Decoys {
|
||||||
|
pub i: u8,
|
||||||
|
pub offsets: Vec<u64>,
|
||||||
|
pub ring: Vec<[EdwardsPoint; 2]>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Decoys {
|
||||||
|
pub fn len(&self) -> usize {
|
||||||
|
self.offsets.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Select decoys using the same distribution as Monero.
|
||||||
|
#[allow(clippy::as_conversions)]
|
||||||
|
pub async fn select<R: Send + RngCore + CryptoRng, RPC: RpcConnection>(
|
||||||
|
rng: &mut R,
|
||||||
|
rpc: &Rpc<RPC>,
|
||||||
|
ring_len: usize,
|
||||||
|
height: usize,
|
||||||
|
inputs: &[SpendableOutput],
|
||||||
|
) -> Result<Vec<Self>, RpcError> {
|
||||||
|
#[cfg(not(feature = "std"))]
|
||||||
|
let mut distribution = DISTRIBUTION().lock();
|
||||||
|
#[cfg(feature = "std")]
|
||||||
|
let mut distribution = DISTRIBUTION().lock().await;
|
||||||
|
|
||||||
|
let decoy_count = ring_len - 1;
|
||||||
|
|
||||||
|
// Convert the inputs in question to the raw output data
|
||||||
|
let mut real = Vec::with_capacity(inputs.len());
|
||||||
|
let mut outputs = Vec::with_capacity(inputs.len());
|
||||||
|
for input in inputs {
|
||||||
|
real.push(input.global_index);
|
||||||
|
outputs.push((real[real.len() - 1], [input.key(), input.commitment().calculate()]));
|
||||||
|
}
|
||||||
|
|
||||||
|
if distribution.len() <= height {
|
||||||
|
let extension = rpc.get_output_distribution(distribution.len(), height).await?;
|
||||||
|
distribution.extend(extension);
|
||||||
|
}
|
||||||
|
// If asked to use an older height than previously asked, truncate to ensure accuracy
|
||||||
|
// Should never happen, yet risks desyncing if it did
|
||||||
|
distribution.truncate(height + 1); // height is inclusive, and 0 is a valid height
|
||||||
|
|
||||||
|
let high = distribution[distribution.len() - 1];
|
||||||
|
let per_second = {
|
||||||
|
let blocks = distribution.len().min(BLOCKS_PER_YEAR);
|
||||||
|
let outputs = high - distribution[distribution.len().saturating_sub(blocks + 1)];
|
||||||
|
(outputs as f64) / ((blocks * BLOCK_TIME) as f64)
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut used = HashSet::<u64>::new();
|
||||||
|
for o in &outputs {
|
||||||
|
used.insert(o.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Create a TX with less than the target amount, as allowed by the protocol
|
||||||
|
if (high - MATURITY) < u64::try_from(inputs.len() * ring_len).unwrap() {
|
||||||
|
Err(RpcError::InternalError("not enough decoy candidates"))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select all decoys for this transaction, assuming we generate a sane transaction
|
||||||
|
// We should almost never naturally generate an insane transaction, hence why this doesn't
|
||||||
|
// bother with an overage
|
||||||
|
let mut decoys = select_n(
|
||||||
|
rng,
|
||||||
|
rpc,
|
||||||
|
&distribution,
|
||||||
|
height,
|
||||||
|
high,
|
||||||
|
per_second,
|
||||||
|
&real,
|
||||||
|
&mut used,
|
||||||
|
inputs.len() * decoy_count,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
real.zeroize();
|
||||||
|
|
||||||
|
let mut res = Vec::with_capacity(inputs.len());
|
||||||
|
for o in outputs {
|
||||||
|
// Grab the decoys for this specific output
|
||||||
|
let mut ring = decoys.drain((decoys.len() - decoy_count) ..).collect::<Vec<_>>();
|
||||||
|
ring.push(o);
|
||||||
|
ring.sort_by(|a, b| a.0.cmp(&b.0));
|
||||||
|
|
||||||
|
// Sanity checks are only run when 1000 outputs are available in Monero
|
||||||
|
// We run this check whenever the highest output index, which we acknowledge, is > 500
|
||||||
|
// This means we assume (for presumably test blockchains) the height being used has not had
|
||||||
|
// 500 outputs since while itself not being a sufficiently mature blockchain
|
||||||
|
// Considering Monero's p2p layer doesn't actually check transaction sanity, it should be
|
||||||
|
// fine for us to not have perfectly matching rules, especially since this code will infinite
|
||||||
|
// loop if it can't determine sanity, which is possible with sufficient inputs on
|
||||||
|
// sufficiently small chains
|
||||||
|
if high > 500 {
|
||||||
|
// Make sure the TX passes the sanity check that the median output is within the last 40%
|
||||||
|
let target_median = high * 3 / 5;
|
||||||
|
while ring[ring_len / 2].0 < target_median {
|
||||||
|
// If it's not, update the bottom half with new values to ensure the median only moves up
|
||||||
|
#[allow(clippy::needless_collect)] // Needed for ownership reasons
|
||||||
|
for removed in ring.drain(0 .. (ring_len / 2)).collect::<Vec<_>>() {
|
||||||
|
// If we removed the real spend, add it back
|
||||||
|
if removed.0 == o.0 {
|
||||||
|
ring.push(o);
|
||||||
|
} else {
|
||||||
|
// We could not remove this, saving CPU time and removing low values as
|
||||||
|
// possibilities, yet it'd increase the amount of decoys required to create this
|
||||||
|
// transaction and some removed outputs may be the best option (as we drop the first
|
||||||
|
// half, not just the bottom n)
|
||||||
|
used.remove(&removed.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select new outputs until we have a full sized ring again
|
||||||
|
ring.extend(
|
||||||
|
select_n(
|
||||||
|
rng,
|
||||||
|
rpc,
|
||||||
|
&distribution,
|
||||||
|
height,
|
||||||
|
high,
|
||||||
|
per_second,
|
||||||
|
&[],
|
||||||
|
&mut used,
|
||||||
|
ring_len - ring.len(),
|
||||||
|
)
|
||||||
|
.await?,
|
||||||
|
);
|
||||||
|
ring.sort_by(|a, b| a.0.cmp(&b.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
// The other sanity check rule is about duplicates, yet we already enforce unique ring
|
||||||
|
// members
|
||||||
|
}
|
||||||
|
|
||||||
|
res.push(Self {
|
||||||
|
// Binary searches for the real spend since we don't know where it sorted to
|
||||||
|
i: u8::try_from(ring.partition_point(|x| x.0 < o.0)).unwrap(),
|
||||||
|
offsets: offset(&ring.iter().map(|output| output.0).collect::<Vec<_>>()),
|
||||||
|
ring: ring.iter().map(|output| output.1).collect(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user