mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-12 14:09:25 +00:00
Compare commits
6 Commits
74a434f2bf
...
firo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d2e5d9184d | ||
|
|
9b3985e120 | ||
|
|
c3cc8d51b7 | ||
|
|
e3ff4f7af6 | ||
|
|
a770e29b0c | ||
|
|
6d9221d56c |
5
.gitattributes
vendored
5
.gitattributes
vendored
@@ -1,5 +0,0 @@
|
|||||||
# Auto detect text files and perform LF normalization
|
|
||||||
* text=auto
|
|
||||||
* text eol=lf
|
|
||||||
|
|
||||||
*.pdf binary
|
|
||||||
21
.github/actions/LICENSE
vendored
21
.github/actions/LICENSE
vendored
@@ -1,21 +0,0 @@
|
|||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2022-2023 Luke Parker
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
40
.github/actions/bitcoin/action.yml
vendored
40
.github/actions/bitcoin/action.yml
vendored
@@ -1,40 +0,0 @@
|
|||||||
name: bitcoin-regtest
|
|
||||||
description: Spawns a regtest Bitcoin daemon
|
|
||||||
|
|
||||||
inputs:
|
|
||||||
version:
|
|
||||||
description: "Version to download and run"
|
|
||||||
required: false
|
|
||||||
default: "27.0"
|
|
||||||
|
|
||||||
runs:
|
|
||||||
using: "composite"
|
|
||||||
steps:
|
|
||||||
- name: Bitcoin Daemon Cache
|
|
||||||
id: cache-bitcoind
|
|
||||||
uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2
|
|
||||||
with:
|
|
||||||
path: bitcoin.tar.gz
|
|
||||||
key: bitcoind-${{ runner.os }}-${{ runner.arch }}-${{ inputs.version }}
|
|
||||||
|
|
||||||
- name: Download the Bitcoin Daemon
|
|
||||||
if: steps.cache-bitcoind.outputs.cache-hit != 'true'
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
RUNNER_OS=linux
|
|
||||||
RUNNER_ARCH=x86_64
|
|
||||||
FILE=bitcoin-${{ inputs.version }}-$RUNNER_ARCH-$RUNNER_OS-gnu.tar.gz
|
|
||||||
|
|
||||||
wget https://bitcoincore.org/bin/bitcoin-core-${{ inputs.version }}/$FILE
|
|
||||||
mv $FILE bitcoin.tar.gz
|
|
||||||
|
|
||||||
- name: Extract the Bitcoin Daemon
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
tar xzvf bitcoin.tar.gz
|
|
||||||
cd bitcoin-${{ inputs.version }}
|
|
||||||
sudo mv bin/* /bin && sudo mv lib/* /lib
|
|
||||||
|
|
||||||
- name: Bitcoin Regtest Daemon
|
|
||||||
shell: bash
|
|
||||||
run: PATH=$PATH:/usr/bin ./orchestration/dev/coins/bitcoin/run.sh -daemon
|
|
||||||
49
.github/actions/build-dependencies/action.yml
vendored
49
.github/actions/build-dependencies/action.yml
vendored
@@ -1,49 +0,0 @@
|
|||||||
name: build-dependencies
|
|
||||||
description: Installs build dependencies for Serai
|
|
||||||
|
|
||||||
runs:
|
|
||||||
using: "composite"
|
|
||||||
steps:
|
|
||||||
- name: Remove unused packages
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
sudo apt remove -y "*msbuild*" "*powershell*" "*nuget*" "*bazel*" "*ansible*" "*terraform*" "*heroku*" "*aws*" azure-cli
|
|
||||||
sudo apt remove -y "*nodejs*" "*npm*" "*yarn*" "*java*" "*kotlin*" "*golang*" "*swift*" "*julia*" "*fortran*" "*android*"
|
|
||||||
sudo apt remove -y "*apache2*" "*nginx*" "*firefox*" "*chromium*" "*chrome*" "*edge*"
|
|
||||||
sudo apt remove -y "*qemu*" "*sql*" "*texinfo*" "*imagemagick*"
|
|
||||||
sudo apt autoremove -y
|
|
||||||
sudo apt clean
|
|
||||||
docker system prune -a --volumes
|
|
||||||
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
|
|
||||||
elif [ "$RUNNER_OS" == "Windows" ]; then
|
|
||||||
choco install protoc
|
|
||||||
elif [ "$RUNNER_OS" == "macOS" ]; then
|
|
||||||
brew install protobuf
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Install solc
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
cargo install svm-rs
|
|
||||||
svm install 0.8.25
|
|
||||||
svm use 0.8.25
|
|
||||||
|
|
||||||
# - name: Cache Rust
|
|
||||||
# uses: Swatinem/rust-cache@a95ba195448af2da9b00fb742d14ffaaf3c21f43
|
|
||||||
49
.github/actions/monero-wallet-rpc/action.yml
vendored
49
.github/actions/monero-wallet-rpc/action.yml
vendored
@@ -1,49 +0,0 @@
|
|||||||
name: monero-wallet-rpc
|
|
||||||
description: Spawns a Monero Wallet-RPC.
|
|
||||||
|
|
||||||
inputs:
|
|
||||||
version:
|
|
||||||
description: "Version to download and run"
|
|
||||||
required: false
|
|
||||||
default: v0.18.3.1
|
|
||||||
|
|
||||||
runs:
|
|
||||||
using: "composite"
|
|
||||||
steps:
|
|
||||||
- name: Monero Wallet RPC Cache
|
|
||||||
id: cache-monero-wallet-rpc
|
|
||||||
uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2
|
|
||||||
with:
|
|
||||||
path: monero-wallet-rpc
|
|
||||||
key: monero-wallet-rpc-${{ runner.os }}-${{ runner.arch }}-${{ inputs.version }}
|
|
||||||
|
|
||||||
- name: Download the Monero Wallet RPC
|
|
||||||
if: steps.cache-monero-wallet-rpc.outputs.cache-hit != 'true'
|
|
||||||
# Calculates OS/ARCH to demonstrate it, yet then locks to linux-x64 due
|
|
||||||
# to the contained folder not following the same naming scheme and
|
|
||||||
# requiring further expansion not worth doing right now
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
RUNNER_OS=${{ runner.os }}
|
|
||||||
RUNNER_ARCH=${{ runner.arch }}
|
|
||||||
|
|
||||||
RUNNER_OS=${RUNNER_OS,,}
|
|
||||||
RUNNER_ARCH=${RUNNER_ARCH,,}
|
|
||||||
|
|
||||||
RUNNER_OS=linux
|
|
||||||
RUNNER_ARCH=x64
|
|
||||||
|
|
||||||
FILE=monero-$RUNNER_OS-$RUNNER_ARCH-${{ inputs.version }}.tar.bz2
|
|
||||||
wget https://downloads.getmonero.org/cli/$FILE
|
|
||||||
tar -xvf $FILE
|
|
||||||
|
|
||||||
mv monero-x86_64-linux-gnu-${{ inputs.version }}/monero-wallet-rpc monero-wallet-rpc
|
|
||||||
|
|
||||||
- name: Monero Wallet RPC
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
./monero-wallet-rpc --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
|
|
||||||
46
.github/actions/monero/action.yml
vendored
46
.github/actions/monero/action.yml
vendored
@@ -1,46 +0,0 @@
|
|||||||
name: monero-regtest
|
|
||||||
description: Spawns a regtest Monero daemon
|
|
||||||
|
|
||||||
inputs:
|
|
||||||
version:
|
|
||||||
description: "Version to download and run"
|
|
||||||
required: false
|
|
||||||
default: v0.18.3.1
|
|
||||||
|
|
||||||
runs:
|
|
||||||
using: "composite"
|
|
||||||
steps:
|
|
||||||
- name: Monero Daemon Cache
|
|
||||||
id: cache-monerod
|
|
||||||
uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2
|
|
||||||
with:
|
|
||||||
path: /usr/bin/monerod
|
|
||||||
key: monerod-${{ runner.os }}-${{ runner.arch }}-${{ inputs.version }}
|
|
||||||
|
|
||||||
- name: Download the Monero Daemon
|
|
||||||
if: steps.cache-monerod.outputs.cache-hit != 'true'
|
|
||||||
# Calculates OS/ARCH to demonstrate it, yet then locks to linux-x64 due
|
|
||||||
# to the contained folder not following the same naming scheme and
|
|
||||||
# requiring further expansion not worth doing right now
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
RUNNER_OS=${{ runner.os }}
|
|
||||||
RUNNER_ARCH=${{ runner.arch }}
|
|
||||||
|
|
||||||
RUNNER_OS=${RUNNER_OS,,}
|
|
||||||
RUNNER_ARCH=${RUNNER_ARCH,,}
|
|
||||||
|
|
||||||
RUNNER_OS=linux
|
|
||||||
RUNNER_ARCH=x64
|
|
||||||
|
|
||||||
FILE=monero-$RUNNER_OS-$RUNNER_ARCH-${{ inputs.version }}.tar.bz2
|
|
||||||
wget https://downloads.getmonero.org/cli/$FILE
|
|
||||||
tar -xvf $FILE
|
|
||||||
|
|
||||||
sudo mv monero-x86_64-linux-gnu-${{ inputs.version }}/monerod /usr/bin/monerod
|
|
||||||
sudo chmod 777 /usr/bin/monerod
|
|
||||||
sudo chmod +x /usr/bin/monerod
|
|
||||||
|
|
||||||
- name: Monero Regtest Daemon
|
|
||||||
shell: bash
|
|
||||||
run: PATH=$PATH:/usr/bin ./orchestration/dev/coins/monero/run.sh --detach
|
|
||||||
38
.github/actions/test-dependencies/action.yml
vendored
38
.github/actions/test-dependencies/action.yml
vendored
@@ -1,38 +0,0 @@
|
|||||||
name: test-dependencies
|
|
||||||
description: Installs test dependencies for Serai
|
|
||||||
|
|
||||||
inputs:
|
|
||||||
monero-version:
|
|
||||||
description: "Monero version to download and run as a regtest node"
|
|
||||||
required: false
|
|
||||||
default: v0.18.3.1
|
|
||||||
|
|
||||||
bitcoin-version:
|
|
||||||
description: "Bitcoin version to download and run as a regtest node"
|
|
||||||
required: false
|
|
||||||
default: "27.0"
|
|
||||||
|
|
||||||
runs:
|
|
||||||
using: "composite"
|
|
||||||
steps:
|
|
||||||
- name: Install Build Dependencies
|
|
||||||
uses: ./.github/actions/build-dependencies
|
|
||||||
|
|
||||||
- name: Install Foundry
|
|
||||||
uses: foundry-rs/foundry-toolchain@8f1998e9878d786675189ef566a2e4bf24869773
|
|
||||||
with:
|
|
||||||
version: nightly-f625d0fa7c51e65b4bf1e8f7931cd1c6e2e285e9
|
|
||||||
cache: false
|
|
||||||
|
|
||||||
- name: Run a Monero Regtest Node
|
|
||||||
uses: ./.github/actions/monero
|
|
||||||
with:
|
|
||||||
version: ${{ inputs.monero-version }}
|
|
||||||
|
|
||||||
- name: Run a Bitcoin Regtest Node
|
|
||||||
uses: ./.github/actions/bitcoin
|
|
||||||
with:
|
|
||||||
version: ${{ inputs.bitcoin-version }}
|
|
||||||
|
|
||||||
- name: Run a Monero Wallet-RPC
|
|
||||||
uses: ./.github/actions/monero-wallet-rpc
|
|
||||||
1
.github/nightly-version
vendored
1
.github/nightly-version
vendored
@@ -1 +0,0 @@
|
|||||||
nightly-2024-06-01
|
|
||||||
36
.github/workflows/coins-tests.yml
vendored
36
.github/workflows/coins-tests.yml
vendored
@@ -1,36 +0,0 @@
|
|||||||
name: coins/ Tests
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- develop
|
|
||||||
paths:
|
|
||||||
- "common/**"
|
|
||||||
- "crypto/**"
|
|
||||||
- "coins/**"
|
|
||||||
|
|
||||||
pull_request:
|
|
||||||
paths:
|
|
||||||
- "common/**"
|
|
||||||
- "crypto/**"
|
|
||||||
- "coins/**"
|
|
||||||
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
test-coins:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac
|
|
||||||
|
|
||||||
- name: Test Dependencies
|
|
||||||
uses: ./.github/actions/test-dependencies
|
|
||||||
|
|
||||||
- name: Run Tests
|
|
||||||
run: |
|
|
||||||
GITHUB_CI=true RUST_BACKTRACE=1 cargo test --all-features \
|
|
||||||
-p bitcoin-serai \
|
|
||||||
-p alloy-simple-request-transport \
|
|
||||||
-p ethereum-serai \
|
|
||||||
-p monero-generators \
|
|
||||||
-p monero-serai
|
|
||||||
32
.github/workflows/common-tests.yml
vendored
32
.github/workflows/common-tests.yml
vendored
@@ -1,32 +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 serai-db \
|
|
||||||
-p serai-env \
|
|
||||||
-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/**"
|
|
||||||
- "coins/**"
|
|
||||||
- "message-queue/**"
|
|
||||||
- "coordinator/**"
|
|
||||||
- "orchestration/**"
|
|
||||||
- "tests/docker/**"
|
|
||||||
- "tests/coordinator/**"
|
|
||||||
|
|
||||||
pull_request:
|
|
||||||
paths:
|
|
||||||
- "common/**"
|
|
||||||
- "crypto/**"
|
|
||||||
- "coins/**"
|
|
||||||
- "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: cd tests/coordinator && GITHUB_CI=true RUST_BACKTRACE=1 cargo test --all-features
|
|
||||||
40
.github/workflows/crypto-tests.yml
vendored
40
.github/workflows/crypto-tests.yml
vendored
@@ -1,40 +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 multiexp \
|
|
||||||
-p schnorr-signatures \
|
|
||||||
-p dleq \
|
|
||||||
-p dkg \
|
|
||||||
-p modular-frost \
|
|
||||||
-p frost-schnorrkel
|
|
||||||
24
.github/workflows/daily-deny.yml
vendored
24
.github/workflows/daily-deny.yml
vendored
@@ -1,24 +0,0 @@
|
|||||||
name: Daily Deny Check
|
|
||||||
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
- cron: "0 0 * * *"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
deny:
|
|
||||||
name: Run cargo deny
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac
|
|
||||||
|
|
||||||
- name: Advisory Cache
|
|
||||||
uses: actions/cache@13aacd865c20de90d75de3b17ebe84f7a17d57d2
|
|
||||||
with:
|
|
||||||
path: ~/.cargo/advisory-db
|
|
||||||
key: rust-advisory-db
|
|
||||||
|
|
||||||
- name: Install cargo deny
|
|
||||||
run: cargo install --locked cargo-deny
|
|
||||||
|
|
||||||
- name: Run cargo deny
|
|
||||||
run: cargo deny -L error --all-features check
|
|
||||||
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: cd tests/full-stack && GITHUB_CI=true RUST_BACKTRACE=1 cargo test --all-features
|
|
||||||
83
.github/workflows/lint.yml
vendored
83
.github/workflows/lint.yml
vendored
@@ -1,83 +0,0 @@
|
|||||||
name: Lint
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- develop
|
|
||||||
pull_request:
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
clippy:
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
os: [ubuntu-latest, macos-13, macos-14, 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 wasm32-unknown-unknown -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@13aacd865c20de90d75de3b17ebe84f7a17d57d2
|
|
||||||
with:
|
|
||||||
path: ~/.cargo/advisory-db
|
|
||||||
key: rust-advisory-db
|
|
||||||
|
|
||||||
- name: Install cargo deny
|
|
||||||
run: cargo install --locked cargo-deny
|
|
||||||
|
|
||||||
- name: Run cargo deny
|
|
||||||
run: cargo deny -L error --all-features check
|
|
||||||
|
|
||||||
fmt:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac
|
|
||||||
|
|
||||||
- name: Get nightly version to use
|
|
||||||
id: nightly
|
|
||||||
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
|
|
||||||
|
|
||||||
machete:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac
|
|
||||||
- name: Verify all dependencies are in use
|
|
||||||
run: |
|
|
||||||
cargo install cargo-machete
|
|
||||||
cargo machete
|
|
||||||
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: cd tests/message-queue && GITHUB_CI=true RUST_BACKTRACE=1 cargo test --all-features
|
|
||||||
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
|
|
||||||
56
.github/workflows/monero-tests.yaml
vendored
56
.github/workflows/monero-tests.yaml
vendored
@@ -1,56 +0,0 @@
|
|||||||
name: Monero Tests
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- develop
|
|
||||||
paths:
|
|
||||||
- "coins/monero/**"
|
|
||||||
- "processor/**"
|
|
||||||
|
|
||||||
pull_request:
|
|
||||||
paths:
|
|
||||||
- "coins/monero/**"
|
|
||||||
- "processor/**"
|
|
||||||
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
# Only run these once since they will be consistent regardless of any node
|
|
||||||
unit-tests:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac
|
|
||||||
|
|
||||||
- name: Test Dependencies
|
|
||||||
uses: ./.github/actions/test-dependencies
|
|
||||||
|
|
||||||
- name: Run Unit Tests Without Features
|
|
||||||
run: GITHUB_CI=true RUST_BACKTRACE=1 cargo test --package monero-serai --lib
|
|
||||||
|
|
||||||
# Doesn't run unit tests with features as the tests workflow will
|
|
||||||
|
|
||||||
integration-tests:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
# Test against all supported protocol versions
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
version: [v0.17.3.2, v0.18.2.0]
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac
|
|
||||||
|
|
||||||
- name: Test Dependencies
|
|
||||||
uses: ./.github/actions/test-dependencies
|
|
||||||
with:
|
|
||||||
monero-version: ${{ matrix.version }}
|
|
||||||
|
|
||||||
- name: Run Integration Tests Without Features
|
|
||||||
# Runs with the binaries feature so the binaries build
|
|
||||||
# https://github.com/rust-lang/cargo/issues/8396
|
|
||||||
run: GITHUB_CI=true RUST_BACKTRACE=1 cargo test --package monero-serai --features binaries --test '*'
|
|
||||||
|
|
||||||
- name: Run Integration Tests
|
|
||||||
# Don't run if the the tests workflow also will
|
|
||||||
if: ${{ matrix.version != 'v0.18.2.0' }}
|
|
||||||
run: GITHUB_CI=true RUST_BACKTRACE=1 cargo test --package monero-serai --all-features --test '*'
|
|
||||||
53
.github/workflows/monthly-nightly-update.yml
vendored
53
.github/workflows/monthly-nightly-update.yml
vendored
@@ -1,53 +0,0 @@
|
|||||||
name: Monthly Nightly Update
|
|
||||||
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
- cron: "0 0 1 * *"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
update:
|
|
||||||
name: Update nightly
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac
|
|
||||||
with:
|
|
||||||
submodules: "recursive"
|
|
||||||
|
|
||||||
- name: Write nightly version
|
|
||||||
run: echo $(date +"nightly-%Y-%m"-01) > .github/nightly-version
|
|
||||||
|
|
||||||
- name: Create the commit
|
|
||||||
run: |
|
|
||||||
git config user.name "GitHub Actions"
|
|
||||||
git config user.email "<>"
|
|
||||||
|
|
||||||
git checkout -b $(date +"nightly-%Y-%m")
|
|
||||||
|
|
||||||
git add .github/nightly-version
|
|
||||||
git commit -m "Update nightly"
|
|
||||||
git push -u origin $(date +"nightly-%Y-%m")
|
|
||||||
|
|
||||||
- name: Pull Request
|
|
||||||
uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410
|
|
||||||
with:
|
|
||||||
script: |
|
|
||||||
const { repo, owner } = context.repo;
|
|
||||||
|
|
||||||
const result = await github.rest.pulls.create({
|
|
||||||
title: (new Date()).toLocaleString(
|
|
||||||
false,
|
|
||||||
{ month: "long", year: "numeric" }
|
|
||||||
) + " - Rust Nightly Update",
|
|
||||||
owner,
|
|
||||||
repo,
|
|
||||||
head: "nightly-" + (new Date()).toISOString().split("-").splice(0, 2).join("-"),
|
|
||||||
base: "develop",
|
|
||||||
body: "PR auto-generated by a GitHub workflow."
|
|
||||||
});
|
|
||||||
|
|
||||||
github.rest.issues.addLabels({
|
|
||||||
owner,
|
|
||||||
repo,
|
|
||||||
issue_number: result.data.number,
|
|
||||||
labels: ["improvement"]
|
|
||||||
});
|
|
||||||
35
.github/workflows/no-std.yml
vendored
35
.github/workflows/no-std.yml
vendored
@@ -1,35 +0,0 @@
|
|||||||
name: no-std build
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- develop
|
|
||||||
paths:
|
|
||||||
- "common/**"
|
|
||||||
- "crypto/**"
|
|
||||||
- "coins/**"
|
|
||||||
- "tests/no-std/**"
|
|
||||||
|
|
||||||
pull_request:
|
|
||||||
paths:
|
|
||||||
- "common/**"
|
|
||||||
- "crypto/**"
|
|
||||||
- "coins/**"
|
|
||||||
- "tests/no-std/**"
|
|
||||||
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac
|
|
||||||
|
|
||||||
- name: Install Build Dependencies
|
|
||||||
uses: ./.github/actions/build-dependencies
|
|
||||||
|
|
||||||
- name: Install RISC-V Toolchain
|
|
||||||
run: sudo apt update && sudo apt install -y gcc-riscv64-unknown-elf gcc-multilib && rustup target add riscv32imac-unknown-none-elf
|
|
||||||
|
|
||||||
- name: Verify no-std builds
|
|
||||||
run: cd tests/no-std && CFLAGS=-I/usr/include cargo build --target riscv32imac-unknown-none-elf
|
|
||||||
90
.github/workflows/pages.yml
vendored
90
.github/workflows/pages.yml
vendored
@@ -1,90 +0,0 @@
|
|||||||
# MIT License
|
|
||||||
#
|
|
||||||
# Copyright (c) 2022 just-the-docs
|
|
||||||
#
|
|
||||||
# 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.
|
|
||||||
|
|
||||||
# This workflow uses actions that are not certified by GitHub.
|
|
||||||
# They are provided by a third-party and are governed by
|
|
||||||
# separate terms of service, privacy policy, and support
|
|
||||||
# documentation.
|
|
||||||
|
|
||||||
# Sample workflow for building and deploying a Jekyll site to GitHub Pages
|
|
||||||
name: Deploy Jekyll site to Pages
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- "develop"
|
|
||||||
paths:
|
|
||||||
- "docs/**"
|
|
||||||
|
|
||||||
# Allows you to run this workflow manually from the Actions tab
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
pages: write
|
|
||||||
id-token: write
|
|
||||||
|
|
||||||
# Allow one concurrent deployment
|
|
||||||
concurrency:
|
|
||||||
group: "pages"
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
# Build job
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
working-directory: docs
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
- name: Setup Ruby
|
|
||||||
uses: ruby/setup-ruby@v1
|
|
||||||
with:
|
|
||||||
bundler-cache: true
|
|
||||||
cache-version: 0
|
|
||||||
working-directory: "${{ github.workspace }}/docs"
|
|
||||||
- name: Setup Pages
|
|
||||||
id: pages
|
|
||||||
uses: actions/configure-pages@v3
|
|
||||||
- name: Build with Jekyll
|
|
||||||
run: bundle exec jekyll build --baseurl "${{ steps.pages.outputs.base_path }}"
|
|
||||||
env:
|
|
||||||
JEKYLL_ENV: production
|
|
||||||
- name: Upload artifact
|
|
||||||
uses: actions/upload-pages-artifact@v1
|
|
||||||
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@v2
|
|
||||||
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/**"
|
|
||||||
- "coins/**"
|
|
||||||
- "message-queue/**"
|
|
||||||
- "processor/**"
|
|
||||||
- "orchestration/**"
|
|
||||||
- "tests/docker/**"
|
|
||||||
- "tests/processor/**"
|
|
||||||
|
|
||||||
pull_request:
|
|
||||||
paths:
|
|
||||||
- "common/**"
|
|
||||||
- "crypto/**"
|
|
||||||
- "coins/**"
|
|
||||||
- "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: cd tests/processor && GITHUB_CI=true RUST_BACKTRACE=1 cargo test --all-features
|
|
||||||
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: cd tests/reproducible-runtime && GITHUB_CI=true RUST_BACKTRACE=1 cargo test --all-features
|
|
||||||
83
.github/workflows/tests.yml
vendored
83
.github/workflows/tests.yml
vendored
@@ -1,83 +0,0 @@
|
|||||||
name: Tests
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- develop
|
|
||||||
paths:
|
|
||||||
- "common/**"
|
|
||||||
- "crypto/**"
|
|
||||||
- "coins/**"
|
|
||||||
- "message-queue/**"
|
|
||||||
- "processor/**"
|
|
||||||
- "coordinator/**"
|
|
||||||
- "substrate/**"
|
|
||||||
|
|
||||||
pull_request:
|
|
||||||
paths:
|
|
||||||
- "common/**"
|
|
||||||
- "crypto/**"
|
|
||||||
- "coins/**"
|
|
||||||
- "message-queue/**"
|
|
||||||
- "processor/**"
|
|
||||||
- "coordinator/**"
|
|
||||||
- "substrate/**"
|
|
||||||
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
test-infra:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac
|
|
||||||
|
|
||||||
- name: Build Dependencies
|
|
||||||
uses: ./.github/actions/build-dependencies
|
|
||||||
|
|
||||||
- name: Run Tests
|
|
||||||
run: |
|
|
||||||
GITHUB_CI=true RUST_BACKTRACE=1 cargo test --all-features \
|
|
||||||
-p serai-message-queue \
|
|
||||||
-p serai-processor-messages \
|
|
||||||
-p serai-processor \
|
|
||||||
-p tendermint-machine \
|
|
||||||
-p tributary-chain \
|
|
||||||
-p serai-coordinator \
|
|
||||||
-p serai-orchestrator \
|
|
||||||
-p serai-docker-tests
|
|
||||||
|
|
||||||
test-substrate:
|
|
||||||
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 serai-primitives \
|
|
||||||
-p serai-coins-primitives \
|
|
||||||
-p serai-coins-pallet \
|
|
||||||
-p serai-dex-pallet \
|
|
||||||
-p serai-validator-sets-primitives \
|
|
||||||
-p serai-validator-sets-pallet \
|
|
||||||
-p serai-in-instructions-primitives \
|
|
||||||
-p serai-in-instructions-pallet \
|
|
||||||
-p serai-signals-primitives \
|
|
||||||
-p serai-signals-pallet \
|
|
||||||
-p serai-abi \
|
|
||||||
-p serai-runtime \
|
|
||||||
-p serai-node
|
|
||||||
|
|
||||||
test-serai-client:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac
|
|
||||||
|
|
||||||
- name: Build Dependencies
|
|
||||||
uses: ./.github/actions/build-dependencies
|
|
||||||
|
|
||||||
- name: Run Tests
|
|
||||||
run: GITHUB_CI=true RUST_BACKTRACE=1 cargo test --all-features -p serai-client
|
|
||||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -1,7 +1,2 @@
|
|||||||
target
|
target
|
||||||
Dockerfile
|
Cargo.lock
|
||||||
Dockerfile.fast-epoch
|
|
||||||
!orchestration/runtime/Dockerfile
|
|
||||||
.test-logs
|
|
||||||
|
|
||||||
.vscode
|
|
||||||
|
|||||||
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[submodule "coins/monero/c/monero"]
|
||||||
|
path = coins/monero/c/monero
|
||||||
|
url = https://github.com/monero-project/monero
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
edition = "2021"
|
|
||||||
tab_spaces = 2
|
|
||||||
|
|
||||||
max_width = 100
|
|
||||||
# Let the developer decide based on the 100 char line limit
|
|
||||||
use_small_heuristics = "Max"
|
|
||||||
|
|
||||||
error_on_line_overflow = true
|
|
||||||
error_on_unformatted = true
|
|
||||||
|
|
||||||
imports_granularity = "Crate"
|
|
||||||
reorder_imports = false
|
|
||||||
reorder_modules = false
|
|
||||||
|
|
||||||
unstable_features = true
|
|
||||||
spaces_around_ranges = true
|
|
||||||
binop_separator = "Back"
|
|
||||||
661
AGPL-3.0
661
AGPL-3.0
@@ -1,661 +0,0 @@
|
|||||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
|
||||||
Version 3, 19 November 2007
|
|
||||||
|
|
||||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
|
||||||
Everyone is permitted to copy and distribute verbatim copies
|
|
||||||
of this license document, but changing it is not allowed.
|
|
||||||
|
|
||||||
Preamble
|
|
||||||
|
|
||||||
The GNU Affero General Public License is a free, copyleft license for
|
|
||||||
software and other kinds of works, specifically designed to ensure
|
|
||||||
cooperation with the community in the case of network server software.
|
|
||||||
|
|
||||||
The licenses for most software and other practical works are designed
|
|
||||||
to take away your freedom to share and change the works. By contrast,
|
|
||||||
our General Public Licenses are intended to guarantee your freedom to
|
|
||||||
share and change all versions of a program--to make sure it remains free
|
|
||||||
software for all its users.
|
|
||||||
|
|
||||||
When we speak of free software, we are referring to freedom, not
|
|
||||||
price. Our General Public Licenses are designed to make sure that you
|
|
||||||
have the freedom to distribute copies of free software (and charge for
|
|
||||||
them if you wish), that you receive source code or can get it if you
|
|
||||||
want it, that you can change the software or use pieces of it in new
|
|
||||||
free programs, and that you know you can do these things.
|
|
||||||
|
|
||||||
Developers that use our General Public Licenses protect your rights
|
|
||||||
with two steps: (1) assert copyright on the software, and (2) offer
|
|
||||||
you this License which gives you legal permission to copy, distribute
|
|
||||||
and/or modify the software.
|
|
||||||
|
|
||||||
A secondary benefit of defending all users' freedom is that
|
|
||||||
improvements made in alternate versions of the program, if they
|
|
||||||
receive widespread use, become available for other developers to
|
|
||||||
incorporate. Many developers of free software are heartened and
|
|
||||||
encouraged by the resulting cooperation. However, in the case of
|
|
||||||
software used on network servers, this result may fail to come about.
|
|
||||||
The GNU General Public License permits making a modified version and
|
|
||||||
letting the public access it on a server without ever releasing its
|
|
||||||
source code to the public.
|
|
||||||
|
|
||||||
The GNU Affero General Public License is designed specifically to
|
|
||||||
ensure that, in such cases, the modified source code becomes available
|
|
||||||
to the community. It requires the operator of a network server to
|
|
||||||
provide the source code of the modified version running there to the
|
|
||||||
users of that server. Therefore, public use of a modified version, on
|
|
||||||
a publicly accessible server, gives the public access to the source
|
|
||||||
code of the modified version.
|
|
||||||
|
|
||||||
An older license, called the Affero General Public License and
|
|
||||||
published by Affero, was designed to accomplish similar goals. This is
|
|
||||||
a different license, not a version of the Affero GPL, but Affero has
|
|
||||||
released a new version of the Affero GPL which permits relicensing under
|
|
||||||
this license.
|
|
||||||
|
|
||||||
The precise terms and conditions for copying, distribution and
|
|
||||||
modification follow.
|
|
||||||
|
|
||||||
TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
0. Definitions.
|
|
||||||
|
|
||||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
|
||||||
|
|
||||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
|
||||||
works, such as semiconductor masks.
|
|
||||||
|
|
||||||
"The Program" refers to any copyrightable work licensed under this
|
|
||||||
License. Each licensee is addressed as "you". "Licensees" and
|
|
||||||
"recipients" may be individuals or organizations.
|
|
||||||
|
|
||||||
To "modify" a work means to copy from or adapt all or part of the work
|
|
||||||
in a fashion requiring copyright permission, other than the making of an
|
|
||||||
exact copy. The resulting work is called a "modified version" of the
|
|
||||||
earlier work or a work "based on" the earlier work.
|
|
||||||
|
|
||||||
A "covered work" means either the unmodified Program or a work based
|
|
||||||
on the Program.
|
|
||||||
|
|
||||||
To "propagate" a work means to do anything with it that, without
|
|
||||||
permission, would make you directly or secondarily liable for
|
|
||||||
infringement under applicable copyright law, except executing it on a
|
|
||||||
computer or modifying a private copy. Propagation includes copying,
|
|
||||||
distribution (with or without modification), making available to the
|
|
||||||
public, and in some countries other activities as well.
|
|
||||||
|
|
||||||
To "convey" a work means any kind of propagation that enables other
|
|
||||||
parties to make or receive copies. Mere interaction with a user through
|
|
||||||
a computer network, with no transfer of a copy, is not conveying.
|
|
||||||
|
|
||||||
An interactive user interface displays "Appropriate Legal Notices"
|
|
||||||
to the extent that it includes a convenient and prominently visible
|
|
||||||
feature that (1) displays an appropriate copyright notice, and (2)
|
|
||||||
tells the user that there is no warranty for the work (except to the
|
|
||||||
extent that warranties are provided), that licensees may convey the
|
|
||||||
work under this License, and how to view a copy of this License. If
|
|
||||||
the interface presents a list of user commands or options, such as a
|
|
||||||
menu, a prominent item in the list meets this criterion.
|
|
||||||
|
|
||||||
1. Source Code.
|
|
||||||
|
|
||||||
The "source code" for a work means the preferred form of the work
|
|
||||||
for making modifications to it. "Object code" means any non-source
|
|
||||||
form of a work.
|
|
||||||
|
|
||||||
A "Standard Interface" means an interface that either is an official
|
|
||||||
standard defined by a recognized standards body, or, in the case of
|
|
||||||
interfaces specified for a particular programming language, one that
|
|
||||||
is widely used among developers working in that language.
|
|
||||||
|
|
||||||
The "System Libraries" of an executable work include anything, other
|
|
||||||
than the work as a whole, that (a) is included in the normal form of
|
|
||||||
packaging a Major Component, but which is not part of that Major
|
|
||||||
Component, and (b) serves only to enable use of the work with that
|
|
||||||
Major Component, or to implement a Standard Interface for which an
|
|
||||||
implementation is available to the public in source code form. A
|
|
||||||
"Major Component", in this context, means a major essential component
|
|
||||||
(kernel, window system, and so on) of the specific operating system
|
|
||||||
(if any) on which the executable work runs, or a compiler used to
|
|
||||||
produce the work, or an object code interpreter used to run it.
|
|
||||||
|
|
||||||
The "Corresponding Source" for a work in object code form means all
|
|
||||||
the source code needed to generate, install, and (for an executable
|
|
||||||
work) run the object code and to modify the work, including scripts to
|
|
||||||
control those activities. However, it does not include the work's
|
|
||||||
System Libraries, or general-purpose tools or generally available free
|
|
||||||
programs which are used unmodified in performing those activities but
|
|
||||||
which are not part of the work. For example, Corresponding Source
|
|
||||||
includes interface definition files associated with source files for
|
|
||||||
the work, and the source code for shared libraries and dynamically
|
|
||||||
linked subprograms that the work is specifically designed to require,
|
|
||||||
such as by intimate data communication or control flow between those
|
|
||||||
subprograms and other parts of the work.
|
|
||||||
|
|
||||||
The Corresponding Source need not include anything that users
|
|
||||||
can regenerate automatically from other parts of the Corresponding
|
|
||||||
Source.
|
|
||||||
|
|
||||||
The Corresponding Source for a work in source code form is that
|
|
||||||
same work.
|
|
||||||
|
|
||||||
2. Basic Permissions.
|
|
||||||
|
|
||||||
All rights granted under this License are granted for the term of
|
|
||||||
copyright on the Program, and are irrevocable provided the stated
|
|
||||||
conditions are met. This License explicitly affirms your unlimited
|
|
||||||
permission to run the unmodified Program. The output from running a
|
|
||||||
covered work is covered by this License only if the output, given its
|
|
||||||
content, constitutes a covered work. This License acknowledges your
|
|
||||||
rights of fair use or other equivalent, as provided by copyright law.
|
|
||||||
|
|
||||||
You may make, run and propagate covered works that you do not
|
|
||||||
convey, without conditions so long as your license otherwise remains
|
|
||||||
in force. You may convey covered works to others for the sole purpose
|
|
||||||
of having them make modifications exclusively for you, or provide you
|
|
||||||
with facilities for running those works, provided that you comply with
|
|
||||||
the terms of this License in conveying all material for which you do
|
|
||||||
not control copyright. Those thus making or running the covered works
|
|
||||||
for you must do so exclusively on your behalf, under your direction
|
|
||||||
and control, on terms that prohibit them from making any copies of
|
|
||||||
your copyrighted material outside their relationship with you.
|
|
||||||
|
|
||||||
Conveying under any other circumstances is permitted solely under
|
|
||||||
the conditions stated below. Sublicensing is not allowed; section 10
|
|
||||||
makes it unnecessary.
|
|
||||||
|
|
||||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
|
||||||
|
|
||||||
No covered work shall be deemed part of an effective technological
|
|
||||||
measure under any applicable law fulfilling obligations under article
|
|
||||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
|
||||||
similar laws prohibiting or restricting circumvention of such
|
|
||||||
measures.
|
|
||||||
|
|
||||||
When you convey a covered work, you waive any legal power to forbid
|
|
||||||
circumvention of technological measures to the extent such circumvention
|
|
||||||
is effected by exercising rights under this License with respect to
|
|
||||||
the covered work, and you disclaim any intention to limit operation or
|
|
||||||
modification of the work as a means of enforcing, against the work's
|
|
||||||
users, your or third parties' legal rights to forbid circumvention of
|
|
||||||
technological measures.
|
|
||||||
|
|
||||||
4. Conveying Verbatim Copies.
|
|
||||||
|
|
||||||
You may convey verbatim copies of the Program's source code as you
|
|
||||||
receive it, in any medium, provided that you conspicuously and
|
|
||||||
appropriately publish on each copy an appropriate copyright notice;
|
|
||||||
keep intact all notices stating that this License and any
|
|
||||||
non-permissive terms added in accord with section 7 apply to the code;
|
|
||||||
keep intact all notices of the absence of any warranty; and give all
|
|
||||||
recipients a copy of this License along with the Program.
|
|
||||||
|
|
||||||
You may charge any price or no price for each copy that you convey,
|
|
||||||
and you may offer support or warranty protection for a fee.
|
|
||||||
|
|
||||||
5. Conveying Modified Source Versions.
|
|
||||||
|
|
||||||
You may convey a work based on the Program, or the modifications to
|
|
||||||
produce it from the Program, in the form of source code under the
|
|
||||||
terms of section 4, provided that you also meet all of these conditions:
|
|
||||||
|
|
||||||
a) The work must carry prominent notices stating that you modified
|
|
||||||
it, and giving a relevant date.
|
|
||||||
|
|
||||||
b) The work must carry prominent notices stating that it is
|
|
||||||
released under this License and any conditions added under section
|
|
||||||
7. This requirement modifies the requirement in section 4 to
|
|
||||||
"keep intact all notices".
|
|
||||||
|
|
||||||
c) You must license the entire work, as a whole, under this
|
|
||||||
License to anyone who comes into possession of a copy. This
|
|
||||||
License will therefore apply, along with any applicable section 7
|
|
||||||
additional terms, to the whole of the work, and all its parts,
|
|
||||||
regardless of how they are packaged. This License gives no
|
|
||||||
permission to license the work in any other way, but it does not
|
|
||||||
invalidate such permission if you have separately received it.
|
|
||||||
|
|
||||||
d) If the work has interactive user interfaces, each must display
|
|
||||||
Appropriate Legal Notices; however, if the Program has interactive
|
|
||||||
interfaces that do not display Appropriate Legal Notices, your
|
|
||||||
work need not make them do so.
|
|
||||||
|
|
||||||
A compilation of a covered work with other separate and independent
|
|
||||||
works, which are not by their nature extensions of the covered work,
|
|
||||||
and which are not combined with it such as to form a larger program,
|
|
||||||
in or on a volume of a storage or distribution medium, is called an
|
|
||||||
"aggregate" if the compilation and its resulting copyright are not
|
|
||||||
used to limit the access or legal rights of the compilation's users
|
|
||||||
beyond what the individual works permit. Inclusion of a covered work
|
|
||||||
in an aggregate does not cause this License to apply to the other
|
|
||||||
parts of the aggregate.
|
|
||||||
|
|
||||||
6. Conveying Non-Source Forms.
|
|
||||||
|
|
||||||
You may convey a covered work in object code form under the terms
|
|
||||||
of sections 4 and 5, provided that you also convey the
|
|
||||||
machine-readable Corresponding Source under the terms of this License,
|
|
||||||
in one of these ways:
|
|
||||||
|
|
||||||
a) Convey the object code in, or embodied in, a physical product
|
|
||||||
(including a physical distribution medium), accompanied by the
|
|
||||||
Corresponding Source fixed on a durable physical medium
|
|
||||||
customarily used for software interchange.
|
|
||||||
|
|
||||||
b) Convey the object code in, or embodied in, a physical product
|
|
||||||
(including a physical distribution medium), accompanied by a
|
|
||||||
written offer, valid for at least three years and valid for as
|
|
||||||
long as you offer spare parts or customer support for that product
|
|
||||||
model, to give anyone who possesses the object code either (1) a
|
|
||||||
copy of the Corresponding Source for all the software in the
|
|
||||||
product that is covered by this License, on a durable physical
|
|
||||||
medium customarily used for software interchange, for a price no
|
|
||||||
more than your reasonable cost of physically performing this
|
|
||||||
conveying of source, or (2) access to copy the
|
|
||||||
Corresponding Source from a network server at no charge.
|
|
||||||
|
|
||||||
c) Convey individual copies of the object code with a copy of the
|
|
||||||
written offer to provide the Corresponding Source. This
|
|
||||||
alternative is allowed only occasionally and noncommercially, and
|
|
||||||
only if you received the object code with such an offer, in accord
|
|
||||||
with subsection 6b.
|
|
||||||
|
|
||||||
d) Convey the object code by offering access from a designated
|
|
||||||
place (gratis or for a charge), and offer equivalent access to the
|
|
||||||
Corresponding Source in the same way through the same place at no
|
|
||||||
further charge. You need not require recipients to copy the
|
|
||||||
Corresponding Source along with the object code. If the place to
|
|
||||||
copy the object code is a network server, the Corresponding Source
|
|
||||||
may be on a different server (operated by you or a third party)
|
|
||||||
that supports equivalent copying facilities, provided you maintain
|
|
||||||
clear directions next to the object code saying where to find the
|
|
||||||
Corresponding Source. Regardless of what server hosts the
|
|
||||||
Corresponding Source, you remain obligated to ensure that it is
|
|
||||||
available for as long as needed to satisfy these requirements.
|
|
||||||
|
|
||||||
e) Convey the object code using peer-to-peer transmission, provided
|
|
||||||
you inform other peers where the object code and Corresponding
|
|
||||||
Source of the work are being offered to the general public at no
|
|
||||||
charge under subsection 6d.
|
|
||||||
|
|
||||||
A separable portion of the object code, whose source code is excluded
|
|
||||||
from the Corresponding Source as a System Library, need not be
|
|
||||||
included in conveying the object code work.
|
|
||||||
|
|
||||||
A "User Product" is either (1) a "consumer product", which means any
|
|
||||||
tangible personal property which is normally used for personal, family,
|
|
||||||
or household purposes, or (2) anything designed or sold for incorporation
|
|
||||||
into a dwelling. In determining whether a product is a consumer product,
|
|
||||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
|
||||||
product received by a particular user, "normally used" refers to a
|
|
||||||
typical or common use of that class of product, regardless of the status
|
|
||||||
of the particular user or of the way in which the particular user
|
|
||||||
actually uses, or expects or is expected to use, the product. A product
|
|
||||||
is a consumer product regardless of whether the product has substantial
|
|
||||||
commercial, industrial or non-consumer uses, unless such uses represent
|
|
||||||
the only significant mode of use of the product.
|
|
||||||
|
|
||||||
"Installation Information" for a User Product means any methods,
|
|
||||||
procedures, authorization keys, or other information required to install
|
|
||||||
and execute modified versions of a covered work in that User Product from
|
|
||||||
a modified version of its Corresponding Source. The information must
|
|
||||||
suffice to ensure that the continued functioning of the modified object
|
|
||||||
code is in no case prevented or interfered with solely because
|
|
||||||
modification has been made.
|
|
||||||
|
|
||||||
If you convey an object code work under this section in, or with, or
|
|
||||||
specifically for use in, a User Product, and the conveying occurs as
|
|
||||||
part of a transaction in which the right of possession and use of the
|
|
||||||
User Product is transferred to the recipient in perpetuity or for a
|
|
||||||
fixed term (regardless of how the transaction is characterized), the
|
|
||||||
Corresponding Source conveyed under this section must be accompanied
|
|
||||||
by the Installation Information. But this requirement does not apply
|
|
||||||
if neither you nor any third party retains the ability to install
|
|
||||||
modified object code on the User Product (for example, the work has
|
|
||||||
been installed in ROM).
|
|
||||||
|
|
||||||
The requirement to provide Installation Information does not include a
|
|
||||||
requirement to continue to provide support service, warranty, or updates
|
|
||||||
for a work that has been modified or installed by the recipient, or for
|
|
||||||
the User Product in which it has been modified or installed. Access to a
|
|
||||||
network may be denied when the modification itself materially and
|
|
||||||
adversely affects the operation of the network or violates the rules and
|
|
||||||
protocols for communication across the network.
|
|
||||||
|
|
||||||
Corresponding Source conveyed, and Installation Information provided,
|
|
||||||
in accord with this section must be in a format that is publicly
|
|
||||||
documented (and with an implementation available to the public in
|
|
||||||
source code form), and must require no special password or key for
|
|
||||||
unpacking, reading or copying.
|
|
||||||
|
|
||||||
7. Additional Terms.
|
|
||||||
|
|
||||||
"Additional permissions" are terms that supplement the terms of this
|
|
||||||
License by making exceptions from one or more of its conditions.
|
|
||||||
Additional permissions that are applicable to the entire Program shall
|
|
||||||
be treated as though they were included in this License, to the extent
|
|
||||||
that they are valid under applicable law. If additional permissions
|
|
||||||
apply only to part of the Program, that part may be used separately
|
|
||||||
under those permissions, but the entire Program remains governed by
|
|
||||||
this License without regard to the additional permissions.
|
|
||||||
|
|
||||||
When you convey a copy of a covered work, you may at your option
|
|
||||||
remove any additional permissions from that copy, or from any part of
|
|
||||||
it. (Additional permissions may be written to require their own
|
|
||||||
removal in certain cases when you modify the work.) You may place
|
|
||||||
additional permissions on material, added by you to a covered work,
|
|
||||||
for which you have or can give appropriate copyright permission.
|
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, for material you
|
|
||||||
add to a covered work, you may (if authorized by the copyright holders of
|
|
||||||
that material) supplement the terms of this License with terms:
|
|
||||||
|
|
||||||
a) Disclaiming warranty or limiting liability differently from the
|
|
||||||
terms of sections 15 and 16 of this License; or
|
|
||||||
|
|
||||||
b) Requiring preservation of specified reasonable legal notices or
|
|
||||||
author attributions in that material or in the Appropriate Legal
|
|
||||||
Notices displayed by works containing it; or
|
|
||||||
|
|
||||||
c) Prohibiting misrepresentation of the origin of that material, or
|
|
||||||
requiring that modified versions of such material be marked in
|
|
||||||
reasonable ways as different from the original version; or
|
|
||||||
|
|
||||||
d) Limiting the use for publicity purposes of names of licensors or
|
|
||||||
authors of the material; or
|
|
||||||
|
|
||||||
e) Declining to grant rights under trademark law for use of some
|
|
||||||
trade names, trademarks, or service marks; or
|
|
||||||
|
|
||||||
f) Requiring indemnification of licensors and authors of that
|
|
||||||
material by anyone who conveys the material (or modified versions of
|
|
||||||
it) with contractual assumptions of liability to the recipient, for
|
|
||||||
any liability that these contractual assumptions directly impose on
|
|
||||||
those licensors and authors.
|
|
||||||
|
|
||||||
All other non-permissive additional terms are considered "further
|
|
||||||
restrictions" within the meaning of section 10. If the Program as you
|
|
||||||
received it, or any part of it, contains a notice stating that it is
|
|
||||||
governed by this License along with a term that is a further
|
|
||||||
restriction, you may remove that term. If a license document contains
|
|
||||||
a further restriction but permits relicensing or conveying under this
|
|
||||||
License, you may add to a covered work material governed by the terms
|
|
||||||
of that license document, provided that the further restriction does
|
|
||||||
not survive such relicensing or conveying.
|
|
||||||
|
|
||||||
If you add terms to a covered work in accord with this section, you
|
|
||||||
must place, in the relevant source files, a statement of the
|
|
||||||
additional terms that apply to those files, or a notice indicating
|
|
||||||
where to find the applicable terms.
|
|
||||||
|
|
||||||
Additional terms, permissive or non-permissive, may be stated in the
|
|
||||||
form of a separately written license, or stated as exceptions;
|
|
||||||
the above requirements apply either way.
|
|
||||||
|
|
||||||
8. Termination.
|
|
||||||
|
|
||||||
You may not propagate or modify a covered work except as expressly
|
|
||||||
provided under this License. Any attempt otherwise to propagate or
|
|
||||||
modify it is void, and will automatically terminate your rights under
|
|
||||||
this License (including any patent licenses granted under the third
|
|
||||||
paragraph of section 11).
|
|
||||||
|
|
||||||
However, if you cease all violation of this License, then your
|
|
||||||
license from a particular copyright holder is reinstated (a)
|
|
||||||
provisionally, unless and until the copyright holder explicitly and
|
|
||||||
finally terminates your license, and (b) permanently, if the copyright
|
|
||||||
holder fails to notify you of the violation by some reasonable means
|
|
||||||
prior to 60 days after the cessation.
|
|
||||||
|
|
||||||
Moreover, your license from a particular copyright holder is
|
|
||||||
reinstated permanently if the copyright holder notifies you of the
|
|
||||||
violation by some reasonable means, this is the first time you have
|
|
||||||
received notice of violation of this License (for any work) from that
|
|
||||||
copyright holder, and you cure the violation prior to 30 days after
|
|
||||||
your receipt of the notice.
|
|
||||||
|
|
||||||
Termination of your rights under this section does not terminate the
|
|
||||||
licenses of parties who have received copies or rights from you under
|
|
||||||
this License. If your rights have been terminated and not permanently
|
|
||||||
reinstated, you do not qualify to receive new licenses for the same
|
|
||||||
material under section 10.
|
|
||||||
|
|
||||||
9. Acceptance Not Required for Having Copies.
|
|
||||||
|
|
||||||
You are not required to accept this License in order to receive or
|
|
||||||
run a copy of the Program. Ancillary propagation of a covered work
|
|
||||||
occurring solely as a consequence of using peer-to-peer transmission
|
|
||||||
to receive a copy likewise does not require acceptance. However,
|
|
||||||
nothing other than this License grants you permission to propagate or
|
|
||||||
modify any covered work. These actions infringe copyright if you do
|
|
||||||
not accept this License. Therefore, by modifying or propagating a
|
|
||||||
covered work, you indicate your acceptance of this License to do so.
|
|
||||||
|
|
||||||
10. Automatic Licensing of Downstream Recipients.
|
|
||||||
|
|
||||||
Each time you convey a covered work, the recipient automatically
|
|
||||||
receives a license from the original licensors, to run, modify and
|
|
||||||
propagate that work, subject to this License. You are not responsible
|
|
||||||
for enforcing compliance by third parties with this License.
|
|
||||||
|
|
||||||
An "entity transaction" is a transaction transferring control of an
|
|
||||||
organization, or substantially all assets of one, or subdividing an
|
|
||||||
organization, or merging organizations. If propagation of a covered
|
|
||||||
work results from an entity transaction, each party to that
|
|
||||||
transaction who receives a copy of the work also receives whatever
|
|
||||||
licenses to the work the party's predecessor in interest had or could
|
|
||||||
give under the previous paragraph, plus a right to possession of the
|
|
||||||
Corresponding Source of the work from the predecessor in interest, if
|
|
||||||
the predecessor has it or can get it with reasonable efforts.
|
|
||||||
|
|
||||||
You may not impose any further restrictions on the exercise of the
|
|
||||||
rights granted or affirmed under this License. For example, you may
|
|
||||||
not impose a license fee, royalty, or other charge for exercise of
|
|
||||||
rights granted under this License, and you may not initiate litigation
|
|
||||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
|
||||||
any patent claim is infringed by making, using, selling, offering for
|
|
||||||
sale, or importing the Program or any portion of it.
|
|
||||||
|
|
||||||
11. Patents.
|
|
||||||
|
|
||||||
A "contributor" is a copyright holder who authorizes use under this
|
|
||||||
License of the Program or a work on which the Program is based. The
|
|
||||||
work thus licensed is called the contributor's "contributor version".
|
|
||||||
|
|
||||||
A contributor's "essential patent claims" are all patent claims
|
|
||||||
owned or controlled by the contributor, whether already acquired or
|
|
||||||
hereafter acquired, that would be infringed by some manner, permitted
|
|
||||||
by this License, of making, using, or selling its contributor version,
|
|
||||||
but do not include claims that would be infringed only as a
|
|
||||||
consequence of further modification of the contributor version. For
|
|
||||||
purposes of this definition, "control" includes the right to grant
|
|
||||||
patent sublicenses in a manner consistent with the requirements of
|
|
||||||
this License.
|
|
||||||
|
|
||||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
|
||||||
patent license under the contributor's essential patent claims, to
|
|
||||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
|
||||||
propagate the contents of its contributor version.
|
|
||||||
|
|
||||||
In the following three paragraphs, a "patent license" is any express
|
|
||||||
agreement or commitment, however denominated, not to enforce a patent
|
|
||||||
(such as an express permission to practice a patent or covenant not to
|
|
||||||
sue for patent infringement). To "grant" such a patent license to a
|
|
||||||
party means to make such an agreement or commitment not to enforce a
|
|
||||||
patent against the party.
|
|
||||||
|
|
||||||
If you convey a covered work, knowingly relying on a patent license,
|
|
||||||
and the Corresponding Source of the work is not available for anyone
|
|
||||||
to copy, free of charge and under the terms of this License, through a
|
|
||||||
publicly available network server or other readily accessible means,
|
|
||||||
then you must either (1) cause the Corresponding Source to be so
|
|
||||||
available, or (2) arrange to deprive yourself of the benefit of the
|
|
||||||
patent license for this particular work, or (3) arrange, in a manner
|
|
||||||
consistent with the requirements of this License, to extend the patent
|
|
||||||
license to downstream recipients. "Knowingly relying" means you have
|
|
||||||
actual knowledge that, but for the patent license, your conveying the
|
|
||||||
covered work in a country, or your recipient's use of the covered work
|
|
||||||
in a country, would infringe one or more identifiable patents in that
|
|
||||||
country that you have reason to believe are valid.
|
|
||||||
|
|
||||||
If, pursuant to or in connection with a single transaction or
|
|
||||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
|
||||||
covered work, and grant a patent license to some of the parties
|
|
||||||
receiving the covered work authorizing them to use, propagate, modify
|
|
||||||
or convey a specific copy of the covered work, then the patent license
|
|
||||||
you grant is automatically extended to all recipients of the covered
|
|
||||||
work and works based on it.
|
|
||||||
|
|
||||||
A patent license is "discriminatory" if it does not include within
|
|
||||||
the scope of its coverage, prohibits the exercise of, or is
|
|
||||||
conditioned on the non-exercise of one or more of the rights that are
|
|
||||||
specifically granted under this License. You may not convey a covered
|
|
||||||
work if you are a party to an arrangement with a third party that is
|
|
||||||
in the business of distributing software, under which you make payment
|
|
||||||
to the third party based on the extent of your activity of conveying
|
|
||||||
the work, and under which the third party grants, to any of the
|
|
||||||
parties who would receive the covered work from you, a discriminatory
|
|
||||||
patent license (a) in connection with copies of the covered work
|
|
||||||
conveyed by you (or copies made from those copies), or (b) primarily
|
|
||||||
for and in connection with specific products or compilations that
|
|
||||||
contain the covered work, unless you entered into that arrangement,
|
|
||||||
or that patent license was granted, prior to 28 March 2007.
|
|
||||||
|
|
||||||
Nothing in this License shall be construed as excluding or limiting
|
|
||||||
any implied license or other defenses to infringement that may
|
|
||||||
otherwise be available to you under applicable patent law.
|
|
||||||
|
|
||||||
12. No Surrender of Others' Freedom.
|
|
||||||
|
|
||||||
If conditions are imposed on you (whether by court order, agreement or
|
|
||||||
otherwise) that contradict the conditions of this License, they do not
|
|
||||||
excuse you from the conditions of this License. If you cannot convey a
|
|
||||||
covered work so as to satisfy simultaneously your obligations under this
|
|
||||||
License and any other pertinent obligations, then as a consequence you may
|
|
||||||
not convey it at all. For example, if you agree to terms that obligate you
|
|
||||||
to collect a royalty for further conveying from those to whom you convey
|
|
||||||
the Program, the only way you could satisfy both those terms and this
|
|
||||||
License would be to refrain entirely from conveying the Program.
|
|
||||||
|
|
||||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, if you modify the
|
|
||||||
Program, your modified version must prominently offer all users
|
|
||||||
interacting with it remotely through a computer network (if your version
|
|
||||||
supports such interaction) an opportunity to receive the Corresponding
|
|
||||||
Source of your version by providing access to the Corresponding Source
|
|
||||||
from a network server at no charge, through some standard or customary
|
|
||||||
means of facilitating copying of software. This Corresponding Source
|
|
||||||
shall include the Corresponding Source for any work covered by version 3
|
|
||||||
of the GNU General Public License that is incorporated pursuant to the
|
|
||||||
following paragraph.
|
|
||||||
|
|
||||||
Notwithstanding any other provision of this License, you have
|
|
||||||
permission to link or combine any covered work with a work licensed
|
|
||||||
under version 3 of the GNU General Public License into a single
|
|
||||||
combined work, and to convey the resulting work. The terms of this
|
|
||||||
License will continue to apply to the part which is the covered work,
|
|
||||||
but the work with which it is combined will remain governed by version
|
|
||||||
3 of the GNU General Public License.
|
|
||||||
|
|
||||||
14. Revised Versions of this License.
|
|
||||||
|
|
||||||
The Free Software Foundation may publish revised and/or new versions of
|
|
||||||
the GNU Affero General Public License from time to time. Such new versions
|
|
||||||
will be similar in spirit to the present version, but may differ in detail to
|
|
||||||
address new problems or concerns.
|
|
||||||
|
|
||||||
Each version is given a distinguishing version number. If the
|
|
||||||
Program specifies that a certain numbered version of the GNU Affero General
|
|
||||||
Public License "or any later version" applies to it, you have the
|
|
||||||
option of following the terms and conditions either of that numbered
|
|
||||||
version or of any later version published by the Free Software
|
|
||||||
Foundation. If the Program does not specify a version number of the
|
|
||||||
GNU Affero General Public License, you may choose any version ever published
|
|
||||||
by the Free Software Foundation.
|
|
||||||
|
|
||||||
If the Program specifies that a proxy can decide which future
|
|
||||||
versions of the GNU Affero General Public License can be used, that proxy's
|
|
||||||
public statement of acceptance of a version permanently authorizes you
|
|
||||||
to choose that version for the Program.
|
|
||||||
|
|
||||||
Later license versions may give you additional or different
|
|
||||||
permissions. However, no additional obligations are imposed on any
|
|
||||||
author or copyright holder as a result of your choosing to follow a
|
|
||||||
later version.
|
|
||||||
|
|
||||||
15. Disclaimer of Warranty.
|
|
||||||
|
|
||||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
|
||||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
|
||||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
|
||||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
|
||||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
||||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
|
||||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
|
||||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
|
||||||
|
|
||||||
16. Limitation of Liability.
|
|
||||||
|
|
||||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
|
||||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
|
||||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
|
||||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
|
||||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
|
||||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
|
||||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
|
||||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
|
||||||
SUCH DAMAGES.
|
|
||||||
|
|
||||||
17. Interpretation of Sections 15 and 16.
|
|
||||||
|
|
||||||
If the disclaimer of warranty and limitation of liability provided
|
|
||||||
above cannot be given local legal effect according to their terms,
|
|
||||||
reviewing courts shall apply local law that most closely approximates
|
|
||||||
an absolute waiver of all civil liability in connection with the
|
|
||||||
Program, unless a warranty or assumption of liability accompanies a
|
|
||||||
copy of the Program in return for a fee.
|
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
How to Apply These Terms to Your New Programs
|
|
||||||
|
|
||||||
If you develop a new program, and you want it to be of the greatest
|
|
||||||
possible use to the public, the best way to achieve this is to make it
|
|
||||||
free software which everyone can redistribute and change under these terms.
|
|
||||||
|
|
||||||
To do so, attach the following notices to the program. It is safest
|
|
||||||
to attach them to the start of each source file to most effectively
|
|
||||||
state the exclusion of warranty; and each file should have at least
|
|
||||||
the "copyright" line and a pointer to where the full notice is found.
|
|
||||||
|
|
||||||
<one line to give the program's name and a brief idea of what it does.>
|
|
||||||
Copyright (C) <year> <name of author>
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU Affero General Public License as published by
|
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU Affero General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
Also add information on how to contact you by electronic and paper mail.
|
|
||||||
|
|
||||||
If your software can interact with users remotely through a computer
|
|
||||||
network, you should also make sure that it provides a way for users to
|
|
||||||
get its source. For example, if your program is a web application, its
|
|
||||||
interface could display a "Source" link that leads users to an archive
|
|
||||||
of the code. There are many ways you could offer source, and different
|
|
||||||
solutions will be better for different programs; see section 13 for the
|
|
||||||
specific requirements.
|
|
||||||
|
|
||||||
You should also get your employer (if you work as a programmer) or school,
|
|
||||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
|
||||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
|
||||||
<https://www.gnu.org/licenses/>.
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
# Contributing
|
|
||||||
|
|
||||||
Contributions come in a variety of forms. Developing Serai, helping document it,
|
|
||||||
using its libraries in another project, using and testing it, and simply sharing
|
|
||||||
it are all valuable ways of contributing.
|
|
||||||
|
|
||||||
This document will specifically focus on contributions to this repository in the
|
|
||||||
form of code and documentation.
|
|
||||||
|
|
||||||
### Rules
|
|
||||||
|
|
||||||
- Stable native Rust, nightly wasm and tools.
|
|
||||||
- `cargo fmt` must be used.
|
|
||||||
- `cargo clippy` must pass, except for the ignored rules (`type_complexity` and
|
|
||||||
`dead_code`).
|
|
||||||
- The CI must pass.
|
|
||||||
|
|
||||||
- Only use uppercase variable names when relevant to cryptography.
|
|
||||||
|
|
||||||
- Use a two-space ident when possible.
|
|
||||||
- Put a space after comment markers.
|
|
||||||
- Don't use multiple newlines between sections of code.
|
|
||||||
- Have a newline before EOF.
|
|
||||||
|
|
||||||
### Guidelines
|
|
||||||
|
|
||||||
- Sort inputs as core, std, third party, and then Serai.
|
|
||||||
- Comment code reasonably.
|
|
||||||
- Include tests for new features.
|
|
||||||
- Sign commits.
|
|
||||||
|
|
||||||
### Submission
|
|
||||||
|
|
||||||
All submissions should be through GitHub. Contributions to a crate will be
|
|
||||||
licensed according to the crate's existing license, with the crate's copyright
|
|
||||||
holders (distinct from authors) having the right to re-license the crate via a
|
|
||||||
unanimous decision.
|
|
||||||
11074
Cargo.lock
generated
11074
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
179
Cargo.toml
179
Cargo.toml
@@ -1,191 +1,16 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
resolver = "2"
|
|
||||||
members = [
|
members = [
|
||||||
# Version patches
|
|
||||||
"patches/parking_lot_core",
|
|
||||||
"patches/parking_lot",
|
|
||||||
"patches/zstd",
|
|
||||||
"patches/rocksdb",
|
|
||||||
"patches/proc-macro-crate",
|
|
||||||
|
|
||||||
# std patches
|
|
||||||
"patches/matches",
|
|
||||||
"patches/is-terminal",
|
|
||||||
|
|
||||||
# Rewrites/redirects
|
|
||||||
"patches/option-ext",
|
|
||||||
"patches/directories-next",
|
|
||||||
|
|
||||||
"common/std-shims",
|
|
||||||
"common/zalloc",
|
|
||||||
"common/db",
|
|
||||||
"common/env",
|
|
||||||
"common/request",
|
|
||||||
|
|
||||||
"crypto/transcript",
|
"crypto/transcript",
|
||||||
|
|
||||||
"crypto/ff-group-tests",
|
|
||||||
"crypto/dalek-ff-group",
|
"crypto/dalek-ff-group",
|
||||||
"crypto/ed448",
|
|
||||||
"crypto/ciphersuite",
|
|
||||||
|
|
||||||
"crypto/multiexp",
|
"crypto/multiexp",
|
||||||
|
|
||||||
"crypto/schnorr",
|
|
||||||
"crypto/dleq",
|
"crypto/dleq",
|
||||||
"crypto/dkg",
|
|
||||||
"crypto/frost",
|
"crypto/frost",
|
||||||
"crypto/schnorrkel",
|
|
||||||
|
|
||||||
"coins/bitcoin",
|
|
||||||
|
|
||||||
"coins/ethereum/alloy-simple-request-transport",
|
|
||||||
"coins/ethereum",
|
|
||||||
"coins/ethereum/relayer",
|
|
||||||
|
|
||||||
"coins/monero/generators",
|
|
||||||
"coins/monero",
|
"coins/monero",
|
||||||
|
"coins/firo",
|
||||||
|
|
||||||
"message-queue",
|
|
||||||
|
|
||||||
"processor/messages",
|
|
||||||
"processor",
|
"processor",
|
||||||
|
|
||||||
"coordinator/tributary/tendermint",
|
|
||||||
"coordinator/tributary",
|
|
||||||
"coordinator",
|
|
||||||
|
|
||||||
"substrate/primitives",
|
|
||||||
|
|
||||||
"substrate/coins/primitives",
|
|
||||||
"substrate/coins/pallet",
|
|
||||||
|
|
||||||
"substrate/in-instructions/primitives",
|
|
||||||
"substrate/in-instructions/pallet",
|
|
||||||
|
|
||||||
"substrate/validator-sets/primitives",
|
|
||||||
"substrate/validator-sets/pallet",
|
|
||||||
|
|
||||||
"substrate/signals/primitives",
|
|
||||||
"substrate/signals/pallet",
|
|
||||||
|
|
||||||
"substrate/abi",
|
|
||||||
|
|
||||||
"substrate/runtime",
|
|
||||||
"substrate/node",
|
|
||||||
|
|
||||||
"substrate/client",
|
|
||||||
|
|
||||||
"orchestration",
|
|
||||||
|
|
||||||
"mini",
|
|
||||||
|
|
||||||
"tests/no-std",
|
|
||||||
|
|
||||||
"tests/docker",
|
|
||||||
"tests/message-queue",
|
|
||||||
"tests/processor",
|
|
||||||
"tests/coordinator",
|
|
||||||
"tests/full-stack",
|
|
||||||
"tests/reproducible-runtime",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
# Always compile Monero (and a variety of dependencies) with optimizations due
|
|
||||||
# to the extensive operations required for Bulletproofs
|
|
||||||
[profile.dev.package]
|
|
||||||
subtle = { opt-level = 3 }
|
|
||||||
curve25519-dalek = { opt-level = 3 }
|
|
||||||
|
|
||||||
ff = { opt-level = 3 }
|
|
||||||
group = { opt-level = 3 }
|
|
||||||
|
|
||||||
crypto-bigint = { opt-level = 3 }
|
|
||||||
dalek-ff-group = { opt-level = 3 }
|
|
||||||
minimal-ed448 = { opt-level = 3 }
|
|
||||||
|
|
||||||
multiexp = { opt-level = 3 }
|
|
||||||
|
|
||||||
monero-serai = { opt-level = 3 }
|
|
||||||
|
|
||||||
[profile.release]
|
|
||||||
panic = "unwind"
|
|
||||||
|
|
||||||
[patch.crates-io]
|
|
||||||
# https://github.com/rust-lang-nursery/lazy-static.rs/issues/201
|
|
||||||
lazy_static = { git = "https://github.com/rust-lang-nursery/lazy-static.rs", rev = "5735630d46572f1e5377c8f2ba0f79d18f53b10c" }
|
|
||||||
|
|
||||||
# Needed due to dockertest's usage of `Rc`s when we need `Arc`s
|
|
||||||
dockertest = { git = "https://github.com/orcalabs/dockertest-rs", rev = "4dd6ae24738aa6dc5c89444cc822ea4745517493" }
|
|
||||||
|
|
||||||
parking_lot_core = { path = "patches/parking_lot_core" }
|
|
||||||
parking_lot = { path = "patches/parking_lot" }
|
|
||||||
# wasmtime pulls in an old version for this
|
|
||||||
zstd = { path = "patches/zstd" }
|
|
||||||
# Needed for WAL compression
|
|
||||||
rocksdb = { path = "patches/rocksdb" }
|
|
||||||
# proc-macro-crate 2 binds to an old version of toml for msrv so we patch to 3
|
|
||||||
proc-macro-crate = { path = "patches/proc-macro-crate" }
|
|
||||||
|
|
||||||
# is-terminal now has an std-based solution with an equivalent API
|
|
||||||
is-terminal = { path = "patches/is-terminal" }
|
|
||||||
# So does matches
|
|
||||||
matches = { path = "patches/matches" }
|
|
||||||
|
|
||||||
# directories-next was created because directories was unmaintained
|
|
||||||
# directories-next is now unmaintained while directories is maintained
|
|
||||||
# The directories author pulls in ridiculously pointless crates and prefers
|
|
||||||
# copyleft licenses
|
|
||||||
# The following two patches resolve everything
|
|
||||||
option-ext = { path = "patches/option-ext" }
|
|
||||||
directories-next = { path = "patches/directories-next" }
|
|
||||||
|
|
||||||
[workspace.lints.clippy]
|
|
||||||
unwrap_or_default = "allow"
|
|
||||||
borrow_as_ptr = "deny"
|
|
||||||
cast_lossless = "deny"
|
|
||||||
cast_possible_truncation = "deny"
|
|
||||||
cast_possible_wrap = "deny"
|
|
||||||
cast_precision_loss = "deny"
|
|
||||||
cast_ptr_alignment = "deny"
|
|
||||||
cast_sign_loss = "deny"
|
|
||||||
checked_conversions = "deny"
|
|
||||||
cloned_instead_of_copied = "deny"
|
|
||||||
enum_glob_use = "deny"
|
|
||||||
expl_impl_clone_on_copy = "deny"
|
|
||||||
explicit_into_iter_loop = "deny"
|
|
||||||
explicit_iter_loop = "deny"
|
|
||||||
flat_map_option = "deny"
|
|
||||||
float_cmp = "deny"
|
|
||||||
fn_params_excessive_bools = "deny"
|
|
||||||
ignored_unit_patterns = "deny"
|
|
||||||
implicit_clone = "deny"
|
|
||||||
inefficient_to_string = "deny"
|
|
||||||
invalid_upcast_comparisons = "deny"
|
|
||||||
large_stack_arrays = "deny"
|
|
||||||
linkedlist = "deny"
|
|
||||||
macro_use_imports = "deny"
|
|
||||||
manual_instant_elapsed = "deny"
|
|
||||||
manual_let_else = "deny"
|
|
||||||
manual_ok_or = "deny"
|
|
||||||
manual_string_new = "deny"
|
|
||||||
map_unwrap_or = "deny"
|
|
||||||
match_bool = "deny"
|
|
||||||
match_same_arms = "deny"
|
|
||||||
missing_fields_in_debug = "deny"
|
|
||||||
needless_continue = "deny"
|
|
||||||
needless_pass_by_value = "deny"
|
|
||||||
ptr_cast_constness = "deny"
|
|
||||||
range_minus_one = "deny"
|
|
||||||
range_plus_one = "deny"
|
|
||||||
redundant_closure_for_method_calls = "deny"
|
|
||||||
redundant_else = "deny"
|
|
||||||
string_add_assign = "deny"
|
|
||||||
unchecked_duration_subtraction = "deny"
|
|
||||||
uninlined_format_args = "deny"
|
|
||||||
unnecessary_box_returns = "deny"
|
|
||||||
unnecessary_join = "deny"
|
|
||||||
unnecessary_wraps = "deny"
|
|
||||||
unnested_or_patterns = "deny"
|
|
||||||
unused_async = "deny"
|
|
||||||
unused_self = "deny"
|
|
||||||
zero_sized_map_values = "deny"
|
|
||||||
|
|||||||
8
LICENSE
8
LICENSE
@@ -1,8 +0,0 @@
|
|||||||
Serai crates are licensed under one of two licenses, either MIT or AGPL-3.0,
|
|
||||||
depending on the crate in question. Each crate declares their license in their
|
|
||||||
`Cargo.toml` and includes a `LICENSE` file detailing its status. Additionally,
|
|
||||||
a full copy of the AGPL-3.0 License is included in the root of this repository
|
|
||||||
as a reference text. This copy should be provided with any distribution of a
|
|
||||||
crate licensed under the AGPL-3.0, as per its terms.
|
|
||||||
|
|
||||||
The GitHub actions (`.github/actions`) are licensed under the MIT license.
|
|
||||||
62
README.md
62
README.md
@@ -1,66 +1,22 @@
|
|||||||
# Serai
|
# Serai
|
||||||
|
|
||||||
Serai is a new DEX, built from the ground up, initially planning on listing
|
Serai is a new DEX, built from the ground up, initially planning on listing
|
||||||
Bitcoin, Ethereum, DAI, and Monero, offering a liquidity-pool-based trading
|
Bitcoin, Ethereum, Monero, DAI, and USDC, offering a liquidity pool trading
|
||||||
experience. Funds are stored in an economically secured threshold-multisig
|
experience. Funds are stored in an economically secured threshold multisig
|
||||||
wallet.
|
wallet.
|
||||||
|
|
||||||
[Getting Started](spec/Getting%20Started.md)
|
|
||||||
|
|
||||||
### Layout
|
### Layout
|
||||||
|
|
||||||
- `audits`: Audits for various parts of Serai.
|
- `docs` - Documentation on the Serai protocol.
|
||||||
|
|
||||||
- `spec`: The specification of the Serai protocol, both internally and as
|
- `coins` - Various coin libraries intended for usage in Serai yet also by the
|
||||||
networked.
|
|
||||||
|
|
||||||
- `docs`: User-facing documentation on the Serai protocol.
|
|
||||||
|
|
||||||
- `common`: Crates containing utilities common to a variety of areas under
|
|
||||||
Serai, none neatly fitting under another category.
|
|
||||||
|
|
||||||
- `crypto`: A series of composable cryptographic libraries built around the
|
|
||||||
`ff`/`group` APIs, achieving a variety of tasks. These range from generic
|
|
||||||
infrastructure, to our IETF-compliant FROST implementation, to a DLEq proof as
|
|
||||||
needed for Bitcoin-Monero atomic swaps.
|
|
||||||
|
|
||||||
- `coins`: Various coin libraries intended for usage in Serai yet also by the
|
|
||||||
wider community. This means they will always support the functionality Serai
|
wider community. This means they will always support the functionality Serai
|
||||||
needs, yet won't disadvantage other use cases when possible.
|
needs, yet won't disadvantage other use cases when possible.
|
||||||
|
|
||||||
- `message-queue`: An ordered message server so services can talk to each other,
|
- `crypto` - A series of composable cryptographic libraries built around the
|
||||||
even when the other is offline.
|
`ff`/`group` APIs achieving a variety of tasks. These range from generic
|
||||||
|
infrastructure, to our IETF-compliant FROST implementation, to a DLEq proof as
|
||||||
|
needed for Bitcoin-Monero atomic swaps.
|
||||||
|
|
||||||
- `processor`: A generic chain processor to process data for Serai and process
|
- `processor` - A generic chain processor to process data for Serai and process
|
||||||
events from Serai, executing transactions as expected and needed.
|
events from Serai, executing transactions as expected and needed.
|
||||||
|
|
||||||
- `coordinator`: A service to manage processors and communicate over a P2P
|
|
||||||
network with other validators.
|
|
||||||
|
|
||||||
- `substrate`: Substrate crates used to instantiate the Serai network.
|
|
||||||
|
|
||||||
- `orchestration`: Dockerfiles and scripts to deploy a Serai node/test
|
|
||||||
environment.
|
|
||||||
|
|
||||||
- `tests`: Tests for various crates. Generally, `crate/src/tests` is used, or
|
|
||||||
`crate/tests`, yet any tests requiring crates' binaries are placed here.
|
|
||||||
|
|
||||||
### Security
|
|
||||||
|
|
||||||
Serai hosts a bug bounty program via
|
|
||||||
[Immunefi](https://immunefi.com/bounty/serai/). For in-scope critical
|
|
||||||
vulnerabilities, we will reward whitehats with up to $30,000.
|
|
||||||
|
|
||||||
Anything not in-scope should still be submitted through Immunefi, with rewards
|
|
||||||
issued at the discretion of the Immunefi program managers.
|
|
||||||
|
|
||||||
### Links
|
|
||||||
|
|
||||||
- [Website](https://serai.exchange/): https://serai.exchange/
|
|
||||||
- [Immunefi](https://immunefi.com/bounty/serai/): https://immunefi.com/bounty/serai/
|
|
||||||
- [Twitter](https://twitter.com/SeraiDEX): https://twitter.com/SeraiDEX
|
|
||||||
- [Mastodon](https://cryptodon.lol/@serai): https://cryptodon.lol/@serai
|
|
||||||
- [Discord](https://discord.gg/mpEUtJR3vz): https://discord.gg/mpEUtJR3vz
|
|
||||||
- [Matrix](https://matrix.to/#/#serai:matrix.org): https://matrix.to/#/#serai:matrix.org
|
|
||||||
- [Reddit](https://www.reddit.com/r/SeraiDEX/): https://www.reddit.com/r/SeraiDEX/
|
|
||||||
- [Telegram](https://t.me/SeraiDEX): https://t.me/SeraiDEX
|
|
||||||
|
|||||||
Binary file not shown.
@@ -1,21 +0,0 @@
|
|||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2023 Cypher Stack
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
# Cypher Stack /coins/bitcoin Audit, August 2023
|
|
||||||
|
|
||||||
This audit was over the /coins/bitcoin folder. It is encompassing up to commit
|
|
||||||
5121ca75199dff7bd34230880a1fdd793012068c.
|
|
||||||
|
|
||||||
Please see https://github.com/cypherstack/serai-btc-audit for provenance.
|
|
||||||
Binary file not shown.
@@ -1,21 +0,0 @@
|
|||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2023 Cypher Stack
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
# Cypher Stack /crypto Audit, March 2023
|
|
||||||
|
|
||||||
This audit was over the /crypto folder, excluding the ed448 crate, the `Ed448`
|
|
||||||
ciphersuite in the ciphersuite crate, and the `dleq/experimental` feature. It is
|
|
||||||
encompassing up to commit 669d2dbffc1dafb82a09d9419ea182667115df06.
|
|
||||||
|
|
||||||
Please see https://github.com/cypherstack/serai-audit for provenance.
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "bitcoin-serai"
|
|
||||||
version = "0.3.0"
|
|
||||||
description = "A Bitcoin library for FROST-signing transactions"
|
|
||||||
license = "MIT"
|
|
||||||
repository = "https://github.com/serai-dex/serai/tree/develop/coins/bitcoin"
|
|
||||||
authors = ["Luke Parker <lukeparker5132@gmail.com>", "Vrx <vrx00@proton.me>"]
|
|
||||||
edition = "2021"
|
|
||||||
rust-version = "1.74"
|
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
|
||||||
all-features = true
|
|
||||||
rustdoc-args = ["--cfg", "docsrs"]
|
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
std-shims = { version = "0.1.1", path = "../../common/std-shims", default-features = false }
|
|
||||||
|
|
||||||
thiserror = { version = "1", default-features = false, optional = true }
|
|
||||||
|
|
||||||
zeroize = { version = "^1.5", default-features = false }
|
|
||||||
rand_core = { version = "0.6", default-features = false }
|
|
||||||
|
|
||||||
bitcoin = { version = "0.32", default-features = false }
|
|
||||||
|
|
||||||
k256 = { version = "^0.13.1", default-features = false, features = ["arithmetic", "bits"] }
|
|
||||||
|
|
||||||
transcript = { package = "flexible-transcript", path = "../../crypto/transcript", version = "0.3", default-features = false, features = ["recommended"], optional = true }
|
|
||||||
frost = { package = "modular-frost", path = "../../crypto/frost", version = "0.8", default-features = false, features = ["secp256k1"], optional = true }
|
|
||||||
|
|
||||||
hex = { version = "0.4", default-features = false, optional = true }
|
|
||||||
serde = { version = "1", default-features = false, features = ["derive"], optional = true }
|
|
||||||
serde_json = { version = "1", default-features = false, optional = true }
|
|
||||||
simple-request = { path = "../../common/request", version = "0.1", default-features = false, features = ["tls", "basic-auth"], optional = true }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
secp256k1 = { version = "0.29", default-features = false, features = ["std"] }
|
|
||||||
|
|
||||||
frost = { package = "modular-frost", path = "../../crypto/frost", features = ["tests"] }
|
|
||||||
|
|
||||||
tokio = { version = "1", features = ["macros"] }
|
|
||||||
|
|
||||||
[features]
|
|
||||||
std = [
|
|
||||||
"std-shims/std",
|
|
||||||
|
|
||||||
"thiserror",
|
|
||||||
|
|
||||||
"zeroize/std",
|
|
||||||
"rand_core/std",
|
|
||||||
|
|
||||||
"bitcoin/std",
|
|
||||||
"bitcoin/serde",
|
|
||||||
|
|
||||||
"k256/std",
|
|
||||||
|
|
||||||
"transcript/std",
|
|
||||||
"frost",
|
|
||||||
|
|
||||||
"hex/std",
|
|
||||||
"serde/std",
|
|
||||||
"serde_json/std",
|
|
||||||
"simple-request",
|
|
||||||
]
|
|
||||||
hazmat = []
|
|
||||||
default = ["std"]
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2022-2023 Luke Parker
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
# bitcoin-serai
|
|
||||||
|
|
||||||
An application of [modular-frost](https://docs.rs/modular-frost) to Bitcoin
|
|
||||||
transactions, enabling extremely-efficient multisigs.
|
|
||||||
@@ -1,166 +0,0 @@
|
|||||||
use k256::{
|
|
||||||
elliptic_curve::sec1::{Tag, ToEncodedPoint},
|
|
||||||
ProjectivePoint,
|
|
||||||
};
|
|
||||||
|
|
||||||
use bitcoin::key::XOnlyPublicKey;
|
|
||||||
|
|
||||||
/// Get the x coordinate of a non-infinity, even point. Panics on invalid input.
|
|
||||||
pub fn x(key: &ProjectivePoint) -> [u8; 32] {
|
|
||||||
let encoded = key.to_encoded_point(true);
|
|
||||||
assert_eq!(encoded.tag(), Tag::CompressedEvenY, "x coordinate of odd key");
|
|
||||||
(*encoded.x().expect("point at infinity")).into()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Convert a non-infinity even point to a XOnlyPublicKey. Panics on invalid input.
|
|
||||||
pub fn x_only(key: &ProjectivePoint) -> XOnlyPublicKey {
|
|
||||||
XOnlyPublicKey::from_slice(&x(key)).expect("x_only was passed a point which was infinity or odd")
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Make a point even by adding the generator until it is even.
|
|
||||||
///
|
|
||||||
/// Returns the even point and the amount of additions required.
|
|
||||||
#[cfg(any(feature = "std", feature = "hazmat"))]
|
|
||||||
pub fn make_even(mut key: ProjectivePoint) -> (ProjectivePoint, u64) {
|
|
||||||
let mut c = 0;
|
|
||||||
while key.to_encoded_point(true).tag() == Tag::CompressedOddY {
|
|
||||||
key += ProjectivePoint::GENERATOR;
|
|
||||||
c += 1;
|
|
||||||
}
|
|
||||||
(key, c)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "std")]
|
|
||||||
mod frost_crypto {
|
|
||||||
use core::fmt::Debug;
|
|
||||||
use std_shims::{vec::Vec, io};
|
|
||||||
|
|
||||||
use zeroize::Zeroizing;
|
|
||||||
use rand_core::{RngCore, CryptoRng};
|
|
||||||
|
|
||||||
use bitcoin::hashes::{HashEngine, Hash, sha256::Hash as Sha256};
|
|
||||||
|
|
||||||
use transcript::Transcript;
|
|
||||||
|
|
||||||
use k256::{elliptic_curve::ops::Reduce, U256, Scalar};
|
|
||||||
|
|
||||||
use frost::{
|
|
||||||
curve::{Ciphersuite, Secp256k1},
|
|
||||||
Participant, ThresholdKeys, ThresholdView, FrostError,
|
|
||||||
algorithm::{Hram as HramTrait, Algorithm, Schnorr as FrostSchnorr},
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
/// A BIP-340 compatible HRAm for use with the modular-frost Schnorr Algorithm.
|
|
||||||
///
|
|
||||||
/// If passed an odd nonce, it will have the generator added until it is even.
|
|
||||||
///
|
|
||||||
/// If the key is odd, this will panic.
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
|
||||||
pub struct Hram;
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
impl HramTrait<Secp256k1> for Hram {
|
|
||||||
fn hram(R: &ProjectivePoint, A: &ProjectivePoint, m: &[u8]) -> Scalar {
|
|
||||||
// Convert the nonce to be even
|
|
||||||
let (R, _) = make_even(*R);
|
|
||||||
|
|
||||||
const TAG_HASH: Sha256 = Sha256::const_hash(b"BIP0340/challenge");
|
|
||||||
|
|
||||||
let mut data = Sha256::engine();
|
|
||||||
data.input(TAG_HASH.as_ref());
|
|
||||||
data.input(TAG_HASH.as_ref());
|
|
||||||
data.input(&x(&R));
|
|
||||||
data.input(&x(A));
|
|
||||||
data.input(m);
|
|
||||||
|
|
||||||
Scalar::reduce(U256::from_be_slice(Sha256::from_engine(data).as_ref()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// BIP-340 Schnorr signature algorithm.
|
|
||||||
///
|
|
||||||
/// This must be used with a ThresholdKeys whose group key is even. If it is odd, this will panic.
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct Schnorr<T: Sync + Clone + Debug + Transcript>(FrostSchnorr<Secp256k1, T, Hram>);
|
|
||||||
impl<T: Sync + Clone + Debug + Transcript> Schnorr<T> {
|
|
||||||
/// Construct a Schnorr algorithm continuing the specified transcript.
|
|
||||||
pub fn new(transcript: T) -> Schnorr<T> {
|
|
||||||
Schnorr(FrostSchnorr::new(transcript))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: Sync + Clone + Debug + Transcript> Algorithm<Secp256k1> for Schnorr<T> {
|
|
||||||
type Transcript = T;
|
|
||||||
type Addendum = ();
|
|
||||||
type Signature = [u8; 64];
|
|
||||||
|
|
||||||
fn transcript(&mut self) -> &mut Self::Transcript {
|
|
||||||
self.0.transcript()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn nonces(&self) -> Vec<Vec<ProjectivePoint>> {
|
|
||||||
self.0.nonces()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn preprocess_addendum<R: RngCore + CryptoRng>(
|
|
||||||
&mut self,
|
|
||||||
rng: &mut R,
|
|
||||||
keys: &ThresholdKeys<Secp256k1>,
|
|
||||||
) {
|
|
||||||
self.0.preprocess_addendum(rng, keys)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_addendum<R: io::Read>(&self, reader: &mut R) -> io::Result<Self::Addendum> {
|
|
||||||
self.0.read_addendum(reader)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn process_addendum(
|
|
||||||
&mut self,
|
|
||||||
view: &ThresholdView<Secp256k1>,
|
|
||||||
i: Participant,
|
|
||||||
addendum: (),
|
|
||||||
) -> Result<(), FrostError> {
|
|
||||||
self.0.process_addendum(view, i, addendum)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn sign_share(
|
|
||||||
&mut self,
|
|
||||||
params: &ThresholdView<Secp256k1>,
|
|
||||||
nonce_sums: &[Vec<<Secp256k1 as Ciphersuite>::G>],
|
|
||||||
nonces: Vec<Zeroizing<<Secp256k1 as Ciphersuite>::F>>,
|
|
||||||
msg: &[u8],
|
|
||||||
) -> <Secp256k1 as Ciphersuite>::F {
|
|
||||||
self.0.sign_share(params, nonce_sums, nonces, msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
fn verify(
|
|
||||||
&self,
|
|
||||||
group_key: ProjectivePoint,
|
|
||||||
nonces: &[Vec<ProjectivePoint>],
|
|
||||||
sum: Scalar,
|
|
||||||
) -> Option<Self::Signature> {
|
|
||||||
self.0.verify(group_key, nonces, sum).map(|mut sig| {
|
|
||||||
// Make the R of the final signature even
|
|
||||||
let offset;
|
|
||||||
(sig.R, offset) = make_even(sig.R);
|
|
||||||
// s = r + cx. Since we added to the r, add to s
|
|
||||||
sig.s += Scalar::from(offset);
|
|
||||||
// Convert to a Bitcoin signature by dropping the byte for the point's sign bit
|
|
||||||
sig.serialize()[1 ..].try_into().unwrap()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn verify_share(
|
|
||||||
&self,
|
|
||||||
verification_share: ProjectivePoint,
|
|
||||||
nonces: &[Vec<ProjectivePoint>],
|
|
||||||
share: Scalar,
|
|
||||||
) -> Result<Vec<(Scalar, ProjectivePoint)>, ()> {
|
|
||||||
self.0.verify_share(verification_share, nonces, share)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[cfg(feature = "std")]
|
|
||||||
pub use frost_crypto::*;
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
|
||||||
#![doc = include_str!("../README.md")]
|
|
||||||
#![cfg_attr(not(feature = "std"), no_std)]
|
|
||||||
|
|
||||||
#[cfg(not(feature = "std"))]
|
|
||||||
extern crate alloc;
|
|
||||||
|
|
||||||
/// The bitcoin Rust library.
|
|
||||||
pub use bitcoin;
|
|
||||||
|
|
||||||
/// Cryptographic helpers.
|
|
||||||
#[cfg(feature = "hazmat")]
|
|
||||||
pub mod crypto;
|
|
||||||
#[cfg(not(feature = "hazmat"))]
|
|
||||||
pub(crate) mod crypto;
|
|
||||||
|
|
||||||
/// Wallet functionality to create transactions.
|
|
||||||
pub mod wallet;
|
|
||||||
/// A minimal asynchronous Bitcoin RPC client.
|
|
||||||
#[cfg(feature = "std")]
|
|
||||||
pub mod rpc;
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests;
|
|
||||||
@@ -1,226 +0,0 @@
|
|||||||
use core::fmt::Debug;
|
|
||||||
use std::collections::HashSet;
|
|
||||||
|
|
||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
use serde::{Deserialize, de::DeserializeOwned};
|
|
||||||
use serde_json::json;
|
|
||||||
|
|
||||||
use simple_request::{hyper, Request, Client};
|
|
||||||
|
|
||||||
use bitcoin::{
|
|
||||||
hashes::{Hash, hex::FromHex},
|
|
||||||
consensus::encode,
|
|
||||||
Txid, Transaction, BlockHash, Block,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug, Deserialize)]
|
|
||||||
pub struct Error {
|
|
||||||
code: isize,
|
|
||||||
message: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
|
||||||
#[serde(untagged)]
|
|
||||||
enum RpcResponse<T> {
|
|
||||||
Ok { result: T },
|
|
||||||
Err { error: Error },
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A minimal asynchronous Bitcoin RPC client.
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct Rpc {
|
|
||||||
client: Client,
|
|
||||||
url: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug, Error)]
|
|
||||||
pub enum RpcError {
|
|
||||||
#[error("couldn't connect to node")]
|
|
||||||
ConnectionError,
|
|
||||||
#[error("request had an error: {0:?}")]
|
|
||||||
RequestError(Error),
|
|
||||||
#[error("node replied with invalid JSON")]
|
|
||||||
InvalidJson(serde_json::error::Category),
|
|
||||||
#[error("node sent an invalid response ({0})")]
|
|
||||||
InvalidResponse(&'static str),
|
|
||||||
#[error("node was missing expected methods")]
|
|
||||||
MissingMethods(HashSet<&'static str>),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Rpc {
|
|
||||||
/// Create a new connection to a Bitcoin RPC.
|
|
||||||
///
|
|
||||||
/// An RPC call is performed to ensure the node is reachable (and that an invalid URL wasn't
|
|
||||||
/// provided).
|
|
||||||
///
|
|
||||||
/// Additionally, a set of expected methods is checked to be offered by the Bitcoin RPC. If these
|
|
||||||
/// methods aren't provided, an error with the missing methods is returned. This ensures all RPC
|
|
||||||
/// routes explicitly provided by this library are at least possible.
|
|
||||||
///
|
|
||||||
/// Each individual RPC route may still fail at time-of-call, regardless of the arguments
|
|
||||||
/// provided to this library, if the RPC has an incompatible argument layout. That is not checked
|
|
||||||
/// at time of RPC creation.
|
|
||||||
pub async fn new(url: String) -> Result<Rpc, RpcError> {
|
|
||||||
let rpc = Rpc { client: Client::with_connection_pool(), url };
|
|
||||||
|
|
||||||
// Make an RPC request to verify the node is reachable and sane
|
|
||||||
let res: String = rpc.rpc_call("help", json!([])).await?;
|
|
||||||
|
|
||||||
// Verify all methods we expect are present
|
|
||||||
// If we had a more expanded RPC, due to differences in RPC versions, it wouldn't make sense to
|
|
||||||
// error if all methods weren't present
|
|
||||||
// We only provide a very minimal set of methods which have been largely consistent, hence why
|
|
||||||
// this is sane
|
|
||||||
let mut expected_methods = HashSet::from([
|
|
||||||
"help",
|
|
||||||
"getblockcount",
|
|
||||||
"getblockhash",
|
|
||||||
"getblockheader",
|
|
||||||
"getblock",
|
|
||||||
"sendrawtransaction",
|
|
||||||
"getrawtransaction",
|
|
||||||
]);
|
|
||||||
for line in res.split('\n') {
|
|
||||||
// This doesn't check if the arguments are as expected
|
|
||||||
// This is due to Bitcoin supporting a large amount of optional arguments, which
|
|
||||||
// occasionally change, with their own mechanism of text documentation, making matching off
|
|
||||||
// it a quite involved task
|
|
||||||
// Instead, once we've confirmed the methods are present, we assume our arguments are aligned
|
|
||||||
// Else we'll error at time of call
|
|
||||||
if expected_methods.remove(line.split(' ').next().unwrap_or("")) &&
|
|
||||||
expected_methods.is_empty()
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !expected_methods.is_empty() {
|
|
||||||
Err(RpcError::MissingMethods(expected_methods))?;
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(rpc)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Perform an arbitrary RPC call.
|
|
||||||
pub async fn rpc_call<Response: DeserializeOwned + Debug>(
|
|
||||||
&self,
|
|
||||||
method: &str,
|
|
||||||
params: serde_json::Value,
|
|
||||||
) -> Result<Response, RpcError> {
|
|
||||||
let mut request = Request::from(
|
|
||||||
hyper::Request::post(&self.url)
|
|
||||||
.header("Content-Type", "application/json")
|
|
||||||
.body(
|
|
||||||
serde_json::to_vec(&json!({ "jsonrpc": "2.0", "method": method, "params": params }))
|
|
||||||
.unwrap()
|
|
||||||
.into(),
|
|
||||||
)
|
|
||||||
.unwrap(),
|
|
||||||
);
|
|
||||||
request.with_basic_auth();
|
|
||||||
let mut res = self
|
|
||||||
.client
|
|
||||||
.request(request)
|
|
||||||
.await
|
|
||||||
.map_err(|_| RpcError::ConnectionError)?
|
|
||||||
.body()
|
|
||||||
.await
|
|
||||||
.map_err(|_| RpcError::ConnectionError)?;
|
|
||||||
|
|
||||||
let res: RpcResponse<Response> =
|
|
||||||
serde_json::from_reader(&mut res).map_err(|e| RpcError::InvalidJson(e.classify()))?;
|
|
||||||
match res {
|
|
||||||
RpcResponse::Ok { result } => Ok(result),
|
|
||||||
RpcResponse::Err { error } => Err(RpcError::RequestError(error)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the latest block's number.
|
|
||||||
///
|
|
||||||
/// The genesis block's 'number' is zero. They increment from there.
|
|
||||||
pub async fn get_latest_block_number(&self) -> Result<usize, RpcError> {
|
|
||||||
// getblockcount doesn't return the amount of blocks on the current chain, yet the "height"
|
|
||||||
// of the current chain. The "height" of the current chain is defined as the "height" of the
|
|
||||||
// tip block of the current chain. The "height" of a block is defined as the amount of blocks
|
|
||||||
// present when the block was created. Accordingly, the genesis block has height 0, and
|
|
||||||
// getblockcount will return 0 when it's only the only block, despite their being one block.
|
|
||||||
self.rpc_call("getblockcount", json!([])).await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the hash of a block by the block's number.
|
|
||||||
pub async fn get_block_hash(&self, number: usize) -> Result<[u8; 32], RpcError> {
|
|
||||||
let mut hash = self
|
|
||||||
.rpc_call::<BlockHash>("getblockhash", json!([number]))
|
|
||||||
.await?
|
|
||||||
.as_raw_hash()
|
|
||||||
.to_byte_array();
|
|
||||||
// bitcoin stores the inner bytes in reverse order.
|
|
||||||
hash.reverse();
|
|
||||||
Ok(hash)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get a block's number by its hash.
|
|
||||||
pub async fn get_block_number(&self, hash: &[u8; 32]) -> Result<usize, RpcError> {
|
|
||||||
#[derive(Deserialize, Debug)]
|
|
||||||
struct Number {
|
|
||||||
height: usize,
|
|
||||||
}
|
|
||||||
Ok(self.rpc_call::<Number>("getblockheader", json!([hex::encode(hash)])).await?.height)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get a block by its hash.
|
|
||||||
pub async fn get_block(&self, hash: &[u8; 32]) -> Result<Block, RpcError> {
|
|
||||||
let hex = self.rpc_call::<String>("getblock", json!([hex::encode(hash), 0])).await?;
|
|
||||||
let bytes: Vec<u8> = FromHex::from_hex(&hex)
|
|
||||||
.map_err(|_| RpcError::InvalidResponse("node didn't use hex to encode the block"))?;
|
|
||||||
let block: Block = encode::deserialize(&bytes)
|
|
||||||
.map_err(|_| RpcError::InvalidResponse("node sent an improperly serialized block"))?;
|
|
||||||
|
|
||||||
let mut block_hash = *block.block_hash().as_raw_hash().as_byte_array();
|
|
||||||
block_hash.reverse();
|
|
||||||
if hash != &block_hash {
|
|
||||||
Err(RpcError::InvalidResponse("node replied with a different block"))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(block)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Publish a transaction.
|
|
||||||
pub async fn send_raw_transaction(&self, tx: &Transaction) -> Result<Txid, RpcError> {
|
|
||||||
let txid = match self.rpc_call("sendrawtransaction", json!([encode::serialize_hex(tx)])).await {
|
|
||||||
Ok(txid) => txid,
|
|
||||||
Err(e) => {
|
|
||||||
// A const from Bitcoin's bitcoin/src/rpc/protocol.h
|
|
||||||
const RPC_VERIFY_ALREADY_IN_CHAIN: isize = -27;
|
|
||||||
// If this was already successfully published, consider this having succeeded
|
|
||||||
if let RpcError::RequestError(Error { code, .. }) = e {
|
|
||||||
if code == RPC_VERIFY_ALREADY_IN_CHAIN {
|
|
||||||
return Ok(tx.compute_txid());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e)?
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if txid != tx.compute_txid() {
|
|
||||||
Err(RpcError::InvalidResponse("returned TX ID inequals calculated TX ID"))?;
|
|
||||||
}
|
|
||||||
Ok(txid)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get a transaction by its hash.
|
|
||||||
pub async fn get_transaction(&self, hash: &[u8; 32]) -> Result<Transaction, RpcError> {
|
|
||||||
let hex = self.rpc_call::<String>("getrawtransaction", json!([hex::encode(hash)])).await?;
|
|
||||||
let bytes: Vec<u8> = FromHex::from_hex(&hex)
|
|
||||||
.map_err(|_| RpcError::InvalidResponse("node didn't use hex to encode the transaction"))?;
|
|
||||||
let tx: Transaction = encode::deserialize(&bytes)
|
|
||||||
.map_err(|_| RpcError::InvalidResponse("node sent an improperly serialized transaction"))?;
|
|
||||||
|
|
||||||
let mut tx_hash = *tx.compute_txid().as_raw_hash().as_byte_array();
|
|
||||||
tx_hash.reverse();
|
|
||||||
if hash != &tx_hash {
|
|
||||||
Err(RpcError::InvalidResponse("node replied with a different transaction"))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(tx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
use rand_core::OsRng;
|
|
||||||
|
|
||||||
use secp256k1::{Secp256k1 as BContext, Message, schnorr::Signature};
|
|
||||||
|
|
||||||
use k256::Scalar;
|
|
||||||
use transcript::{Transcript, RecommendedTranscript};
|
|
||||||
use frost::{
|
|
||||||
curve::Secp256k1,
|
|
||||||
Participant,
|
|
||||||
tests::{algorithm_machines, key_gen, sign},
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
bitcoin::hashes::{Hash as HashTrait, sha256::Hash},
|
|
||||||
crypto::{x_only, make_even, Schnorr},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_algorithm() {
|
|
||||||
let mut keys = key_gen::<_, Secp256k1>(&mut OsRng);
|
|
||||||
const MESSAGE: &[u8] = b"Hello, World!";
|
|
||||||
|
|
||||||
for keys in keys.values_mut() {
|
|
||||||
let (_, offset) = make_even(keys.group_key());
|
|
||||||
*keys = keys.offset(Scalar::from(offset));
|
|
||||||
}
|
|
||||||
|
|
||||||
let algo =
|
|
||||||
Schnorr::<RecommendedTranscript>::new(RecommendedTranscript::new(b"bitcoin-serai sign test"));
|
|
||||||
let sig = sign(
|
|
||||||
&mut OsRng,
|
|
||||||
&algo,
|
|
||||||
keys.clone(),
|
|
||||||
algorithm_machines(&mut OsRng, &algo, &keys),
|
|
||||||
Hash::hash(MESSAGE).as_ref(),
|
|
||||||
);
|
|
||||||
|
|
||||||
BContext::new()
|
|
||||||
.verify_schnorr(
|
|
||||||
&Signature::from_slice(&sig)
|
|
||||||
.expect("couldn't convert produced signature to secp256k1::Signature"),
|
|
||||||
&Message::from_digest_slice(Hash::hash(MESSAGE).as_ref()).unwrap(),
|
|
||||||
&x_only(&keys[&Participant::new(1).unwrap()].group_key()),
|
|
||||||
)
|
|
||||||
.unwrap()
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
mod crypto;
|
|
||||||
@@ -1,193 +0,0 @@
|
|||||||
use std_shims::{
|
|
||||||
vec::Vec,
|
|
||||||
collections::HashMap,
|
|
||||||
io::{self, Write},
|
|
||||||
};
|
|
||||||
#[cfg(feature = "std")]
|
|
||||||
use std::io::{Read, BufReader};
|
|
||||||
|
|
||||||
use k256::{
|
|
||||||
elliptic_curve::sec1::{Tag, ToEncodedPoint},
|
|
||||||
Scalar, ProjectivePoint,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[cfg(feature = "std")]
|
|
||||||
use frost::{
|
|
||||||
curve::{Ciphersuite, Secp256k1},
|
|
||||||
ThresholdKeys,
|
|
||||||
};
|
|
||||||
|
|
||||||
use bitcoin::{
|
|
||||||
consensus::encode::serialize, key::TweakedPublicKey, OutPoint, ScriptBuf, TxOut, Transaction,
|
|
||||||
Block,
|
|
||||||
};
|
|
||||||
#[cfg(feature = "std")]
|
|
||||||
use bitcoin::consensus::encode::Decodable;
|
|
||||||
|
|
||||||
use crate::crypto::x_only;
|
|
||||||
#[cfg(feature = "std")]
|
|
||||||
use crate::crypto::make_even;
|
|
||||||
|
|
||||||
#[cfg(feature = "std")]
|
|
||||||
mod send;
|
|
||||||
#[cfg(feature = "std")]
|
|
||||||
pub use send::*;
|
|
||||||
|
|
||||||
/// Tweak keys to ensure they're usable with Bitcoin.
|
|
||||||
///
|
|
||||||
/// Taproot keys, which these keys are used as, must be even. This offsets the keys until they're
|
|
||||||
/// even.
|
|
||||||
#[cfg(feature = "std")]
|
|
||||||
pub fn tweak_keys(keys: &ThresholdKeys<Secp256k1>) -> ThresholdKeys<Secp256k1> {
|
|
||||||
let (_, offset) = make_even(keys.group_key());
|
|
||||||
keys.offset(Scalar::from(offset))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return the Taproot address payload for a public key.
|
|
||||||
///
|
|
||||||
/// If the key is odd, this will return None.
|
|
||||||
pub fn p2tr_script_buf(key: ProjectivePoint) -> Option<ScriptBuf> {
|
|
||||||
if key.to_encoded_point(true).tag() != Tag::CompressedEvenY {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(ScriptBuf::new_p2tr_tweaked(TweakedPublicKey::dangerous_assume_tweaked(x_only(&key))))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A spendable output.
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
|
||||||
pub struct ReceivedOutput {
|
|
||||||
// The scalar offset to obtain the key usable to spend this output.
|
|
||||||
offset: Scalar,
|
|
||||||
// The output to spend.
|
|
||||||
output: TxOut,
|
|
||||||
// The TX ID and vout of the output to spend.
|
|
||||||
outpoint: OutPoint,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ReceivedOutput {
|
|
||||||
/// The offset for this output.
|
|
||||||
pub fn offset(&self) -> Scalar {
|
|
||||||
self.offset
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The Bitcoin output for this output.
|
|
||||||
pub fn output(&self) -> &TxOut {
|
|
||||||
&self.output
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The outpoint for this output.
|
|
||||||
pub fn outpoint(&self) -> &OutPoint {
|
|
||||||
&self.outpoint
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The value of this output.
|
|
||||||
pub fn value(&self) -> u64 {
|
|
||||||
self.output.value.to_sat()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read a ReceivedOutput from a generic satisfying Read.
|
|
||||||
#[cfg(feature = "std")]
|
|
||||||
pub fn read<R: Read>(r: &mut R) -> io::Result<ReceivedOutput> {
|
|
||||||
let offset = Secp256k1::read_F(r)?;
|
|
||||||
let output;
|
|
||||||
let outpoint;
|
|
||||||
{
|
|
||||||
let mut buf_r = BufReader::with_capacity(0, r);
|
|
||||||
output =
|
|
||||||
TxOut::consensus_decode(&mut buf_r).map_err(|_| io::Error::other("invalid TxOut"))?;
|
|
||||||
outpoint =
|
|
||||||
OutPoint::consensus_decode(&mut buf_r).map_err(|_| io::Error::other("invalid OutPoint"))?;
|
|
||||||
}
|
|
||||||
Ok(ReceivedOutput { offset, output, outpoint })
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Write a ReceivedOutput to a generic satisfying Write.
|
|
||||||
pub fn write<W: Write>(&self, w: &mut W) -> io::Result<()> {
|
|
||||||
w.write_all(&self.offset.to_bytes())?;
|
|
||||||
w.write_all(&serialize(&self.output))?;
|
|
||||||
w.write_all(&serialize(&self.outpoint))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Serialize a ReceivedOutput to a `Vec<u8>`.
|
|
||||||
pub fn serialize(&self) -> Vec<u8> {
|
|
||||||
let mut res = Vec::new();
|
|
||||||
self.write(&mut res).unwrap();
|
|
||||||
res
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A transaction scanner capable of being used with HDKD schemes.
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct Scanner {
|
|
||||||
key: ProjectivePoint,
|
|
||||||
scripts: HashMap<ScriptBuf, Scalar>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Scanner {
|
|
||||||
/// Construct a Scanner for a key.
|
|
||||||
///
|
|
||||||
/// Returns None if this key can't be scanned for.
|
|
||||||
pub fn new(key: ProjectivePoint) -> Option<Scanner> {
|
|
||||||
let mut scripts = HashMap::new();
|
|
||||||
scripts.insert(p2tr_script_buf(key)?, Scalar::ZERO);
|
|
||||||
Some(Scanner { key, scripts })
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Register an offset to scan for.
|
|
||||||
///
|
|
||||||
/// Due to Bitcoin's requirement that points are even, not every offset may be used.
|
|
||||||
/// If an offset isn't usable, it will be incremented until it is. If this offset is already
|
|
||||||
/// present, None is returned. Else, Some(offset) will be, with the used offset.
|
|
||||||
///
|
|
||||||
/// This means offsets are surjective, not bijective, and the order offsets are registered in
|
|
||||||
/// may determine the validity of future offsets.
|
|
||||||
pub fn register_offset(&mut self, mut offset: Scalar) -> Option<Scalar> {
|
|
||||||
// This loop will terminate as soon as an even point is found, with any point having a ~50%
|
|
||||||
// chance of being even
|
|
||||||
// That means this should terminate within a very small amount of iterations
|
|
||||||
loop {
|
|
||||||
match p2tr_script_buf(self.key + (ProjectivePoint::GENERATOR * offset)) {
|
|
||||||
Some(script) => {
|
|
||||||
if self.scripts.contains_key(&script) {
|
|
||||||
None?;
|
|
||||||
}
|
|
||||||
self.scripts.insert(script, offset);
|
|
||||||
return Some(offset);
|
|
||||||
}
|
|
||||||
None => offset += Scalar::ONE,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Scan a transaction.
|
|
||||||
pub fn scan_transaction(&self, tx: &Transaction) -> Vec<ReceivedOutput> {
|
|
||||||
let mut res = Vec::new();
|
|
||||||
for (vout, output) in tx.output.iter().enumerate() {
|
|
||||||
// If the vout index exceeds 2**32, stop scanning outputs
|
|
||||||
let Ok(vout) = u32::try_from(vout) else { break };
|
|
||||||
|
|
||||||
if let Some(offset) = self.scripts.get(&output.script_pubkey) {
|
|
||||||
res.push(ReceivedOutput {
|
|
||||||
offset: *offset,
|
|
||||||
output: output.clone(),
|
|
||||||
outpoint: OutPoint::new(tx.compute_txid(), vout),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
res
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Scan a block.
|
|
||||||
///
|
|
||||||
/// This will also scan the coinbase transaction which is bound by maturity. If received outputs
|
|
||||||
/// must be immediately spendable, a post-processing pass is needed to remove those outputs.
|
|
||||||
/// Alternatively, scan_transaction can be called on `block.txdata[1 ..]`.
|
|
||||||
pub fn scan_block(&self, block: &Block) -> Vec<ReceivedOutput> {
|
|
||||||
let mut res = Vec::new();
|
|
||||||
for tx in &block.txdata {
|
|
||||||
res.extend(self.scan_transaction(tx));
|
|
||||||
}
|
|
||||||
res
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,446 +0,0 @@
|
|||||||
use std_shims::{
|
|
||||||
io::{self, Read},
|
|
||||||
collections::HashMap,
|
|
||||||
};
|
|
||||||
|
|
||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
use rand_core::{RngCore, CryptoRng};
|
|
||||||
|
|
||||||
use transcript::{Transcript, RecommendedTranscript};
|
|
||||||
|
|
||||||
use k256::{elliptic_curve::sec1::ToEncodedPoint, Scalar};
|
|
||||||
use frost::{curve::Secp256k1, Participant, ThresholdKeys, FrostError, sign::*};
|
|
||||||
|
|
||||||
use bitcoin::{
|
|
||||||
hashes::Hash,
|
|
||||||
sighash::{TapSighashType, SighashCache, Prevouts},
|
|
||||||
absolute::LockTime,
|
|
||||||
script::{PushBytesBuf, ScriptBuf},
|
|
||||||
transaction::{Version, Transaction},
|
|
||||||
OutPoint, Sequence, Witness, TxIn, Amount, TxOut,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
crypto::Schnorr,
|
|
||||||
wallet::{ReceivedOutput, p2tr_script_buf},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[rustfmt::skip]
|
|
||||||
// https://github.com/bitcoin/bitcoin/blob/306ccd4927a2efe325c8d84be1bdb79edeb29b04/src/policy/policy.cpp#L26-L63
|
|
||||||
// As the above notes, a lower amount may not be considered dust if contained in a SegWit output
|
|
||||||
// This doesn't bother with delineation due to how marginal these values are, and because it isn't
|
|
||||||
// worth the complexity to implement differentation
|
|
||||||
pub const DUST: u64 = 546;
|
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug, Error)]
|
|
||||||
pub enum TransactionError {
|
|
||||||
#[error("no inputs were specified")]
|
|
||||||
NoInputs,
|
|
||||||
#[error("no outputs were created")]
|
|
||||||
NoOutputs,
|
|
||||||
#[error("a specified payment's amount was less than bitcoin's required minimum")]
|
|
||||||
DustPayment,
|
|
||||||
#[error("too much data was specified")]
|
|
||||||
TooMuchData,
|
|
||||||
#[error("fee was too low to pass the default minimum fee rate")]
|
|
||||||
TooLowFee,
|
|
||||||
#[error("not enough funds for these payments")]
|
|
||||||
NotEnoughFunds,
|
|
||||||
#[error("transaction was too large")]
|
|
||||||
TooLargeTransaction,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A signable transaction, clone-able across attempts.
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
|
||||||
pub struct SignableTransaction {
|
|
||||||
tx: Transaction,
|
|
||||||
offsets: Vec<Scalar>,
|
|
||||||
prevouts: Vec<TxOut>,
|
|
||||||
needed_fee: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SignableTransaction {
|
|
||||||
fn calculate_weight(
|
|
||||||
inputs: usize,
|
|
||||||
payments: &[(ScriptBuf, u64)],
|
|
||||||
change: Option<&ScriptBuf>,
|
|
||||||
) -> u64 {
|
|
||||||
// Expand this a full transaction in order to use the bitcoin library's weight function
|
|
||||||
let mut tx = Transaction {
|
|
||||||
version: Version(2),
|
|
||||||
lock_time: LockTime::ZERO,
|
|
||||||
input: vec![
|
|
||||||
TxIn {
|
|
||||||
// This is a fixed size
|
|
||||||
// See https://developer.bitcoin.org/reference/transactions.html#raw-transaction-format
|
|
||||||
previous_output: OutPoint::default(),
|
|
||||||
// This is empty for a Taproot spend
|
|
||||||
script_sig: ScriptBuf::new(),
|
|
||||||
// This is fixed size, yet we do use Sequence::MAX
|
|
||||||
sequence: Sequence::MAX,
|
|
||||||
// Our witnesses contains a single 64-byte signature
|
|
||||||
witness: Witness::from_slice(&[vec![0; 64]])
|
|
||||||
};
|
|
||||||
inputs
|
|
||||||
],
|
|
||||||
output: payments
|
|
||||||
.iter()
|
|
||||||
// The payment is a fixed size so we don't have to use it here
|
|
||||||
// The script pub key is not of a fixed size and does have to be used here
|
|
||||||
.map(|payment| TxOut {
|
|
||||||
value: Amount::from_sat(payment.1),
|
|
||||||
script_pubkey: payment.0.clone(),
|
|
||||||
})
|
|
||||||
.collect(),
|
|
||||||
};
|
|
||||||
if let Some(change) = change {
|
|
||||||
// Use a 0 value since we're currently unsure what the change amount will be, and since
|
|
||||||
// the value is fixed size (so any value could be used here)
|
|
||||||
tx.output.push(TxOut { value: Amount::ZERO, script_pubkey: change.clone() });
|
|
||||||
}
|
|
||||||
u64::from(tx.weight())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the fee necessary for this transaction to achieve the fee rate specified at
|
|
||||||
/// construction.
|
|
||||||
///
|
|
||||||
/// The actual fee this transaction will use is `sum(inputs) - sum(outputs)`.
|
|
||||||
pub fn needed_fee(&self) -> u64 {
|
|
||||||
self.needed_fee
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the fee this transaction will use.
|
|
||||||
pub fn fee(&self) -> u64 {
|
|
||||||
self.prevouts.iter().map(|prevout| prevout.value.to_sat()).sum::<u64>() -
|
|
||||||
self.tx.output.iter().map(|prevout| prevout.value.to_sat()).sum::<u64>()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a new SignableTransaction.
|
|
||||||
///
|
|
||||||
/// If a change address is specified, any leftover funds will be sent to it if the leftover funds
|
|
||||||
/// exceed the minimum output amount. If a change address isn't specified, all leftover funds
|
|
||||||
/// will become part of the paid fee.
|
|
||||||
///
|
|
||||||
/// If data is specified, an OP_RETURN output will be added with it.
|
|
||||||
pub fn new(
|
|
||||||
mut inputs: Vec<ReceivedOutput>,
|
|
||||||
payments: &[(ScriptBuf, u64)],
|
|
||||||
change: Option<ScriptBuf>,
|
|
||||||
data: Option<Vec<u8>>,
|
|
||||||
fee_per_weight: u64,
|
|
||||||
) -> Result<SignableTransaction, TransactionError> {
|
|
||||||
if inputs.is_empty() {
|
|
||||||
Err(TransactionError::NoInputs)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if payments.is_empty() && change.is_none() && data.is_none() {
|
|
||||||
Err(TransactionError::NoOutputs)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (_, amount) in payments {
|
|
||||||
if *amount < DUST {
|
|
||||||
Err(TransactionError::DustPayment)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if data.as_ref().map_or(0, Vec::len) > 80 {
|
|
||||||
Err(TransactionError::TooMuchData)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let input_sat = inputs.iter().map(|input| input.output.value.to_sat()).sum::<u64>();
|
|
||||||
let offsets = inputs.iter().map(|input| input.offset).collect();
|
|
||||||
let tx_ins = inputs
|
|
||||||
.iter()
|
|
||||||
.map(|input| TxIn {
|
|
||||||
previous_output: input.outpoint,
|
|
||||||
script_sig: ScriptBuf::new(),
|
|
||||||
sequence: Sequence::MAX,
|
|
||||||
witness: Witness::new(),
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
let payment_sat = payments.iter().map(|payment| payment.1).sum::<u64>();
|
|
||||||
let mut tx_outs = payments
|
|
||||||
.iter()
|
|
||||||
.map(|payment| TxOut { value: Amount::from_sat(payment.1), script_pubkey: payment.0.clone() })
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
// Add the OP_RETURN output
|
|
||||||
if let Some(data) = data {
|
|
||||||
tx_outs.push(TxOut {
|
|
||||||
value: Amount::ZERO,
|
|
||||||
script_pubkey: ScriptBuf::new_op_return(
|
|
||||||
PushBytesBuf::try_from(data)
|
|
||||||
.expect("data didn't fit into PushBytes depsite being checked"),
|
|
||||||
),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut weight = Self::calculate_weight(tx_ins.len(), payments, None);
|
|
||||||
let mut needed_fee = fee_per_weight * weight;
|
|
||||||
|
|
||||||
// "Virtual transaction size" is weight ceildiv 4 per
|
|
||||||
// https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki
|
|
||||||
|
|
||||||
// https://github.com/bitcoin/bitcoin/blob/306ccd4927a2efe325c8d84be1bdb79edeb29b04/
|
|
||||||
// src/policy/policy.cpp#L295-L298
|
|
||||||
// implements this as expected
|
|
||||||
|
|
||||||
// Technically, it takes whatever's greater, the weight or the amount of signature operations
|
|
||||||
// multiplied by DEFAULT_BYTES_PER_SIGOP (20)
|
|
||||||
// We only use 1 signature per input, and our inputs have a weight exceeding 20
|
|
||||||
// Accordingly, our inputs' weight will always be greater than the cost of the signature ops
|
|
||||||
let vsize = weight.div_ceil(4);
|
|
||||||
debug_assert_eq!(
|
|
||||||
u64::try_from(bitcoin::policy::get_virtual_tx_size(
|
|
||||||
weight.try_into().unwrap(),
|
|
||||||
tx_ins.len().try_into().unwrap()
|
|
||||||
))
|
|
||||||
.unwrap(),
|
|
||||||
vsize
|
|
||||||
);
|
|
||||||
// Technically, if there isn't change, this TX may still pay enough of a fee to pass the
|
|
||||||
// minimum fee. Such edge cases aren't worth programming when they go against intent, as the
|
|
||||||
// specified fee rate is too low to be valid
|
|
||||||
// bitcoin::policy::DEFAULT_MIN_RELAY_TX_FEE is in sats/kilo-vbyte
|
|
||||||
if needed_fee < ((u64::from(bitcoin::policy::DEFAULT_MIN_RELAY_TX_FEE) * vsize) / 1000) {
|
|
||||||
Err(TransactionError::TooLowFee)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if input_sat < (payment_sat + needed_fee) {
|
|
||||||
Err(TransactionError::NotEnoughFunds)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If there's a change address, check if there's change to give it
|
|
||||||
if let Some(change) = change {
|
|
||||||
let weight_with_change = Self::calculate_weight(tx_ins.len(), payments, Some(&change));
|
|
||||||
let fee_with_change = fee_per_weight * weight_with_change;
|
|
||||||
if let Some(value) = input_sat.checked_sub(payment_sat + fee_with_change) {
|
|
||||||
if value >= DUST {
|
|
||||||
tx_outs.push(TxOut { value: Amount::from_sat(value), script_pubkey: change });
|
|
||||||
weight = weight_with_change;
|
|
||||||
needed_fee = fee_with_change;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if tx_outs.is_empty() {
|
|
||||||
Err(TransactionError::NoOutputs)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if weight > u64::from(bitcoin::policy::MAX_STANDARD_TX_WEIGHT) {
|
|
||||||
Err(TransactionError::TooLargeTransaction)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(SignableTransaction {
|
|
||||||
tx: Transaction {
|
|
||||||
version: Version(2),
|
|
||||||
lock_time: LockTime::ZERO,
|
|
||||||
input: tx_ins,
|
|
||||||
output: tx_outs,
|
|
||||||
},
|
|
||||||
offsets,
|
|
||||||
prevouts: inputs.drain(..).map(|input| input.output).collect(),
|
|
||||||
needed_fee,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the TX ID of the transaction this will create.
|
|
||||||
pub fn txid(&self) -> [u8; 32] {
|
|
||||||
let mut res = self.tx.compute_txid().to_byte_array();
|
|
||||||
res.reverse();
|
|
||||||
res
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the outputs this transaction will create.
|
|
||||||
pub fn outputs(&self) -> &[TxOut] {
|
|
||||||
&self.tx.output
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a multisig machine for this transaction.
|
|
||||||
///
|
|
||||||
/// Returns None if the wrong keys are used.
|
|
||||||
pub fn multisig(
|
|
||||||
self,
|
|
||||||
keys: &ThresholdKeys<Secp256k1>,
|
|
||||||
mut transcript: RecommendedTranscript,
|
|
||||||
) -> Option<TransactionMachine> {
|
|
||||||
transcript.domain_separate(b"bitcoin_transaction");
|
|
||||||
transcript.append_message(b"root_key", keys.group_key().to_encoded_point(true).as_bytes());
|
|
||||||
|
|
||||||
// Transcript the inputs and outputs
|
|
||||||
let tx = &self.tx;
|
|
||||||
for input in &tx.input {
|
|
||||||
transcript.append_message(b"input_hash", input.previous_output.txid);
|
|
||||||
transcript.append_message(b"input_output_index", input.previous_output.vout.to_le_bytes());
|
|
||||||
}
|
|
||||||
for payment in &tx.output {
|
|
||||||
transcript.append_message(b"output_script", payment.script_pubkey.as_bytes());
|
|
||||||
transcript.append_message(b"output_amount", payment.value.to_sat().to_le_bytes());
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut sigs = vec![];
|
|
||||||
for i in 0 .. tx.input.len() {
|
|
||||||
let mut transcript = transcript.clone();
|
|
||||||
// This unwrap is safe since any transaction with this many inputs violates the maximum
|
|
||||||
// size allowed under standards, which this lib will error on creation of
|
|
||||||
transcript.append_message(b"signing_input", u32::try_from(i).unwrap().to_le_bytes());
|
|
||||||
|
|
||||||
let offset = keys.clone().offset(self.offsets[i]);
|
|
||||||
if p2tr_script_buf(offset.group_key())? != self.prevouts[i].script_pubkey {
|
|
||||||
None?;
|
|
||||||
}
|
|
||||||
|
|
||||||
sigs.push(AlgorithmMachine::new(
|
|
||||||
Schnorr::new(transcript),
|
|
||||||
keys.clone().offset(self.offsets[i]),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(TransactionMachine { tx: self, sigs })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A FROST signing machine to produce a Bitcoin transaction.
|
|
||||||
///
|
|
||||||
/// This does not support caching its preprocess. When sign is called, the message must be empty.
|
|
||||||
/// This will panic if either `cache` is called or the message isn't empty.
|
|
||||||
pub struct TransactionMachine {
|
|
||||||
tx: SignableTransaction,
|
|
||||||
sigs: Vec<AlgorithmMachine<Secp256k1, Schnorr<RecommendedTranscript>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PreprocessMachine for TransactionMachine {
|
|
||||||
type Preprocess = Vec<Preprocess<Secp256k1, ()>>;
|
|
||||||
type Signature = Transaction;
|
|
||||||
type SignMachine = TransactionSignMachine;
|
|
||||||
|
|
||||||
fn preprocess<R: RngCore + CryptoRng>(
|
|
||||||
mut self,
|
|
||||||
rng: &mut R,
|
|
||||||
) -> (Self::SignMachine, Self::Preprocess) {
|
|
||||||
let mut preprocesses = Vec::with_capacity(self.sigs.len());
|
|
||||||
let sigs = self
|
|
||||||
.sigs
|
|
||||||
.drain(..)
|
|
||||||
.map(|sig| {
|
|
||||||
let (sig, preprocess) = sig.preprocess(rng);
|
|
||||||
preprocesses.push(preprocess);
|
|
||||||
sig
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
(TransactionSignMachine { tx: self.tx, sigs }, preprocesses)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct TransactionSignMachine {
|
|
||||||
tx: SignableTransaction,
|
|
||||||
sigs: Vec<AlgorithmSignMachine<Secp256k1, Schnorr<RecommendedTranscript>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SignMachine<Transaction> for TransactionSignMachine {
|
|
||||||
type Params = ();
|
|
||||||
type Keys = ThresholdKeys<Secp256k1>;
|
|
||||||
type Preprocess = Vec<Preprocess<Secp256k1, ()>>;
|
|
||||||
type SignatureShare = Vec<SignatureShare<Secp256k1>>;
|
|
||||||
type SignatureMachine = TransactionSignatureMachine;
|
|
||||||
|
|
||||||
fn cache(self) -> CachedPreprocess {
|
|
||||||
unimplemented!(
|
|
||||||
"Bitcoin transactions don't support caching their preprocesses due to {}",
|
|
||||||
"being already bound to a specific transaction"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn from_cache(
|
|
||||||
(): (),
|
|
||||||
_: ThresholdKeys<Secp256k1>,
|
|
||||||
_: CachedPreprocess,
|
|
||||||
) -> (Self, Self::Preprocess) {
|
|
||||||
unimplemented!(
|
|
||||||
"Bitcoin transactions don't support caching their preprocesses due to {}",
|
|
||||||
"being already bound to a specific transaction"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_preprocess<R: Read>(&self, reader: &mut R) -> io::Result<Self::Preprocess> {
|
|
||||||
self.sigs.iter().map(|sig| sig.read_preprocess(reader)).collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn sign(
|
|
||||||
mut self,
|
|
||||||
commitments: HashMap<Participant, Self::Preprocess>,
|
|
||||||
msg: &[u8],
|
|
||||||
) -> Result<(TransactionSignatureMachine, Self::SignatureShare), FrostError> {
|
|
||||||
if !msg.is_empty() {
|
|
||||||
panic!("message was passed to the TransactionSignMachine when it generates its own");
|
|
||||||
}
|
|
||||||
|
|
||||||
let commitments = (0 .. self.sigs.len())
|
|
||||||
.map(|c| {
|
|
||||||
commitments
|
|
||||||
.iter()
|
|
||||||
.map(|(l, commitments)| (*l, commitments[c].clone()))
|
|
||||||
.collect::<HashMap<_, _>>()
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
let mut cache = SighashCache::new(&self.tx.tx);
|
|
||||||
// Sign committing to all inputs
|
|
||||||
let prevouts = Prevouts::All(&self.tx.prevouts);
|
|
||||||
|
|
||||||
let mut shares = Vec::with_capacity(self.sigs.len());
|
|
||||||
let sigs = self
|
|
||||||
.sigs
|
|
||||||
.drain(..)
|
|
||||||
.enumerate()
|
|
||||||
.map(|(i, sig)| {
|
|
||||||
let (sig, share) = sig.sign(
|
|
||||||
commitments[i].clone(),
|
|
||||||
cache
|
|
||||||
.taproot_key_spend_signature_hash(i, &prevouts, TapSighashType::Default)
|
|
||||||
// This should never happen since the inputs align with the TX the cache was
|
|
||||||
// constructed with, and because i is always < prevouts.len()
|
|
||||||
.expect("taproot_key_spend_signature_hash failed to return a hash")
|
|
||||||
.as_ref(),
|
|
||||||
)?;
|
|
||||||
shares.push(share);
|
|
||||||
Ok(sig)
|
|
||||||
})
|
|
||||||
.collect::<Result<_, _>>()?;
|
|
||||||
|
|
||||||
Ok((TransactionSignatureMachine { tx: self.tx.tx, sigs }, shares))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct TransactionSignatureMachine {
|
|
||||||
tx: Transaction,
|
|
||||||
sigs: Vec<AlgorithmSignatureMachine<Secp256k1, Schnorr<RecommendedTranscript>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SignatureMachine<Transaction> for TransactionSignatureMachine {
|
|
||||||
type SignatureShare = Vec<SignatureShare<Secp256k1>>;
|
|
||||||
|
|
||||||
fn read_share<R: Read>(&self, reader: &mut R) -> io::Result<Self::SignatureShare> {
|
|
||||||
self.sigs.iter().map(|sig| sig.read_share(reader)).collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn complete(
|
|
||||||
mut self,
|
|
||||||
mut shares: HashMap<Participant, Self::SignatureShare>,
|
|
||||||
) -> Result<Transaction, FrostError> {
|
|
||||||
for (input, schnorr) in self.tx.input.iter_mut().zip(self.sigs.drain(..)) {
|
|
||||||
let sig = schnorr.complete(
|
|
||||||
shares.iter_mut().map(|(l, shares)| (*l, shares.remove(0))).collect::<HashMap<_, _>>(),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let mut witness = Witness::new();
|
|
||||||
witness.push(sig);
|
|
||||||
input.witness = witness;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(self.tx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
use bitcoin_serai::{bitcoin::hashes::Hash as HashTrait, rpc::RpcError};
|
|
||||||
|
|
||||||
mod runner;
|
|
||||||
use runner::rpc;
|
|
||||||
|
|
||||||
async_sequential! {
|
|
||||||
async fn test_rpc() {
|
|
||||||
let rpc = rpc().await;
|
|
||||||
|
|
||||||
// Test get_latest_block_number and get_block_hash by round tripping them
|
|
||||||
let latest = rpc.get_latest_block_number().await.unwrap();
|
|
||||||
let hash = rpc.get_block_hash(latest).await.unwrap();
|
|
||||||
assert_eq!(rpc.get_block_number(&hash).await.unwrap(), latest);
|
|
||||||
|
|
||||||
// Test this actually is the latest block number by checking asking for the next block's errors
|
|
||||||
assert!(matches!(rpc.get_block_hash(latest + 1).await, Err(RpcError::RequestError(_))));
|
|
||||||
|
|
||||||
// Test get_block by checking the received block's hash matches the request
|
|
||||||
let block = rpc.get_block(&hash).await.unwrap();
|
|
||||||
// Hashes are stored in reverse. It's bs from Satoshi
|
|
||||||
let mut block_hash = *block.block_hash().as_raw_hash().as_byte_array();
|
|
||||||
block_hash.reverse();
|
|
||||||
assert_eq!(hash, block_hash);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
use std::sync::OnceLock;
|
|
||||||
|
|
||||||
use bitcoin_serai::rpc::Rpc;
|
|
||||||
|
|
||||||
use tokio::sync::Mutex;
|
|
||||||
|
|
||||||
static SEQUENTIAL_CELL: OnceLock<Mutex<()>> = OnceLock::new();
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
pub fn SEQUENTIAL() -> &'static Mutex<()> {
|
|
||||||
SEQUENTIAL_CELL.get_or_init(|| Mutex::new(()))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub(crate) async fn rpc() -> Rpc {
|
|
||||||
let rpc = Rpc::new("http://serai:seraidex@127.0.0.1:8332".to_string()).await.unwrap();
|
|
||||||
|
|
||||||
// If this node has already been interacted with, clear its chain
|
|
||||||
if rpc.get_latest_block_number().await.unwrap() > 0 {
|
|
||||||
rpc
|
|
||||||
.rpc_call(
|
|
||||||
"invalidateblock",
|
|
||||||
serde_json::json!([hex::encode(rpc.get_block_hash(1).await.unwrap())]),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
rpc
|
|
||||||
}
|
|
||||||
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! async_sequential {
|
|
||||||
($(async fn $name: ident() $body: block)*) => {
|
|
||||||
$(
|
|
||||||
#[tokio::test]
|
|
||||||
async fn $name() {
|
|
||||||
let guard = runner::SEQUENTIAL().lock().await;
|
|
||||||
let local = tokio::task::LocalSet::new();
|
|
||||||
local.run_until(async move {
|
|
||||||
if let Err(err) = tokio::task::spawn_local(async move { $body }).await {
|
|
||||||
drop(guard);
|
|
||||||
Err(err).unwrap()
|
|
||||||
}
|
|
||||||
}).await;
|
|
||||||
}
|
|
||||||
)*
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,363 +0,0 @@
|
|||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use rand_core::{RngCore, OsRng};
|
|
||||||
|
|
||||||
use transcript::{Transcript, RecommendedTranscript};
|
|
||||||
|
|
||||||
use k256::{
|
|
||||||
elliptic_curve::{
|
|
||||||
group::{ff::Field, Group},
|
|
||||||
sec1::{Tag, ToEncodedPoint},
|
|
||||||
},
|
|
||||||
Scalar, ProjectivePoint,
|
|
||||||
};
|
|
||||||
use frost::{
|
|
||||||
curve::Secp256k1,
|
|
||||||
Participant, ThresholdKeys,
|
|
||||||
tests::{THRESHOLD, key_gen, sign_without_caching},
|
|
||||||
};
|
|
||||||
|
|
||||||
use bitcoin_serai::{
|
|
||||||
bitcoin::{
|
|
||||||
hashes::Hash as HashTrait,
|
|
||||||
blockdata::opcodes::all::OP_RETURN,
|
|
||||||
script::{PushBytesBuf, Instruction, Instructions, Script},
|
|
||||||
OutPoint, Amount, TxOut, Transaction, Network, Address,
|
|
||||||
},
|
|
||||||
wallet::{
|
|
||||||
tweak_keys, p2tr_script_buf, ReceivedOutput, Scanner, TransactionError, SignableTransaction,
|
|
||||||
},
|
|
||||||
rpc::Rpc,
|
|
||||||
};
|
|
||||||
|
|
||||||
mod runner;
|
|
||||||
use runner::rpc;
|
|
||||||
|
|
||||||
const FEE: u64 = 20;
|
|
||||||
|
|
||||||
fn is_even(key: ProjectivePoint) -> bool {
|
|
||||||
key.to_encoded_point(true).tag() == Tag::CompressedEvenY
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn send_and_get_output(rpc: &Rpc, scanner: &Scanner, key: ProjectivePoint) -> ReceivedOutput {
|
|
||||||
let block_number = rpc.get_latest_block_number().await.unwrap() + 1;
|
|
||||||
|
|
||||||
rpc
|
|
||||||
.rpc_call::<Vec<String>>(
|
|
||||||
"generatetoaddress",
|
|
||||||
serde_json::json!([
|
|
||||||
1,
|
|
||||||
Address::from_script(&p2tr_script_buf(key).unwrap(), Network::Regtest).unwrap()
|
|
||||||
]),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// Mine until maturity
|
|
||||||
rpc
|
|
||||||
.rpc_call::<Vec<String>>(
|
|
||||||
"generatetoaddress",
|
|
||||||
serde_json::json!([100, Address::p2sh(Script::new(), Network::Regtest).unwrap()]),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let block = rpc.get_block(&rpc.get_block_hash(block_number).await.unwrap()).await.unwrap();
|
|
||||||
|
|
||||||
let mut outputs = scanner.scan_block(&block);
|
|
||||||
assert_eq!(outputs, scanner.scan_transaction(&block.txdata[0]));
|
|
||||||
|
|
||||||
assert_eq!(outputs.len(), 1);
|
|
||||||
assert_eq!(outputs[0].outpoint(), &OutPoint::new(block.txdata[0].compute_txid(), 0));
|
|
||||||
assert_eq!(outputs[0].value(), block.txdata[0].output[0].value.to_sat());
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
ReceivedOutput::read::<&[u8]>(&mut outputs[0].serialize().as_ref()).unwrap(),
|
|
||||||
outputs[0]
|
|
||||||
);
|
|
||||||
|
|
||||||
outputs.swap_remove(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn keys() -> (HashMap<Participant, ThresholdKeys<Secp256k1>>, ProjectivePoint) {
|
|
||||||
let mut keys = key_gen(&mut OsRng);
|
|
||||||
for keys in keys.values_mut() {
|
|
||||||
*keys = tweak_keys(keys);
|
|
||||||
}
|
|
||||||
let key = keys.values().next().unwrap().group_key();
|
|
||||||
(keys, key)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn sign(
|
|
||||||
keys: &HashMap<Participant, ThresholdKeys<Secp256k1>>,
|
|
||||||
tx: &SignableTransaction,
|
|
||||||
) -> Transaction {
|
|
||||||
let mut machines = HashMap::new();
|
|
||||||
for i in (1 ..= THRESHOLD).map(|i| Participant::new(i).unwrap()) {
|
|
||||||
machines.insert(
|
|
||||||
i,
|
|
||||||
tx.clone()
|
|
||||||
.multisig(&keys[&i].clone(), RecommendedTranscript::new(b"bitcoin-serai Test Transaction"))
|
|
||||||
.unwrap(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
sign_without_caching(&mut OsRng, machines, &[])
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_tweak_keys() {
|
|
||||||
let mut even = false;
|
|
||||||
let mut odd = false;
|
|
||||||
|
|
||||||
// Generate keys until we get an even set and an odd set
|
|
||||||
while !(even && odd) {
|
|
||||||
let mut keys = key_gen(&mut OsRng).drain().next().unwrap().1;
|
|
||||||
if is_even(keys.group_key()) {
|
|
||||||
// Tweaking should do nothing
|
|
||||||
assert_eq!(tweak_keys(&keys).group_key(), keys.group_key());
|
|
||||||
|
|
||||||
even = true;
|
|
||||||
} else {
|
|
||||||
let tweaked = tweak_keys(&keys).group_key();
|
|
||||||
assert_ne!(tweaked, keys.group_key());
|
|
||||||
// Tweaking should produce an even key
|
|
||||||
assert!(is_even(tweaked));
|
|
||||||
|
|
||||||
// Verify it uses the smallest possible offset
|
|
||||||
while keys.group_key().to_encoded_point(true).tag() == Tag::CompressedOddY {
|
|
||||||
keys = keys.offset(Scalar::ONE);
|
|
||||||
}
|
|
||||||
assert_eq!(tweaked, keys.group_key());
|
|
||||||
|
|
||||||
odd = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async_sequential! {
|
|
||||||
async fn test_scanner() {
|
|
||||||
// Test Scanners are creatable for even keys.
|
|
||||||
for _ in 0 .. 128 {
|
|
||||||
let key = ProjectivePoint::random(&mut OsRng);
|
|
||||||
assert_eq!(Scanner::new(key).is_some(), is_even(key));
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut key = ProjectivePoint::random(&mut OsRng);
|
|
||||||
while !is_even(key) {
|
|
||||||
key += ProjectivePoint::GENERATOR;
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
let mut scanner = Scanner::new(key).unwrap();
|
|
||||||
for _ in 0 .. 128 {
|
|
||||||
let mut offset = Scalar::random(&mut OsRng);
|
|
||||||
let registered = scanner.register_offset(offset).unwrap();
|
|
||||||
// Registering this again should return None
|
|
||||||
assert!(scanner.register_offset(offset).is_none());
|
|
||||||
|
|
||||||
// We can only register offsets resulting in even keys
|
|
||||||
// Make this even
|
|
||||||
while !is_even(key + (ProjectivePoint::GENERATOR * offset)) {
|
|
||||||
offset += Scalar::ONE;
|
|
||||||
}
|
|
||||||
// Ensure it matches the registered offset
|
|
||||||
assert_eq!(registered, offset);
|
|
||||||
// Assert registering this again fails
|
|
||||||
assert!(scanner.register_offset(offset).is_none());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let rpc = rpc().await;
|
|
||||||
let mut scanner = Scanner::new(key).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(send_and_get_output(&rpc, &scanner, key).await.offset(), Scalar::ZERO);
|
|
||||||
|
|
||||||
// Register an offset and test receiving to it
|
|
||||||
let offset = scanner.register_offset(Scalar::random(&mut OsRng)).unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
send_and_get_output(&rpc, &scanner, key + (ProjectivePoint::GENERATOR * offset))
|
|
||||||
.await
|
|
||||||
.offset(),
|
|
||||||
offset
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn test_transaction_errors() {
|
|
||||||
let (_, key) = keys();
|
|
||||||
|
|
||||||
let rpc = rpc().await;
|
|
||||||
let scanner = Scanner::new(key).unwrap();
|
|
||||||
|
|
||||||
let output = send_and_get_output(&rpc, &scanner, key).await;
|
|
||||||
assert_eq!(output.offset(), Scalar::ZERO);
|
|
||||||
|
|
||||||
let inputs = vec![output];
|
|
||||||
let addr = || p2tr_script_buf(key).unwrap();
|
|
||||||
let payments = vec![(addr(), 1000)];
|
|
||||||
|
|
||||||
assert!(SignableTransaction::new(inputs.clone(), &payments, None, None, FEE).is_ok());
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
SignableTransaction::new(vec![], &payments, None, None, FEE),
|
|
||||||
Err(TransactionError::NoInputs)
|
|
||||||
);
|
|
||||||
|
|
||||||
// No change
|
|
||||||
assert!(SignableTransaction::new(inputs.clone(), &[(addr(), 1000)], None, None, FEE).is_ok());
|
|
||||||
// Consolidation TX
|
|
||||||
assert!(SignableTransaction::new(inputs.clone(), &[], Some(addr()), None, FEE).is_ok());
|
|
||||||
// Data
|
|
||||||
assert!(SignableTransaction::new(inputs.clone(), &[], None, Some(vec![]), FEE).is_ok());
|
|
||||||
// No outputs
|
|
||||||
assert_eq!(
|
|
||||||
SignableTransaction::new(inputs.clone(), &[], None, None, FEE),
|
|
||||||
Err(TransactionError::NoOutputs),
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
SignableTransaction::new(inputs.clone(), &[(addr(), 1)], None, None, FEE),
|
|
||||||
Err(TransactionError::DustPayment),
|
|
||||||
);
|
|
||||||
|
|
||||||
assert!(
|
|
||||||
SignableTransaction::new(inputs.clone(), &payments, None, Some(vec![0; 80]), FEE).is_ok()
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
SignableTransaction::new(inputs.clone(), &payments, None, Some(vec![0; 81]), FEE),
|
|
||||||
Err(TransactionError::TooMuchData),
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
SignableTransaction::new(inputs.clone(), &[], Some(addr()), None, 0),
|
|
||||||
Err(TransactionError::TooLowFee),
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
SignableTransaction::new(inputs.clone(), &[(addr(), inputs[0].value() * 2)], None, None, FEE),
|
|
||||||
Err(TransactionError::NotEnoughFunds),
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
SignableTransaction::new(inputs, &vec![(addr(), 1000); 10000], None, None, FEE),
|
|
||||||
Err(TransactionError::TooLargeTransaction),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn test_send() {
|
|
||||||
let (keys, key) = keys();
|
|
||||||
|
|
||||||
let rpc = rpc().await;
|
|
||||||
let mut scanner = Scanner::new(key).unwrap();
|
|
||||||
|
|
||||||
// Get inputs, one not offset and one offset
|
|
||||||
let output = send_and_get_output(&rpc, &scanner, key).await;
|
|
||||||
assert_eq!(output.offset(), Scalar::ZERO);
|
|
||||||
|
|
||||||
let offset = scanner.register_offset(Scalar::random(&mut OsRng)).unwrap();
|
|
||||||
let offset_key = key + (ProjectivePoint::GENERATOR * offset);
|
|
||||||
let offset_output = send_and_get_output(&rpc, &scanner, offset_key).await;
|
|
||||||
assert_eq!(offset_output.offset(), offset);
|
|
||||||
|
|
||||||
// Declare payments, change, fee
|
|
||||||
let payments = [
|
|
||||||
(p2tr_script_buf(key).unwrap(), 1005),
|
|
||||||
(p2tr_script_buf(offset_key).unwrap(), 1007)
|
|
||||||
];
|
|
||||||
|
|
||||||
let change_offset = scanner.register_offset(Scalar::random(&mut OsRng)).unwrap();
|
|
||||||
let change_key = key + (ProjectivePoint::GENERATOR * change_offset);
|
|
||||||
let change_addr = p2tr_script_buf(change_key).unwrap();
|
|
||||||
|
|
||||||
// Create and sign the TX
|
|
||||||
let tx = SignableTransaction::new(
|
|
||||||
vec![output.clone(), offset_output.clone()],
|
|
||||||
&payments,
|
|
||||||
Some(change_addr.clone()),
|
|
||||||
None,
|
|
||||||
FEE
|
|
||||||
).unwrap();
|
|
||||||
let needed_fee = tx.needed_fee();
|
|
||||||
let expected_id = tx.txid();
|
|
||||||
let tx = sign(&keys, &tx);
|
|
||||||
|
|
||||||
assert_eq!(tx.output.len(), 3);
|
|
||||||
|
|
||||||
// Ensure we can scan it
|
|
||||||
let outputs = scanner.scan_transaction(&tx);
|
|
||||||
for (o, output) in outputs.iter().enumerate() {
|
|
||||||
assert_eq!(output.outpoint(), &OutPoint::new(tx.compute_txid(), u32::try_from(o).unwrap()));
|
|
||||||
assert_eq!(&ReceivedOutput::read::<&[u8]>(&mut output.serialize().as_ref()).unwrap(), output);
|
|
||||||
}
|
|
||||||
|
|
||||||
assert_eq!(outputs[0].offset(), Scalar::ZERO);
|
|
||||||
assert_eq!(outputs[1].offset(), offset);
|
|
||||||
assert_eq!(outputs[2].offset(), change_offset);
|
|
||||||
|
|
||||||
// Make sure the payments were properly created
|
|
||||||
for ((output, scanned), payment) in tx.output.iter().zip(outputs.iter()).zip(payments.iter()) {
|
|
||||||
assert_eq!(
|
|
||||||
output,
|
|
||||||
&TxOut { script_pubkey: payment.0.clone(), value: Amount::from_sat(payment.1) },
|
|
||||||
);
|
|
||||||
assert_eq!(scanned.value(), payment.1 );
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make sure the change is correct
|
|
||||||
assert_eq!(needed_fee, u64::from(tx.weight()) * FEE);
|
|
||||||
let input_value = output.value() + offset_output.value();
|
|
||||||
let output_value = tx.output.iter().map(|output| output.value.to_sat()).sum::<u64>();
|
|
||||||
assert_eq!(input_value - output_value, needed_fee);
|
|
||||||
|
|
||||||
let change_amount =
|
|
||||||
input_value - payments.iter().map(|payment| payment.1).sum::<u64>() - needed_fee;
|
|
||||||
assert_eq!(
|
|
||||||
tx.output[2],
|
|
||||||
TxOut { script_pubkey: change_addr, value: Amount::from_sat(change_amount) },
|
|
||||||
);
|
|
||||||
|
|
||||||
// This also tests send_raw_transaction and get_transaction, which the RPC test can't
|
|
||||||
// effectively test
|
|
||||||
rpc.send_raw_transaction(&tx).await.unwrap();
|
|
||||||
let mut hash = *tx.compute_txid().as_raw_hash().as_byte_array();
|
|
||||||
hash.reverse();
|
|
||||||
assert_eq!(tx, rpc.get_transaction(&hash).await.unwrap());
|
|
||||||
assert_eq!(expected_id, hash);
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn test_data() {
|
|
||||||
let (keys, key) = keys();
|
|
||||||
|
|
||||||
let rpc = rpc().await;
|
|
||||||
let scanner = Scanner::new(key).unwrap();
|
|
||||||
|
|
||||||
let output = send_and_get_output(&rpc, &scanner, key).await;
|
|
||||||
assert_eq!(output.offset(), Scalar::ZERO);
|
|
||||||
|
|
||||||
let data_len = 60 + usize::try_from(OsRng.next_u64() % 21).unwrap();
|
|
||||||
let mut data = vec![0; data_len];
|
|
||||||
OsRng.fill_bytes(&mut data);
|
|
||||||
|
|
||||||
let tx = sign(
|
|
||||||
&keys,
|
|
||||||
&SignableTransaction::new(
|
|
||||||
vec![output],
|
|
||||||
&[],
|
|
||||||
Some(p2tr_script_buf(key).unwrap()),
|
|
||||||
Some(data.clone()),
|
|
||||||
FEE
|
|
||||||
).unwrap()
|
|
||||||
);
|
|
||||||
|
|
||||||
assert!(tx.output[0].script_pubkey.is_op_return());
|
|
||||||
let check = |mut instructions: Instructions| {
|
|
||||||
assert_eq!(instructions.next().unwrap().unwrap(), Instruction::Op(OP_RETURN));
|
|
||||||
assert_eq!(
|
|
||||||
instructions.next().unwrap().unwrap(),
|
|
||||||
Instruction::PushBytes(&PushBytesBuf::try_from(data.clone()).unwrap()),
|
|
||||||
);
|
|
||||||
assert!(instructions.next().is_none());
|
|
||||||
};
|
|
||||||
check(tx.output[0].script_pubkey.instructions());
|
|
||||||
check(tx.output[0].script_pubkey.instructions_minimal());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
3
coins/ethereum/.gitignore
vendored
3
coins/ethereum/.gitignore
vendored
@@ -1,3 +0,0 @@
|
|||||||
# Solidity build outputs
|
|
||||||
cache
|
|
||||||
artifacts
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "ethereum-serai"
|
|
||||||
version = "0.1.0"
|
|
||||||
description = "An Ethereum library supporting Schnorr signing and on-chain verification"
|
|
||||||
license = "AGPL-3.0-only"
|
|
||||||
repository = "https://github.com/serai-dex/serai/tree/develop/coins/ethereum"
|
|
||||||
authors = ["Luke Parker <lukeparker5132@gmail.com>", "Elizabeth Binks <elizabethjbinks@gmail.com>"]
|
|
||||||
edition = "2021"
|
|
||||||
publish = false
|
|
||||||
rust-version = "1.74"
|
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
|
||||||
all-features = true
|
|
||||||
rustdoc-args = ["--cfg", "docsrs"]
|
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
thiserror = { version = "1", default-features = false }
|
|
||||||
|
|
||||||
rand_core = { version = "0.6", default-features = false, features = ["std"] }
|
|
||||||
|
|
||||||
transcript = { package = "flexible-transcript", path = "../../crypto/transcript", default-features = false, features = ["recommended"] }
|
|
||||||
|
|
||||||
group = { version = "0.13", default-features = false }
|
|
||||||
k256 = { version = "^0.13.1", default-features = false, features = ["std", "ecdsa", "arithmetic"] }
|
|
||||||
frost = { package = "modular-frost", path = "../../crypto/frost", default-features = false, features = ["secp256k1"] }
|
|
||||||
|
|
||||||
alloy-core = { version = "0.7", default-features = false }
|
|
||||||
alloy-sol-types = { version = "0.7", default-features = false, features = ["json"] }
|
|
||||||
alloy-consensus = { git = "https://github.com/alloy-rs/alloy", rev = "64feb9bc51c8021ea08535694c44de84222f474e", default-features = false, features = ["k256"] }
|
|
||||||
alloy-network = { git = "https://github.com/alloy-rs/alloy", rev = "64feb9bc51c8021ea08535694c44de84222f474e", default-features = false }
|
|
||||||
alloy-rpc-types = { git = "https://github.com/alloy-rs/alloy", rev = "64feb9bc51c8021ea08535694c44de84222f474e", default-features = false }
|
|
||||||
alloy-rpc-client = { git = "https://github.com/alloy-rs/alloy", rev = "64feb9bc51c8021ea08535694c44de84222f474e", default-features = false }
|
|
||||||
alloy-simple-request-transport = { path = "./alloy-simple-request-transport", default-features = false }
|
|
||||||
alloy-provider = { git = "https://github.com/alloy-rs/alloy", rev = "64feb9bc51c8021ea08535694c44de84222f474e", default-features = false }
|
|
||||||
|
|
||||||
alloy-node-bindings = { git = "https://github.com/alloy-rs/alloy", rev = "64feb9bc51c8021ea08535694c44de84222f474e", default-features = false, optional = true }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
frost = { package = "modular-frost", path = "../../crypto/frost", default-features = false, features = ["tests"] }
|
|
||||||
|
|
||||||
tokio = { version = "1", features = ["macros"] }
|
|
||||||
|
|
||||||
alloy-node-bindings = { git = "https://github.com/alloy-rs/alloy", rev = "64feb9bc51c8021ea08535694c44de84222f474e", default-features = false }
|
|
||||||
|
|
||||||
[features]
|
|
||||||
tests = ["alloy-node-bindings", "frost/tests"]
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
AGPL-3.0-only license
|
|
||||||
|
|
||||||
Copyright (c) 2022-2023 Luke Parker
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU Affero General Public License Version 3 as
|
|
||||||
published by the Free Software Foundation.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU Affero General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
# Ethereum
|
|
||||||
|
|
||||||
This package contains Ethereum-related functionality, specifically deploying and
|
|
||||||
interacting with Serai contracts.
|
|
||||||
|
|
||||||
While `monero-serai` and `bitcoin-serai` are general purpose libraries,
|
|
||||||
`ethereum-serai` is Serai specific. If any of the utilities are generally
|
|
||||||
desired, please fork and maintain your own copy to ensure the desired
|
|
||||||
functionality is preserved, or open an issue to request we make this library
|
|
||||||
general purpose.
|
|
||||||
|
|
||||||
### Dependencies
|
|
||||||
|
|
||||||
- solc
|
|
||||||
- [Foundry](https://github.com/foundry-rs/foundry)
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "alloy-simple-request-transport"
|
|
||||||
version = "0.1.0"
|
|
||||||
description = "A transport for alloy based off simple-request"
|
|
||||||
license = "MIT"
|
|
||||||
repository = "https://github.com/serai-dex/serai/tree/develop/coins/ethereum/alloy-simple-request-transport"
|
|
||||||
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
|
|
||||||
edition = "2021"
|
|
||||||
rust-version = "1.74"
|
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
|
||||||
all-features = true
|
|
||||||
rustdoc-args = ["--cfg", "docsrs"]
|
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
tower = "0.4"
|
|
||||||
|
|
||||||
serde_json = { version = "1", default-features = false }
|
|
||||||
simple-request = { path = "../../../common/request", default-features = false }
|
|
||||||
|
|
||||||
alloy-json-rpc = { git = "https://github.com/alloy-rs/alloy", rev = "64feb9bc51c8021ea08535694c44de84222f474e", default-features = false }
|
|
||||||
alloy-transport = { git = "https://github.com/alloy-rs/alloy", rev = "64feb9bc51c8021ea08535694c44de84222f474e", default-features = false }
|
|
||||||
|
|
||||||
[features]
|
|
||||||
default = ["tls"]
|
|
||||||
tls = ["simple-request/tls"]
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 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.
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
# Alloy Simple Request Transport
|
|
||||||
|
|
||||||
A transport for alloy based on simple-request, a small HTTP client built around
|
|
||||||
hyper.
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
|
||||||
#![doc = include_str!("../README.md")]
|
|
||||||
|
|
||||||
use core::task;
|
|
||||||
use std::io;
|
|
||||||
|
|
||||||
use alloy_json_rpc::{RequestPacket, ResponsePacket};
|
|
||||||
use alloy_transport::{TransportError, TransportErrorKind, TransportFut};
|
|
||||||
|
|
||||||
use simple_request::{hyper, Request, Client};
|
|
||||||
|
|
||||||
use tower::Service;
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct SimpleRequest {
|
|
||||||
client: Client,
|
|
||||||
url: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SimpleRequest {
|
|
||||||
pub fn new(url: String) -> Self {
|
|
||||||
Self { client: Client::with_connection_pool(), url }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Service<RequestPacket> for SimpleRequest {
|
|
||||||
type Response = ResponsePacket;
|
|
||||||
type Error = TransportError;
|
|
||||||
type Future = TransportFut<'static>;
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn poll_ready(&mut self, _cx: &mut task::Context<'_>) -> task::Poll<Result<(), Self::Error>> {
|
|
||||||
task::Poll::Ready(Ok(()))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn call(&mut self, req: RequestPacket) -> Self::Future {
|
|
||||||
let inner = self.clone();
|
|
||||||
Box::pin(async move {
|
|
||||||
let packet = req.serialize().map_err(TransportError::SerError)?;
|
|
||||||
let request = Request::from(
|
|
||||||
hyper::Request::post(&inner.url)
|
|
||||||
.header("Content-Type", "application/json")
|
|
||||||
.body(serde_json::to_vec(&packet).map_err(TransportError::SerError)?.into())
|
|
||||||
.unwrap(),
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut res = inner
|
|
||||||
.client
|
|
||||||
.request(request)
|
|
||||||
.await
|
|
||||||
.map_err(|e| TransportErrorKind::custom(io::Error::other(format!("{e:?}"))))?
|
|
||||||
.body()
|
|
||||||
.await
|
|
||||||
.map_err(|e| TransportErrorKind::custom(io::Error::other(format!("{e:?}"))))?;
|
|
||||||
|
|
||||||
serde_json::from_reader(&mut res).map_err(|e| TransportError::deser_err(e, ""))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
use std::process::Command;
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
println!("cargo:rerun-if-changed=contracts/*");
|
|
||||||
println!("cargo:rerun-if-changed=artifacts/*");
|
|
||||||
|
|
||||||
for line in String::from_utf8(Command::new("solc").args(["--version"]).output().unwrap().stdout)
|
|
||||||
.unwrap()
|
|
||||||
.lines()
|
|
||||||
{
|
|
||||||
if let Some(version) = line.strip_prefix("Version: ") {
|
|
||||||
let version = version.split('+').next().unwrap();
|
|
||||||
assert_eq!(version, "0.8.25");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[rustfmt::skip]
|
|
||||||
let args = [
|
|
||||||
"--base-path", ".",
|
|
||||||
"-o", "./artifacts", "--overwrite",
|
|
||||||
"--bin", "--abi",
|
|
||||||
"--via-ir", "--optimize",
|
|
||||||
|
|
||||||
"./contracts/IERC20.sol",
|
|
||||||
|
|
||||||
"./contracts/Schnorr.sol",
|
|
||||||
"./contracts/Deployer.sol",
|
|
||||||
"./contracts/Sandbox.sol",
|
|
||||||
"./contracts/Router.sol",
|
|
||||||
|
|
||||||
"./src/tests/contracts/Schnorr.sol",
|
|
||||||
"./src/tests/contracts/ERC20.sol",
|
|
||||||
|
|
||||||
"--no-color",
|
|
||||||
];
|
|
||||||
let solc = Command::new("solc").args(args).output().unwrap();
|
|
||||||
assert!(solc.status.success());
|
|
||||||
for line in String::from_utf8(solc.stderr).unwrap().lines() {
|
|
||||||
assert!(!line.starts_with("Error:"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
// SPDX-License-Identifier: AGPLv3
|
|
||||||
pragma solidity ^0.8.0;
|
|
||||||
|
|
||||||
/*
|
|
||||||
The expected deployment process of the Router is as follows:
|
|
||||||
|
|
||||||
1) A transaction deploying Deployer is made. Then, a deterministic signature is
|
|
||||||
created such that an account with an unknown private key is the creator of
|
|
||||||
the contract. Anyone can fund this address, and once anyone does, the
|
|
||||||
transaction deploying Deployer can be published by anyone. No other
|
|
||||||
transaction may be made from that account.
|
|
||||||
|
|
||||||
2) Anyone deploys the Router through the Deployer. This uses a sequential nonce
|
|
||||||
such that meet-in-the-middle attacks, with complexity 2**80, aren't feasible.
|
|
||||||
While such attacks would still be feasible if the Deployer's address was
|
|
||||||
controllable, the usage of a deterministic signature with a NUMS method
|
|
||||||
prevents that.
|
|
||||||
|
|
||||||
This doesn't have any denial-of-service risks and will resolve once anyone steps
|
|
||||||
forward as deployer. This does fail to guarantee an identical address across
|
|
||||||
every chain, though it enables letting anyone efficiently ask the Deployer for
|
|
||||||
the address (with the Deployer having an identical address on every chain).
|
|
||||||
|
|
||||||
Unfortunately, guaranteeing identical addresses aren't feasible. We'd need the
|
|
||||||
Deployer contract to use a consistent salt for the Router, yet the Router must
|
|
||||||
be deployed with a specific public key for Serai. Since Ethereum isn't able to
|
|
||||||
determine a valid public key (one the result of a Serai DKG) from a dishonest
|
|
||||||
public key, we have to allow multiple deployments with Serai being the one to
|
|
||||||
determine which to use.
|
|
||||||
|
|
||||||
The alternative would be to have a council publish the Serai key on-Ethereum,
|
|
||||||
with Serai verifying the published result. This would introduce a DoS risk in
|
|
||||||
the council not publishing the correct key/not publishing any key.
|
|
||||||
*/
|
|
||||||
|
|
||||||
contract Deployer {
|
|
||||||
event Deployment(bytes32 indexed init_code_hash, address created);
|
|
||||||
|
|
||||||
error DeploymentFailed();
|
|
||||||
|
|
||||||
function deploy(bytes memory init_code) external {
|
|
||||||
address created;
|
|
||||||
assembly {
|
|
||||||
created := create(0, add(init_code, 0x20), mload(init_code))
|
|
||||||
}
|
|
||||||
if (created == address(0)) {
|
|
||||||
revert DeploymentFailed();
|
|
||||||
}
|
|
||||||
// These may be emitted out of order upon re-entrancy
|
|
||||||
emit Deployment(keccak256(init_code), created);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
// SPDX-License-Identifier: CC0
|
|
||||||
pragma solidity ^0.8.0;
|
|
||||||
|
|
||||||
interface IERC20 {
|
|
||||||
event Transfer(address indexed from, address indexed to, uint256 value);
|
|
||||||
event Approval(address indexed owner, address indexed spender, uint256 value);
|
|
||||||
|
|
||||||
function name() external view returns (string memory);
|
|
||||||
function symbol() external view returns (string memory);
|
|
||||||
function decimals() external view returns (uint8);
|
|
||||||
|
|
||||||
function totalSupply() external view returns (uint256);
|
|
||||||
|
|
||||||
function balanceOf(address owner) external view returns (uint256);
|
|
||||||
function transfer(address to, uint256 value) external returns (bool);
|
|
||||||
function transferFrom(address from, address to, uint256 value) external returns (bool);
|
|
||||||
|
|
||||||
function approve(address spender, uint256 value) external returns (bool);
|
|
||||||
function allowance(address owner, address spender) external view returns (uint256);
|
|
||||||
}
|
|
||||||
@@ -1,222 +0,0 @@
|
|||||||
// SPDX-License-Identifier: AGPLv3
|
|
||||||
pragma solidity ^0.8.0;
|
|
||||||
|
|
||||||
import "./IERC20.sol";
|
|
||||||
|
|
||||||
import "./Schnorr.sol";
|
|
||||||
import "./Sandbox.sol";
|
|
||||||
|
|
||||||
contract Router {
|
|
||||||
// Nonce is incremented for each batch of transactions executed/key update
|
|
||||||
uint256 public nonce;
|
|
||||||
|
|
||||||
// Current public key's x-coordinate
|
|
||||||
// This key must always have the parity defined within the Schnorr contract
|
|
||||||
bytes32 public seraiKey;
|
|
||||||
|
|
||||||
struct OutInstruction {
|
|
||||||
address to;
|
|
||||||
Call[] calls;
|
|
||||||
|
|
||||||
uint256 value;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Signature {
|
|
||||||
bytes32 c;
|
|
||||||
bytes32 s;
|
|
||||||
}
|
|
||||||
|
|
||||||
event SeraiKeyUpdated(
|
|
||||||
uint256 indexed nonce,
|
|
||||||
bytes32 indexed key,
|
|
||||||
Signature signature
|
|
||||||
);
|
|
||||||
event InInstruction(
|
|
||||||
address indexed from,
|
|
||||||
address indexed coin,
|
|
||||||
uint256 amount,
|
|
||||||
bytes instruction
|
|
||||||
);
|
|
||||||
// success is a uint256 representing a bitfield of transaction successes
|
|
||||||
event Executed(
|
|
||||||
uint256 indexed nonce,
|
|
||||||
bytes32 indexed batch,
|
|
||||||
uint256 success,
|
|
||||||
Signature signature
|
|
||||||
);
|
|
||||||
|
|
||||||
// error types
|
|
||||||
error InvalidKey();
|
|
||||||
error InvalidSignature();
|
|
||||||
error InvalidAmount();
|
|
||||||
error FailedTransfer();
|
|
||||||
error TooManyTransactions();
|
|
||||||
|
|
||||||
modifier _updateSeraiKeyAtEndOfFn(
|
|
||||||
uint256 _nonce,
|
|
||||||
bytes32 key,
|
|
||||||
Signature memory sig
|
|
||||||
) {
|
|
||||||
if (
|
|
||||||
(key == bytes32(0)) ||
|
|
||||||
((bytes32(uint256(key) % Schnorr.Q)) != key)
|
|
||||||
) {
|
|
||||||
revert InvalidKey();
|
|
||||||
}
|
|
||||||
|
|
||||||
_;
|
|
||||||
|
|
||||||
seraiKey = key;
|
|
||||||
emit SeraiKeyUpdated(_nonce, key, sig);
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(bytes32 _seraiKey) _updateSeraiKeyAtEndOfFn(
|
|
||||||
0,
|
|
||||||
_seraiKey,
|
|
||||||
Signature({ c: bytes32(0), s: bytes32(0) })
|
|
||||||
) {
|
|
||||||
nonce = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// updateSeraiKey validates the given Schnorr signature against the current
|
|
||||||
// public key, and if successful, updates the contract's public key to the
|
|
||||||
// given one.
|
|
||||||
function updateSeraiKey(
|
|
||||||
bytes32 _seraiKey,
|
|
||||||
Signature calldata sig
|
|
||||||
) external _updateSeraiKeyAtEndOfFn(nonce, _seraiKey, sig) {
|
|
||||||
bytes memory message =
|
|
||||||
abi.encodePacked("updateSeraiKey", block.chainid, nonce, _seraiKey);
|
|
||||||
nonce++;
|
|
||||||
|
|
||||||
if (!Schnorr.verify(seraiKey, message, sig.c, sig.s)) {
|
|
||||||
revert InvalidSignature();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function inInstruction(
|
|
||||||
address coin,
|
|
||||||
uint256 amount,
|
|
||||||
bytes memory instruction
|
|
||||||
) external payable {
|
|
||||||
if (coin == address(0)) {
|
|
||||||
if (amount != msg.value) {
|
|
||||||
revert InvalidAmount();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
(bool success, bytes memory res) =
|
|
||||||
address(coin).call(
|
|
||||||
abi.encodeWithSelector(
|
|
||||||
IERC20.transferFrom.selector,
|
|
||||||
msg.sender,
|
|
||||||
address(this),
|
|
||||||
amount
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Require there was nothing returned, which is done by some non-standard
|
|
||||||
// tokens, or that the ERC20 contract did in fact return true
|
|
||||||
bool nonStandardResOrTrue =
|
|
||||||
(res.length == 0) || abi.decode(res, (bool));
|
|
||||||
if (!(success && nonStandardResOrTrue)) {
|
|
||||||
revert FailedTransfer();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Due to fee-on-transfer tokens, emitting the amount directly is frowned upon.
|
|
||||||
The amount instructed to transfer may not actually be the amount
|
|
||||||
transferred.
|
|
||||||
|
|
||||||
If we add nonReentrant to every single function which can effect the
|
|
||||||
balance, we can check the amount exactly matches. This prevents transfers of
|
|
||||||
less value than expected occurring, at least, not without an additional
|
|
||||||
transfer to top up the difference (which isn't routed through this contract
|
|
||||||
and accordingly isn't trying to artificially create events).
|
|
||||||
|
|
||||||
If we don't add nonReentrant, a transfer can be started, and then a new
|
|
||||||
transfer for the difference can follow it up (again and again until a
|
|
||||||
rounding error is reached). This contract would believe all transfers were
|
|
||||||
done in full, despite each only being done in part (except for the last
|
|
||||||
one).
|
|
||||||
|
|
||||||
Given fee-on-transfer tokens aren't intended to be supported, the only
|
|
||||||
token planned to be supported is Dai and it doesn't have any fee-on-transfer
|
|
||||||
logic, fee-on-transfer tokens aren't even able to be supported at this time,
|
|
||||||
we simply classify this entire class of tokens as non-standard
|
|
||||||
implementations which induce undefined behavior. It is the Serai network's
|
|
||||||
role not to add support for any non-standard implementations.
|
|
||||||
*/
|
|
||||||
emit InInstruction(msg.sender, coin, amount, instruction);
|
|
||||||
}
|
|
||||||
|
|
||||||
// execute accepts a list of transactions to execute as well as a signature.
|
|
||||||
// if signature verification passes, the given transactions are executed.
|
|
||||||
// if signature verification fails, this function will revert.
|
|
||||||
function execute(
|
|
||||||
OutInstruction[] calldata transactions,
|
|
||||||
Signature calldata sig
|
|
||||||
) external {
|
|
||||||
if (transactions.length > 256) {
|
|
||||||
revert TooManyTransactions();
|
|
||||||
}
|
|
||||||
|
|
||||||
bytes memory message =
|
|
||||||
abi.encode("execute", block.chainid, nonce, transactions);
|
|
||||||
uint256 executed_with_nonce = nonce;
|
|
||||||
// This prevents re-entrancy from causing double spends yet does allow
|
|
||||||
// out-of-order execution via re-entrancy
|
|
||||||
nonce++;
|
|
||||||
|
|
||||||
if (!Schnorr.verify(seraiKey, message, sig.c, sig.s)) {
|
|
||||||
revert InvalidSignature();
|
|
||||||
}
|
|
||||||
|
|
||||||
uint256 successes;
|
|
||||||
for (uint256 i = 0; i < transactions.length; i++) {
|
|
||||||
bool success;
|
|
||||||
|
|
||||||
// If there are no calls, send to `to` the value
|
|
||||||
if (transactions[i].calls.length == 0) {
|
|
||||||
(success, ) = transactions[i].to.call{
|
|
||||||
value: transactions[i].value,
|
|
||||||
gas: 5_000
|
|
||||||
}("");
|
|
||||||
} else {
|
|
||||||
// If there are calls, ignore `to`. Deploy a new Sandbox and proxy the
|
|
||||||
// calls through that
|
|
||||||
//
|
|
||||||
// We could use a single sandbox in order to reduce gas costs, yet that
|
|
||||||
// risks one person creating an approval that's hooked before another
|
|
||||||
// user's intended action executes, in order to drain their coins
|
|
||||||
//
|
|
||||||
// While technically, that would be a flaw in the sandboxed flow, this
|
|
||||||
// is robust and prevents such flaws from being possible
|
|
||||||
//
|
|
||||||
// We also don't want people to set state via the Sandbox and expect it
|
|
||||||
// future available when anyone else could set a distinct value
|
|
||||||
Sandbox sandbox = new Sandbox();
|
|
||||||
(success, ) = address(sandbox).call{
|
|
||||||
value: transactions[i].value,
|
|
||||||
// TODO: Have the Call specify the gas up front
|
|
||||||
gas: 350_000
|
|
||||||
}(
|
|
||||||
abi.encodeWithSelector(
|
|
||||||
Sandbox.sandbox.selector,
|
|
||||||
transactions[i].calls
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
assembly {
|
|
||||||
successes := or(successes, shl(i, success))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
emit Executed(
|
|
||||||
executed_with_nonce,
|
|
||||||
keccak256(message),
|
|
||||||
successes,
|
|
||||||
sig
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
// SPDX-License-Identifier: AGPLv3
|
|
||||||
pragma solidity ^0.8.24;
|
|
||||||
|
|
||||||
struct Call {
|
|
||||||
address to;
|
|
||||||
uint256 value;
|
|
||||||
bytes data;
|
|
||||||
}
|
|
||||||
|
|
||||||
// A minimal sandbox focused on gas efficiency.
|
|
||||||
//
|
|
||||||
// The first call is executed if any of the calls fail, making it a fallback.
|
|
||||||
// All other calls are executed sequentially.
|
|
||||||
contract Sandbox {
|
|
||||||
error AlreadyCalled();
|
|
||||||
error CallsFailed();
|
|
||||||
|
|
||||||
function sandbox(Call[] calldata calls) external payable {
|
|
||||||
// Prevent re-entrancy due to this executing arbitrary calls from anyone
|
|
||||||
// and anywhere
|
|
||||||
bool called;
|
|
||||||
assembly { called := tload(0) }
|
|
||||||
if (called) {
|
|
||||||
revert AlreadyCalled();
|
|
||||||
}
|
|
||||||
assembly { tstore(0, 1) }
|
|
||||||
|
|
||||||
// Execute the calls, starting from 1
|
|
||||||
for (uint256 i = 1; i < calls.length; i++) {
|
|
||||||
(bool success, ) =
|
|
||||||
calls[i].to.call{ value: calls[i].value }(calls[i].data);
|
|
||||||
|
|
||||||
// If this call failed, execute the fallback (call 0)
|
|
||||||
if (!success) {
|
|
||||||
(success, ) =
|
|
||||||
calls[0].to.call{ value: address(this).balance }(calls[0].data);
|
|
||||||
// If this call also failed, revert entirely
|
|
||||||
if (!success) {
|
|
||||||
revert CallsFailed();
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// We don't clear the re-entrancy guard as this contract should never be
|
|
||||||
// called again, so there's no reason to spend the effort
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
// SPDX-License-Identifier: AGPLv3
|
|
||||||
pragma solidity ^0.8.0;
|
|
||||||
|
|
||||||
// see https://github.com/noot/schnorr-verify for implementation details
|
|
||||||
library Schnorr {
|
|
||||||
// secp256k1 group order
|
|
||||||
uint256 constant public Q =
|
|
||||||
0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141;
|
|
||||||
|
|
||||||
// Fixed parity for the public keys used in this contract
|
|
||||||
// This avoids spending a word passing the parity in a similar style to
|
|
||||||
// Bitcoin's Taproot
|
|
||||||
uint8 constant public KEY_PARITY = 27;
|
|
||||||
|
|
||||||
error InvalidSOrA();
|
|
||||||
error MalformedSignature();
|
|
||||||
|
|
||||||
// px := public key x-coord, where the public key has a parity of KEY_PARITY
|
|
||||||
// message := 32-byte hash of the message
|
|
||||||
// c := schnorr signature challenge
|
|
||||||
// s := schnorr signature
|
|
||||||
function verify(
|
|
||||||
bytes32 px,
|
|
||||||
bytes memory message,
|
|
||||||
bytes32 c,
|
|
||||||
bytes32 s
|
|
||||||
) internal pure returns (bool) {
|
|
||||||
// ecrecover = (m, v, r, s) -> key
|
|
||||||
// We instead pass the following to obtain the nonce (not the key)
|
|
||||||
// Then we hash it and verify it matches the challenge
|
|
||||||
bytes32 sa = bytes32(Q - mulmod(uint256(s), uint256(px), Q));
|
|
||||||
bytes32 ca = bytes32(Q - mulmod(uint256(c), uint256(px), Q));
|
|
||||||
|
|
||||||
// For safety, we want each input to ecrecover to be 0 (sa, px, ca)
|
|
||||||
// The ecreover precomple checks `r` and `s` (`px` and `ca`) are non-zero
|
|
||||||
// That leaves us to check `sa` are non-zero
|
|
||||||
if (sa == 0) revert InvalidSOrA();
|
|
||||||
address R = ecrecover(sa, KEY_PARITY, px, ca);
|
|
||||||
if (R == address(0)) revert MalformedSignature();
|
|
||||||
|
|
||||||
// Check the signature is correct by rebuilding the challenge
|
|
||||||
return c == keccak256(abi.encodePacked(R, px, message));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "serai-ethereum-relayer"
|
|
||||||
version = "0.1.0"
|
|
||||||
description = "A relayer for Serai's Ethereum transactions"
|
|
||||||
license = "AGPL-3.0-only"
|
|
||||||
repository = "https://github.com/serai-dex/serai/tree/develop/coins/ethereum/relayer"
|
|
||||||
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
|
|
||||||
keywords = []
|
|
||||||
edition = "2021"
|
|
||||||
publish = false
|
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
|
||||||
all-features = true
|
|
||||||
rustdoc-args = ["--cfg", "docsrs"]
|
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
log = { version = "0.4", default-features = false, features = ["std"] }
|
|
||||||
env_logger = { version = "0.10", default-features = false, features = ["humantime"] }
|
|
||||||
|
|
||||||
tokio = { version = "1", default-features = false, features = ["rt", "time", "io-util", "net", "macros"] }
|
|
||||||
|
|
||||||
serai-env = { path = "../../../common/env" }
|
|
||||||
serai-db = { path = "../../../common/db" }
|
|
||||||
|
|
||||||
[features]
|
|
||||||
parity-db = ["serai-db/parity-db"]
|
|
||||||
rocksdb = ["serai-db/rocksdb"]
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
AGPL-3.0-only license
|
|
||||||
|
|
||||||
Copyright (c) 2023-2024 Luke Parker
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU Affero General Public License Version 3 as
|
|
||||||
published by the Free Software Foundation.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU Affero General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
# Ethereum Transaction Relayer
|
|
||||||
|
|
||||||
This server collects Ethereum router commands to be published, offering an RPC
|
|
||||||
to fetch them.
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
pub(crate) use tokio::{
|
|
||||||
io::{AsyncReadExt, AsyncWriteExt},
|
|
||||||
net::TcpListener,
|
|
||||||
};
|
|
||||||
|
|
||||||
use serai_db::{Get, DbTxn, Db as DbTrait};
|
|
||||||
|
|
||||||
#[tokio::main(flavor = "current_thread")]
|
|
||||||
async fn main() {
|
|
||||||
// Override the panic handler with one which will panic if any tokio task panics
|
|
||||||
{
|
|
||||||
let existing = std::panic::take_hook();
|
|
||||||
std::panic::set_hook(Box::new(move |panic| {
|
|
||||||
existing(panic);
|
|
||||||
const MSG: &str = "exiting the process due to a task panicking";
|
|
||||||
println!("{MSG}");
|
|
||||||
log::error!("{MSG}");
|
|
||||||
std::process::exit(1);
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
if std::env::var("RUST_LOG").is_err() {
|
|
||||||
std::env::set_var("RUST_LOG", serai_env::var("RUST_LOG").unwrap_or_else(|| "info".to_string()));
|
|
||||||
}
|
|
||||||
env_logger::init();
|
|
||||||
|
|
||||||
log::info!("Starting Ethereum relayer server...");
|
|
||||||
|
|
||||||
// Open the DB
|
|
||||||
#[allow(unused_variables, unreachable_code)]
|
|
||||||
let db = {
|
|
||||||
#[cfg(all(feature = "parity-db", feature = "rocksdb"))]
|
|
||||||
panic!("built with parity-db and rocksdb");
|
|
||||||
#[cfg(all(feature = "parity-db", not(feature = "rocksdb")))]
|
|
||||||
let db =
|
|
||||||
serai_db::new_parity_db(&serai_env::var("DB_PATH").expect("path to DB wasn't specified"));
|
|
||||||
#[cfg(feature = "rocksdb")]
|
|
||||||
let db =
|
|
||||||
serai_db::new_rocksdb(&serai_env::var("DB_PATH").expect("path to DB wasn't specified"));
|
|
||||||
db
|
|
||||||
};
|
|
||||||
|
|
||||||
// Start command recipience server
|
|
||||||
// This should not be publicly exposed
|
|
||||||
// TODO: Add auth
|
|
||||||
tokio::spawn({
|
|
||||||
let db = db.clone();
|
|
||||||
async move {
|
|
||||||
// 5132 ^ ((b'E' << 8) | b'R')
|
|
||||||
let server = TcpListener::bind("0.0.0.0:20830").await.unwrap();
|
|
||||||
loop {
|
|
||||||
let (mut socket, _) = server.accept().await.unwrap();
|
|
||||||
let db = db.clone();
|
|
||||||
tokio::spawn(async move {
|
|
||||||
let mut db = db.clone();
|
|
||||||
loop {
|
|
||||||
let Ok(msg_len) = socket.read_u32_le().await else { break };
|
|
||||||
let mut buf = vec![0; usize::try_from(msg_len).unwrap()];
|
|
||||||
let Ok(_) = socket.read_exact(&mut buf).await else { break };
|
|
||||||
|
|
||||||
if buf.len() < 5 {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
let nonce = u32::from_le_bytes(buf[.. 4].try_into().unwrap());
|
|
||||||
let mut txn = db.txn();
|
|
||||||
txn.put(nonce.to_le_bytes(), &buf[4 ..]);
|
|
||||||
txn.commit();
|
|
||||||
|
|
||||||
let Ok(()) = socket.write_all(&[1]).await else { break };
|
|
||||||
|
|
||||||
log::info!("received signed command #{nonce}");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Start command fetch server
|
|
||||||
// 5132 ^ ((b'E' << 8) | b'R') + 1
|
|
||||||
let server = TcpListener::bind("0.0.0.0:20831").await.unwrap();
|
|
||||||
loop {
|
|
||||||
let (mut socket, _) = server.accept().await.unwrap();
|
|
||||||
let db = db.clone();
|
|
||||||
tokio::spawn(async move {
|
|
||||||
let db = db.clone();
|
|
||||||
loop {
|
|
||||||
// Nonce to get the router comamnd for
|
|
||||||
let mut buf = vec![0; 4];
|
|
||||||
let Ok(_) = socket.read_exact(&mut buf).await else { break };
|
|
||||||
|
|
||||||
let command = db.get(&buf[.. 4]).unwrap_or(vec![]);
|
|
||||||
let Ok(()) = socket.write_all(&u32::try_from(command.len()).unwrap().to_le_bytes()).await
|
|
||||||
else {
|
|
||||||
break;
|
|
||||||
};
|
|
||||||
let Ok(()) = socket.write_all(&command).await else { break };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
use alloy_sol_types::sol;
|
|
||||||
|
|
||||||
#[rustfmt::skip]
|
|
||||||
#[allow(warnings)]
|
|
||||||
#[allow(needless_pass_by_value)]
|
|
||||||
#[allow(clippy::all)]
|
|
||||||
#[allow(clippy::ignored_unit_patterns)]
|
|
||||||
#[allow(clippy::redundant_closure_for_method_calls)]
|
|
||||||
mod erc20_container {
|
|
||||||
use super::*;
|
|
||||||
sol!("contracts/IERC20.sol");
|
|
||||||
}
|
|
||||||
pub use erc20_container::IERC20 as erc20;
|
|
||||||
|
|
||||||
#[rustfmt::skip]
|
|
||||||
#[allow(warnings)]
|
|
||||||
#[allow(needless_pass_by_value)]
|
|
||||||
#[allow(clippy::all)]
|
|
||||||
#[allow(clippy::ignored_unit_patterns)]
|
|
||||||
#[allow(clippy::redundant_closure_for_method_calls)]
|
|
||||||
mod deployer_container {
|
|
||||||
use super::*;
|
|
||||||
sol!("contracts/Deployer.sol");
|
|
||||||
}
|
|
||||||
pub use deployer_container::Deployer as deployer;
|
|
||||||
|
|
||||||
#[rustfmt::skip]
|
|
||||||
#[allow(warnings)]
|
|
||||||
#[allow(needless_pass_by_value)]
|
|
||||||
#[allow(clippy::all)]
|
|
||||||
#[allow(clippy::ignored_unit_patterns)]
|
|
||||||
#[allow(clippy::redundant_closure_for_method_calls)]
|
|
||||||
mod router_container {
|
|
||||||
use super::*;
|
|
||||||
sol!(Router, "artifacts/Router.abi");
|
|
||||||
}
|
|
||||||
pub use router_container::Router as router;
|
|
||||||
@@ -1,188 +0,0 @@
|
|||||||
use group::ff::PrimeField;
|
|
||||||
use k256::{
|
|
||||||
elliptic_curve::{ops::Reduce, point::AffineCoordinates, sec1::ToEncodedPoint},
|
|
||||||
ProjectivePoint, Scalar, U256 as KU256,
|
|
||||||
};
|
|
||||||
#[cfg(test)]
|
|
||||||
use k256::{elliptic_curve::point::DecompressPoint, AffinePoint};
|
|
||||||
|
|
||||||
use frost::{
|
|
||||||
algorithm::{Hram, SchnorrSignature},
|
|
||||||
curve::{Ciphersuite, Secp256k1},
|
|
||||||
};
|
|
||||||
|
|
||||||
use alloy_core::primitives::{Parity, Signature as AlloySignature};
|
|
||||||
use alloy_consensus::{SignableTransaction, Signed, TxLegacy};
|
|
||||||
|
|
||||||
use crate::abi::router::{Signature as AbiSignature};
|
|
||||||
|
|
||||||
pub(crate) fn keccak256(data: &[u8]) -> [u8; 32] {
|
|
||||||
alloy_core::primitives::keccak256(data).into()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn hash_to_scalar(data: &[u8]) -> Scalar {
|
|
||||||
<Scalar as Reduce<KU256>>::reduce_bytes(&keccak256(data).into())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn address(point: &ProjectivePoint) -> [u8; 20] {
|
|
||||||
let encoded_point = point.to_encoded_point(false);
|
|
||||||
// Last 20 bytes of the hash of the concatenated x and y coordinates
|
|
||||||
// We obtain the concatenated x and y coordinates via the uncompressed encoding of the point
|
|
||||||
keccak256(&encoded_point.as_ref()[1 .. 65])[12 ..].try_into().unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Deterministically sign a transaction.
|
|
||||||
///
|
|
||||||
/// This function panics if passed a transaction with a non-None chain ID.
|
|
||||||
pub fn deterministically_sign(tx: &TxLegacy) -> Signed<TxLegacy> {
|
|
||||||
assert!(
|
|
||||||
tx.chain_id.is_none(),
|
|
||||||
"chain ID was Some when deterministically signing a TX (causing a non-deterministic signer)"
|
|
||||||
);
|
|
||||||
|
|
||||||
let sig_hash = tx.signature_hash().0;
|
|
||||||
let mut r = hash_to_scalar(&[sig_hash.as_slice(), b"r"].concat());
|
|
||||||
let mut s = hash_to_scalar(&[sig_hash.as_slice(), b"s"].concat());
|
|
||||||
loop {
|
|
||||||
let r_bytes: [u8; 32] = r.to_repr().into();
|
|
||||||
let s_bytes: [u8; 32] = s.to_repr().into();
|
|
||||||
let v = Parity::NonEip155(false);
|
|
||||||
let signature =
|
|
||||||
AlloySignature::from_scalars_and_parity(r_bytes.into(), s_bytes.into(), v).unwrap();
|
|
||||||
let tx = tx.clone().into_signed(signature);
|
|
||||||
if tx.recover_signer().is_ok() {
|
|
||||||
return tx;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Re-hash until valid
|
|
||||||
r = hash_to_scalar(r_bytes.as_ref());
|
|
||||||
s = hash_to_scalar(s_bytes.as_ref());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The public key for a Schnorr-signing account.
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
|
||||||
pub struct PublicKey {
|
|
||||||
pub(crate) A: ProjectivePoint,
|
|
||||||
pub(crate) px: Scalar,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PublicKey {
|
|
||||||
/// Construct a new `PublicKey`.
|
|
||||||
///
|
|
||||||
/// This will return None if the provided point isn't eligible to be a public key (due to
|
|
||||||
/// bounds such as parity).
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
pub fn new(A: ProjectivePoint) -> Option<PublicKey> {
|
|
||||||
let affine = A.to_affine();
|
|
||||||
// Only allow even keys to save a word within Ethereum
|
|
||||||
let is_odd = bool::from(affine.y_is_odd());
|
|
||||||
if is_odd {
|
|
||||||
None?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let x_coord = affine.x();
|
|
||||||
let x_coord_scalar = <Scalar as Reduce<KU256>>::reduce_bytes(&x_coord);
|
|
||||||
// Return None if a reduction would occur
|
|
||||||
// Reductions would be incredibly unlikely and shouldn't be an issue, yet it's one less
|
|
||||||
// headache/concern to have
|
|
||||||
// This does ban a trivial amoount of public keys
|
|
||||||
if x_coord_scalar.to_repr() != x_coord {
|
|
||||||
None?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(PublicKey { A, px: x_coord_scalar })
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn point(&self) -> ProjectivePoint {
|
|
||||||
self.A
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn eth_repr(&self) -> [u8; 32] {
|
|
||||||
self.px.to_repr().into()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
pub(crate) fn from_eth_repr(repr: [u8; 32]) -> Option<Self> {
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
let A = Option::<AffinePoint>::from(AffinePoint::decompress(&repr.into(), 0.into()))?.into();
|
|
||||||
Option::from(Scalar::from_repr(repr.into())).map(|px| PublicKey { A, px })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The HRAm to use for the Schnorr contract.
|
|
||||||
#[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 x_coord = A.to_affine().x();
|
|
||||||
|
|
||||||
let mut data = address(R).to_vec();
|
|
||||||
data.extend(x_coord.as_slice());
|
|
||||||
data.extend(m);
|
|
||||||
|
|
||||||
<Scalar as Reduce<KU256>>::reduce_bytes(&keccak256(&data).into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A signature for the Schnorr contract.
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
|
||||||
pub struct Signature {
|
|
||||||
pub(crate) c: Scalar,
|
|
||||||
pub(crate) s: Scalar,
|
|
||||||
}
|
|
||||||
impl Signature {
|
|
||||||
pub fn verify(&self, public_key: &PublicKey, message: &[u8]) -> bool {
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
let R = (Secp256k1::generator() * self.s) - (public_key.A * self.c);
|
|
||||||
EthereumHram::hram(&R, &public_key.A, message) == self.c
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Construct a new `Signature`.
|
|
||||||
///
|
|
||||||
/// This will return None if the signature is invalid.
|
|
||||||
pub fn new(
|
|
||||||
public_key: &PublicKey,
|
|
||||||
message: &[u8],
|
|
||||||
signature: SchnorrSignature<Secp256k1>,
|
|
||||||
) -> Option<Signature> {
|
|
||||||
let c = EthereumHram::hram(&signature.R, &public_key.A, message);
|
|
||||||
if !signature.verify(public_key.A, c) {
|
|
||||||
None?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let res = Signature { c, s: signature.s };
|
|
||||||
assert!(res.verify(public_key, message));
|
|
||||||
Some(res)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn c(&self) -> Scalar {
|
|
||||||
self.c
|
|
||||||
}
|
|
||||||
pub fn s(&self) -> Scalar {
|
|
||||||
self.s
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn to_bytes(&self) -> [u8; 64] {
|
|
||||||
let mut res = [0; 64];
|
|
||||||
res[.. 32].copy_from_slice(self.c.to_repr().as_ref());
|
|
||||||
res[32 ..].copy_from_slice(self.s.to_repr().as_ref());
|
|
||||||
res
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn from_bytes(bytes: [u8; 64]) -> std::io::Result<Self> {
|
|
||||||
let mut reader = bytes.as_slice();
|
|
||||||
let c = Secp256k1::read_F(&mut reader)?;
|
|
||||||
let s = Secp256k1::read_F(&mut reader)?;
|
|
||||||
Ok(Signature { c, s })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl From<&Signature> for AbiSignature {
|
|
||||||
fn from(sig: &Signature) -> AbiSignature {
|
|
||||||
let c: [u8; 32] = sig.c.to_repr().into();
|
|
||||||
let s: [u8; 32] = sig.s.to_repr().into();
|
|
||||||
AbiSignature { c: c.into(), s: s.into() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,113 +0,0 @@
|
|||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use alloy_core::primitives::{hex::FromHex, Address, B256, U256, Bytes, TxKind};
|
|
||||||
use alloy_consensus::{Signed, TxLegacy};
|
|
||||||
|
|
||||||
use alloy_sol_types::{SolCall, SolEvent};
|
|
||||||
|
|
||||||
use alloy_rpc_types::{BlockNumberOrTag, Filter};
|
|
||||||
use alloy_simple_request_transport::SimpleRequest;
|
|
||||||
use alloy_provider::{Provider, RootProvider};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
Error,
|
|
||||||
crypto::{self, keccak256, PublicKey},
|
|
||||||
router::Router,
|
|
||||||
};
|
|
||||||
pub use crate::abi::deployer as abi;
|
|
||||||
|
|
||||||
/// The Deployer contract for the Router contract.
|
|
||||||
///
|
|
||||||
/// This Deployer has a deterministic address, letting it be immediately identified on any
|
|
||||||
/// compatible chain. It then supports retrieving the Router contract's address (which isn't
|
|
||||||
/// deterministic) using a single log query.
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct Deployer;
|
|
||||||
impl Deployer {
|
|
||||||
/// Obtain the transaction to deploy this contract, already signed.
|
|
||||||
///
|
|
||||||
/// The account this transaction is sent from (which is populated in `from`) must be sufficiently
|
|
||||||
/// funded for this transaction to be submitted. This account has no known private key to anyone,
|
|
||||||
/// so ETH sent can be neither misappropriated nor returned.
|
|
||||||
pub fn deployment_tx() -> Signed<TxLegacy> {
|
|
||||||
let bytecode = include_str!("../artifacts/Deployer.bin");
|
|
||||||
let bytecode =
|
|
||||||
Bytes::from_hex(bytecode).expect("compiled-in Deployer bytecode wasn't valid hex");
|
|
||||||
|
|
||||||
let tx = TxLegacy {
|
|
||||||
chain_id: None,
|
|
||||||
nonce: 0,
|
|
||||||
gas_price: 100_000_000_000u128,
|
|
||||||
// TODO: Use a more accurate gas limit
|
|
||||||
gas_limit: 1_000_000u128,
|
|
||||||
to: TxKind::Create,
|
|
||||||
value: U256::ZERO,
|
|
||||||
input: bytecode,
|
|
||||||
};
|
|
||||||
|
|
||||||
crypto::deterministically_sign(&tx)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Obtain the deterministic address for this contract.
|
|
||||||
pub fn address() -> [u8; 20] {
|
|
||||||
let deployer_deployer =
|
|
||||||
Self::deployment_tx().recover_signer().expect("deployment_tx didn't have a valid signature");
|
|
||||||
**Address::create(&deployer_deployer, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Construct a new view of the `Deployer`.
|
|
||||||
pub async fn new(provider: Arc<RootProvider<SimpleRequest>>) -> Result<Option<Self>, Error> {
|
|
||||||
let address = Self::address();
|
|
||||||
let code = provider.get_code_at(address.into()).await.map_err(|_| Error::ConnectionError)?;
|
|
||||||
// Contract has yet to be deployed
|
|
||||||
if code.is_empty() {
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
Ok(Some(Self))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Yield the `ContractCall` necessary to deploy the Router.
|
|
||||||
pub fn deploy_router(&self, key: &PublicKey) -> TxLegacy {
|
|
||||||
TxLegacy {
|
|
||||||
to: TxKind::Call(Self::address().into()),
|
|
||||||
input: abi::deployCall::new((Router::init_code(key).into(),)).abi_encode().into(),
|
|
||||||
gas_limit: 1_000_000,
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Find the first Router deployed with the specified key as its first key.
|
|
||||||
///
|
|
||||||
/// This is the Router Serai will use, and is the only way to construct a `Router`.
|
|
||||||
pub async fn find_router(
|
|
||||||
&self,
|
|
||||||
provider: Arc<RootProvider<SimpleRequest>>,
|
|
||||||
key: &PublicKey,
|
|
||||||
) -> Result<Option<Router>, Error> {
|
|
||||||
let init_code = Router::init_code(key);
|
|
||||||
let init_code_hash = keccak256(&init_code);
|
|
||||||
|
|
||||||
#[cfg(not(test))]
|
|
||||||
let to_block = BlockNumberOrTag::Finalized;
|
|
||||||
#[cfg(test)]
|
|
||||||
let to_block = BlockNumberOrTag::Latest;
|
|
||||||
|
|
||||||
// Find the first log using this init code (where the init code is binding to the key)
|
|
||||||
// TODO: Make an abstraction for event filtering (de-duplicating common code)
|
|
||||||
let filter =
|
|
||||||
Filter::new().from_block(0).to_block(to_block).address(Address::from(Self::address()));
|
|
||||||
let filter = filter.event_signature(abi::Deployment::SIGNATURE_HASH);
|
|
||||||
let filter = filter.topic1(B256::from(init_code_hash));
|
|
||||||
let logs = provider.get_logs(&filter).await.map_err(|_| Error::ConnectionError)?;
|
|
||||||
|
|
||||||
let Some(first_log) = logs.first() else { return Ok(None) };
|
|
||||||
let router = first_log
|
|
||||||
.log_decode::<abi::Deployment>()
|
|
||||||
.map_err(|_| Error::ConnectionError)?
|
|
||||||
.inner
|
|
||||||
.data
|
|
||||||
.created;
|
|
||||||
|
|
||||||
Ok(Some(Router::new(provider, router)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
use std::{sync::Arc, collections::HashSet};
|
|
||||||
|
|
||||||
use alloy_core::primitives::{Address, B256, U256};
|
|
||||||
|
|
||||||
use alloy_sol_types::{SolInterface, SolEvent};
|
|
||||||
|
|
||||||
use alloy_rpc_types::Filter;
|
|
||||||
use alloy_simple_request_transport::SimpleRequest;
|
|
||||||
use alloy_provider::{Provider, RootProvider};
|
|
||||||
|
|
||||||
use crate::Error;
|
|
||||||
pub use crate::abi::erc20 as abi;
|
|
||||||
use abi::{IERC20Calls, Transfer, transferCall, transferFromCall};
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct TopLevelErc20Transfer {
|
|
||||||
pub id: [u8; 32],
|
|
||||||
pub from: [u8; 20],
|
|
||||||
pub amount: U256,
|
|
||||||
pub data: Vec<u8>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A view for an ERC20 contract.
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct Erc20(Arc<RootProvider<SimpleRequest>>, Address);
|
|
||||||
impl Erc20 {
|
|
||||||
/// Construct a new view of the specified ERC20 contract.
|
|
||||||
pub fn new(provider: Arc<RootProvider<SimpleRequest>>, address: [u8; 20]) -> Self {
|
|
||||||
Self(provider, Address::from(&address))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn top_level_transfers(
|
|
||||||
&self,
|
|
||||||
block: u64,
|
|
||||||
to: [u8; 20],
|
|
||||||
) -> Result<Vec<TopLevelErc20Transfer>, Error> {
|
|
||||||
let filter = Filter::new().from_block(block).to_block(block).address(self.1);
|
|
||||||
let filter = filter.event_signature(Transfer::SIGNATURE_HASH);
|
|
||||||
let mut to_topic = [0; 32];
|
|
||||||
to_topic[12 ..].copy_from_slice(&to);
|
|
||||||
let filter = filter.topic2(B256::from(to_topic));
|
|
||||||
let logs = self.0.get_logs(&filter).await.map_err(|_| Error::ConnectionError)?;
|
|
||||||
|
|
||||||
let mut handled = HashSet::new();
|
|
||||||
|
|
||||||
let mut top_level_transfers = vec![];
|
|
||||||
for log in logs {
|
|
||||||
// Double check the address which emitted this log
|
|
||||||
if log.address() != self.1 {
|
|
||||||
Err(Error::ConnectionError)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let tx_id = log.transaction_hash.ok_or(Error::ConnectionError)?;
|
|
||||||
let tx =
|
|
||||||
self.0.get_transaction_by_hash(tx_id).await.ok().flatten().ok_or(Error::ConnectionError)?;
|
|
||||||
|
|
||||||
// If this is a top-level call...
|
|
||||||
if tx.to == Some(self.1) {
|
|
||||||
// And we recognize the call...
|
|
||||||
// Don't validate the encoding as this can't be re-encoded to an identical bytestring due
|
|
||||||
// to the InInstruction appended
|
|
||||||
if let Ok(call) = IERC20Calls::abi_decode(&tx.input, false) {
|
|
||||||
// Extract the top-level call's from/to/value
|
|
||||||
let (from, call_to, value) = match call {
|
|
||||||
IERC20Calls::transfer(transferCall { to: call_to, value }) => (tx.from, call_to, value),
|
|
||||||
IERC20Calls::transferFrom(transferFromCall { from, to: call_to, value }) => {
|
|
||||||
(from, call_to, value)
|
|
||||||
}
|
|
||||||
// Treat any other function selectors as unrecognized
|
|
||||||
_ => continue,
|
|
||||||
};
|
|
||||||
|
|
||||||
let log = log.log_decode::<Transfer>().map_err(|_| Error::ConnectionError)?.inner.data;
|
|
||||||
|
|
||||||
// Ensure the top-level transfer is equivalent, and this presumably isn't a log for an
|
|
||||||
// internal transfer
|
|
||||||
if (log.from != from) || (call_to != to) || (value != log.value) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now that the top-level transfer is confirmed to be equivalent to the log, ensure it's
|
|
||||||
// the only log we handle
|
|
||||||
if handled.contains(&tx_id) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
handled.insert(tx_id);
|
|
||||||
|
|
||||||
// Read the data appended after
|
|
||||||
let encoded = call.abi_encode();
|
|
||||||
let data = tx.input.as_ref()[encoded.len() ..].to_vec();
|
|
||||||
|
|
||||||
// Push the transfer
|
|
||||||
top_level_transfers.push(TopLevelErc20Transfer {
|
|
||||||
// Since we'll only handle one log for this TX, set the ID to the TX ID
|
|
||||||
id: *tx_id,
|
|
||||||
from: *log.from.0,
|
|
||||||
amount: log.value,
|
|
||||||
data,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(top_level_transfers)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
pub mod alloy {
|
|
||||||
pub use alloy_core::primitives;
|
|
||||||
pub use alloy_core as core;
|
|
||||||
pub use alloy_sol_types as sol_types;
|
|
||||||
|
|
||||||
pub use alloy_consensus as consensus;
|
|
||||||
pub use alloy_network as network;
|
|
||||||
pub use alloy_rpc_types as rpc_types;
|
|
||||||
pub use alloy_simple_request_transport as simple_request_transport;
|
|
||||||
pub use alloy_rpc_client as rpc_client;
|
|
||||||
pub use alloy_provider as provider;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub mod crypto;
|
|
||||||
|
|
||||||
pub(crate) mod abi;
|
|
||||||
|
|
||||||
pub mod erc20;
|
|
||||||
pub mod deployer;
|
|
||||||
pub mod router;
|
|
||||||
|
|
||||||
pub mod machine;
|
|
||||||
|
|
||||||
#[cfg(any(test, feature = "tests"))]
|
|
||||||
pub mod tests;
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Error)]
|
|
||||||
pub enum Error {
|
|
||||||
#[error("failed to verify Schnorr signature")]
|
|
||||||
InvalidSignature,
|
|
||||||
#[error("couldn't make call/send TX")]
|
|
||||||
ConnectionError,
|
|
||||||
}
|
|
||||||
@@ -1,414 +0,0 @@
|
|||||||
use std::{
|
|
||||||
io::{self, Read},
|
|
||||||
collections::HashMap,
|
|
||||||
};
|
|
||||||
|
|
||||||
use rand_core::{RngCore, CryptoRng};
|
|
||||||
|
|
||||||
use transcript::{Transcript, RecommendedTranscript};
|
|
||||||
|
|
||||||
use group::GroupEncoding;
|
|
||||||
use frost::{
|
|
||||||
curve::{Ciphersuite, Secp256k1},
|
|
||||||
Participant, ThresholdKeys, FrostError,
|
|
||||||
algorithm::Schnorr,
|
|
||||||
sign::*,
|
|
||||||
};
|
|
||||||
|
|
||||||
use alloy_core::primitives::U256;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
crypto::{PublicKey, EthereumHram, Signature},
|
|
||||||
router::{
|
|
||||||
abi::{Call as AbiCall, OutInstruction as AbiOutInstruction},
|
|
||||||
Router,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
|
||||||
pub struct Call {
|
|
||||||
pub to: [u8; 20],
|
|
||||||
pub value: U256,
|
|
||||||
pub data: Vec<u8>,
|
|
||||||
}
|
|
||||||
impl Call {
|
|
||||||
pub fn read<R: io::Read>(reader: &mut R) -> io::Result<Self> {
|
|
||||||
let mut to = [0; 20];
|
|
||||||
reader.read_exact(&mut to)?;
|
|
||||||
|
|
||||||
let value = {
|
|
||||||
let mut value_bytes = [0; 32];
|
|
||||||
reader.read_exact(&mut value_bytes)?;
|
|
||||||
U256::from_le_slice(&value_bytes)
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut data_len = {
|
|
||||||
let mut data_len = [0; 4];
|
|
||||||
reader.read_exact(&mut data_len)?;
|
|
||||||
usize::try_from(u32::from_le_bytes(data_len)).expect("u32 couldn't fit within a usize")
|
|
||||||
};
|
|
||||||
|
|
||||||
// A valid DoS would be to claim a 4 GB data is present for only 4 bytes
|
|
||||||
// We read this in 1 KB chunks to only read data actually present (with a max DoS of 1 KB)
|
|
||||||
let mut data = vec![];
|
|
||||||
while data_len > 0 {
|
|
||||||
let chunk_len = data_len.min(1024);
|
|
||||||
let mut chunk = vec![0; chunk_len];
|
|
||||||
reader.read_exact(&mut chunk)?;
|
|
||||||
data.extend(&chunk);
|
|
||||||
data_len -= chunk_len;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Call { to, value, data })
|
|
||||||
}
|
|
||||||
|
|
||||||
fn write<W: io::Write>(&self, writer: &mut W) -> io::Result<()> {
|
|
||||||
writer.write_all(&self.to)?;
|
|
||||||
writer.write_all(&self.value.as_le_bytes())?;
|
|
||||||
|
|
||||||
let data_len = u32::try_from(self.data.len())
|
|
||||||
.map_err(|_| io::Error::other("call data length exceeded 2**32"))?;
|
|
||||||
writer.write_all(&data_len.to_le_bytes())?;
|
|
||||||
writer.write_all(&self.data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl From<Call> for AbiCall {
|
|
||||||
fn from(call: Call) -> AbiCall {
|
|
||||||
AbiCall { to: call.to.into(), value: call.value, data: call.data.into() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
|
||||||
pub enum OutInstructionTarget {
|
|
||||||
Direct([u8; 20]),
|
|
||||||
Calls(Vec<Call>),
|
|
||||||
}
|
|
||||||
impl OutInstructionTarget {
|
|
||||||
fn read<R: io::Read>(reader: &mut R) -> io::Result<Self> {
|
|
||||||
let mut kind = [0xff];
|
|
||||||
reader.read_exact(&mut kind)?;
|
|
||||||
|
|
||||||
match kind[0] {
|
|
||||||
0 => {
|
|
||||||
let mut addr = [0; 20];
|
|
||||||
reader.read_exact(&mut addr)?;
|
|
||||||
Ok(OutInstructionTarget::Direct(addr))
|
|
||||||
}
|
|
||||||
1 => {
|
|
||||||
let mut calls_len = [0; 4];
|
|
||||||
reader.read_exact(&mut calls_len)?;
|
|
||||||
let calls_len = u32::from_le_bytes(calls_len);
|
|
||||||
|
|
||||||
let mut calls = vec![];
|
|
||||||
for _ in 0 .. calls_len {
|
|
||||||
calls.push(Call::read(reader)?);
|
|
||||||
}
|
|
||||||
Ok(OutInstructionTarget::Calls(calls))
|
|
||||||
}
|
|
||||||
_ => Err(io::Error::other("unrecognized OutInstructionTarget"))?,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn write<W: io::Write>(&self, writer: &mut W) -> io::Result<()> {
|
|
||||||
match self {
|
|
||||||
OutInstructionTarget::Direct(addr) => {
|
|
||||||
writer.write_all(&[0])?;
|
|
||||||
writer.write_all(addr)?;
|
|
||||||
}
|
|
||||||
OutInstructionTarget::Calls(calls) => {
|
|
||||||
writer.write_all(&[1])?;
|
|
||||||
let call_len = u32::try_from(calls.len())
|
|
||||||
.map_err(|_| io::Error::other("amount of calls exceeded 2**32"))?;
|
|
||||||
writer.write_all(&call_len.to_le_bytes())?;
|
|
||||||
for call in calls {
|
|
||||||
call.write(writer)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
|
||||||
pub struct OutInstruction {
|
|
||||||
pub target: OutInstructionTarget,
|
|
||||||
pub value: U256,
|
|
||||||
}
|
|
||||||
impl OutInstruction {
|
|
||||||
fn read<R: io::Read>(reader: &mut R) -> io::Result<Self> {
|
|
||||||
let target = OutInstructionTarget::read(reader)?;
|
|
||||||
|
|
||||||
let value = {
|
|
||||||
let mut value_bytes = [0; 32];
|
|
||||||
reader.read_exact(&mut value_bytes)?;
|
|
||||||
U256::from_le_slice(&value_bytes)
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(OutInstruction { target, value })
|
|
||||||
}
|
|
||||||
fn write<W: io::Write>(&self, writer: &mut W) -> io::Result<()> {
|
|
||||||
self.target.write(writer)?;
|
|
||||||
writer.write_all(&self.value.as_le_bytes())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl From<OutInstruction> for AbiOutInstruction {
|
|
||||||
fn from(instruction: OutInstruction) -> AbiOutInstruction {
|
|
||||||
match instruction.target {
|
|
||||||
OutInstructionTarget::Direct(addr) => {
|
|
||||||
AbiOutInstruction { to: addr.into(), calls: vec![], value: instruction.value }
|
|
||||||
}
|
|
||||||
OutInstructionTarget::Calls(calls) => AbiOutInstruction {
|
|
||||||
to: [0; 20].into(),
|
|
||||||
calls: calls.into_iter().map(Into::into).collect(),
|
|
||||||
value: instruction.value,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
|
||||||
pub enum RouterCommand {
|
|
||||||
UpdateSeraiKey { chain_id: U256, nonce: U256, key: PublicKey },
|
|
||||||
Execute { chain_id: U256, nonce: U256, outs: Vec<OutInstruction> },
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RouterCommand {
|
|
||||||
pub fn msg(&self) -> Vec<u8> {
|
|
||||||
match self {
|
|
||||||
RouterCommand::UpdateSeraiKey { chain_id, nonce, key } => {
|
|
||||||
Router::update_serai_key_message(*chain_id, *nonce, key)
|
|
||||||
}
|
|
||||||
RouterCommand::Execute { chain_id, nonce, outs } => Router::execute_message(
|
|
||||||
*chain_id,
|
|
||||||
*nonce,
|
|
||||||
outs.iter().map(|out| out.clone().into()).collect(),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn read<R: io::Read>(reader: &mut R) -> io::Result<Self> {
|
|
||||||
let mut kind = [0xff];
|
|
||||||
reader.read_exact(&mut kind)?;
|
|
||||||
|
|
||||||
match kind[0] {
|
|
||||||
0 => {
|
|
||||||
let mut chain_id = [0; 32];
|
|
||||||
reader.read_exact(&mut chain_id)?;
|
|
||||||
|
|
||||||
let mut nonce = [0; 32];
|
|
||||||
reader.read_exact(&mut nonce)?;
|
|
||||||
|
|
||||||
let key = PublicKey::new(Secp256k1::read_G(reader)?)
|
|
||||||
.ok_or(io::Error::other("key for RouterCommand doesn't have an eth representation"))?;
|
|
||||||
Ok(RouterCommand::UpdateSeraiKey {
|
|
||||||
chain_id: U256::from_le_slice(&chain_id),
|
|
||||||
nonce: U256::from_le_slice(&nonce),
|
|
||||||
key,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
1 => {
|
|
||||||
let mut chain_id = [0; 32];
|
|
||||||
reader.read_exact(&mut chain_id)?;
|
|
||||||
let chain_id = U256::from_le_slice(&chain_id);
|
|
||||||
|
|
||||||
let mut nonce = [0; 32];
|
|
||||||
reader.read_exact(&mut nonce)?;
|
|
||||||
let nonce = U256::from_le_slice(&nonce);
|
|
||||||
|
|
||||||
let mut outs_len = [0; 4];
|
|
||||||
reader.read_exact(&mut outs_len)?;
|
|
||||||
let outs_len = u32::from_le_bytes(outs_len);
|
|
||||||
|
|
||||||
let mut outs = vec![];
|
|
||||||
for _ in 0 .. outs_len {
|
|
||||||
outs.push(OutInstruction::read(reader)?);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(RouterCommand::Execute { chain_id, nonce, outs })
|
|
||||||
}
|
|
||||||
_ => Err(io::Error::other("reading unknown type of RouterCommand"))?,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn write<W: io::Write>(&self, writer: &mut W) -> io::Result<()> {
|
|
||||||
match self {
|
|
||||||
RouterCommand::UpdateSeraiKey { chain_id, nonce, key } => {
|
|
||||||
writer.write_all(&[0])?;
|
|
||||||
writer.write_all(&chain_id.as_le_bytes())?;
|
|
||||||
writer.write_all(&nonce.as_le_bytes())?;
|
|
||||||
writer.write_all(&key.A.to_bytes())
|
|
||||||
}
|
|
||||||
RouterCommand::Execute { chain_id, nonce, outs } => {
|
|
||||||
writer.write_all(&[1])?;
|
|
||||||
writer.write_all(&chain_id.as_le_bytes())?;
|
|
||||||
writer.write_all(&nonce.as_le_bytes())?;
|
|
||||||
writer.write_all(&u32::try_from(outs.len()).unwrap().to_le_bytes())?;
|
|
||||||
for out in outs {
|
|
||||||
out.write(writer)?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn serialize(&self) -> Vec<u8> {
|
|
||||||
let mut res = vec![];
|
|
||||||
self.write(&mut res).unwrap();
|
|
||||||
res
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
|
||||||
pub struct SignedRouterCommand {
|
|
||||||
command: RouterCommand,
|
|
||||||
signature: Signature,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SignedRouterCommand {
|
|
||||||
pub fn new(key: &PublicKey, command: RouterCommand, signature: &[u8; 64]) -> Option<Self> {
|
|
||||||
let c = Secp256k1::read_F(&mut &signature[.. 32]).ok()?;
|
|
||||||
let s = Secp256k1::read_F(&mut &signature[32 ..]).ok()?;
|
|
||||||
let signature = Signature { c, s };
|
|
||||||
|
|
||||||
if !signature.verify(key, &command.msg()) {
|
|
||||||
None?
|
|
||||||
}
|
|
||||||
Some(SignedRouterCommand { command, signature })
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn command(&self) -> &RouterCommand {
|
|
||||||
&self.command
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn signature(&self) -> &Signature {
|
|
||||||
&self.signature
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn read<R: io::Read>(reader: &mut R) -> io::Result<Self> {
|
|
||||||
let command = RouterCommand::read(reader)?;
|
|
||||||
|
|
||||||
let mut sig = [0; 64];
|
|
||||||
reader.read_exact(&mut sig)?;
|
|
||||||
let signature = Signature::from_bytes(sig)?;
|
|
||||||
|
|
||||||
Ok(SignedRouterCommand { command, signature })
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn write<W: io::Write>(&self, writer: &mut W) -> io::Result<()> {
|
|
||||||
self.command.write(writer)?;
|
|
||||||
writer.write_all(&self.signature.to_bytes())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct RouterCommandMachine {
|
|
||||||
key: PublicKey,
|
|
||||||
command: RouterCommand,
|
|
||||||
machine: AlgorithmMachine<Secp256k1, Schnorr<Secp256k1, RecommendedTranscript, EthereumHram>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RouterCommandMachine {
|
|
||||||
pub fn new(keys: ThresholdKeys<Secp256k1>, command: RouterCommand) -> Option<Self> {
|
|
||||||
// The Schnorr algorithm should be fine without this, even when using the IETF variant
|
|
||||||
// If this is better and more comprehensive, we should do it, even if not necessary
|
|
||||||
let mut transcript = RecommendedTranscript::new(b"ethereum-serai RouterCommandMachine v0.1");
|
|
||||||
let key = keys.group_key();
|
|
||||||
transcript.append_message(b"key", key.to_bytes());
|
|
||||||
transcript.append_message(b"command", command.serialize());
|
|
||||||
|
|
||||||
Some(Self {
|
|
||||||
key: PublicKey::new(key)?,
|
|
||||||
command,
|
|
||||||
machine: AlgorithmMachine::new(Schnorr::new(transcript), keys),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PreprocessMachine for RouterCommandMachine {
|
|
||||||
type Preprocess = Preprocess<Secp256k1, ()>;
|
|
||||||
type Signature = SignedRouterCommand;
|
|
||||||
type SignMachine = RouterCommandSignMachine;
|
|
||||||
|
|
||||||
fn preprocess<R: RngCore + CryptoRng>(
|
|
||||||
self,
|
|
||||||
rng: &mut R,
|
|
||||||
) -> (Self::SignMachine, Self::Preprocess) {
|
|
||||||
let (machine, preprocess) = self.machine.preprocess(rng);
|
|
||||||
|
|
||||||
(RouterCommandSignMachine { key: self.key, command: self.command, machine }, preprocess)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct RouterCommandSignMachine {
|
|
||||||
key: PublicKey,
|
|
||||||
command: RouterCommand,
|
|
||||||
machine: AlgorithmSignMachine<Secp256k1, Schnorr<Secp256k1, RecommendedTranscript, EthereumHram>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SignMachine<SignedRouterCommand> for RouterCommandSignMachine {
|
|
||||||
type Params = ();
|
|
||||||
type Keys = ThresholdKeys<Secp256k1>;
|
|
||||||
type Preprocess = Preprocess<Secp256k1, ()>;
|
|
||||||
type SignatureShare = SignatureShare<Secp256k1>;
|
|
||||||
type SignatureMachine = RouterCommandSignatureMachine;
|
|
||||||
|
|
||||||
fn cache(self) -> CachedPreprocess {
|
|
||||||
unimplemented!(
|
|
||||||
"RouterCommand machines don't support caching their preprocesses due to {}",
|
|
||||||
"being already bound to a specific command"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn from_cache(
|
|
||||||
(): (),
|
|
||||||
_: ThresholdKeys<Secp256k1>,
|
|
||||||
_: CachedPreprocess,
|
|
||||||
) -> (Self, Self::Preprocess) {
|
|
||||||
unimplemented!(
|
|
||||||
"RouterCommand machines don't support caching their preprocesses due to {}",
|
|
||||||
"being already bound to a specific command"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_preprocess<R: Read>(&self, reader: &mut R) -> io::Result<Self::Preprocess> {
|
|
||||||
self.machine.read_preprocess(reader)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn sign(
|
|
||||||
self,
|
|
||||||
commitments: HashMap<Participant, Self::Preprocess>,
|
|
||||||
msg: &[u8],
|
|
||||||
) -> Result<(RouterCommandSignatureMachine, Self::SignatureShare), FrostError> {
|
|
||||||
if !msg.is_empty() {
|
|
||||||
panic!("message was passed to a RouterCommand machine when it generates its own");
|
|
||||||
}
|
|
||||||
|
|
||||||
let (machine, share) = self.machine.sign(commitments, &self.command.msg())?;
|
|
||||||
|
|
||||||
Ok((RouterCommandSignatureMachine { key: self.key, command: self.command, machine }, share))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct RouterCommandSignatureMachine {
|
|
||||||
key: PublicKey,
|
|
||||||
command: RouterCommand,
|
|
||||||
machine:
|
|
||||||
AlgorithmSignatureMachine<Secp256k1, Schnorr<Secp256k1, RecommendedTranscript, EthereumHram>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SignatureMachine<SignedRouterCommand> for RouterCommandSignatureMachine {
|
|
||||||
type SignatureShare = SignatureShare<Secp256k1>;
|
|
||||||
|
|
||||||
fn read_share<R: Read>(&self, reader: &mut R) -> io::Result<Self::SignatureShare> {
|
|
||||||
self.machine.read_share(reader)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn complete(
|
|
||||||
self,
|
|
||||||
shares: HashMap<Participant, Self::SignatureShare>,
|
|
||||||
) -> Result<SignedRouterCommand, FrostError> {
|
|
||||||
let sig = self.machine.complete(shares)?;
|
|
||||||
let signature = Signature::new(&self.key, &self.command.msg(), sig)
|
|
||||||
.expect("machine produced an invalid signature");
|
|
||||||
Ok(SignedRouterCommand { command: self.command, signature })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,443 +0,0 @@
|
|||||||
use std::{sync::Arc, io, collections::HashSet};
|
|
||||||
|
|
||||||
use k256::{
|
|
||||||
elliptic_curve::{group::GroupEncoding, sec1},
|
|
||||||
ProjectivePoint,
|
|
||||||
};
|
|
||||||
|
|
||||||
use alloy_core::primitives::{hex::FromHex, Address, U256, Bytes, TxKind};
|
|
||||||
#[cfg(test)]
|
|
||||||
use alloy_core::primitives::B256;
|
|
||||||
use alloy_consensus::TxLegacy;
|
|
||||||
|
|
||||||
use alloy_sol_types::{SolValue, SolConstructor, SolCall, SolEvent};
|
|
||||||
|
|
||||||
use alloy_rpc_types::Filter;
|
|
||||||
#[cfg(test)]
|
|
||||||
use alloy_rpc_types::{BlockId, TransactionRequest, TransactionInput};
|
|
||||||
use alloy_simple_request_transport::SimpleRequest;
|
|
||||||
use alloy_provider::{Provider, RootProvider};
|
|
||||||
|
|
||||||
pub use crate::{
|
|
||||||
Error,
|
|
||||||
crypto::{PublicKey, Signature},
|
|
||||||
abi::{erc20::Transfer, router as abi},
|
|
||||||
};
|
|
||||||
use abi::{SeraiKeyUpdated, InInstruction as InInstructionEvent, Executed as ExecutedEvent};
|
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
|
||||||
pub enum Coin {
|
|
||||||
Ether,
|
|
||||||
Erc20([u8; 20]),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Coin {
|
|
||||||
pub fn read<R: io::Read>(reader: &mut R) -> io::Result<Self> {
|
|
||||||
let mut kind = [0xff];
|
|
||||||
reader.read_exact(&mut kind)?;
|
|
||||||
Ok(match kind[0] {
|
|
||||||
0 => Coin::Ether,
|
|
||||||
1 => {
|
|
||||||
let mut address = [0; 20];
|
|
||||||
reader.read_exact(&mut address)?;
|
|
||||||
Coin::Erc20(address)
|
|
||||||
}
|
|
||||||
_ => Err(io::Error::other("unrecognized Coin type"))?,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn write<W: io::Write>(&self, writer: &mut W) -> io::Result<()> {
|
|
||||||
match self {
|
|
||||||
Coin::Ether => writer.write_all(&[0]),
|
|
||||||
Coin::Erc20(token) => {
|
|
||||||
writer.write_all(&[1])?;
|
|
||||||
writer.write_all(token)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
|
||||||
pub struct InInstruction {
|
|
||||||
pub id: ([u8; 32], u64),
|
|
||||||
pub from: [u8; 20],
|
|
||||||
pub coin: Coin,
|
|
||||||
pub amount: U256,
|
|
||||||
pub data: Vec<u8>,
|
|
||||||
pub key_at_end_of_block: ProjectivePoint,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl InInstruction {
|
|
||||||
pub fn read<R: io::Read>(reader: &mut R) -> io::Result<Self> {
|
|
||||||
let id = {
|
|
||||||
let mut id_hash = [0; 32];
|
|
||||||
reader.read_exact(&mut id_hash)?;
|
|
||||||
let mut id_pos = [0; 8];
|
|
||||||
reader.read_exact(&mut id_pos)?;
|
|
||||||
let id_pos = u64::from_le_bytes(id_pos);
|
|
||||||
(id_hash, id_pos)
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut from = [0; 20];
|
|
||||||
reader.read_exact(&mut from)?;
|
|
||||||
|
|
||||||
let coin = Coin::read(reader)?;
|
|
||||||
let mut amount = [0; 32];
|
|
||||||
reader.read_exact(&mut amount)?;
|
|
||||||
let amount = U256::from_le_slice(&amount);
|
|
||||||
|
|
||||||
let mut data_len = [0; 4];
|
|
||||||
reader.read_exact(&mut data_len)?;
|
|
||||||
let data_len = usize::try_from(u32::from_le_bytes(data_len))
|
|
||||||
.map_err(|_| io::Error::other("InInstruction data exceeded 2**32 in length"))?;
|
|
||||||
let mut data = vec![0; data_len];
|
|
||||||
reader.read_exact(&mut data)?;
|
|
||||||
|
|
||||||
let mut key_at_end_of_block = <ProjectivePoint as GroupEncoding>::Repr::default();
|
|
||||||
reader.read_exact(&mut key_at_end_of_block)?;
|
|
||||||
let key_at_end_of_block = Option::from(ProjectivePoint::from_bytes(&key_at_end_of_block))
|
|
||||||
.ok_or(io::Error::other("InInstruction had key at end of block which wasn't valid"))?;
|
|
||||||
|
|
||||||
Ok(InInstruction { id, from, coin, amount, data, key_at_end_of_block })
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn write<W: io::Write>(&self, writer: &mut W) -> io::Result<()> {
|
|
||||||
writer.write_all(&self.id.0)?;
|
|
||||||
writer.write_all(&self.id.1.to_le_bytes())?;
|
|
||||||
|
|
||||||
writer.write_all(&self.from)?;
|
|
||||||
|
|
||||||
self.coin.write(writer)?;
|
|
||||||
writer.write_all(&self.amount.as_le_bytes())?;
|
|
||||||
|
|
||||||
writer.write_all(
|
|
||||||
&u32::try_from(self.data.len())
|
|
||||||
.map_err(|_| {
|
|
||||||
io::Error::other("InInstruction being written had data exceeding 2**32 in length")
|
|
||||||
})?
|
|
||||||
.to_le_bytes(),
|
|
||||||
)?;
|
|
||||||
writer.write_all(&self.data)?;
|
|
||||||
|
|
||||||
writer.write_all(&self.key_at_end_of_block.to_bytes())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
|
||||||
pub struct Executed {
|
|
||||||
pub tx_id: [u8; 32],
|
|
||||||
pub nonce: u64,
|
|
||||||
pub signature: [u8; 64],
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The contract Serai uses to manage its state.
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct Router(Arc<RootProvider<SimpleRequest>>, Address);
|
|
||||||
impl Router {
|
|
||||||
pub(crate) fn code() -> Vec<u8> {
|
|
||||||
let bytecode = include_str!("../artifacts/Router.bin");
|
|
||||||
Bytes::from_hex(bytecode).expect("compiled-in Router bytecode wasn't valid hex").to_vec()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn init_code(key: &PublicKey) -> Vec<u8> {
|
|
||||||
let mut bytecode = Self::code();
|
|
||||||
// Append the constructor arguments
|
|
||||||
bytecode.extend((abi::constructorCall { _seraiKey: key.eth_repr().into() }).abi_encode());
|
|
||||||
bytecode
|
|
||||||
}
|
|
||||||
|
|
||||||
// This isn't pub in order to force users to use `Deployer::find_router`.
|
|
||||||
pub(crate) fn new(provider: Arc<RootProvider<SimpleRequest>>, address: Address) -> Self {
|
|
||||||
Self(provider, address)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn address(&self) -> [u8; 20] {
|
|
||||||
**self.1
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the key for Serai at the specified block.
|
|
||||||
#[cfg(test)]
|
|
||||||
pub async fn serai_key(&self, at: [u8; 32]) -> Result<PublicKey, Error> {
|
|
||||||
let call = TransactionRequest::default()
|
|
||||||
.to(self.1)
|
|
||||||
.input(TransactionInput::new(abi::seraiKeyCall::new(()).abi_encode().into()));
|
|
||||||
let bytes = self
|
|
||||||
.0
|
|
||||||
.call(&call)
|
|
||||||
.block(BlockId::Hash(B256::from(at).into()))
|
|
||||||
.await
|
|
||||||
.map_err(|_| Error::ConnectionError)?;
|
|
||||||
let res =
|
|
||||||
abi::seraiKeyCall::abi_decode_returns(&bytes, true).map_err(|_| Error::ConnectionError)?;
|
|
||||||
PublicKey::from_eth_repr(res._0.0).ok_or(Error::ConnectionError)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the message to be signed in order to update the key for Serai.
|
|
||||||
pub(crate) fn update_serai_key_message(chain_id: U256, nonce: U256, key: &PublicKey) -> Vec<u8> {
|
|
||||||
let mut buffer = b"updateSeraiKey".to_vec();
|
|
||||||
buffer.extend(&chain_id.to_be_bytes::<32>());
|
|
||||||
buffer.extend(&nonce.to_be_bytes::<32>());
|
|
||||||
buffer.extend(&key.eth_repr());
|
|
||||||
buffer
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Update the key representing Serai.
|
|
||||||
pub fn update_serai_key(&self, public_key: &PublicKey, sig: &Signature) -> TxLegacy {
|
|
||||||
// TODO: Set a more accurate gas
|
|
||||||
TxLegacy {
|
|
||||||
to: TxKind::Call(self.1),
|
|
||||||
input: abi::updateSeraiKeyCall::new((public_key.eth_repr().into(), sig.into()))
|
|
||||||
.abi_encode()
|
|
||||||
.into(),
|
|
||||||
gas_limit: 100_000,
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the current nonce for the published batches.
|
|
||||||
#[cfg(test)]
|
|
||||||
pub async fn nonce(&self, at: [u8; 32]) -> Result<U256, Error> {
|
|
||||||
let call = TransactionRequest::default()
|
|
||||||
.to(self.1)
|
|
||||||
.input(TransactionInput::new(abi::nonceCall::new(()).abi_encode().into()));
|
|
||||||
let bytes = self
|
|
||||||
.0
|
|
||||||
.call(&call)
|
|
||||||
.block(BlockId::Hash(B256::from(at).into()))
|
|
||||||
.await
|
|
||||||
.map_err(|_| Error::ConnectionError)?;
|
|
||||||
let res =
|
|
||||||
abi::nonceCall::abi_decode_returns(&bytes, true).map_err(|_| Error::ConnectionError)?;
|
|
||||||
Ok(res._0)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the message to be signed in order to update the key for Serai.
|
|
||||||
pub(crate) fn execute_message(
|
|
||||||
chain_id: U256,
|
|
||||||
nonce: U256,
|
|
||||||
outs: Vec<abi::OutInstruction>,
|
|
||||||
) -> Vec<u8> {
|
|
||||||
("execute".to_string(), chain_id, nonce, outs).abi_encode_params()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Execute a batch of `OutInstruction`s.
|
|
||||||
pub fn execute(&self, outs: &[abi::OutInstruction], sig: &Signature) -> TxLegacy {
|
|
||||||
TxLegacy {
|
|
||||||
to: TxKind::Call(self.1),
|
|
||||||
input: abi::executeCall::new((outs.to_vec(), sig.into())).abi_encode().into(),
|
|
||||||
// TODO
|
|
||||||
gas_limit: 100_000 + ((200_000 + 10_000) * u128::try_from(outs.len()).unwrap()),
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn key_at_end_of_block(&self, block: u64) -> Result<Option<ProjectivePoint>, Error> {
|
|
||||||
let filter = Filter::new().from_block(0).to_block(block).address(self.1);
|
|
||||||
let filter = filter.event_signature(SeraiKeyUpdated::SIGNATURE_HASH);
|
|
||||||
let all_keys = self.0.get_logs(&filter).await.map_err(|_| Error::ConnectionError)?;
|
|
||||||
if all_keys.is_empty() {
|
|
||||||
return Ok(None);
|
|
||||||
};
|
|
||||||
|
|
||||||
let last_key_x_coordinate_log = all_keys.last().ok_or(Error::ConnectionError)?;
|
|
||||||
let last_key_x_coordinate = last_key_x_coordinate_log
|
|
||||||
.log_decode::<SeraiKeyUpdated>()
|
|
||||||
.map_err(|_| Error::ConnectionError)?
|
|
||||||
.inner
|
|
||||||
.data
|
|
||||||
.key;
|
|
||||||
|
|
||||||
let mut compressed_point = <ProjectivePoint as GroupEncoding>::Repr::default();
|
|
||||||
compressed_point[0] = u8::from(sec1::Tag::CompressedEvenY);
|
|
||||||
compressed_point[1 ..].copy_from_slice(last_key_x_coordinate.as_slice());
|
|
||||||
|
|
||||||
let key =
|
|
||||||
Option::from(ProjectivePoint::from_bytes(&compressed_point)).ok_or(Error::ConnectionError)?;
|
|
||||||
Ok(Some(key))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn in_instructions(
|
|
||||||
&self,
|
|
||||||
block: u64,
|
|
||||||
allowed_tokens: &HashSet<[u8; 20]>,
|
|
||||||
) -> Result<Vec<InInstruction>, Error> {
|
|
||||||
let Some(key_at_end_of_block) = self.key_at_end_of_block(block).await? else {
|
|
||||||
return Ok(vec![]);
|
|
||||||
};
|
|
||||||
|
|
||||||
let filter = Filter::new().from_block(block).to_block(block).address(self.1);
|
|
||||||
let filter = filter.event_signature(InInstructionEvent::SIGNATURE_HASH);
|
|
||||||
let logs = self.0.get_logs(&filter).await.map_err(|_| Error::ConnectionError)?;
|
|
||||||
|
|
||||||
let mut transfer_check = HashSet::new();
|
|
||||||
let mut in_instructions = vec![];
|
|
||||||
for log in logs {
|
|
||||||
// Double check the address which emitted this log
|
|
||||||
if log.address() != self.1 {
|
|
||||||
Err(Error::ConnectionError)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let id = (
|
|
||||||
log.block_hash.ok_or(Error::ConnectionError)?.into(),
|
|
||||||
log.log_index.ok_or(Error::ConnectionError)?,
|
|
||||||
);
|
|
||||||
|
|
||||||
let tx_hash = log.transaction_hash.ok_or(Error::ConnectionError)?;
|
|
||||||
let tx = self
|
|
||||||
.0
|
|
||||||
.get_transaction_by_hash(tx_hash)
|
|
||||||
.await
|
|
||||||
.ok()
|
|
||||||
.flatten()
|
|
||||||
.ok_or(Error::ConnectionError)?;
|
|
||||||
|
|
||||||
let log =
|
|
||||||
log.log_decode::<InInstructionEvent>().map_err(|_| Error::ConnectionError)?.inner.data;
|
|
||||||
|
|
||||||
let coin = if log.coin.0 == [0; 20] {
|
|
||||||
Coin::Ether
|
|
||||||
} else {
|
|
||||||
let token = *log.coin.0;
|
|
||||||
|
|
||||||
if !allowed_tokens.contains(&token) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If this also counts as a top-level transfer via the token, drop it
|
|
||||||
//
|
|
||||||
// Necessary in order to handle a potential edge case with some theoretical token
|
|
||||||
// implementations
|
|
||||||
//
|
|
||||||
// This will either let it be handled by the top-level transfer hook or will drop it
|
|
||||||
// entirely on the side of caution
|
|
||||||
if tx.to == Some(token.into()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get all logs for this TX
|
|
||||||
let receipt = self
|
|
||||||
.0
|
|
||||||
.get_transaction_receipt(tx_hash)
|
|
||||||
.await
|
|
||||||
.map_err(|_| Error::ConnectionError)?
|
|
||||||
.ok_or(Error::ConnectionError)?;
|
|
||||||
let tx_logs = receipt.inner.logs();
|
|
||||||
|
|
||||||
// Find a matching transfer log
|
|
||||||
let mut found_transfer = false;
|
|
||||||
for tx_log in tx_logs {
|
|
||||||
let log_index = tx_log.log_index.ok_or(Error::ConnectionError)?;
|
|
||||||
// Ensure we didn't already use this transfer to check a distinct InInstruction event
|
|
||||||
if transfer_check.contains(&log_index) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if this log is from the token we expected to be transferred
|
|
||||||
if tx_log.address().0 != token {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// Check if this is a transfer log
|
|
||||||
// https://github.com/alloy-rs/core/issues/589
|
|
||||||
if tx_log.topics()[0] != Transfer::SIGNATURE_HASH {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let Ok(transfer) = Transfer::decode_log(&tx_log.inner.clone(), true) else { continue };
|
|
||||||
// Check if this is a transfer to us for the expected amount
|
|
||||||
if (transfer.to == self.1) && (transfer.value == log.amount) {
|
|
||||||
transfer_check.insert(log_index);
|
|
||||||
found_transfer = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !found_transfer {
|
|
||||||
// This shouldn't be a ConnectionError
|
|
||||||
// This is an exploit, a non-conforming ERC20, or an invalid connection
|
|
||||||
// This should halt the process which is sufficient, yet this is sub-optimal
|
|
||||||
// TODO
|
|
||||||
Err(Error::ConnectionError)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Coin::Erc20(token)
|
|
||||||
};
|
|
||||||
|
|
||||||
in_instructions.push(InInstruction {
|
|
||||||
id,
|
|
||||||
from: *log.from.0,
|
|
||||||
coin,
|
|
||||||
amount: log.amount,
|
|
||||||
data: log.instruction.as_ref().to_vec(),
|
|
||||||
key_at_end_of_block,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(in_instructions)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn executed_commands(&self, block: u64) -> Result<Vec<Executed>, Error> {
|
|
||||||
let mut res = vec![];
|
|
||||||
|
|
||||||
{
|
|
||||||
let filter = Filter::new().from_block(block).to_block(block).address(self.1);
|
|
||||||
let filter = filter.event_signature(SeraiKeyUpdated::SIGNATURE_HASH);
|
|
||||||
let logs = self.0.get_logs(&filter).await.map_err(|_| Error::ConnectionError)?;
|
|
||||||
|
|
||||||
for log in logs {
|
|
||||||
// Double check the address which emitted this log
|
|
||||||
if log.address() != self.1 {
|
|
||||||
Err(Error::ConnectionError)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let tx_id = log.transaction_hash.ok_or(Error::ConnectionError)?.into();
|
|
||||||
|
|
||||||
let log =
|
|
||||||
log.log_decode::<SeraiKeyUpdated>().map_err(|_| Error::ConnectionError)?.inner.data;
|
|
||||||
|
|
||||||
let mut signature = [0; 64];
|
|
||||||
signature[.. 32].copy_from_slice(log.signature.c.as_ref());
|
|
||||||
signature[32 ..].copy_from_slice(log.signature.s.as_ref());
|
|
||||||
res.push(Executed {
|
|
||||||
tx_id,
|
|
||||||
nonce: log.nonce.try_into().map_err(|_| Error::ConnectionError)?,
|
|
||||||
signature,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
let filter = Filter::new().from_block(block).to_block(block).address(self.1);
|
|
||||||
let filter = filter.event_signature(ExecutedEvent::SIGNATURE_HASH);
|
|
||||||
let logs = self.0.get_logs(&filter).await.map_err(|_| Error::ConnectionError)?;
|
|
||||||
|
|
||||||
for log in logs {
|
|
||||||
// Double check the address which emitted this log
|
|
||||||
if log.address() != self.1 {
|
|
||||||
Err(Error::ConnectionError)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let tx_id = log.transaction_hash.ok_or(Error::ConnectionError)?.into();
|
|
||||||
|
|
||||||
let log = log.log_decode::<ExecutedEvent>().map_err(|_| Error::ConnectionError)?.inner.data;
|
|
||||||
|
|
||||||
let mut signature = [0; 64];
|
|
||||||
signature[.. 32].copy_from_slice(log.signature.c.as_ref());
|
|
||||||
signature[32 ..].copy_from_slice(log.signature.s.as_ref());
|
|
||||||
res.push(Executed {
|
|
||||||
tx_id,
|
|
||||||
nonce: log.nonce.try_into().map_err(|_| Error::ConnectionError)?,
|
|
||||||
signature,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(res)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "tests")]
|
|
||||||
pub fn key_updated_filter(&self) -> Filter {
|
|
||||||
Filter::new().address(self.1).event_signature(SeraiKeyUpdated::SIGNATURE_HASH)
|
|
||||||
}
|
|
||||||
#[cfg(feature = "tests")]
|
|
||||||
pub fn executed_filter(&self) -> Filter {
|
|
||||||
Filter::new().address(self.1).event_signature(ExecutedEvent::SIGNATURE_HASH)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
use alloy_sol_types::sol;
|
|
||||||
|
|
||||||
#[rustfmt::skip]
|
|
||||||
#[allow(warnings)]
|
|
||||||
#[allow(needless_pass_by_value)]
|
|
||||||
#[allow(clippy::all)]
|
|
||||||
#[allow(clippy::ignored_unit_patterns)]
|
|
||||||
#[allow(clippy::redundant_closure_for_method_calls)]
|
|
||||||
mod schnorr_container {
|
|
||||||
use super::*;
|
|
||||||
sol!("src/tests/contracts/Schnorr.sol");
|
|
||||||
}
|
|
||||||
pub(crate) use schnorr_container::TestSchnorr as schnorr;
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
// SPDX-License-Identifier: AGPLv3
|
|
||||||
pragma solidity ^0.8.0;
|
|
||||||
|
|
||||||
contract TestERC20 {
|
|
||||||
event Transfer(address indexed from, address indexed to, uint256 value);
|
|
||||||
event Approval(address indexed owner, address indexed spender, uint256 value);
|
|
||||||
|
|
||||||
function name() public pure returns (string memory) {
|
|
||||||
return "Test ERC20";
|
|
||||||
}
|
|
||||||
function symbol() public pure returns (string memory) {
|
|
||||||
return "TEST";
|
|
||||||
}
|
|
||||||
function decimals() public pure returns (uint8) {
|
|
||||||
return 18;
|
|
||||||
}
|
|
||||||
|
|
||||||
function totalSupply() public pure returns (uint256) {
|
|
||||||
return 1_000_000 * 10e18;
|
|
||||||
}
|
|
||||||
|
|
||||||
mapping(address => uint256) balances;
|
|
||||||
mapping(address => mapping(address => uint256)) allowances;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
balances[msg.sender] = totalSupply();
|
|
||||||
}
|
|
||||||
|
|
||||||
function balanceOf(address owner) public view returns (uint256) {
|
|
||||||
return balances[owner];
|
|
||||||
}
|
|
||||||
function transfer(address to, uint256 value) public returns (bool) {
|
|
||||||
balances[msg.sender] -= value;
|
|
||||||
balances[to] += value;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
function transferFrom(address from, address to, uint256 value) public returns (bool) {
|
|
||||||
allowances[from][msg.sender] -= value;
|
|
||||||
balances[from] -= value;
|
|
||||||
balances[to] += value;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function approve(address spender, uint256 value) public returns (bool) {
|
|
||||||
allowances[msg.sender][spender] = value;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
function allowance(address owner, address spender) public view returns (uint256) {
|
|
||||||
return allowances[owner][spender];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
// SPDX-License-Identifier: AGPLv3
|
|
||||||
pragma solidity ^0.8.0;
|
|
||||||
|
|
||||||
import "../../../contracts/Schnorr.sol";
|
|
||||||
|
|
||||||
contract TestSchnorr {
|
|
||||||
function verify(
|
|
||||||
bytes32 px,
|
|
||||||
bytes calldata message,
|
|
||||||
bytes32 c,
|
|
||||||
bytes32 s
|
|
||||||
) external pure returns (bool) {
|
|
||||||
return Schnorr.verify(px, message, c, s);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
use rand_core::OsRng;
|
|
||||||
|
|
||||||
use group::ff::{Field, PrimeField};
|
|
||||||
use k256::{
|
|
||||||
ecdsa::{
|
|
||||||
self, hazmat::SignPrimitive, signature::hazmat::PrehashVerifier, SigningKey, VerifyingKey,
|
|
||||||
},
|
|
||||||
Scalar, ProjectivePoint,
|
|
||||||
};
|
|
||||||
|
|
||||||
use frost::{
|
|
||||||
curve::{Ciphersuite, Secp256k1},
|
|
||||||
algorithm::{Hram, IetfSchnorr},
|
|
||||||
tests::{algorithm_machines, sign},
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{crypto::*, tests::key_gen};
|
|
||||||
|
|
||||||
// The ecrecover opcode, yet with parity replacing v
|
|
||||||
pub(crate) fn ecrecover(message: Scalar, odd_y: bool, r: Scalar, s: Scalar) -> Option<[u8; 20]> {
|
|
||||||
let sig = ecdsa::Signature::from_scalars(r, s).ok()?;
|
|
||||||
let message: [u8; 32] = message.to_repr().into();
|
|
||||||
alloy_core::primitives::Signature::from_signature_and_parity(
|
|
||||||
sig,
|
|
||||||
alloy_core::primitives::Parity::Parity(odd_y),
|
|
||||||
)
|
|
||||||
.ok()?
|
|
||||||
.recover_address_from_prehash(&alloy_core::primitives::B256::from(message))
|
|
||||||
.ok()
|
|
||||||
.map(Into::into)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_ecrecover() {
|
|
||||||
let private = SigningKey::random(&mut OsRng);
|
|
||||||
let public = VerifyingKey::from(&private);
|
|
||||||
|
|
||||||
// Sign the signature
|
|
||||||
const MESSAGE: &[u8] = b"Hello, World!";
|
|
||||||
let (sig, recovery_id) = private
|
|
||||||
.as_nonzero_scalar()
|
|
||||||
.try_sign_prehashed(
|
|
||||||
<Secp256k1 as Ciphersuite>::F::random(&mut OsRng),
|
|
||||||
&keccak256(MESSAGE).into(),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// Sanity check the signature verifies
|
|
||||||
#[allow(clippy::unit_cmp)] // Intended to assert this wasn't changed to Result<bool>
|
|
||||||
{
|
|
||||||
assert_eq!(public.verify_prehash(&keccak256(MESSAGE), &sig).unwrap(), ());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Perform the ecrecover
|
|
||||||
assert_eq!(
|
|
||||||
ecrecover(
|
|
||||||
hash_to_scalar(MESSAGE),
|
|
||||||
u8::from(recovery_id.unwrap().is_y_odd()) == 1,
|
|
||||||
*sig.r(),
|
|
||||||
*sig.s()
|
|
||||||
)
|
|
||||||
.unwrap(),
|
|
||||||
address(&ProjectivePoint::from(public.as_affine()))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run the sign test with the EthereumHram
|
|
||||||
#[test]
|
|
||||||
fn test_signing() {
|
|
||||||
let (keys, _) = key_gen();
|
|
||||||
|
|
||||||
const MESSAGE: &[u8] = b"Hello, World!";
|
|
||||||
|
|
||||||
let algo = IetfSchnorr::<Secp256k1, EthereumHram>::ietf();
|
|
||||||
let _sig =
|
|
||||||
sign(&mut OsRng, &algo, keys.clone(), algorithm_machines(&mut OsRng, &algo, &keys), MESSAGE);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
pub fn preprocess_signature_for_ecrecover(
|
|
||||||
R: ProjectivePoint,
|
|
||||||
public_key: &PublicKey,
|
|
||||||
m: &[u8],
|
|
||||||
s: Scalar,
|
|
||||||
) -> (Scalar, Scalar) {
|
|
||||||
let c = EthereumHram::hram(&R, &public_key.A, m);
|
|
||||||
let sa = -(s * public_key.px);
|
|
||||||
let ca = -(c * public_key.px);
|
|
||||||
(sa, ca)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_ecrecover_hack() {
|
|
||||||
let (keys, public_key) = key_gen();
|
|
||||||
|
|
||||||
const MESSAGE: &[u8] = b"Hello, World!";
|
|
||||||
|
|
||||||
let algo = IetfSchnorr::<Secp256k1, EthereumHram>::ietf();
|
|
||||||
let sig =
|
|
||||||
sign(&mut OsRng, &algo, keys.clone(), algorithm_machines(&mut OsRng, &algo, &keys), MESSAGE);
|
|
||||||
|
|
||||||
let (sa, ca) = preprocess_signature_for_ecrecover(sig.R, &public_key, MESSAGE, sig.s);
|
|
||||||
let q = ecrecover(sa, false, public_key.px, ca).unwrap();
|
|
||||||
assert_eq!(q, address(&sig.R));
|
|
||||||
}
|
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
use std::{sync::Arc, collections::HashMap};
|
|
||||||
|
|
||||||
use rand_core::OsRng;
|
|
||||||
|
|
||||||
use k256::{Scalar, ProjectivePoint};
|
|
||||||
use frost::{curve::Secp256k1, Participant, ThresholdKeys, tests::key_gen as frost_key_gen};
|
|
||||||
|
|
||||||
use alloy_core::{
|
|
||||||
primitives::{Address, U256, Bytes, TxKind},
|
|
||||||
hex::FromHex,
|
|
||||||
};
|
|
||||||
use alloy_consensus::{SignableTransaction, TxLegacy};
|
|
||||||
|
|
||||||
use alloy_rpc_types::TransactionReceipt;
|
|
||||||
use alloy_simple_request_transport::SimpleRequest;
|
|
||||||
use alloy_provider::{Provider, RootProvider};
|
|
||||||
|
|
||||||
use crate::crypto::{address, deterministically_sign, PublicKey};
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod crypto;
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod abi;
|
|
||||||
#[cfg(test)]
|
|
||||||
mod schnorr;
|
|
||||||
#[cfg(test)]
|
|
||||||
mod router;
|
|
||||||
|
|
||||||
pub fn key_gen() -> (HashMap<Participant, ThresholdKeys<Secp256k1>>, PublicKey) {
|
|
||||||
let mut keys = frost_key_gen::<_, Secp256k1>(&mut OsRng);
|
|
||||||
let mut group_key = keys[&Participant::new(1).unwrap()].group_key();
|
|
||||||
|
|
||||||
let mut offset = Scalar::ZERO;
|
|
||||||
while PublicKey::new(group_key).is_none() {
|
|
||||||
offset += Scalar::ONE;
|
|
||||||
group_key += ProjectivePoint::GENERATOR;
|
|
||||||
}
|
|
||||||
for keys in keys.values_mut() {
|
|
||||||
*keys = keys.offset(offset);
|
|
||||||
}
|
|
||||||
let public_key = PublicKey::new(group_key).unwrap();
|
|
||||||
|
|
||||||
(keys, public_key)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Use a proper error here
|
|
||||||
pub async fn send(
|
|
||||||
provider: &RootProvider<SimpleRequest>,
|
|
||||||
wallet: &k256::ecdsa::SigningKey,
|
|
||||||
mut tx: TxLegacy,
|
|
||||||
) -> Option<TransactionReceipt> {
|
|
||||||
let verifying_key = *wallet.verifying_key().as_affine();
|
|
||||||
let address = Address::from(address(&verifying_key.into()));
|
|
||||||
|
|
||||||
// https://github.com/alloy-rs/alloy/issues/539
|
|
||||||
// let chain_id = provider.get_chain_id().await.unwrap();
|
|
||||||
// tx.chain_id = Some(chain_id);
|
|
||||||
tx.chain_id = None;
|
|
||||||
tx.nonce = provider.get_transaction_count(address).await.unwrap();
|
|
||||||
// 100 gwei
|
|
||||||
tx.gas_price = 100_000_000_000u128;
|
|
||||||
|
|
||||||
let sig = wallet.sign_prehash_recoverable(tx.signature_hash().as_ref()).unwrap();
|
|
||||||
assert_eq!(address, tx.clone().into_signed(sig.into()).recover_signer().unwrap());
|
|
||||||
assert!(
|
|
||||||
provider.get_balance(address).await.unwrap() >
|
|
||||||
((U256::from(tx.gas_price) * U256::from(tx.gas_limit)) + tx.value)
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut bytes = vec![];
|
|
||||||
tx.encode_with_signature_fields(&sig.into(), &mut bytes);
|
|
||||||
let pending_tx = provider.send_raw_transaction(&bytes).await.ok()?;
|
|
||||||
pending_tx.get_receipt().await.ok()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn fund_account(
|
|
||||||
provider: &RootProvider<SimpleRequest>,
|
|
||||||
wallet: &k256::ecdsa::SigningKey,
|
|
||||||
to_fund: Address,
|
|
||||||
value: U256,
|
|
||||||
) -> Option<()> {
|
|
||||||
let funding_tx =
|
|
||||||
TxLegacy { to: TxKind::Call(to_fund), gas_limit: 21_000, value, ..Default::default() };
|
|
||||||
assert!(send(provider, wallet, funding_tx).await.unwrap().status());
|
|
||||||
|
|
||||||
Some(())
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Use a proper error here
|
|
||||||
pub async fn deploy_contract(
|
|
||||||
client: Arc<RootProvider<SimpleRequest>>,
|
|
||||||
wallet: &k256::ecdsa::SigningKey,
|
|
||||||
name: &str,
|
|
||||||
) -> Option<Address> {
|
|
||||||
let hex_bin_buf = std::fs::read_to_string(format!("./artifacts/{name}.bin")).unwrap();
|
|
||||||
let hex_bin =
|
|
||||||
if let Some(stripped) = hex_bin_buf.strip_prefix("0x") { stripped } else { &hex_bin_buf };
|
|
||||||
let bin = Bytes::from_hex(hex_bin).unwrap();
|
|
||||||
|
|
||||||
let deployment_tx = TxLegacy {
|
|
||||||
chain_id: None,
|
|
||||||
nonce: 0,
|
|
||||||
// 100 gwei
|
|
||||||
gas_price: 100_000_000_000u128,
|
|
||||||
gas_limit: 1_000_000,
|
|
||||||
to: TxKind::Create,
|
|
||||||
value: U256::ZERO,
|
|
||||||
input: bin,
|
|
||||||
};
|
|
||||||
|
|
||||||
let deployment_tx = deterministically_sign(&deployment_tx);
|
|
||||||
|
|
||||||
// Fund the deployer address
|
|
||||||
fund_account(
|
|
||||||
&client,
|
|
||||||
wallet,
|
|
||||||
deployment_tx.recover_signer().unwrap(),
|
|
||||||
U256::from(deployment_tx.tx().gas_limit) * U256::from(deployment_tx.tx().gas_price),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let (deployment_tx, sig, _) = deployment_tx.into_parts();
|
|
||||||
let mut bytes = vec![];
|
|
||||||
deployment_tx.encode_with_signature_fields(&sig, &mut bytes);
|
|
||||||
let pending_tx = client.send_raw_transaction(&bytes).await.ok()?;
|
|
||||||
let receipt = pending_tx.get_receipt().await.ok()?;
|
|
||||||
assert!(receipt.status());
|
|
||||||
|
|
||||||
Some(receipt.contract_address.unwrap())
|
|
||||||
}
|
|
||||||
@@ -1,183 +0,0 @@
|
|||||||
use std::{convert::TryFrom, sync::Arc, collections::HashMap};
|
|
||||||
|
|
||||||
use rand_core::OsRng;
|
|
||||||
|
|
||||||
use group::Group;
|
|
||||||
use k256::ProjectivePoint;
|
|
||||||
use frost::{
|
|
||||||
curve::Secp256k1,
|
|
||||||
Participant, ThresholdKeys,
|
|
||||||
algorithm::IetfSchnorr,
|
|
||||||
tests::{algorithm_machines, sign},
|
|
||||||
};
|
|
||||||
|
|
||||||
use alloy_core::primitives::{Address, U256};
|
|
||||||
|
|
||||||
use alloy_simple_request_transport::SimpleRequest;
|
|
||||||
use alloy_rpc_client::ClientBuilder;
|
|
||||||
use alloy_provider::{Provider, RootProvider};
|
|
||||||
|
|
||||||
use alloy_node_bindings::{Anvil, AnvilInstance};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
crypto::*,
|
|
||||||
deployer::Deployer,
|
|
||||||
router::{Router, abi as router},
|
|
||||||
tests::{key_gen, send, fund_account},
|
|
||||||
};
|
|
||||||
|
|
||||||
async fn setup_test() -> (
|
|
||||||
AnvilInstance,
|
|
||||||
Arc<RootProvider<SimpleRequest>>,
|
|
||||||
u64,
|
|
||||||
Router,
|
|
||||||
HashMap<Participant, ThresholdKeys<Secp256k1>>,
|
|
||||||
PublicKey,
|
|
||||||
) {
|
|
||||||
let anvil = Anvil::new().spawn();
|
|
||||||
|
|
||||||
let provider = RootProvider::new(
|
|
||||||
ClientBuilder::default().transport(SimpleRequest::new(anvil.endpoint()), true),
|
|
||||||
);
|
|
||||||
let chain_id = provider.get_chain_id().await.unwrap();
|
|
||||||
let wallet = anvil.keys()[0].clone().into();
|
|
||||||
let client = Arc::new(provider);
|
|
||||||
|
|
||||||
// Make sure the Deployer constructor returns None, as it doesn't exist yet
|
|
||||||
assert!(Deployer::new(client.clone()).await.unwrap().is_none());
|
|
||||||
|
|
||||||
// Deploy the Deployer
|
|
||||||
let tx = Deployer::deployment_tx();
|
|
||||||
fund_account(
|
|
||||||
&client,
|
|
||||||
&wallet,
|
|
||||||
tx.recover_signer().unwrap(),
|
|
||||||
U256::from(tx.tx().gas_limit) * U256::from(tx.tx().gas_price),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let (tx, sig, _) = tx.into_parts();
|
|
||||||
let mut bytes = vec![];
|
|
||||||
tx.encode_with_signature_fields(&sig, &mut bytes);
|
|
||||||
|
|
||||||
let pending_tx = client.send_raw_transaction(&bytes).await.unwrap();
|
|
||||||
let receipt = pending_tx.get_receipt().await.unwrap();
|
|
||||||
assert!(receipt.status());
|
|
||||||
let deployer =
|
|
||||||
Deployer::new(client.clone()).await.expect("network error").expect("deployer wasn't deployed");
|
|
||||||
|
|
||||||
let (keys, public_key) = key_gen();
|
|
||||||
|
|
||||||
// Verify the Router constructor returns None, as it doesn't exist yet
|
|
||||||
assert!(deployer.find_router(client.clone(), &public_key).await.unwrap().is_none());
|
|
||||||
|
|
||||||
// Deploy the router
|
|
||||||
let receipt = send(&client, &anvil.keys()[0].clone().into(), deployer.deploy_router(&public_key))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
assert!(receipt.status());
|
|
||||||
let contract = deployer.find_router(client.clone(), &public_key).await.unwrap().unwrap();
|
|
||||||
|
|
||||||
(anvil, client, chain_id, contract, keys, public_key)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn latest_block_hash(client: &RootProvider<SimpleRequest>) -> [u8; 32] {
|
|
||||||
client
|
|
||||||
.get_block(client.get_block_number().await.unwrap().into(), false)
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.unwrap()
|
|
||||||
.header
|
|
||||||
.hash
|
|
||||||
.unwrap()
|
|
||||||
.0
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_deploy_contract() {
|
|
||||||
let (_anvil, client, _, router, _, public_key) = setup_test().await;
|
|
||||||
|
|
||||||
let block_hash = latest_block_hash(&client).await;
|
|
||||||
assert_eq!(router.serai_key(block_hash).await.unwrap(), public_key);
|
|
||||||
assert_eq!(router.nonce(block_hash).await.unwrap(), U256::try_from(1u64).unwrap());
|
|
||||||
// TODO: Check it emitted SeraiKeyUpdated(public_key) at its genesis
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn hash_and_sign(
|
|
||||||
keys: &HashMap<Participant, ThresholdKeys<Secp256k1>>,
|
|
||||||
public_key: &PublicKey,
|
|
||||||
message: &[u8],
|
|
||||||
) -> Signature {
|
|
||||||
let algo = IetfSchnorr::<Secp256k1, EthereumHram>::ietf();
|
|
||||||
let sig =
|
|
||||||
sign(&mut OsRng, &algo, keys.clone(), algorithm_machines(&mut OsRng, &algo, keys), message);
|
|
||||||
|
|
||||||
Signature::new(public_key, message, sig).unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_router_update_serai_key() {
|
|
||||||
let (anvil, client, chain_id, contract, keys, public_key) = setup_test().await;
|
|
||||||
|
|
||||||
let next_key = loop {
|
|
||||||
let point = ProjectivePoint::random(&mut OsRng);
|
|
||||||
let Some(next_key) = PublicKey::new(point) else { continue };
|
|
||||||
break next_key;
|
|
||||||
};
|
|
||||||
|
|
||||||
let message = Router::update_serai_key_message(
|
|
||||||
U256::try_from(chain_id).unwrap(),
|
|
||||||
U256::try_from(1u64).unwrap(),
|
|
||||||
&next_key,
|
|
||||||
);
|
|
||||||
let sig = hash_and_sign(&keys, &public_key, &message);
|
|
||||||
|
|
||||||
let first_block_hash = latest_block_hash(&client).await;
|
|
||||||
assert_eq!(contract.serai_key(first_block_hash).await.unwrap(), public_key);
|
|
||||||
|
|
||||||
let receipt =
|
|
||||||
send(&client, &anvil.keys()[0].clone().into(), contract.update_serai_key(&next_key, &sig))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
assert!(receipt.status());
|
|
||||||
|
|
||||||
let second_block_hash = latest_block_hash(&client).await;
|
|
||||||
assert_eq!(contract.serai_key(second_block_hash).await.unwrap(), next_key);
|
|
||||||
// Check this does still offer the historical state
|
|
||||||
assert_eq!(contract.serai_key(first_block_hash).await.unwrap(), public_key);
|
|
||||||
// TODO: Check logs
|
|
||||||
|
|
||||||
println!("gas used: {:?}", receipt.gas_used);
|
|
||||||
// println!("logs: {:?}", receipt.logs);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_router_execute() {
|
|
||||||
let (anvil, client, chain_id, contract, keys, public_key) = setup_test().await;
|
|
||||||
|
|
||||||
let to = Address::from([0; 20]);
|
|
||||||
let value = U256::ZERO;
|
|
||||||
let tx = router::OutInstruction { to, value, calls: vec![] };
|
|
||||||
let txs = vec![tx];
|
|
||||||
|
|
||||||
let first_block_hash = latest_block_hash(&client).await;
|
|
||||||
let nonce = contract.nonce(first_block_hash).await.unwrap();
|
|
||||||
assert_eq!(nonce, U256::try_from(1u64).unwrap());
|
|
||||||
|
|
||||||
let message = Router::execute_message(U256::try_from(chain_id).unwrap(), nonce, txs.clone());
|
|
||||||
let sig = hash_and_sign(&keys, &public_key, &message);
|
|
||||||
|
|
||||||
let receipt =
|
|
||||||
send(&client, &anvil.keys()[0].clone().into(), contract.execute(&txs, &sig)).await.unwrap();
|
|
||||||
assert!(receipt.status());
|
|
||||||
|
|
||||||
let second_block_hash = latest_block_hash(&client).await;
|
|
||||||
assert_eq!(contract.nonce(second_block_hash).await.unwrap(), U256::try_from(2u64).unwrap());
|
|
||||||
// Check this does still offer the historical state
|
|
||||||
assert_eq!(contract.nonce(first_block_hash).await.unwrap(), U256::try_from(1u64).unwrap());
|
|
||||||
// TODO: Check logs
|
|
||||||
|
|
||||||
println!("gas used: {:?}", receipt.gas_used);
|
|
||||||
// println!("logs: {:?}", receipt.logs);
|
|
||||||
}
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use rand_core::OsRng;
|
|
||||||
|
|
||||||
use group::ff::PrimeField;
|
|
||||||
use k256::Scalar;
|
|
||||||
|
|
||||||
use frost::{
|
|
||||||
curve::Secp256k1,
|
|
||||||
algorithm::IetfSchnorr,
|
|
||||||
tests::{algorithm_machines, sign},
|
|
||||||
};
|
|
||||||
|
|
||||||
use alloy_core::primitives::Address;
|
|
||||||
|
|
||||||
use alloy_sol_types::SolCall;
|
|
||||||
|
|
||||||
use alloy_rpc_types::{TransactionInput, TransactionRequest};
|
|
||||||
use alloy_simple_request_transport::SimpleRequest;
|
|
||||||
use alloy_rpc_client::ClientBuilder;
|
|
||||||
use alloy_provider::{Provider, RootProvider};
|
|
||||||
|
|
||||||
use alloy_node_bindings::{Anvil, AnvilInstance};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
Error,
|
|
||||||
crypto::*,
|
|
||||||
tests::{key_gen, deploy_contract, abi::schnorr as abi},
|
|
||||||
};
|
|
||||||
|
|
||||||
async fn setup_test() -> (AnvilInstance, Arc<RootProvider<SimpleRequest>>, Address) {
|
|
||||||
let anvil = Anvil::new().spawn();
|
|
||||||
|
|
||||||
let provider = RootProvider::new(
|
|
||||||
ClientBuilder::default().transport(SimpleRequest::new(anvil.endpoint()), true),
|
|
||||||
);
|
|
||||||
let wallet = anvil.keys()[0].clone().into();
|
|
||||||
let client = Arc::new(provider);
|
|
||||||
|
|
||||||
let address = deploy_contract(client.clone(), &wallet, "TestSchnorr").await.unwrap();
|
|
||||||
(anvil, client, address)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_deploy_contract() {
|
|
||||||
setup_test().await;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn call_verify(
|
|
||||||
provider: &RootProvider<SimpleRequest>,
|
|
||||||
contract: Address,
|
|
||||||
public_key: &PublicKey,
|
|
||||||
message: &[u8],
|
|
||||||
signature: &Signature,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
let px: [u8; 32] = public_key.px.to_repr().into();
|
|
||||||
let c_bytes: [u8; 32] = signature.c.to_repr().into();
|
|
||||||
let s_bytes: [u8; 32] = signature.s.to_repr().into();
|
|
||||||
let call = TransactionRequest::default().to(contract).input(TransactionInput::new(
|
|
||||||
abi::verifyCall::new((px.into(), message.to_vec().into(), c_bytes.into(), s_bytes.into()))
|
|
||||||
.abi_encode()
|
|
||||||
.into(),
|
|
||||||
));
|
|
||||||
let bytes = provider.call(&call).await.map_err(|_| Error::ConnectionError)?;
|
|
||||||
let res =
|
|
||||||
abi::verifyCall::abi_decode_returns(&bytes, true).map_err(|_| Error::ConnectionError)?;
|
|
||||||
|
|
||||||
if res._0 {
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err(Error::InvalidSignature)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_ecrecover_hack() {
|
|
||||||
let (_anvil, client, contract) = setup_test().await;
|
|
||||||
|
|
||||||
let (keys, public_key) = key_gen();
|
|
||||||
|
|
||||||
const MESSAGE: &[u8] = b"Hello, World!";
|
|
||||||
|
|
||||||
let algo = IetfSchnorr::<Secp256k1, EthereumHram>::ietf();
|
|
||||||
let sig =
|
|
||||||
sign(&mut OsRng, &algo, keys.clone(), algorithm_machines(&mut OsRng, &algo, &keys), MESSAGE);
|
|
||||||
let sig = Signature::new(&public_key, MESSAGE, sig).unwrap();
|
|
||||||
|
|
||||||
call_verify(&client, contract, &public_key, MESSAGE, &sig).await.unwrap();
|
|
||||||
// Test an invalid signature fails
|
|
||||||
let mut sig = sig;
|
|
||||||
sig.s += Scalar::ONE;
|
|
||||||
assert!(call_verify(&client, contract, &public_key, MESSAGE, &sig).await.is_err());
|
|
||||||
}
|
|
||||||
30
coins/firo/Cargo.toml
Normal file
30
coins/firo/Cargo.toml
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
[package]
|
||||||
|
name = "firo"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "A modern Firo wallet library"
|
||||||
|
license = "MIT"
|
||||||
|
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
lazy_static = "1"
|
||||||
|
thiserror = "1"
|
||||||
|
|
||||||
|
rand_core = "0.6"
|
||||||
|
rand_chacha = { version = "0.3", optional = true }
|
||||||
|
|
||||||
|
sha2 = "0.10"
|
||||||
|
|
||||||
|
ff = "0.12"
|
||||||
|
group = "0.12"
|
||||||
|
k256 = { version = "0.11", features = ["arithmetic"] }
|
||||||
|
|
||||||
|
blake2 = { version = "0.10", optional = true }
|
||||||
|
transcript = { path = "../../crypto/transcript", package = "flexible-transcript", features = ["recommended"], optional = true }
|
||||||
|
frost = { path = "../../crypto/frost", package = "modular-frost", features = ["secp256k1"], optional = true }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
rand = "0.8"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
multisig = ["blake2", "transcript", "frost", "rand_chacha"]
|
||||||
4
coins/firo/src/lib.rs
Normal file
4
coins/firo/src/lib.rs
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
pub mod spark;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests;
|
||||||
183
coins/firo/src/spark/chaum/mod.rs
Normal file
183
coins/firo/src/spark/chaum/mod.rs
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
#![allow(non_snake_case)]
|
||||||
|
|
||||||
|
use rand_core::{RngCore, CryptoRng};
|
||||||
|
|
||||||
|
use sha2::{Digest, Sha512};
|
||||||
|
|
||||||
|
use ff::Field;
|
||||||
|
use group::{Group, GroupEncoding};
|
||||||
|
use k256::{
|
||||||
|
elliptic_curve::{bigint::{ArrayEncoding, U512}, ops::Reduce},
|
||||||
|
Scalar, ProjectivePoint
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::spark::{F, G, H, U, GENERATORS_TRANSCRIPT};
|
||||||
|
|
||||||
|
#[cfg(feature = "frost")]
|
||||||
|
mod multisig;
|
||||||
|
#[cfg(feature = "frost")]
|
||||||
|
pub use multisig::ChaumMultisig;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct ChaumStatement {
|
||||||
|
context: Vec<u8>,
|
||||||
|
S_T: Vec<(ProjectivePoint, ProjectivePoint)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ChaumStatement {
|
||||||
|
pub fn new(context: Vec<u8>, S_T: Vec<(ProjectivePoint, ProjectivePoint)>) -> ChaumStatement {
|
||||||
|
ChaumStatement { context, S_T }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn transcript(&self) -> Vec<u8> {
|
||||||
|
let mut transcript = self.context.clone();
|
||||||
|
for S_T in &self.S_T {
|
||||||
|
transcript.extend(S_T.0.to_bytes());
|
||||||
|
transcript.extend(S_T.1.to_bytes());
|
||||||
|
}
|
||||||
|
transcript
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct ChaumWitness {
|
||||||
|
statement: ChaumStatement,
|
||||||
|
xz: Vec<(Scalar, Scalar)>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ChaumWitness {
|
||||||
|
pub fn new(statement: ChaumStatement, xz: Vec<(Scalar, Scalar)>) -> ChaumWitness {
|
||||||
|
assert!(statement.S_T.len() != 0);
|
||||||
|
assert_eq!(statement.S_T.len(), xz.len());
|
||||||
|
ChaumWitness { statement, xz }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq, Debug)]
|
||||||
|
pub(crate) struct ChaumCommitments {
|
||||||
|
A1: ProjectivePoint,
|
||||||
|
A2: Vec<ProjectivePoint>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ChaumCommitments {
|
||||||
|
fn transcript(&self) -> Vec<u8> {
|
||||||
|
let mut transcript = Vec::with_capacity((self.A2.len() + 1) * 33);
|
||||||
|
transcript.extend(self.A1.to_bytes());
|
||||||
|
for A in &self.A2 {
|
||||||
|
transcript.extend(A.to_bytes());
|
||||||
|
}
|
||||||
|
transcript
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, PartialEq, Debug)]
|
||||||
|
pub struct ChaumProof {
|
||||||
|
commitments: ChaumCommitments,
|
||||||
|
t1: Vec<Scalar>,
|
||||||
|
t2: Scalar,
|
||||||
|
t3: Scalar
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ChaumProof {
|
||||||
|
fn r_t_commitments<R: RngCore + CryptoRng>(
|
||||||
|
rng: &mut R,
|
||||||
|
witness: &ChaumWitness
|
||||||
|
) -> (Vec<Scalar>, Scalar, ChaumCommitments) {
|
||||||
|
let len = witness.xz.len();
|
||||||
|
let mut rs = Vec::with_capacity(len);
|
||||||
|
let mut r_sum = Scalar::zero();
|
||||||
|
|
||||||
|
let mut commitments = ChaumCommitments {
|
||||||
|
A1: ProjectivePoint::IDENTITY,
|
||||||
|
A2: Vec::with_capacity(len)
|
||||||
|
};
|
||||||
|
|
||||||
|
for (_, T) in &witness.statement.S_T {
|
||||||
|
let r = Scalar::random(&mut *rng);
|
||||||
|
r_sum += r;
|
||||||
|
commitments.A2.push(T * &r);
|
||||||
|
rs.push(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
let t = Scalar::random(&mut *rng);
|
||||||
|
commitments.A1 = (*F * r_sum) + (*H * t);
|
||||||
|
|
||||||
|
(rs, t, commitments)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn t_prove(
|
||||||
|
witness: &ChaumWitness,
|
||||||
|
rs: &[Scalar],
|
||||||
|
mut t3: Scalar,
|
||||||
|
commitments: ChaumCommitments,
|
||||||
|
nonces: &[Scalar],
|
||||||
|
y: &Scalar
|
||||||
|
) -> (Scalar, ChaumProof) {
|
||||||
|
let challenge = ChaumProof::challenge(&witness.statement, &commitments);
|
||||||
|
let mut t1 = Vec::with_capacity(rs.len());
|
||||||
|
let mut t2 = Scalar::zero();
|
||||||
|
|
||||||
|
let mut accum = challenge;
|
||||||
|
for (i, (x, z)) in witness.xz.iter().enumerate() {
|
||||||
|
t1.push(rs[i] + (accum * x));
|
||||||
|
t2 += nonces[i] + (accum * y);
|
||||||
|
t3 += accum * z;
|
||||||
|
accum *= challenge;
|
||||||
|
}
|
||||||
|
|
||||||
|
(challenge, ChaumProof { commitments, t1, t2, t3 })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn challenge(statement: &ChaumStatement, commitments: &ChaumCommitments) -> Scalar {
|
||||||
|
let mut transcript = b"Chaum".to_vec();
|
||||||
|
transcript.extend(&*GENERATORS_TRANSCRIPT);
|
||||||
|
transcript.extend(&statement.transcript());
|
||||||
|
transcript.extend(&commitments.transcript());
|
||||||
|
Scalar::from_uint_reduced(U512::from_be_byte_array(Sha512::digest(transcript)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn prove<R: RngCore + CryptoRng>(
|
||||||
|
rng: &mut R,
|
||||||
|
witness: &ChaumWitness,
|
||||||
|
y: &Scalar
|
||||||
|
) -> ChaumProof {
|
||||||
|
let len = witness.xz.len();
|
||||||
|
let (rs, t3, mut commitments) = Self::r_t_commitments(rng, witness);
|
||||||
|
|
||||||
|
let mut s_sum = Scalar::zero();
|
||||||
|
let mut ss = Vec::with_capacity(len);
|
||||||
|
for i in 0 .. len {
|
||||||
|
let s = Scalar::random(&mut *rng);
|
||||||
|
s_sum += s;
|
||||||
|
commitments.A2[i] += *G * s;
|
||||||
|
ss.push(s);
|
||||||
|
}
|
||||||
|
commitments.A1 += *G * s_sum;
|
||||||
|
|
||||||
|
let (_, proof) = Self::t_prove(&witness, &rs, t3, commitments, &ss, y);
|
||||||
|
proof
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn verify(&self, statement: &ChaumStatement) -> bool {
|
||||||
|
let len = statement.S_T.len();
|
||||||
|
assert_eq!(len, self.commitments.A2.len());
|
||||||
|
assert_eq!(len, self.t1.len());
|
||||||
|
|
||||||
|
let challenge = Self::challenge(&statement, &self.commitments);
|
||||||
|
|
||||||
|
let mut one = self.commitments.A1 - ((*G * self.t2) + (*H * self.t3));
|
||||||
|
let mut two = -(*G * self.t2);
|
||||||
|
|
||||||
|
let mut accum = challenge;
|
||||||
|
for i in 0 .. len {
|
||||||
|
one += statement.S_T[i].0 * accum;
|
||||||
|
one -= *F * self.t1[i];
|
||||||
|
|
||||||
|
two += self.commitments.A2[i] + (*U * accum);
|
||||||
|
two -= statement.S_T[i].1 * self.t1[i];
|
||||||
|
accum *= challenge;
|
||||||
|
}
|
||||||
|
|
||||||
|
one.is_identity().into() && two.is_identity().into()
|
||||||
|
}
|
||||||
|
}
|
||||||
132
coins/firo/src/spark/chaum/multisig.rs
Normal file
132
coins/firo/src/spark/chaum/multisig.rs
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
use std::io::Read;
|
||||||
|
|
||||||
|
use rand_core::{RngCore, CryptoRng, SeedableRng};
|
||||||
|
use rand_chacha::ChaCha12Rng;
|
||||||
|
|
||||||
|
use ff::Field;
|
||||||
|
use k256::{Scalar, ProjectivePoint};
|
||||||
|
|
||||||
|
use transcript::{Transcript, RecommendedTranscript};
|
||||||
|
use frost::{curve::Secp256k1, FrostError, FrostView, algorithm::Algorithm};
|
||||||
|
|
||||||
|
use crate::spark::{G, GENERATORS_TRANSCRIPT, chaum::{ChaumWitness, ChaumProof}};
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct ChaumMultisig {
|
||||||
|
transcript: RecommendedTranscript,
|
||||||
|
len: usize,
|
||||||
|
witness: ChaumWitness,
|
||||||
|
|
||||||
|
challenge: Scalar,
|
||||||
|
proof: Option<ChaumProof>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ChaumMultisig {
|
||||||
|
pub fn new(mut transcript: RecommendedTranscript, witness: ChaumWitness) -> ChaumMultisig {
|
||||||
|
transcript.domain_separate(b"Chaum");
|
||||||
|
transcript.append_message(b"generators", &*GENERATORS_TRANSCRIPT);
|
||||||
|
transcript.append_message(b"statement", &witness.statement.transcript());
|
||||||
|
for (x, z) in &witness.xz {
|
||||||
|
transcript.append_message(b"x", &x.to_bytes());
|
||||||
|
transcript.append_message(b"z", &z.to_bytes());
|
||||||
|
}
|
||||||
|
|
||||||
|
let len = witness.xz.len();
|
||||||
|
ChaumMultisig {
|
||||||
|
transcript,
|
||||||
|
len,
|
||||||
|
witness,
|
||||||
|
|
||||||
|
challenge: Scalar::zero(),
|
||||||
|
proof: None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Algorithm<Secp256k1> for ChaumMultisig {
|
||||||
|
type Transcript = RecommendedTranscript;
|
||||||
|
type Signature = ChaumProof;
|
||||||
|
|
||||||
|
fn transcript(&mut self) -> &mut Self::Transcript {
|
||||||
|
&mut self.transcript
|
||||||
|
}
|
||||||
|
|
||||||
|
fn nonces(&self) -> Vec<Vec<ProjectivePoint>> {
|
||||||
|
vec![vec![*G]; self.len]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn preprocess_addendum<R: RngCore + CryptoRng>(
|
||||||
|
&mut self,
|
||||||
|
_: &mut R,
|
||||||
|
_: &FrostView<Secp256k1>
|
||||||
|
) -> Vec<u8> {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process_addendum<Re: Read>(
|
||||||
|
&mut self,
|
||||||
|
_: &FrostView<Secp256k1>,
|
||||||
|
_: u16,
|
||||||
|
_: &mut Re
|
||||||
|
) -> Result<(), FrostError> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sign_share(
|
||||||
|
&mut self,
|
||||||
|
view: &FrostView<Secp256k1>,
|
||||||
|
nonce_sums: &[Vec<ProjectivePoint>],
|
||||||
|
nonces: &[Scalar],
|
||||||
|
_: &[u8]
|
||||||
|
) -> Scalar {
|
||||||
|
let (rs, t3, mut commitments) = ChaumProof::r_t_commitments(
|
||||||
|
&mut ChaCha12Rng::from_seed(self.transcript.rng_seed(b"r_t")),
|
||||||
|
&self.witness
|
||||||
|
);
|
||||||
|
|
||||||
|
for i in 0 .. self.len {
|
||||||
|
commitments.A2[i] += nonce_sums[i][0];
|
||||||
|
}
|
||||||
|
commitments.A1 += nonce_sums.iter().map(|sum| sum[0]).sum::<ProjectivePoint>();
|
||||||
|
|
||||||
|
let (challenge, proof) = ChaumProof::t_prove(
|
||||||
|
&self.witness,
|
||||||
|
&rs,
|
||||||
|
t3,
|
||||||
|
commitments,
|
||||||
|
nonces,
|
||||||
|
&view.secret_share()
|
||||||
|
);
|
||||||
|
self.challenge = challenge;
|
||||||
|
let t2 = proof.t2;
|
||||||
|
self.proof = Some(proof);
|
||||||
|
t2
|
||||||
|
}
|
||||||
|
|
||||||
|
fn verify(
|
||||||
|
&self,
|
||||||
|
_: ProjectivePoint,
|
||||||
|
_: &[Vec<ProjectivePoint>],
|
||||||
|
sum: Scalar
|
||||||
|
) -> Option<Self::Signature> {
|
||||||
|
let mut proof = self.proof.clone().unwrap();
|
||||||
|
proof.t2 = sum;
|
||||||
|
Some(proof).filter(|proof| proof.verify(&self.witness.statement))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn verify_share(
|
||||||
|
&self,
|
||||||
|
_: u16,
|
||||||
|
verification_share: ProjectivePoint,
|
||||||
|
nonces: &[Vec<ProjectivePoint>],
|
||||||
|
share: Scalar
|
||||||
|
) -> bool {
|
||||||
|
let mut t2 = ProjectivePoint::IDENTITY;
|
||||||
|
let mut accum = self.challenge;
|
||||||
|
for i in 0 .. self.len {
|
||||||
|
t2 += nonces[i][0] + (verification_share * accum);
|
||||||
|
accum *= self.challenge;
|
||||||
|
}
|
||||||
|
(*G * share) == t2
|
||||||
|
}
|
||||||
|
}
|
||||||
42
coins/firo/src/spark/mod.rs
Normal file
42
coins/firo/src/spark/mod.rs
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
use lazy_static::lazy_static;
|
||||||
|
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
|
||||||
|
use group::GroupEncoding;
|
||||||
|
use k256::{ProjectivePoint, CompressedPoint};
|
||||||
|
|
||||||
|
pub mod chaum;
|
||||||
|
|
||||||
|
// Extremely basic hash to curve, which should not be used, yet which offers the needed generators
|
||||||
|
fn generator(letter: u8) -> ProjectivePoint {
|
||||||
|
if letter == b'G' {
|
||||||
|
return ProjectivePoint::GENERATOR;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut point = [2; 33];
|
||||||
|
let mut g = b"Generator ".to_vec();
|
||||||
|
|
||||||
|
let mut res;
|
||||||
|
while {
|
||||||
|
g.push(letter);
|
||||||
|
point[1..].copy_from_slice(&Sha256::digest(&g));
|
||||||
|
res = ProjectivePoint::from_bytes(&CompressedPoint::from(point));
|
||||||
|
res.is_none().into()
|
||||||
|
} {}
|
||||||
|
res.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
pub static ref F: ProjectivePoint = generator(b'F');
|
||||||
|
pub static ref G: ProjectivePoint = generator(b'G');
|
||||||
|
pub static ref H: ProjectivePoint = generator(b'H');
|
||||||
|
pub static ref U: ProjectivePoint = generator(b'U');
|
||||||
|
pub static ref GENERATORS_TRANSCRIPT: Vec<u8> = {
|
||||||
|
let mut transcript = Vec::with_capacity(4 * 33);
|
||||||
|
transcript.extend(&F.to_bytes());
|
||||||
|
transcript.extend(&G.to_bytes());
|
||||||
|
transcript.extend(&H.to_bytes());
|
||||||
|
transcript.extend(&U.to_bytes());
|
||||||
|
transcript
|
||||||
|
};
|
||||||
|
}
|
||||||
72
coins/firo/src/tests/mod.rs
Normal file
72
coins/firo/src/tests/mod.rs
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
use rand::rngs::OsRng;
|
||||||
|
|
||||||
|
use ff::Field;
|
||||||
|
use k256::Scalar;
|
||||||
|
|
||||||
|
#[cfg(feature = "multisig")]
|
||||||
|
use transcript::{Transcript, RecommendedTranscript};
|
||||||
|
#[cfg(feature = "multisig")]
|
||||||
|
use frost::{curve::Secp256k1, tests::{key_gen, algorithm_machines, sign}};
|
||||||
|
|
||||||
|
use crate::spark::{F, G, H, U, chaum::*};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn chaum() {
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
let mut S_T = vec![];
|
||||||
|
let mut xz = vec![];
|
||||||
|
let y = Scalar::random(&mut OsRng);
|
||||||
|
for _ in 0 .. 2 {
|
||||||
|
let x = Scalar::random(&mut OsRng);
|
||||||
|
let z = Scalar::random(&mut OsRng);
|
||||||
|
|
||||||
|
S_T.push((
|
||||||
|
(*F * x) + (*G * y) + (*H * z),
|
||||||
|
// U = (x * T) + (y * G)
|
||||||
|
// T = (U - (y * G)) * x^-1
|
||||||
|
(*U - (*G * y)) * x.invert().unwrap()
|
||||||
|
));
|
||||||
|
|
||||||
|
xz.push((x, z));
|
||||||
|
}
|
||||||
|
|
||||||
|
let statement = ChaumStatement::new(b"Hello, World!".to_vec(), S_T);
|
||||||
|
let witness = ChaumWitness::new(statement.clone(), xz);
|
||||||
|
assert!(ChaumProof::prove(&mut OsRng, &witness, &y).verify(&statement));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "multisig")]
|
||||||
|
#[test]
|
||||||
|
fn chaum_multisig() {
|
||||||
|
let keys = key_gen::<_, Secp256k1>(&mut OsRng);
|
||||||
|
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
let mut S_T = vec![];
|
||||||
|
let mut xz = vec![];
|
||||||
|
for _ in 0 .. 5 {
|
||||||
|
let x = Scalar::random(&mut OsRng);
|
||||||
|
let z = Scalar::random(&mut OsRng);
|
||||||
|
|
||||||
|
S_T.push((
|
||||||
|
(*F * x) + keys[&1].group_key() + (*H * z),
|
||||||
|
(*U - keys[&1].group_key()) * x.invert().unwrap()
|
||||||
|
));
|
||||||
|
|
||||||
|
xz.push((x, z));
|
||||||
|
}
|
||||||
|
|
||||||
|
let statement = ChaumStatement::new(b"Hello, Multisig World!".to_vec(), S_T);
|
||||||
|
let witness = ChaumWitness::new(statement.clone(), xz);
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
sign(
|
||||||
|
&mut OsRng,
|
||||||
|
algorithm_machines(
|
||||||
|
&mut OsRng,
|
||||||
|
ChaumMultisig::new(RecommendedTranscript::new(b"Firo Serai Chaum Test"), witness),
|
||||||
|
&keys
|
||||||
|
),
|
||||||
|
&[]
|
||||||
|
).verify(&statement)
|
||||||
|
);
|
||||||
|
}
|
||||||
1
coins/monero/.gitignore
vendored
Normal file
1
coins/monero/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
c/.build
|
||||||
@@ -1,111 +1,52 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "monero-serai"
|
name = "monero-serai"
|
||||||
version = "0.1.4-alpha"
|
version = "0.1.0"
|
||||||
description = "A modern Monero transaction library"
|
description = "A modern Monero wallet library"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
repository = "https://github.com/serai-dex/serai/tree/develop/coins/monero"
|
|
||||||
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
|
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.74"
|
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
|
||||||
all-features = true
|
|
||||||
rustdoc-args = ["--cfg", "docsrs"]
|
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
std-shims = { path = "../../common/std-shims", version = "^0.1.1", default-features = false }
|
|
||||||
|
|
||||||
async-trait = { version = "0.1", default-features = false }
|
|
||||||
thiserror = { version = "1", default-features = false, optional = true }
|
|
||||||
|
|
||||||
zeroize = { version = "^1.5", default-features = false, features = ["zeroize_derive"] }
|
|
||||||
subtle = { version = "^2.4", default-features = false }
|
|
||||||
|
|
||||||
rand_core = { version = "0.6", default-features = false }
|
|
||||||
# Used to send transactions
|
|
||||||
rand = { version = "0.8", default-features = false }
|
|
||||||
rand_chacha = { version = "0.3", default-features = false }
|
|
||||||
# Used to select decoys
|
|
||||||
rand_distr = { version = "0.4", default-features = false }
|
|
||||||
|
|
||||||
sha3 = { version = "0.10", default-features = false }
|
|
||||||
pbkdf2 = { version = "0.12", features = ["simple"], default-features = false }
|
|
||||||
|
|
||||||
curve25519-dalek = { version = "4", default-features = false, features = ["alloc", "zeroize", "precomputed-tables"] }
|
|
||||||
|
|
||||||
# Used for the hash to curve, along with the more complicated proofs
|
|
||||||
group = { version = "0.13", default-features = false }
|
|
||||||
dalek-ff-group = { path = "../../crypto/dalek-ff-group", version = "0.4", default-features = false }
|
|
||||||
multiexp = { path = "../../crypto/multiexp", version = "0.4", default-features = false, features = ["batch"] }
|
|
||||||
|
|
||||||
# Needed for multisig
|
|
||||||
transcript = { package = "flexible-transcript", path = "../../crypto/transcript", version = "0.3", default-features = false, features = ["recommended"], optional = true }
|
|
||||||
frost = { package = "modular-frost", path = "../../crypto/frost", version = "0.8", default-features = false, features = ["ed25519"], optional = true }
|
|
||||||
|
|
||||||
monero-generators = { path = "generators", version = "0.4", default-features = false }
|
|
||||||
|
|
||||||
async-lock = { version = "3", default-features = false, optional = true }
|
|
||||||
|
|
||||||
hex-literal = "0.4"
|
|
||||||
hex = { version = "0.4", default-features = false, features = ["alloc"] }
|
|
||||||
serde = { version = "1", default-features = false, features = ["derive", "alloc"] }
|
|
||||||
serde_json = { version = "1", default-features = false, features = ["alloc"] }
|
|
||||||
|
|
||||||
base58-monero = { version = "2", default-features = false, features = ["check"] }
|
|
||||||
|
|
||||||
# Used for the provided HTTP RPC
|
|
||||||
digest_auth = { version = "0.3", default-features = false, optional = true }
|
|
||||||
simple-request = { path = "../../common/request", version = "0.1", default-features = false, features = ["tls"], optional = true }
|
|
||||||
tokio = { version = "1", default-features = false, optional = true }
|
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
dalek-ff-group = { path = "../../crypto/dalek-ff-group", version = "0.4", default-features = false }
|
cc = "1.0"
|
||||||
monero-generators = { path = "generators", version = "0.4", default-features = false }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dependencies]
|
||||||
tokio = { version = "1", features = ["sync", "macros"] }
|
hex-literal = "0.3"
|
||||||
|
lazy_static = "1"
|
||||||
|
thiserror = "1"
|
||||||
|
|
||||||
frost = { package = "modular-frost", path = "../../crypto/frost", features = ["tests"] }
|
rand_core = "0.6"
|
||||||
|
rand_chacha = { version = "0.3", optional = true }
|
||||||
|
rand = "0.8"
|
||||||
|
rand_distr = "0.4"
|
||||||
|
|
||||||
|
subtle = "2.4"
|
||||||
|
|
||||||
|
tiny-keccak = { version = "2", features = ["keccak"] }
|
||||||
|
blake2 = { version = "0.10", optional = true }
|
||||||
|
|
||||||
|
curve25519-dalek = { version = "3", features = ["std"] }
|
||||||
|
|
||||||
|
group = { version = "0.12" }
|
||||||
|
dalek-ff-group = { path = "../../crypto/dalek-ff-group" }
|
||||||
|
|
||||||
|
transcript = { package = "flexible-transcript", path = "../../crypto/transcript", features = ["recommended"], optional = true }
|
||||||
|
frost = { package = "modular-frost", path = "../../crypto/frost", features = ["ed25519"], optional = true }
|
||||||
|
dleq = { package = "dleq-serai", path = "../../crypto/dleq", features = ["serialize"], optional = true }
|
||||||
|
|
||||||
|
hex = "0.4"
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_json = "1.0"
|
||||||
|
|
||||||
|
base58-monero = "1"
|
||||||
|
monero-epee-bin-serde = "1.0"
|
||||||
|
monero = "0.16"
|
||||||
|
|
||||||
|
reqwest = { version = "0.11", features = ["json"] }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
std = [
|
|
||||||
"std-shims/std",
|
|
||||||
|
|
||||||
"thiserror",
|
|
||||||
|
|
||||||
"zeroize/std",
|
|
||||||
"subtle/std",
|
|
||||||
|
|
||||||
"rand_core/std",
|
|
||||||
"rand/std",
|
|
||||||
"rand_chacha/std",
|
|
||||||
"rand_distr/std",
|
|
||||||
|
|
||||||
"sha3/std",
|
|
||||||
"pbkdf2/std",
|
|
||||||
|
|
||||||
"multiexp/std",
|
|
||||||
|
|
||||||
"transcript/std",
|
|
||||||
|
|
||||||
"monero-generators/std",
|
|
||||||
|
|
||||||
"async-lock?/std",
|
|
||||||
|
|
||||||
"hex/std",
|
|
||||||
"serde/std",
|
|
||||||
"serde_json/std",
|
|
||||||
|
|
||||||
"base58-monero/std",
|
|
||||||
]
|
|
||||||
|
|
||||||
cache-distribution = ["async-lock"]
|
|
||||||
http-rpc = ["digest_auth", "simple-request", "tokio"]
|
|
||||||
multisig = ["transcript", "frost", "std"]
|
|
||||||
binaries = ["tokio/rt-multi-thread", "tokio/macros", "http-rpc"]
|
|
||||||
experimental = []
|
experimental = []
|
||||||
|
multisig = ["rand_chacha", "blake2", "transcript", "frost", "dleq"]
|
||||||
|
|
||||||
default = ["std", "http-rpc"]
|
[dev-dependencies]
|
||||||
|
sha2 = "0.10"
|
||||||
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2022-2023 Luke Parker
|
Copyright (c) 2022 Luke Parker
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
@@ -4,46 +4,4 @@ A modern Monero transaction library intended for usage in wallets. It prides
|
|||||||
itself on accuracy, correctness, and removing common pit falls developers may
|
itself on accuracy, correctness, and removing common pit falls developers may
|
||||||
face.
|
face.
|
||||||
|
|
||||||
monero-serai also offers the following features:
|
Threshold multisignature support is available via the `multisig` feature.
|
||||||
|
|
||||||
- Featured Addresses
|
|
||||||
- A FROST-based multisig orders of magnitude more performant than Monero's
|
|
||||||
|
|
||||||
### Purpose and support
|
|
||||||
|
|
||||||
monero-serai was written for Serai, a decentralized exchange aiming to support
|
|
||||||
Monero. Despite this, monero-serai is intended to be a widely usable library,
|
|
||||||
accurate to Monero. monero-serai guarantees the functionality needed for Serai,
|
|
||||||
yet will not deprive functionality from other users.
|
|
||||||
|
|
||||||
Various legacy transaction formats are not currently implemented, yet we are
|
|
||||||
willing to add support for them. There aren't active development efforts around
|
|
||||||
them however.
|
|
||||||
|
|
||||||
### Caveats
|
|
||||||
|
|
||||||
This library DOES attempt to do the following:
|
|
||||||
|
|
||||||
- Create on-chain transactions identical to how wallet2 would (unless told not
|
|
||||||
to)
|
|
||||||
- Not be detectable as monero-serai when scanning outputs
|
|
||||||
- Not reveal spent outputs to the connected RPC node
|
|
||||||
|
|
||||||
This library DOES NOT attempt to do the following:
|
|
||||||
|
|
||||||
- Have identical RPC behavior when creating transactions
|
|
||||||
- Be a wallet
|
|
||||||
|
|
||||||
This means that monero-serai shouldn't be fingerprintable on-chain. It also
|
|
||||||
shouldn't be fingerprintable if a targeted attack occurs to detect if the
|
|
||||||
receiving wallet is monero-serai or wallet2. It also should be generally safe
|
|
||||||
for usage with remote nodes.
|
|
||||||
|
|
||||||
It won't hide from remote nodes it's monero-serai however, potentially
|
|
||||||
allowing a remote node to profile you. The implications of this are left to the
|
|
||||||
user to consider.
|
|
||||||
|
|
||||||
It also won't act as a wallet, just as a transaction library. wallet2 has
|
|
||||||
several *non-transaction-level* policies, such as always attempting to use two
|
|
||||||
inputs to create transactions. These are considered out of scope to
|
|
||||||
monero-serai.
|
|
||||||
|
|||||||
@@ -1,67 +1,72 @@
|
|||||||
use std::{
|
use std::{env, path::Path, process::Command};
|
||||||
io::Write,
|
|
||||||
env,
|
|
||||||
path::Path,
|
|
||||||
fs::{File, remove_file},
|
|
||||||
};
|
|
||||||
|
|
||||||
use dalek_ff_group::EdwardsPoint;
|
|
||||||
|
|
||||||
use monero_generators::bulletproofs_generators;
|
|
||||||
|
|
||||||
fn serialize(generators_string: &mut String, points: &[EdwardsPoint]) {
|
|
||||||
for generator in points {
|
|
||||||
generators_string.extend(
|
|
||||||
format!(
|
|
||||||
"
|
|
||||||
dalek_ff_group::EdwardsPoint(
|
|
||||||
curve25519_dalek::edwards::CompressedEdwardsY({:?}).decompress().unwrap()
|
|
||||||
),
|
|
||||||
",
|
|
||||||
generator.compress().to_bytes()
|
|
||||||
)
|
|
||||||
.chars(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn generators(prefix: &'static str, path: &str) {
|
|
||||||
let generators = bulletproofs_generators(prefix.as_bytes());
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
let mut G_str = String::new();
|
|
||||||
serialize(&mut G_str, &generators.G);
|
|
||||||
#[allow(non_snake_case)]
|
|
||||||
let mut H_str = String::new();
|
|
||||||
serialize(&mut H_str, &generators.H);
|
|
||||||
|
|
||||||
let path = Path::new(&env::var("OUT_DIR").unwrap()).join(path);
|
|
||||||
let _ = remove_file(&path);
|
|
||||||
File::create(&path)
|
|
||||||
.unwrap()
|
|
||||||
.write_all(
|
|
||||||
format!(
|
|
||||||
"
|
|
||||||
pub(crate) static GENERATORS_CELL: OnceLock<Generators> = OnceLock::new();
|
|
||||||
pub fn GENERATORS() -> &'static Generators {{
|
|
||||||
GENERATORS_CELL.get_or_init(|| Generators {{
|
|
||||||
G: vec![
|
|
||||||
{G_str}
|
|
||||||
],
|
|
||||||
H: vec![
|
|
||||||
{H_str}
|
|
||||||
],
|
|
||||||
}})
|
|
||||||
}}
|
|
||||||
",
|
|
||||||
)
|
|
||||||
.as_bytes(),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
println!("cargo:rerun-if-changed=build.rs");
|
if !Command::new("git").args(&["submodule", "update", "--init", "--recursive"]).status().unwrap().success() {
|
||||||
|
panic!("git failed to init submodules");
|
||||||
|
}
|
||||||
|
|
||||||
generators("bulletproof", "generators.rs");
|
if !Command ::new("mkdir").args(&["-p", ".build"])
|
||||||
generators("bulletproof_plus", "generators_plus.rs");
|
.current_dir(&Path::new("c")).status().unwrap().success() {
|
||||||
|
panic!("failed to create a directory to track build progress");
|
||||||
|
}
|
||||||
|
|
||||||
|
let out_dir = &env::var("OUT_DIR").unwrap();
|
||||||
|
|
||||||
|
// Use a file to signal if Monero was already built, as that should never be rebuilt
|
||||||
|
// If the signaling file was deleted, run this script again to rebuild Monero though
|
||||||
|
println!("cargo:rerun-if-changed=c/.build/monero");
|
||||||
|
if !Path::new("c/.build/monero").exists() {
|
||||||
|
if !Command::new("make").arg(format!("-j{}", &env::var("THREADS").unwrap_or("2".to_string())))
|
||||||
|
.current_dir(&Path::new("c/monero")).status().unwrap().success() {
|
||||||
|
panic!("make failed to build Monero. Please check your dependencies");
|
||||||
|
}
|
||||||
|
|
||||||
|
if !Command::new("touch").arg("monero")
|
||||||
|
.current_dir(&Path::new("c/.build")).status().unwrap().success() {
|
||||||
|
panic!("failed to create a file to label Monero as built");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("cargo:rerun-if-changed=c/wrapper.cpp");
|
||||||
|
cc::Build::new()
|
||||||
|
.static_flag(true)
|
||||||
|
.warnings(false)
|
||||||
|
.extra_warnings(false)
|
||||||
|
.flag("-Wno-deprecated-declarations")
|
||||||
|
|
||||||
|
.include("c/monero/external/supercop/include")
|
||||||
|
.include("c/monero/contrib/epee/include")
|
||||||
|
.include("c/monero/src")
|
||||||
|
.include("c/monero/build/release/generated_include")
|
||||||
|
|
||||||
|
.define("AUTO_INITIALIZE_EASYLOGGINGPP", None)
|
||||||
|
.include("c/monero/external/easylogging++")
|
||||||
|
.file("c/monero/external/easylogging++/easylogging++.cc")
|
||||||
|
|
||||||
|
.file("c/monero/src/common/aligned.c")
|
||||||
|
.file("c/monero/src/common/perf_timer.cpp")
|
||||||
|
|
||||||
|
.include("c/monero/src/crypto")
|
||||||
|
.file("c/monero/src/crypto/crypto-ops-data.c")
|
||||||
|
.file("c/monero/src/crypto/crypto-ops.c")
|
||||||
|
.file("c/monero/src/crypto/keccak.c")
|
||||||
|
.file("c/monero/src/crypto/hash.c")
|
||||||
|
|
||||||
|
.include("c/monero/src/device")
|
||||||
|
.file("c/monero/src/device/device_default.cpp")
|
||||||
|
|
||||||
|
.include("c/monero/src/ringct")
|
||||||
|
.file("c/monero/src/ringct/rctCryptoOps.c")
|
||||||
|
.file("c/monero/src/ringct/rctTypes.cpp")
|
||||||
|
.file("c/monero/src/ringct/rctOps.cpp")
|
||||||
|
.file("c/monero/src/ringct/multiexp.cc")
|
||||||
|
.file("c/monero/src/ringct/bulletproofs.cc")
|
||||||
|
.file("c/monero/src/ringct/rctSigs.cpp")
|
||||||
|
|
||||||
|
.file("c/wrapper.cpp")
|
||||||
|
.compile("wrapper");
|
||||||
|
|
||||||
|
println!("cargo:rustc-link-search={}", out_dir);
|
||||||
|
println!("cargo:rustc-link-lib=wrapper");
|
||||||
|
println!("cargo:rustc-link-lib=stdc++");
|
||||||
}
|
}
|
||||||
|
|||||||
1
coins/monero/c/monero
Submodule
1
coins/monero/c/monero
Submodule
Submodule coins/monero/c/monero added at 424e4de16b
158
coins/monero/c/wrapper.cpp
Normal file
158
coins/monero/c/wrapper.cpp
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
#include <mutex>
|
||||||
|
|
||||||
|
#include "device/device_default.hpp"
|
||||||
|
|
||||||
|
#include "ringct/bulletproofs.h"
|
||||||
|
#include "ringct/rctSigs.h"
|
||||||
|
|
||||||
|
typedef std::lock_guard<std::mutex> lock;
|
||||||
|
|
||||||
|
std::mutex rng_mutex;
|
||||||
|
uint8_t rng_entropy[64];
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
void rng(uint8_t* seed) {
|
||||||
|
// Set the first half to the seed
|
||||||
|
memcpy(rng_entropy, seed, 32);
|
||||||
|
// Set the second half to the hash of a DST to ensure a lack of collisions
|
||||||
|
crypto::cn_fast_hash("RNG_entropy_seed", 16, (char*) &rng_entropy[32]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" void monero_wide_reduce(uint8_t* value);
|
||||||
|
namespace crypto {
|
||||||
|
void generate_random_bytes_not_thread_safe(size_t n, void* value) {
|
||||||
|
size_t written = 0;
|
||||||
|
while (written != n) {
|
||||||
|
uint8_t hash[32];
|
||||||
|
crypto::cn_fast_hash(rng_entropy, 64, (char*) hash);
|
||||||
|
// Step the RNG by setting the latter half to the most recent result
|
||||||
|
// Does not leak the RNG, even if the values are leaked (which they are
|
||||||
|
// expected to be) due to the first half remaining constant and
|
||||||
|
// undisclosed
|
||||||
|
memcpy(&rng_entropy[32], hash, 32);
|
||||||
|
|
||||||
|
size_t next = n - written;
|
||||||
|
if (next > 32) {
|
||||||
|
next = 32;
|
||||||
|
}
|
||||||
|
memcpy(&((uint8_t*) value)[written], hash, next);
|
||||||
|
written += next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void random32_unbiased(unsigned char *bytes) {
|
||||||
|
uint8_t value[64];
|
||||||
|
generate_random_bytes_not_thread_safe(64, value);
|
||||||
|
monero_wide_reduce(value);
|
||||||
|
memcpy(bytes, value, 32);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
void c_hash_to_point(uint8_t* point) {
|
||||||
|
rct::key key_point;
|
||||||
|
ge_p3 e_p3;
|
||||||
|
memcpy(key_point.bytes, point, 32);
|
||||||
|
rct::hash_to_p3(e_p3, key_point);
|
||||||
|
ge_p3_tobytes(point, &e_p3);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t* c_generate_bp(uint8_t* seed, uint8_t len, uint64_t* a, uint8_t* m) {
|
||||||
|
lock guard(rng_mutex);
|
||||||
|
rng(seed);
|
||||||
|
|
||||||
|
rct::keyV masks;
|
||||||
|
std::vector<uint64_t> amounts;
|
||||||
|
masks.resize(len);
|
||||||
|
amounts.resize(len);
|
||||||
|
for (uint8_t i = 0; i < len; i++) {
|
||||||
|
memcpy(masks[i].bytes, m + (i * 32), 32);
|
||||||
|
amounts[i] = a[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
rct::Bulletproof bp = rct::bulletproof_PROVE(amounts, masks);
|
||||||
|
|
||||||
|
std::stringstream ss;
|
||||||
|
binary_archive<true> ba(ss);
|
||||||
|
::serialization::serialize(ba, bp);
|
||||||
|
uint8_t* res = (uint8_t*) calloc(ss.str().size(), 1);
|
||||||
|
memcpy(res, ss.str().data(), ss.str().size());
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool c_verify_bp(
|
||||||
|
uint8_t* seed,
|
||||||
|
uint s_len,
|
||||||
|
uint8_t* s,
|
||||||
|
uint8_t c_len,
|
||||||
|
uint8_t* c
|
||||||
|
) {
|
||||||
|
// BPs are batch verified which use RNG based weights to ensure individual
|
||||||
|
// integrity
|
||||||
|
// That's why this must also have control over RNG, to prevent interrupting
|
||||||
|
// multisig signing while not using known seeds. Considering this doesn't
|
||||||
|
// actually define a batch, and it's only verifying a single BP,
|
||||||
|
// it'd probably be fine, but...
|
||||||
|
lock guard(rng_mutex);
|
||||||
|
rng(seed);
|
||||||
|
|
||||||
|
rct::Bulletproof bp;
|
||||||
|
std::stringstream ss;
|
||||||
|
std::string str;
|
||||||
|
str.assign((char*) s, (size_t) s_len);
|
||||||
|
ss << str;
|
||||||
|
binary_archive<false> ba(ss);
|
||||||
|
::serialization::serialize(ba, bp);
|
||||||
|
if (!ss.good()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bp.V.resize(c_len);
|
||||||
|
for (uint8_t i = 0; i < c_len; i++) {
|
||||||
|
memcpy(bp.V[i].bytes, &c[i * 32], 32);
|
||||||
|
}
|
||||||
|
|
||||||
|
try { return rct::bulletproof_VERIFY(bp); } catch(...) { return false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
bool c_verify_clsag(
|
||||||
|
uint s_len,
|
||||||
|
uint8_t* s,
|
||||||
|
uint8_t k_len,
|
||||||
|
uint8_t* k,
|
||||||
|
uint8_t* I,
|
||||||
|
uint8_t* p,
|
||||||
|
uint8_t* m
|
||||||
|
) {
|
||||||
|
rct::clsag clsag;
|
||||||
|
std::stringstream ss;
|
||||||
|
std::string str;
|
||||||
|
str.assign((char*) s, (size_t) s_len);
|
||||||
|
ss << str;
|
||||||
|
binary_archive<false> ba(ss);
|
||||||
|
::serialization::serialize(ba, clsag);
|
||||||
|
if (!ss.good()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
rct::ctkeyV keys;
|
||||||
|
keys.resize(k_len);
|
||||||
|
for (uint8_t i = 0; i < k_len; i++) {
|
||||||
|
memcpy(keys[i].dest.bytes, &k[(i * 2) * 32], 32);
|
||||||
|
memcpy(keys[i].mask.bytes, &k[((i * 2) + 1) * 32], 32);
|
||||||
|
}
|
||||||
|
|
||||||
|
memcpy(clsag.I.bytes, I, 32);
|
||||||
|
|
||||||
|
rct::key pseudo_out;
|
||||||
|
memcpy(pseudo_out.bytes, p, 32);
|
||||||
|
|
||||||
|
rct::key msg;
|
||||||
|
memcpy(msg.bytes, m, 32);
|
||||||
|
|
||||||
|
try {
|
||||||
|
return verRctCLSAGSimple(msg, clsag, keys, pseudo_out);
|
||||||
|
} catch(...) { return false; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "monero-generators"
|
|
||||||
version = "0.4.0"
|
|
||||||
description = "Monero's hash_to_point and generators"
|
|
||||||
license = "MIT"
|
|
||||||
repository = "https://github.com/serai-dex/serai/tree/develop/coins/monero/generators"
|
|
||||||
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
|
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
|
||||||
all-features = true
|
|
||||||
rustdoc-args = ["--cfg", "docsrs"]
|
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
std-shims = { path = "../../../common/std-shims", version = "^0.1.1", default-features = false }
|
|
||||||
|
|
||||||
subtle = { version = "^2.4", default-features = false }
|
|
||||||
|
|
||||||
sha3 = { version = "0.10", default-features = false }
|
|
||||||
|
|
||||||
curve25519-dalek = { version = "4", default-features = false, features = ["alloc", "zeroize", "precomputed-tables"] }
|
|
||||||
|
|
||||||
group = { version = "0.13", default-features = false }
|
|
||||||
dalek-ff-group = { path = "../../../crypto/dalek-ff-group", version = "0.4", default-features = false }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
hex = "0.4"
|
|
||||||
|
|
||||||
[features]
|
|
||||||
std = ["std-shims/std", "subtle/std", "sha3/std", "dalek-ff-group/std"]
|
|
||||||
default = ["std"]
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2022-2023 Luke Parker
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
# Monero Generators
|
|
||||||
|
|
||||||
Generators used by Monero in both its Pedersen commitments and Bulletproofs(+).
|
|
||||||
An implementation of Monero's `ge_fromfe_frombytes_vartime`, simply called
|
|
||||||
`hash_to_point` here, is included, as needed to generate generators.
|
|
||||||
|
|
||||||
This library is usable under no-std when the `std` feature is disabled.
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user