diff --git a/.github/actions/bitcoin/action.yml b/.github/actions/bitcoin/action.yml index 2a6dbce3..6f628172 100644 --- a/.github/actions/bitcoin/action.yml +++ b/.github/actions/bitcoin/action.yml @@ -37,4 +37,4 @@ runs: - name: Bitcoin Regtest Daemon shell: bash - run: PATH=$PATH:/usr/bin ./orchestration/dev/coins/bitcoin/run.sh -daemon + run: PATH=$PATH:/usr/bin ./orchestration/dev/networks/bitcoin/run.sh -daemon diff --git a/.github/actions/monero/action.yml b/.github/actions/monero/action.yml index 8dff093a..4edbb4d4 100644 --- a/.github/actions/monero/action.yml +++ b/.github/actions/monero/action.yml @@ -43,4 +43,4 @@ runs: - name: Monero Regtest Daemon shell: bash - run: PATH=$PATH:/usr/bin ./orchestration/dev/coins/monero/run.sh --detach + run: PATH=$PATH:/usr/bin ./orchestration/dev/networks/monero/run.sh --detach diff --git a/.github/actions/test-dependencies/action.yml b/.github/actions/test-dependencies/action.yml index 49c2fa64..9aa90fc2 100644 --- a/.github/actions/test-dependencies/action.yml +++ b/.github/actions/test-dependencies/action.yml @@ -10,7 +10,7 @@ inputs: bitcoin-version: description: "Bitcoin version to download and run as a regtest node" required: false - default: "27.0" + default: "27.1" runs: using: "composite" diff --git a/.github/nightly-version b/.github/nightly-version index 514aef61..9f98e758 100644 --- a/.github/nightly-version +++ b/.github/nightly-version @@ -1 +1 @@ -nightly-2024-05-01 +nightly-2024-07-01 diff --git a/.github/workflows/coordinator-tests.yml b/.github/workflows/coordinator-tests.yml index 138fd106..b956f752 100644 --- a/.github/workflows/coordinator-tests.yml +++ b/.github/workflows/coordinator-tests.yml @@ -7,7 +7,7 @@ on: paths: - "common/**" - "crypto/**" - - "coins/**" + - "networks/**" - "message-queue/**" - "coordinator/**" - "orchestration/**" @@ -18,7 +18,7 @@ on: paths: - "common/**" - "crypto/**" - - "coins/**" + - "networks/**" - "message-queue/**" - "coordinator/**" - "orchestration/**" @@ -37,4 +37,4 @@ jobs: uses: ./.github/actions/build-dependencies - name: Run coordinator Docker tests - run: cd tests/coordinator && GITHUB_CI=true RUST_BACKTRACE=1 cargo test --all-features + run: GITHUB_CI=true RUST_BACKTRACE=1 cargo test --all-features -p serai-coordinator-tests diff --git a/.github/workflows/full-stack-tests.yml b/.github/workflows/full-stack-tests.yml index baacf774..7bcce866 100644 --- a/.github/workflows/full-stack-tests.yml +++ b/.github/workflows/full-stack-tests.yml @@ -19,4 +19,4 @@ jobs: 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 + run: GITHUB_CI=true RUST_BACKTRACE=1 cargo test --all-features -p serai-full-stack-tests diff --git a/.github/workflows/message-queue-tests.yml b/.github/workflows/message-queue-tests.yml index 7894549c..aa6f9328 100644 --- a/.github/workflows/message-queue-tests.yml +++ b/.github/workflows/message-queue-tests.yml @@ -33,4 +33,4 @@ jobs: 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 + run: GITHUB_CI=true RUST_BACKTRACE=1 cargo test --all-features -p serai-message-queue-tests diff --git a/.github/workflows/monero-tests.yaml b/.github/workflows/monero-tests.yaml index 3f2127ce..a05adeac 100644 --- a/.github/workflows/monero-tests.yaml +++ b/.github/workflows/monero-tests.yaml @@ -5,12 +5,12 @@ on: branches: - develop paths: - - "coins/monero/**" + - "networks/monero/**" - "processor/**" pull_request: paths: - - "coins/monero/**" + - "networks/monero/**" - "processor/**" workflow_dispatch: @@ -26,7 +26,22 @@ jobs: uses: ./.github/actions/test-dependencies - name: Run Unit Tests Without Features - run: GITHUB_CI=true RUST_BACKTRACE=1 cargo test --package monero-serai --lib + run: | + GITHUB_CI=true RUST_BACKTRACE=1 cargo test --package monero-io --lib + GITHUB_CI=true RUST_BACKTRACE=1 cargo test --package monero-generators --lib + GITHUB_CI=true RUST_BACKTRACE=1 cargo test --package monero-primitives --lib + GITHUB_CI=true RUST_BACKTRACE=1 cargo test --package monero-mlsag --lib + GITHUB_CI=true RUST_BACKTRACE=1 cargo test --package monero-clsag --lib + GITHUB_CI=true RUST_BACKTRACE=1 cargo test --package monero-borromean --lib + GITHUB_CI=true RUST_BACKTRACE=1 cargo test --package monero-bulletproofs --lib + GITHUB_CI=true RUST_BACKTRACE=1 cargo test --package monero-serai --lib + GITHUB_CI=true RUST_BACKTRACE=1 cargo test --package monero-rpc --lib + GITHUB_CI=true RUST_BACKTRACE=1 cargo test --package monero-simple-request-rpc --lib + GITHUB_CI=true RUST_BACKTRACE=1 cargo test --package monero-address --lib + GITHUB_CI=true RUST_BACKTRACE=1 cargo test --package monero-wallet --lib + GITHUB_CI=true RUST_BACKTRACE=1 cargo test --package monero-seed --lib + GITHUB_CI=true RUST_BACKTRACE=1 cargo test --package polyseed --lib + GITHUB_CI=true RUST_BACKTRACE=1 cargo test --package monero-wallet-util --lib # Doesn't run unit tests with features as the tests workflow will @@ -46,11 +61,17 @@ jobs: 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 '*' + run: | + GITHUB_CI=true RUST_BACKTRACE=1 cargo test --package monero-serai --test '*' + GITHUB_CI=true RUST_BACKTRACE=1 cargo test --package monero-simple-request-rpc --test '*' + GITHUB_CI=true RUST_BACKTRACE=1 cargo test --package monero-wallet --test '*' + GITHUB_CI=true RUST_BACKTRACE=1 cargo test --package monero-wallet-util --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 '*' + run: | + GITHUB_CI=true RUST_BACKTRACE=1 cargo test --package monero-serai --all-features --test '*' + GITHUB_CI=true RUST_BACKTRACE=1 cargo test --package monero-simple-request-rpc --test '*' + GITHUB_CI=true RUST_BACKTRACE=1 cargo test --package monero-wallet --all-features --test '*' + GITHUB_CI=true RUST_BACKTRACE=1 cargo test --package monero-wallet-util --all-features --test '*' diff --git a/.github/workflows/coins-tests.yml b/.github/workflows/networks-tests.yml similarity index 51% rename from .github/workflows/coins-tests.yml rename to .github/workflows/networks-tests.yml index f94e9fd5..f346b986 100644 --- a/.github/workflows/coins-tests.yml +++ b/.github/workflows/networks-tests.yml @@ -1,4 +1,4 @@ -name: coins/ Tests +name: networks/ Tests on: push: @@ -7,18 +7,18 @@ on: paths: - "common/**" - "crypto/**" - - "coins/**" + - "networks/**" pull_request: paths: - "common/**" - "crypto/**" - - "coins/**" + - "networks/**" workflow_dispatch: jobs: - test-coins: + test-networks: runs-on: ubuntu-latest steps: - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac @@ -32,5 +32,20 @@ jobs: -p bitcoin-serai \ -p alloy-simple-request-transport \ -p ethereum-serai \ + -p serai-ethereum-relayer \ + -p monero-io \ -p monero-generators \ - -p monero-serai + -p monero-primitives \ + -p monero-mlsag \ + -p monero-clsag \ + -p monero-borromean \ + -p monero-bulletproofs \ + -p monero-serai \ + -p monero-rpc \ + -p monero-simple-request-rpc \ + -p monero-address \ + -p monero-wallet \ + -p monero-seed \ + -p polyseed \ + -p monero-wallet-util \ + -p monero-serai-verify-chain diff --git a/.github/workflows/no-std.yml b/.github/workflows/no-std.yml index 79ea5767..fa39690a 100644 --- a/.github/workflows/no-std.yml +++ b/.github/workflows/no-std.yml @@ -7,14 +7,14 @@ on: paths: - "common/**" - "crypto/**" - - "coins/**" + - "networks/**" - "tests/no-std/**" pull_request: paths: - "common/**" - "crypto/**" - - "coins/**" + - "networks/**" - "tests/no-std/**" workflow_dispatch: @@ -32,4 +32,4 @@ jobs: 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 + run: CFLAGS=-I/usr/include cargo build --target riscv32imac-unknown-none-elf -p serai-no-std-tests diff --git a/.github/workflows/processor-tests.yml b/.github/workflows/processor-tests.yml index 0b5ecbbe..5f6043eb 100644 --- a/.github/workflows/processor-tests.yml +++ b/.github/workflows/processor-tests.yml @@ -7,7 +7,7 @@ on: paths: - "common/**" - "crypto/**" - - "coins/**" + - "networks/**" - "message-queue/**" - "processor/**" - "orchestration/**" @@ -18,7 +18,7 @@ on: paths: - "common/**" - "crypto/**" - - "coins/**" + - "networks/**" - "message-queue/**" - "processor/**" - "orchestration/**" @@ -37,4 +37,4 @@ jobs: uses: ./.github/actions/build-dependencies - name: Run processor Docker tests - run: cd tests/processor && GITHUB_CI=true RUST_BACKTRACE=1 cargo test --all-features + run: GITHUB_CI=true RUST_BACKTRACE=1 cargo test --all-features -p serai-processor-tests diff --git a/.github/workflows/reproducible-runtime.yml b/.github/workflows/reproducible-runtime.yml index d34e5ca5..2c418bd5 100644 --- a/.github/workflows/reproducible-runtime.yml +++ b/.github/workflows/reproducible-runtime.yml @@ -33,4 +33,4 @@ jobs: 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 + run: GITHUB_CI=true RUST_BACKTRACE=1 cargo test --all-features -p serai-reproducible-runtime-tests diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e32d2119..0270bee2 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -7,7 +7,7 @@ on: paths: - "common/**" - "crypto/**" - - "coins/**" + - "networks/**" - "message-queue/**" - "processor/**" - "coordinator/**" @@ -17,7 +17,7 @@ on: paths: - "common/**" - "crypto/**" - - "coins/**" + - "networks/**" - "message-queue/**" - "processor/**" - "coordinator/**" diff --git a/Cargo.lock b/Cargo.lock index afa7f118..7bf97243 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -23,11 +23,11 @@ dependencies = [ [[package]] name = "addr2line" -version = "0.21.0" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" dependencies = [ - "gimli 0.28.1", + "gimli 0.29.0", ] [[package]] @@ -99,10 +99,21 @@ version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" +[[package]] +name = "alloy-chains" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1752d7d62e2665da650a36d84abbf239f812534475d51f072a49a533513b7cdd" +dependencies = [ + "num_enum", + "strum 0.26.3", +] + [[package]] name = "alloy-consensus" -version = "0.1.0" -source = "git+https://github.com/alloy-rs/alloy?rev=b79db21734cffddc11753fe62ba571565c896f42#b79db21734cffddc11753fe62ba571565c896f42" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da374e868f54c7f4ad2ad56829827badca388efd645f8cf5fccc61c2b5343504" dependencies = [ "alloy-eips", "alloy-primitives", @@ -114,17 +125,18 @@ dependencies = [ [[package]] name = "alloy-core" -version = "0.7.0" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcbd9ee412dfb4e81d23cd1ae816d828c494a77d1eb00358035043695d4c5808" +checksum = "529fc6310dc1126c8de51c376cbc59c79c7f662bd742be7dc67055d5421a81b4" dependencies = [ "alloy-primitives", ] [[package]] name = "alloy-eips" -version = "0.1.0" -source = "git+https://github.com/alloy-rs/alloy?rev=b79db21734cffddc11753fe62ba571565c896f42#b79db21734cffddc11753fe62ba571565c896f42" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f76ecab54890cdea1e4808fc0891c7e6cfcf71fe1a9fe26810c7280ef768f4ed" dependencies = [ "alloy-primitives", "alloy-rlp", @@ -137,20 +149,20 @@ dependencies = [ [[package]] name = "alloy-genesis" -version = "0.1.0" -source = "git+https://github.com/alloy-rs/alloy?rev=b79db21734cffddc11753fe62ba571565c896f42#b79db21734cffddc11753fe62ba571565c896f42" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bca15afde1b6d15e3fc1c97421262b1bbb37aee45752e3c8b6d6f13f776554ff" dependencies = [ "alloy-primitives", "alloy-serde", "serde", - "serde_json", ] [[package]] name = "alloy-json-abi" -version = "0.7.2" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "786689872ec4e7d354810ab0dffd48bb40b838c047522eb031cbd47d15634849" +checksum = "bc05b04ac331a9f07e3a4036ef7926e49a8bf84a99a1ccfc7e2ab55a5fcbb372" dependencies = [ "alloy-primitives", "alloy-sol-type-parser", @@ -159,8 +171,9 @@ dependencies = [ [[package]] name = "alloy-json-rpc" -version = "0.1.0" -source = "git+https://github.com/alloy-rs/alloy?rev=b79db21734cffddc11753fe62ba571565c896f42#b79db21734cffddc11753fe62ba571565c896f42" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d6f34930b7e3e2744bcc79056c217f00cb2abb33bc5d4ff88da7623c5bb078b" dependencies = [ "alloy-primitives", "serde", @@ -171,25 +184,29 @@ dependencies = [ [[package]] name = "alloy-network" -version = "0.1.0" -source = "git+https://github.com/alloy-rs/alloy?rev=b79db21734cffddc11753fe62ba571565c896f42#b79db21734cffddc11753fe62ba571565c896f42" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25f6895fc31b48fa12306ef9b4f78b7764f8bd6d7d91cdb0a40e233704a0f23f" dependencies = [ "alloy-consensus", "alloy-eips", "alloy-json-rpc", "alloy-primitives", - "alloy-rpc-types", + "alloy-rpc-types-eth", + "alloy-serde", "alloy-signer", "alloy-sol-types", "async-trait", + "auto_impl", "futures-utils-wasm", "thiserror", ] [[package]] name = "alloy-node-bindings" -version = "0.1.0" -source = "git+https://github.com/alloy-rs/alloy?rev=b79db21734cffddc11753fe62ba571565c896f42#b79db21734cffddc11753fe62ba571565c896f42" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "494b2fb0276a78ec13791446a417c2517eee5c8e8a8c520ae0681975b8056e5c" dependencies = [ "alloy-genesis", "alloy-primitives", @@ -203,9 +220,9 @@ dependencies = [ [[package]] name = "alloy-primitives" -version = "0.7.2" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "525448f6afc1b70dd0f9d0a8145631bf2f5e434678ab23ab18409ca264cae6b3" +checksum = "ccb3ead547f4532bc8af961649942f0b9c16ee9226e26caa3f38420651cc0bf4" dependencies = [ "alloy-rlp", "bytes", @@ -225,16 +242,18 @@ dependencies = [ [[package]] name = "alloy-provider" -version = "0.1.0" -source = "git+https://github.com/alloy-rs/alloy?rev=b79db21734cffddc11753fe62ba571565c896f42#b79db21734cffddc11753fe62ba571565c896f42" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c538bfa893d07e27cb4f3c1ab5f451592b7c526d511d62b576a2ce59e146e4a" dependencies = [ + "alloy-chains", + "alloy-consensus", "alloy-eips", "alloy-json-rpc", "alloy-network", "alloy-primitives", "alloy-rpc-client", - "alloy-rpc-types", - "alloy-rpc-types-trace", + "alloy-rpc-types-eth", "alloy-transport", "async-stream", "async-trait", @@ -243,6 +262,8 @@ dependencies = [ "futures", "futures-utils-wasm", "lru", + "pin-project", + "serde", "serde_json", "tokio", "tracing", @@ -250,9 +271,9 @@ dependencies = [ [[package]] name = "alloy-rlp" -version = "0.3.4" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d58d9f5da7b40e9bfff0b7e7816700be4019db97d4b6359fe7f94a9e22e42ac" +checksum = "a43b18702501396fa9bcdeecd533bc85fac75150d308fc0f6800a01e6234a003" dependencies = [ "alloy-rlp-derive", "arrayvec", @@ -261,19 +282,20 @@ dependencies = [ [[package]] name = "alloy-rlp-derive" -version = "0.3.4" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a047897373be4bbb0224c1afdabca92648dc57a9c9ef6e7b0be3aff7a859c83" +checksum = "d83524c1f6162fcb5b0decf775498a125066c86dda6066ed609531b0e912f85a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.71", ] [[package]] name = "alloy-rpc-client" -version = "0.1.0" -source = "git+https://github.com/alloy-rs/alloy?rev=b79db21734cffddc11753fe62ba571565c896f42#b79db21734cffddc11753fe62ba571565c896f42" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ba31bae67773fd5a60020bea900231f8396202b7feca4d0c70c6b59308ab4a8" dependencies = [ "alloy-json-rpc", "alloy-transport", @@ -289,39 +311,28 @@ dependencies = [ ] [[package]] -name = "alloy-rpc-types" -version = "0.1.0" -source = "git+https://github.com/alloy-rs/alloy?rev=b79db21734cffddc11753fe62ba571565c896f42#b79db21734cffddc11753fe62ba571565c896f42" +name = "alloy-rpc-types-eth" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab4123ee21f99ba4bd31bfa36ba89112a18a500f8b452f02b35708b1b951e2b9" dependencies = [ "alloy-consensus", "alloy-eips", - "alloy-genesis", "alloy-primitives", "alloy-rlp", "alloy-serde", "alloy-sol-types", - "itertools 0.12.1", + "itertools 0.13.0", "serde", "serde_json", "thiserror", ] -[[package]] -name = "alloy-rpc-types-trace" -version = "0.1.0" -source = "git+https://github.com/alloy-rs/alloy?rev=b79db21734cffddc11753fe62ba571565c896f42#b79db21734cffddc11753fe62ba571565c896f42" -dependencies = [ - "alloy-primitives", - "alloy-rpc-types", - "alloy-serde", - "serde", - "serde_json", -] - [[package]] name = "alloy-serde" -version = "0.1.0" -source = "git+https://github.com/alloy-rs/alloy?rev=b79db21734cffddc11753fe62ba571565c896f42#b79db21734cffddc11753fe62ba571565c896f42" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9416c52959e66ead795a11f4a86c248410e9e368a0765710e57055b8a1774dd6" dependencies = [ "alloy-primitives", "serde", @@ -330,8 +341,9 @@ dependencies = [ [[package]] name = "alloy-signer" -version = "0.1.0" -source = "git+https://github.com/alloy-rs/alloy?rev=b79db21734cffddc11753fe62ba571565c896f42#b79db21734cffddc11753fe62ba571565c896f42" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b33753c09fa1ad85e5b092b8dc2372f1e337a42e84b9b4cff9fede75ba4adb32" dependencies = [ "alloy-primitives", "async-trait", @@ -354,28 +366,42 @@ dependencies = [ [[package]] name = "alloy-sol-macro" -version = "0.7.2" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89c80a2cb97e7aa48611cbb63950336f9824a174cdf670527cc6465078a26ea1" +checksum = "2b40397ddcdcc266f59f959770f601ce1280e699a91fc1862f29cef91707cd09" +dependencies = [ + "alloy-sol-macro-expander", + "alloy-sol-macro-input", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.71", +] + +[[package]] +name = "alloy-sol-macro-expander" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "867a5469d61480fea08c7333ffeca52d5b621f5ca2e44f271b117ec1fc9a0525" dependencies = [ "alloy-json-abi", "alloy-sol-macro-input", "const-hex", - "heck 0.4.1", + "heck 0.5.0", "indexmap 2.2.6", "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.71", "syn-solidity", "tiny-keccak", ] [[package]] name = "alloy-sol-macro-input" -version = "0.7.2" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c58894b58ac50979eeac6249661991ac40b9d541830d9a725f7714cc9ef08c23" +checksum = "2e482dc33a32b6fadbc0f599adea520bd3aaa585c141a80b404d0a3e3fa72528" dependencies = [ "alloy-json-abi", "const-hex", @@ -384,24 +410,25 @@ dependencies = [ "proc-macro2", "quote", "serde_json", - "syn 2.0.60", + "syn 2.0.71", "syn-solidity", ] [[package]] name = "alloy-sol-type-parser" -version = "0.7.2" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8e71ea68e780cc203919e03f69f59e7afe92d2696fb1dcb6662f61e4031b6" +checksum = "cbcba3ca07cf7975f15d871b721fb18031eec8bce51103907f6dcce00b255d98" dependencies = [ - "winnow 0.6.6", + "serde", + "winnow 0.6.13", ] [[package]] name = "alloy-sol-types" -version = "0.7.2" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "399287f68d1081ed8b1f4903c49687658b95b142207d7cb4ae2f4813915343ef" +checksum = "a91ca40fa20793ae9c3841b83e74569d1cc9af29a2f5237314fd3452d51e38c7" dependencies = [ "alloy-json-abi", "alloy-primitives", @@ -411,11 +438,12 @@ dependencies = [ [[package]] name = "alloy-transport" -version = "0.1.0" -source = "git+https://github.com/alloy-rs/alloy?rev=b79db21734cffddc11753fe62ba571565c896f42#b79db21734cffddc11753fe62ba571565c896f42" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01b51a291f949f755e6165c3ed562883175c97423703703355f4faa4b7d0a57c" dependencies = [ "alloy-json-rpc", - "base64 0.22.0", + "base64 0.22.1", "futures-util", "futures-utils-wasm", "serde", @@ -423,14 +451,15 @@ dependencies = [ "thiserror", "tokio", "tower", + "tracing", "url", - "wasm-bindgen-futures", ] [[package]] name = "alloy-transport-http" -version = "0.1.0" -source = "git+https://github.com/alloy-rs/alloy?rev=b79db21734cffddc11753fe62ba571565c896f42#b79db21734cffddc11753fe62ba571565c896f42" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86d65871f9f1cafe1ed25cde2f1303be83e6473e995a2d56c275ae4fcce6119c" dependencies = [ "alloy-transport", "url", @@ -462,47 +491,48 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.13" +version = "0.6.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb" +checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", + "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" -version = "1.0.6" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" +checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" [[package]] name = "anstyle-parse" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" +checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.0.2" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" +checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391" dependencies = [ "windows-sys 0.52.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.2" +version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" +checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" dependencies = [ "anstyle", "windows-sys 0.52.0", @@ -510,9 +540,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.82" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519" +checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" [[package]] name = "approx" @@ -655,9 +685,9 @@ dependencies = [ [[package]] name = "array-bytes" -version = "6.2.2" +version = "6.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f840fb7195bcfc5e17ea40c26e5ce6d5b9ce5d584466e17703209657e459ae0" +checksum = "5d5dde061bd34119e902bbb2d9b90c5692635cf59fb91d582c2b68043f1b8293" [[package]] name = "arrayref" @@ -723,9 +753,9 @@ dependencies = [ [[package]] name = "async-io" -version = "2.3.2" +version = "2.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcccb0f599cfa2f8ace422d3555572f47424da5648a4382a9dd0310ff8210884" +checksum = "0d6baa8f0178795da0e71bc42c9e5d13261aac7ee549853162e66a241ba17964" dependencies = [ "async-lock", "cfg-if", @@ -742,13 +772,13 @@ dependencies = [ [[package]] name = "async-lock" -version = "3.3.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d034b430882f8381900d3fe6f0aaa3ad94f2cb4ac519b429692a1bc2dda4ae7b" +checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" dependencies = [ - "event-listener 4.0.3", + "event-listener 5.3.1", "event-listener-strategy", - "pin-project-lite 0.2.14", + "pin-project-lite", ] [[package]] @@ -759,7 +789,7 @@ checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" dependencies = [ "async-stream-impl", "futures-core", - "pin-project-lite 0.2.14", + "pin-project-lite", ] [[package]] @@ -770,18 +800,18 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.71", ] [[package]] name = "async-trait" -version = "0.1.80" +version = "0.1.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" +checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.71", ] [[package]] @@ -794,7 +824,7 @@ dependencies = [ "futures-sink", "futures-util", "memchr", - "pin-project-lite 0.2.14", + "pin-project-lite", ] [[package]] @@ -816,27 +846,27 @@ checksum = "3c87f3f15e7794432337fc718554eaa4dc8f04c9677a950ffe366f20a162ae42" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.71", ] [[package]] name = "autocfg" -version = "1.2.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "backtrace" -version = "0.3.69" +version = "0.3.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" dependencies = [ - "addr2line 0.21.0", + "addr2line 0.22.0", "cc", "cfg-if", "libc", "miniz_oxide", - "object 0.32.2", + "object 0.36.1", "rustc-demangle", ] @@ -853,13 +883,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" [[package]] -name = "base58-monero" -version = "2.0.0" +name = "base58ck" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978e81a45367d2409ecd33369a45dda2e9a3ca516153ec194de1fbda4b9fb79d" +checksum = "2c8d66485a3a2ea485c1913c4572ce0256067a5377ac8c75c4960e1cda98605f" dependencies = [ - "thiserror", - "tiny-keccak", + "bitcoin-internals", + "bitcoin_hashes", ] [[package]] @@ -876,9 +906,9 @@ checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" [[package]] name = "base64" -version = "0.22.0" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" @@ -888,9 +918,9 @@ checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" [[package]] name = "bech32" -version = "0.10.0-beta" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98f7eed2b2781a6f0b5c903471d48e15f56fb4e1165df8a9a2337fd1a59d45ea" +checksum = "d965446196e3b7decd44aa7ee49e31d630118f90ef12f97900f262eb915c951d" [[package]] name = "beef" @@ -916,7 +946,7 @@ version = "0.69.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a00dc851838a2120612785d195287475a3ac45514741da670b735818822129a0" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "cexpr", "clang-sys", "itertools 0.12.1", @@ -927,7 +957,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.60", + "syn 2.0.71", ] [[package]] @@ -947,14 +977,16 @@ checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" [[package]] name = "bitcoin" -version = "0.31.2" +version = "0.32.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c85783c2fe40083ea54a33aa2f0ba58831d90fcd190f5bdc47e74e84d2a96ae" +checksum = "ea507acc1cd80fc084ace38544bbcf7ced7c2aa65b653b102de0ce718df668f6" dependencies = [ + "base58ck", "bech32", "bitcoin-internals", + "bitcoin-io", + "bitcoin-units", "bitcoin_hashes", - "core2 0.3.3", "hex-conservative", "hex_lit", "secp256k1", @@ -963,13 +995,19 @@ dependencies = [ [[package]] name = "bitcoin-internals" -version = "0.2.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9425c3bf7089c983facbae04de54513cce73b41c7f9ff8c845b54e7bc64ebbfb" +checksum = "30bdbe14aa07b06e6cfeffc529a1f099e5fbe249524f8125358604df99a4bed2" dependencies = [ "serde", ] +[[package]] +name = "bitcoin-io" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "340e09e8399c7bd8912f495af6aa58bea0c9214773417ffaa8f6460f93aaee56" + [[package]] name = "bitcoin-serai" version = "0.3.0" @@ -991,13 +1029,22 @@ dependencies = [ ] [[package]] -name = "bitcoin_hashes" -version = "0.13.0" +name = "bitcoin-units" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1930a4dabfebb8d7d9992db18ebe3ae2876f0a305fab206fd168df931ede293b" +checksum = "5285c8bcaa25876d07f37e3d30c303f2609179716e11d688f51e8f1fe70063e2" dependencies = [ "bitcoin-internals", - "core2 0.3.3", + "serde", +] + +[[package]] +name = "bitcoin_hashes" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb18c03d0db0247e147a21a6faafd5a7eb851c743db062de72018b6b7e8e4d16" +dependencies = [ + "bitcoin-io", "hex-conservative", "serde", ] @@ -1010,9 +1057,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" [[package]] name = "bitvec" @@ -1059,9 +1106,9 @@ dependencies = [ [[package]] name = "blake3" -version = "1.5.1" +version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30cca6d3674597c30ddf2c587bf8d9d65c9a84d2326d941cc79c9842dfe0ef52" +checksum = "e9ec96fe9a81b5e365f9db71fe00edc4fe4ca2cc7dcb7861f0603012a7caa210" dependencies = [ "arrayref", "arrayvec", @@ -1112,9 +1159,9 @@ dependencies = [ [[package]] name = "blst" -version = "0.3.11" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c94087b935a822949d3291a9989ad2b2051ea141eda0fd4e478a75f6aa3e604b" +checksum = "62dc83a094a71d43eeadd254b1ec2d24cb6a0bb6cadce00df51f0db594711a32" dependencies = [ "cc", "glob", @@ -1135,10 +1182,10 @@ dependencies = [ "futures-util", "hex", "http 0.2.12", - "hyper 0.14.28", + "hyper 0.14.30", "hyperlocal", "log", - "pin-project-lite 0.2.14", + "pin-project-lite", "serde", "serde_derive", "serde_json", @@ -1164,9 +1211,9 @@ dependencies = [ [[package]] name = "borsh" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0901fc8eb0aca4c83be0106d6f2db17d86a08dfc2c25f0e84464bf381158add6" +checksum = "dbe5b10e214954177fb1dc9fbd20a1a2608fe99e6c832033bdc7cea287a20d77" dependencies = [ "borsh-derive", "cfg_aliases", @@ -1174,15 +1221,15 @@ dependencies = [ [[package]] name = "borsh-derive" -version = "1.4.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51670c3aa053938b0ee3bd67c3817e471e626151131b934038e83c5bf8de48f5" +checksum = "c3ef8005764f53cd4dca619f5bf64cafd4664dada50ece25e4d81de54c80cc0b" dependencies = [ "once_cell", "proc-macro-crate 3.1.0", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.71", "syn_derive", ] @@ -1240,9 +1287,9 @@ checksum = "c3ac9f8b63eca6fd385229b3675f6cc0dc5c8a5c8a54a59d4f52ffd670d87b0c" [[package]] name = "bytemuck" -version = "1.15.0" +version = "1.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d6d68c57235a3a081186990eca2867354726650f42f7516ca50c28d6281fd15" +checksum = "b236fc92302c97ed75b38da1f4917b5cdda4984745740f153a5d3059e48d725e" [[package]] name = "byteorder" @@ -1252,9 +1299,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.6.0" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" +checksum = "a12916984aab3fa6e39d655a33e09c0071eb36d6ab3aea5c2d78551f1df6d952" dependencies = [ "serde", ] @@ -1272,9 +1319,9 @@ dependencies = [ [[package]] name = "c-kzg" -version = "1.0.0" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3130f3d8717cc02e668a896af24984d5d5d4e8bf12e278e982e0f1bd88a0f9af" +checksum = "cdf100c4cea8f207e883ff91ca886d621d8a166cb04971dfaa9bb8fd99ed95df" dependencies = [ "blst", "cc", @@ -1286,9 +1333,9 @@ dependencies = [ [[package]] name = "camino" -version = "1.1.6" +version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c59e92b5a388f549b863a7bea62612c09f24c8393560709a54558a9abdfb3b9c" +checksum = "e0ec6b951b160caa93cc0c7b209e5a3bff7aae9062213451ac99493cd844c239" dependencies = [ "serde", ] @@ -1310,7 +1357,7 @@ checksum = "e7daec1a2a2129eeba1644b220b4647ec537b0b5d4bfd6876fcc5a540056b592" dependencies = [ "camino", "cargo-platform", - "semver 1.0.22", + "semver 1.0.23", "serde", "serde_json", "thiserror", @@ -1318,10 +1365,11 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.88" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02f341c093d19155a6e41631ce5971aac4e9a868262212153124c15fa22d1cdc" +checksum = "324c74f2155653c90b04f25b2a47a8a631360cb908f92a772695f430c7e31052" dependencies = [ + "jobserver", "libc", ] @@ -1391,7 +1439,7 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-targets 0.52.4", + "windows-targets 0.52.6", ] [[package]] @@ -1400,7 +1448,7 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd94671561e36e4e7de75f753f577edafb0e7c05d6e4547229fdf7938fbcd2c3" dependencies = [ - "core2 0.4.0", + "core2", "multibase", "multihash 0.18.1", "serde", @@ -1443,9 +1491,9 @@ dependencies = [ [[package]] name = "clang-sys" -version = "1.7.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67523a3b4be3ce1989d607a828d036249522dd9c1c8de7f4dd2dae43a37369d1" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" dependencies = [ "glob", "libc", @@ -1454,9 +1502,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.4" +version = "4.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" +checksum = "64acc1846d54c1fe936a78dc189c34e28d3f5afc348403f28ecf53660b9b8462" dependencies = [ "clap_builder", "clap_derive", @@ -1464,9 +1512,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.2" +version = "4.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" +checksum = "6fb8393d67ba2e7bfaf28a23458e4e2b543cc73a99595511eb207fdb8aede942" dependencies = [ "anstream", "anstyle", @@ -1476,21 +1524,21 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.4" +version = "4.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" +checksum = "2bac35c6dafb060fd4d275d9a4ffae97917c13a6327903a8be2153cd964f7085" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.71", ] [[package]] name = "clap_lex" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" +checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70" [[package]] name = "codespan-reporting" @@ -1504,24 +1552,24 @@ dependencies = [ [[package]] name = "colorchoice" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" [[package]] name = "concurrent-queue" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d16048cd947b08fa32c24458a22f5dc5e835264f689f4f5653210c69fd107363" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" dependencies = [ "crossbeam-utils", ] [[package]] name = "const-hex" -version = "1.11.3" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ba00838774b4ab0233e355d26710fbfc8327a05c017f6dc4873f876d1f79f78" +checksum = "94fb8a24a26d37e1ffd45343323dc9fe6654ceea44c12f2fcb3d7ac29e610bc6" dependencies = [ "cfg-if", "cpufeatures", @@ -1584,15 +1632,6 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" -[[package]] -name = "core2" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "239fa3ae9b63c2dc74bd3fa852d4792b8b305ae64eeede946265b6af62f1fff3" -dependencies = [ - "memchr", -] - [[package]] name = "core2" version = "0.4.0" @@ -1730,9 +1769,9 @@ dependencies = [ [[package]] name = "crc32fast" -version = "1.4.0" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" dependencies = [ "cfg-if", ] @@ -1758,9 +1797,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.19" +version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" [[package]] name = "crunchy" @@ -1802,9 +1841,9 @@ dependencies = [ [[package]] name = "curve25519-dalek" -version = "4.1.2" +version = "4.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a677b8922c94e01bdbb12126b0bc852f00447528dee1782229af9c720c3f348" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" dependencies = [ "cfg-if", "cpufeatures", @@ -1812,7 +1851,6 @@ dependencies = [ "digest 0.10.7", "fiat-crypto", "group", - "platforms", "rand_core", "rustc_version 0.4.0", "subtle", @@ -1827,14 +1865,14 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.71", ] [[package]] name = "cxx" -version = "1.0.121" +version = "1.0.124" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21db378d04296a84d8b7d047c36bb3954f0b46529db725d7e62fb02f9ba53ccc" +checksum = "273dcfd3acd4e1e276af13ed2a43eea7001318823e7a726a6b3ed39b4acc0b82" dependencies = [ "cc", "cxxbridge-flags", @@ -1844,9 +1882,9 @@ dependencies = [ [[package]] name = "cxx-build" -version = "1.0.121" +version = "1.0.124" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e5262a7fa3f0bae2a55b767c223ba98032d7c328f5c13fa5cdc980b77fc0658" +checksum = "d8b2766fbd92be34e9ed143898fce6c572dc009de39506ed6903e5a05b68914e" dependencies = [ "cc", "codespan-reporting", @@ -1854,24 +1892,24 @@ dependencies = [ "proc-macro2", "quote", "scratch", - "syn 2.0.60", + "syn 2.0.71", ] [[package]] name = "cxxbridge-flags" -version = "1.0.121" +version = "1.0.124" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be8dcadd2e2fb4a501e1d9e93d6e88e6ea494306d8272069c92d5a9edf8855c0" +checksum = "839fcd5e43464614ffaa989eaf1c139ef1f0c51672a1ed08023307fa1b909ccd" [[package]] name = "cxxbridge-macro" -version = "1.0.121" +version = "1.0.124" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad08a837629ad949b73d032c637653d069e909cffe4ee7870b02301939ce39cc" +checksum = "4b2c1c1776b986979be68bb2285da855f8d8a35851a769fca8740df7c3d07877" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.71", ] [[package]] @@ -1897,23 +1935,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" dependencies = [ "cfg-if", - "hashbrown 0.14.3", + "hashbrown 0.14.5", "lock_api", "once_cell", - "parking_lot_core 0.9.9", + "parking_lot_core 0.9.10", ] [[package]] name = "data-encoding" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" +checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" [[package]] name = "data-encoding-macro" -version = "0.1.14" +version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20c01c06f5f429efdf2bae21eb67c28b3df3cf85b7dd2d8ef09c0838dac5d33e" +checksum = "f1559b6cba622276d6d63706db152618eeb15b89b3e4041446b05876e352e639" dependencies = [ "data-encoding", "data-encoding-macro-internal", @@ -1921,9 +1959,9 @@ dependencies = [ [[package]] name = "data-encoding-macro-internal" -version = "0.1.12" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0047d07f2c89b17dd631c80450d69841a6b5d7fb17278cbc43d7e4cfcf2576f3" +checksum = "332d754c0af53bc87c108fed664d121ecf59207ec4196041f04d6ab9002ad33f" dependencies = [ "data-encoding", "syn 1.0.109", @@ -1996,15 +2034,15 @@ dependencies = [ [[package]] name = "derive_more" -version = "0.99.17" +version = "0.99.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" +checksum = "5f33878137e4dafd7fa914ad4e259e18a4e8e532b9617a2d0150262bf53abfce" dependencies = [ "convert_case", "proc-macro2", "quote", "rustc_version 0.4.0", - "syn 1.0.109", + "syn 2.0.71", ] [[package]] @@ -2077,13 +2115,13 @@ dependencies = [ [[package]] name = "displaydoc" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.71", ] [[package]] @@ -2235,7 +2273,7 @@ checksum = "7d9ce6874da5d4415896cd45ffbc4d1cfc0c4f9c079427bd870742c30f2f65a9" dependencies = [ "curve25519-dalek", "ed25519", - "hashbrown 0.14.3", + "hashbrown 0.14.5", "hex", "rand_core", "sha2", @@ -2244,9 +2282,9 @@ dependencies = [ [[package]] name = "either" -version = "1.11.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] name = "elliptic-curve" @@ -2289,7 +2327,7 @@ dependencies = [ "heck 0.4.1", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.71", ] [[package]] @@ -2319,9 +2357,9 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" dependencies = [ "libc", "windows-sys 0.52.0", @@ -2337,7 +2375,7 @@ dependencies = [ "alloy-node-bindings", "alloy-provider", "alloy-rpc-client", - "alloy-rpc-types", + "alloy-rpc-types-eth", "alloy-simple-request-transport", "alloy-sol-types", "flexible-transcript", @@ -2357,23 +2395,23 @@ checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" [[package]] name = "event-listener" -version = "4.0.3" +version = "5.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b215c49b2b248c855fb73579eb1f4f26c38ffdc12973e20e07b91d78d5646e" +checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba" dependencies = [ "concurrent-queue", "parking", - "pin-project-lite 0.2.14", + "pin-project-lite", ] [[package]] name = "event-listener-strategy" -version = "0.4.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "958e4d70b6d5e81971bebec42271ec641e7ff4e170a6fa605f2b8a8b65cb97d3" +checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1" dependencies = [ - "event-listener 4.0.3", - "pin-project-lite 0.2.14", + "event-listener 5.3.1", + "pin-project-lite", ] [[package]] @@ -2395,7 +2433,7 @@ dependencies = [ "fs-err", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.71", ] [[package]] @@ -2406,9 +2444,9 @@ checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" [[package]] name = "fastrand" -version = "2.0.2" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "658bd65b1cf4c852a3cc96f18a8ce7b5640f6b703f905c7d74532294c2a63984" +checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" [[package]] name = "fastrlp" @@ -2457,9 +2495,9 @@ dependencies = [ [[package]] name = "fiat-crypto" -version = "0.2.7" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c007b1ae3abe1cb6f85a16305acd418b7ca6343b953633fee2b76d8f108b830f" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" [[package]] name = "file-per-thread-logger" @@ -2495,7 +2533,7 @@ dependencies = [ "log", "num-traits", "parity-scale-codec", - "parking_lot 0.12.1", + "parking_lot 0.12.3", "scale-info", ] @@ -2670,7 +2708,7 @@ dependencies = [ "proc-macro-warning", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.71", ] [[package]] @@ -2682,7 +2720,7 @@ dependencies = [ "proc-macro-crate 1.3.1", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.71", ] [[package]] @@ -2692,7 +2730,7 @@ source = "git+https://github.com/serai-dex/substrate#6e3f07bf5c98a6a3ec15f2b1a46 dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.71", ] [[package]] @@ -2840,7 +2878,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5" dependencies = [ "futures-core", - "pin-project-lite 0.2.14", + "pin-project-lite", ] [[package]] @@ -2851,7 +2889,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.71", ] [[package]] @@ -2861,7 +2899,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35bd3cf68c183738046838e300353e4716c674dc5e56890de4826801a6622a28" dependencies = [ "futures-io", - "rustls 0.21.11", + "rustls 0.21.12", ] [[package]] @@ -2906,7 +2944,7 @@ dependencies = [ "futures-sink", "futures-task", "memchr", - "pin-project-lite 0.2.14", + "pin-project-lite", "pin-utils", "slab", ] @@ -2932,7 +2970,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27d12c0aed7f1e24276a241aadc4cb8ea9f83000f34bc062b7cc2d51e3b0fabd" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "debugid", "fxhash", "serde", @@ -2941,15 +2979,16 @@ dependencies = [ [[package]] name = "generator" -version = "0.7.5" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cc16584ff22b460a382b7feec54b23d2908d858152e5739a120b949293bd74e" +checksum = "186014d53bc231d0090ef8d6f03e0920c54d85a5ed22f4f2f74315ec56cf83fb" dependencies = [ "cc", + "cfg-if", "libc", "log", "rustversion", - "windows 0.48.0", + "windows 0.54.0", ] [[package]] @@ -2965,24 +3004,34 @@ dependencies = [ [[package]] name = "generic-array" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe739944a5406424e080edccb6add95685130b9f160d5407c639c7df0c5836b0" +checksum = "96512db27971c2c3eece70a1e106fbe6c87760234e31e8f7e5634912fe52794a" dependencies = [ "typenum", ] [[package]] name = "getrandom" -version = "0.2.12" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", "wasi", ] +[[package]] +name = "getrandom_or_panic" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea1015b5a70616b688dc230cfe50c8af89d972cb132d5a622814d29773b10b9" +dependencies = [ + "rand", + "rand_core", +] + [[package]] name = "ghash" version = "0.5.1" @@ -3006,9 +3055,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.28.1" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" [[package]] name = "glob" @@ -3025,8 +3074,8 @@ dependencies = [ "aho-corasick", "bstr", "log", - "regex-automata 0.4.6", - "regex-syntax 0.8.3", + "regex-automata 0.4.7", + "regex-syntax 0.8.4", ] [[package]] @@ -3091,9 +3140,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.14.3" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ "ahash", "allocator-api2", @@ -3128,11 +3177,11 @@ dependencies = [ [[package]] name = "hex-conservative" -version = "0.1.1" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30ed443af458ccb6d81c1e7e661545f94d3176752fb1df2f543b902a1e0f51e2" +checksum = "5313b072ce3c597065a808dbf612c4c8e8590bdbf8b579508bf7a762c5eae6cd" dependencies = [ - "core2 0.3.3", + "arrayvec", ] [[package]] @@ -3221,14 +3270,14 @@ checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" dependencies = [ "bytes", "http 0.2.12", - "pin-project-lite 0.2.14", + "pin-project-lite", ] [[package]] name = "http-body" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", "http 1.1.0", @@ -3236,15 +3285,15 @@ dependencies = [ [[package]] name = "http-body-util" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0475f8b2ac86659c21b64320d5d653f9efe42acd2a4e560073ec61a155a34f1d" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" dependencies = [ "bytes", - "futures-core", + "futures-util", "http 1.1.0", - "http-body 1.0.0", - "pin-project-lite 0.2.14", + "http-body 1.0.1", + "pin-project-lite", ] [[package]] @@ -3255,9 +3304,9 @@ checksum = "add0ab9360ddbd88cfeb3bd9574a1d85cfdfa14db10b3e21d3700dbc4328758f" [[package]] name = "httparse" -version = "1.8.0" +version = "1.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" +checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" [[package]] name = "httpdate" @@ -3273,9 +3322,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.28" +version = "0.14.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" +checksum = "a152ddd61dfaec7273fe8419ab357f33aee0d914c5f4efbf0d96fa749eea5ec9" dependencies = [ "bytes", "futures-channel", @@ -3287,7 +3336,7 @@ dependencies = [ "httparse", "httpdate", "itoa", - "pin-project-lite 0.2.14", + "pin-project-lite", "socket2 0.4.10", "tokio", "tower-service", @@ -3297,18 +3346,18 @@ dependencies = [ [[package]] name = "hyper" -version = "1.3.1" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe575dd17d0862a9a33781c8c4696a55c320909004a67a00fb286ba8b1bc496d" +checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05" dependencies = [ "bytes", "futures-channel", "futures-util", "http 1.1.0", - "http-body 1.0.0", + "http-body 1.0.1", "httparse", "itoa", - "pin-project-lite 0.2.14", + "pin-project-lite", "smallvec", "tokio", "want", @@ -3316,15 +3365,15 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.1" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "908bb38696d7a037a01ebcc68a00634112ac2bbf8ca74e30a2c3d2f4f021302b" +checksum = "5ee4be2c948921a1a5320b629c4193916ed787a7f7f293fd3f7f5a6c9de74155" dependencies = [ "futures-util", "http 1.1.0", - "hyper 1.3.1", + "hyper 1.4.1", "hyper-util", - "rustls 0.23.5", + "rustls 0.23.11", "rustls-native-certs", "rustls-pki-types", "tokio", @@ -3334,18 +3383,18 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.3" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca38ef113da30126bbff9cd1705f9273e15d45498615d138b0c20279ac7a76aa" +checksum = "3ab92f4f49ee4fb4f997c784b7a2e0fa70050211e0b6a287f898c3c9785ca956" dependencies = [ "bytes", "futures-channel", "futures-util", "http 1.1.0", - "http-body 1.0.0", - "hyper 1.3.1", - "pin-project-lite 0.2.14", - "socket2 0.5.6", + "http-body 1.0.1", + "hyper 1.4.1", + "pin-project-lite", + "socket2 0.5.7", "tokio", "tower", "tower-service", @@ -3360,7 +3409,7 @@ checksum = "0fafdf7b2b2de7c9784f76e02c0935e65a8117ec3b768644379983ab333ac98c" dependencies = [ "futures-util", "hex", - "hyper 0.14.28", + "hyper 0.14.30", "pin-project", "tokio", ] @@ -3376,7 +3425,7 @@ dependencies = [ "iana-time-zone-haiku", "js-sys", "wasm-bindgen", - "windows-core", + "windows-core 0.51.1", ] [[package]] @@ -3459,7 +3508,7 @@ dependencies = [ "bytes", "futures", "http 0.2.12", - "hyper 0.14.28", + "hyper 0.14.30", "log", "rand", "tokio", @@ -3514,7 +3563,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" dependencies = [ "equivalent", - "hashbrown 0.14.3", + "hashbrown 0.14.5", "serde", ] @@ -3530,9 +3579,9 @@ dependencies = [ [[package]] name = "instant" -version = "0.1.12" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" dependencies = [ "cfg-if", ] @@ -3558,7 +3607,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" dependencies = [ - "socket2 0.5.6", + "socket2 0.5.7", "widestring", "windows-sys 0.48.0", "winreg", @@ -3574,6 +3623,12 @@ checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" name = "is-terminal" version = "0.4.10" +[[package]] +name = "is_terminal_polyfill" +version = "1.70.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" + [[package]] name = "itertools" version = "0.10.5" @@ -3592,12 +3647,30 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +[[package]] +name = "jobserver" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2b099aaa34a9751c5bf0878add70444e1ed2dd73f347be99003d4577277de6e" +dependencies = [ + "libc", +] + [[package]] name = "js-sys" version = "0.3.69" @@ -3633,14 +3706,14 @@ dependencies = [ "futures-channel", "futures-util", "globset", - "hyper 0.14.28", + "hyper 0.14.30", "jsonrpsee-types", - "parking_lot 0.12.1", + "parking_lot 0.12.3", "rand", "rustc-hash", "serde", "serde_json", - "soketto", + "soketto 0.7.1", "thiserror", "tokio", "tracing", @@ -3668,12 +3741,12 @@ dependencies = [ "futures-channel", "futures-util", "http 0.2.12", - "hyper 0.14.28", + "hyper 0.14.30", "jsonrpsee-core", "jsonrpsee-types", "serde", "serde_json", - "soketto", + "soketto 0.7.1", "tokio", "tokio-stream", "tokio-util", @@ -3706,6 +3779,7 @@ dependencies = [ "elliptic-curve", "once_cell", "sha2", + "signature", ] [[package]] @@ -3719,9 +3793,9 @@ dependencies = [ [[package]] name = "keccak-asm" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb8515fff80ed850aea4a1595f2e519c003e2a00a82fe168ebf5269196caf444" +checksum = "47a3633291834c4fbebf8673acbc1b04ec9d151418ff9b8e26dcd79129928758" dependencies = [ "digest 0.10.7", "sha3-asm", @@ -3743,7 +3817,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf7a85fe66f9ff9cd74e169fdd2c94c6e1e74c412c99a73b4df3200b5d3760b2" dependencies = [ "kvdb", - "parking_lot 0.12.1", + "parking_lot 0.12.3", ] [[package]] @@ -3754,7 +3828,7 @@ checksum = "b644c70b92285f66bfc2032922a79000ea30af7bc2ab31902992a5dcb9b434f6" dependencies = [ "kvdb", "num_cpus", - "parking_lot 0.12.1", + "parking_lot 0.12.3", "regex", "rocksdb 0.21.0", "smallvec", @@ -3782,18 +3856,18 @@ checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" [[package]] name = "libc" -version = "0.2.153" +version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" [[package]] name = "libloading" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19" +checksum = "e310b3a6b5907f99202fcdb4960ff45b93735d7c7d96b760fcff8db2dc0e103d" dependencies = [ "cfg-if", - "windows-targets 0.52.4", + "windows-targets 0.48.5", ] [[package]] @@ -3881,7 +3955,7 @@ dependencies = [ "multihash 0.19.1", "multistream-select", "once_cell", - "parking_lot 0.12.1", + "parking_lot 0.12.3", "pin-project", "quick-protobuf", "rand", @@ -3903,7 +3977,7 @@ dependencies = [ "libp2p-core", "libp2p-identity", "log", - "parking_lot 0.12.1", + "parking_lot 0.12.3", "smallvec", "trust-dns-resolver", ] @@ -3965,9 +4039,9 @@ dependencies = [ [[package]] name = "libp2p-identity" -version = "0.2.8" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "999ec70441b2fb35355076726a6bc466c932e9bdc66f6a11c6c0aa17c7ab9be0" +checksum = "55cca1eb2bc1fd29f099f3daaab7effd01e1a54b7c577d0ed082521034d912e8" dependencies = [ "bs58", "ed25519-dalek", @@ -4025,7 +4099,7 @@ dependencies = [ "log", "rand", "smallvec", - "socket2 0.5.6", + "socket2 0.5.7", "tokio", "trust-dns-proto 0.22.0", "void", @@ -4106,12 +4180,12 @@ dependencies = [ "libp2p-identity", "libp2p-tls", "log", - "parking_lot 0.12.1", + "parking_lot 0.12.3", "quinn", "rand", "ring 0.16.20", - "rustls 0.21.11", - "socket2 0.5.6", + "rustls 0.21.12", + "socket2 0.5.7", "thiserror", "tokio", ] @@ -4167,7 +4241,7 @@ dependencies = [ "proc-macro-warning", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.71", ] [[package]] @@ -4183,7 +4257,7 @@ dependencies = [ "libp2p-core", "libp2p-identity", "log", - "socket2 0.5.6", + "socket2 0.5.7", "tokio", ] @@ -4199,7 +4273,7 @@ dependencies = [ "libp2p-identity", "rcgen", "ring 0.16.20", - "rustls 0.21.11", + "rustls 0.21.12", "rustls-webpki 0.101.7", "thiserror", "x509-parser", @@ -4238,9 +4312,9 @@ dependencies = [ [[package]] name = "libp2p-websocket" -version = "0.42.1" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3facf0691bab65f571bc97c6c65ffa836248ca631d631b7691ac91deb7fceb5f" +checksum = "004ee9c4a4631435169aee6aad2f62e3984dc031c43b6d29731e8e82a016c538" dependencies = [ "either", "futures", @@ -4248,10 +4322,11 @@ dependencies = [ "libp2p-core", "libp2p-identity", "log", - "parking_lot 0.12.1", - "quicksink", + "parking_lot 0.12.3", + "pin-project-lite", "rw-stream-sink", - "soketto", + "soketto 0.8.0", + "thiserror", "url", "webpki-roots", ] @@ -4269,6 +4344,16 @@ dependencies = [ "yamux", ] +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags 2.6.0", + "libc", +] + [[package]] name = "librocksdb-sys" version = "0.16.0+8.10.0" @@ -4287,9 +4372,9 @@ dependencies = [ [[package]] name = "libz-sys" -version = "1.1.16" +version = "1.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e143b5e666b2695d28f6bca6497720813f699c9602dd7f5cac91008b8ada7f9" +checksum = "c15da26e5af7e25c90b37a2d75cdbf940cf4a55316de9d84c679c9b8bfabf82e" dependencies = [ "cc", "pkg-config", @@ -4331,15 +4416,15 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] name = "lock_api" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", @@ -4347,15 +4432,15 @@ dependencies = [ [[package]] name = "log" -version = "0.4.21" +version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "loom" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e045d70ddfbc984eacfa964ded019534e8f6cbf36f6410aee0ed5cefa5a9175" +checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca" dependencies = [ "cfg-if", "generator", @@ -4370,7 +4455,7 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3262e75e648fce39813cb56ac41f3c3e3f65217ebf3844d818d1f9398cfb0dc" dependencies = [ - "hashbrown 0.14.3", + "hashbrown 0.14.5", ] [[package]] @@ -4384,9 +4469,9 @@ dependencies = [ [[package]] name = "lz4" -version = "1.24.0" +version = "1.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e9e2dd86df36ce760a60f6ff6ad526f7ba1f14ba0356f8254fb6905e6494df1" +checksum = "d6eab492fe7f8651add23237ea56dbf11b3c4ff762ab83d40a47f11433421f91" dependencies = [ "libc", "lz4-sys", @@ -4394,9 +4479,9 @@ dependencies = [ [[package]] name = "lz4-sys" -version = "1.9.4" +version = "1.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57d27b317e207b10f69f5e75494119e391a96f48861ae870d1da6edac98ca900" +checksum = "e9764018d143cc854c9f17f0b907de70f14393b1f502da6375dce70f00514eb3" dependencies = [ "cc", "libc", @@ -4420,7 +4505,7 @@ dependencies = [ "macro_magic_core", "macro_magic_macros", "quote", - "syn 2.0.60", + "syn 2.0.71", ] [[package]] @@ -4434,7 +4519,7 @@ dependencies = [ "macro_magic_core_macros", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.71", ] [[package]] @@ -4445,7 +4530,7 @@ checksum = "d710e1214dffbab3b5dacb21475dde7d6ed84c69ff722b3a47a782668d44fbac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.71", ] [[package]] @@ -4456,7 +4541,7 @@ checksum = "b8fb85ec1620619edf2984a7693497d4ec88a9665d8b87e942856884c92dbf2a" dependencies = [ "macro_magic_core", "quote", - "syn 2.0.60", + "syn 2.0.71", ] [[package]] @@ -4509,9 +4594,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.2" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "memfd" @@ -4575,7 +4660,7 @@ dependencies = [ "crypto-bigint", "ff", "ff-group-tests", - "generic-array 1.0.0", + "generic-array 1.1.0", "group", "hex", "rand_core", @@ -4592,9 +4677,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.7.2" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" dependencies = [ "adler", ] @@ -4658,6 +4743,70 @@ dependencies = [ "zeroize", ] +[[package]] +name = "monero-address" +version = "0.1.0" +dependencies = [ + "curve25519-dalek", + "hex", + "hex-literal", + "monero-io", + "monero-primitives", + "rand_core", + "serde", + "serde_json", + "std-shims", + "thiserror", + "zeroize", +] + +[[package]] +name = "monero-borromean" +version = "0.1.0" +dependencies = [ + "curve25519-dalek", + "monero-generators", + "monero-io", + "monero-primitives", + "std-shims", + "zeroize", +] + +[[package]] +name = "monero-bulletproofs" +version = "0.1.0" +dependencies = [ + "curve25519-dalek", + "hex-literal", + "monero-generators", + "monero-io", + "monero-primitives", + "rand_core", + "std-shims", + "thiserror", + "zeroize", +] + +[[package]] +name = "monero-clsag" +version = "0.1.0" +dependencies = [ + "curve25519-dalek", + "dalek-ff-group", + "flexible-transcript", + "group", + "modular-frost", + "monero-generators", + "monero-io", + "monero-primitives", + "rand_chacha", + "rand_core", + "std-shims", + "subtle", + "thiserror", + "zeroize", +] + [[package]] name = "monero-generators" version = "0.4.0" @@ -4666,44 +4815,164 @@ dependencies = [ "dalek-ff-group", "group", "hex", + "monero-io", "sha3", "std-shims", "subtle", ] +[[package]] +name = "monero-io" +version = "0.1.0" +dependencies = [ + "curve25519-dalek", + "std-shims", +] + +[[package]] +name = "monero-mlsag" +version = "0.1.0" +dependencies = [ + "curve25519-dalek", + "monero-generators", + "monero-io", + "monero-primitives", + "std-shims", + "thiserror", + "zeroize", +] + +[[package]] +name = "monero-primitives" +version = "0.1.0" +dependencies = [ + "curve25519-dalek", + "hex", + "monero-generators", + "monero-io", + "sha3", + "std-shims", + "zeroize", +] + +[[package]] +name = "monero-rpc" +version = "0.1.0" +dependencies = [ + "async-trait", + "curve25519-dalek", + "hex", + "monero-address", + "monero-serai", + "serde", + "serde_json", + "std-shims", + "thiserror", + "zeroize", +] + +[[package]] +name = "monero-seed" +version = "0.1.0" +dependencies = [ + "curve25519-dalek", + "hex", + "monero-primitives", + "rand_core", + "std-shims", + "thiserror", + "zeroize", +] + [[package]] name = "monero-serai" version = "0.1.4-alpha" dependencies = [ - "async-lock", + "curve25519-dalek", + "hex", + "hex-literal", + "monero-borromean", + "monero-bulletproofs", + "monero-clsag", + "monero-generators", + "monero-io", + "monero-mlsag", + "monero-primitives", + "serde", + "serde_json", + "std-shims", + "zeroize", +] + +[[package]] +name = "monero-serai-verify-chain" +version = "0.1.0" +dependencies = [ + "curve25519-dalek", + "hex", + "monero-rpc", + "monero-serai", + "monero-simple-request-rpc", + "rand_core", + "serde", + "serde_json", + "tokio", +] + +[[package]] +name = "monero-simple-request-rpc" +version = "0.1.0" +dependencies = [ "async-trait", - "base58-monero", + "digest_auth", + "hex", + "monero-address", + "monero-rpc", + "simple-request", + "tokio", +] + +[[package]] +name = "monero-wallet" +version = "0.1.0" +dependencies = [ "curve25519-dalek", "dalek-ff-group", - "digest_auth", "flexible-transcript", "group", "hex", - "hex-literal", "modular-frost", - "monero-generators", - "multiexp", - "pbkdf2 0.12.2", + "monero-address", + "monero-rpc", + "monero-serai", + "monero-simple-request-rpc", "rand", "rand_chacha", "rand_core", "rand_distr", "serde", "serde_json", - "sha3", - "simple-request", "std-shims", - "subtle", "thiserror", "tokio", "zeroize", ] +[[package]] +name = "monero-wallet-util" +version = "0.1.0" +dependencies = [ + "curve25519-dalek", + "hex", + "monero-seed", + "monero-wallet", + "polyseed", + "rand_core", + "std-shims", + "thiserror", + "zeroize", +] + [[package]] name = "multiaddr" version = "0.18.1" @@ -4757,7 +5026,7 @@ dependencies = [ "blake2b_simd", "blake2s_simd", "blake3", - "core2 0.4.0", + "core2", "digest 0.10.7", "multihash-derive 0.8.0", "sha2", @@ -4771,7 +5040,7 @@ version = "0.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "076d548d76a0e2a0d4ab471d0b1c36c577786dfc4471242035d97a12a735c492" dependencies = [ - "core2 0.4.0", + "core2", "unsigned-varint", ] @@ -4784,7 +5053,7 @@ dependencies = [ "blake2b_simd", "blake2s_simd", "blake3", - "core2 0.4.0", + "core2", "digest 0.10.7", "multihash-derive 0.9.0", "ripemd", @@ -4814,7 +5083,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "890e72cb7396cb99ed98c1246a97b243cc16394470d94e0bc8b0c2c11d84290e" dependencies = [ - "core2 0.4.0", + "core2", "multihash 0.19.1", "multihash-derive-impl", ] @@ -4855,9 +5124,9 @@ dependencies = [ [[package]] name = "nalgebra" -version = "0.32.5" +version = "0.32.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ea4908d4f23254adda3daa60ffef0f1ac7b8c3e9a864cf3cc154b251908a2ef" +checksum = "7b5c17de023a86f59ed79891b2e5d5a94c705dbe904a5b5c9c952ea6221b03e4" dependencies = [ "approx", "matrixmultiply", @@ -4871,13 +5140,13 @@ dependencies = [ [[package]] name = "nalgebra-macros" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91761aed67d03ad966ef783ae962ef9bbaca728d2dd7ceb7939ec110fffad998" +checksum = "254a5372af8fc138e36684761d3c0cdb758a4410e938babcff1c860ce14ddbfc" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.71", ] [[package]] @@ -5000,24 +5269,29 @@ dependencies = [ [[package]] name = "num-bigint" -version = "0.4.4" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" dependencies = [ - "autocfg", "num-integer", "num-traits", ] [[package]] name = "num-complex" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23c6602fda94a57c990fe0df199a035d83576b496aa29f4e634a8ac6004e68a6" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" dependencies = [ "num-traits", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num-format" version = "0.4.4" @@ -5039,11 +5313,10 @@ dependencies = [ [[package]] name = "num-rational" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" dependencies = [ - "autocfg", "num-bigint", "num-integer", "num-traits", @@ -5051,9 +5324,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", "libm", @@ -5069,6 +5342,26 @@ dependencies = [ "libc", ] +[[package]] +name = "num_enum" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02339744ee7253741199f897151b38e72257d13802d4ee837285cc2990a90845" +dependencies = [ + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "681030a937600a36906c185595136d26abfebb4aa9c65701cefcaf8578bb982b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.71", +] + [[package]] name = "object" version = "0.31.1" @@ -5083,9 +5376,9 @@ dependencies = [ [[package]] name = "object" -version = "0.32.2" +version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +checksum = "081b846d1d56ddfc18fdf1a922e4f6e07a11768ea1b92dec44e42b72712ccfce" dependencies = [ "memchr", ] @@ -5305,7 +5598,7 @@ dependencies = [ "log", "lz4", "memmap2", - "parking_lot 0.12.1", + "parking_lot 0.12.3", "rand", "siphasher", "snap", @@ -5314,9 +5607,9 @@ dependencies = [ [[package]] name = "parity-scale-codec" -version = "3.6.9" +version = "3.6.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "881331e34fa842a2fb61cc2db9643a8fedc615e47cfcc52597d1af0db9a7e8fe" +checksum = "306800abfa29c7f16596b5970a588435e3d5b3149683d00c12b699cc19f895ee" dependencies = [ "arrayvec", "bitvec", @@ -5329,11 +5622,11 @@ dependencies = [ [[package]] name = "parity-scale-codec-derive" -version = "3.6.9" +version = "3.6.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be30eaf4b0a9fba5336683b38de57bb86d179a35862ba6bfcf57625d006bde5b" +checksum = "d830939c76d294956402033aee57a6da7b438f2294eb94864c37b0569053a42c" dependencies = [ - "proc-macro-crate 2.0.1", + "proc-macro-crate 3.1.0", "proc-macro2", "quote", "syn 1.0.109", @@ -5354,49 +5647,38 @@ checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" [[package]] name = "parking_lot" version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" dependencies = [ - "instant", - "lock_api", - "parking_lot_core 0.8.6", + "parking_lot 0.12.3", ] [[package]] name = "parking_lot" -version = "0.12.1" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" dependencies = [ "lock_api", - "parking_lot_core 0.9.9", + "parking_lot_core 0.9.10", ] [[package]] name = "parking_lot_core" version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" dependencies = [ - "cfg-if", - "instant", - "libc", - "redox_syscall 0.2.16", - "smallvec", - "winapi", + "parking_lot_core 0.9.10", ] [[package]] name = "parking_lot_core" -version = "0.9.9" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.4.1", + "redox_syscall 0.5.3", "smallvec", - "windows-targets 0.48.5", + "windows-targets 0.52.6", ] [[package]] @@ -5433,9 +5715,9 @@ dependencies = [ [[package]] name = "paste" -version = "1.0.14" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "pbkdf2" @@ -5475,9 +5757,9 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pest" -version = "2.7.9" +version = "2.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "311fb059dee1a7b802f036316d790138c613a4e8b180c822e3925a662e9f0c95" +checksum = "cd53dff83f26735fdc1ca837098ccf133605d794cdae66acfc2bfac3ec809d95" dependencies = [ "memchr", "thiserror", @@ -5486,9 +5768,9 @@ dependencies = [ [[package]] name = "petgraph" -version = "0.6.4" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ "fixedbitset", "indexmap 2.2.6", @@ -5511,15 +5793,9 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.71", ] -[[package]] -name = "pin-project-lite" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "257b64915a082f7811703966789728173279bdebb956b143dbcd23f6f970a777" - [[package]] name = "pin-project-lite" version = "0.2.14" @@ -5548,22 +5824,16 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" -[[package]] -name = "platforms" -version = "3.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db23d408679286588f4d4644f965003d056e3dd5abcaaa938116871d7ce2fee7" - [[package]] name = "polling" -version = "3.6.0" +version = "3.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0c976a60b2d7e99d6f229e414670a9b85d13ac305cc6d1e9c134de58c5aaaf6" +checksum = "5e6a007746f34ed64099e88783b0ae369eaa3da6392868ba262e2af9b8fbaea1" dependencies = [ "cfg-if", "concurrent-queue", "hermit-abi", - "pin-project-lite 0.2.14", + "pin-project-lite", "rustix", "tracing", "windows-sys 0.52.0", @@ -5580,6 +5850,20 @@ dependencies = [ "universal-hash", ] +[[package]] +name = "polyseed" +version = "0.1.0" +dependencies = [ + "hex", + "pbkdf2 0.12.2", + "rand_core", + "sha3", + "std-shims", + "subtle", + "thiserror", + "zeroize", +] + [[package]] name = "polyval" version = "0.6.2" @@ -5724,29 +6008,29 @@ checksum = "3d1eaa7fa0aa1929ffdf7eeb6eac234dde6268914a14ad44d23521ab6a9b258e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.71", ] [[package]] name = "proc-macro2" -version = "1.0.81" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" dependencies = [ "unicode-ident", ] [[package]] name = "prometheus" -version = "0.13.3" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "449811d15fbdf5ceb5c1144416066429cf82316e2ec8ce0c1f6f8a02e7bbcf8c" +checksum = "3d33c28a30771f7f96db69893f78b857f7450d7e0237e9c8fc6427a81bae7ed1" dependencies = [ "cfg-if", "fnv", "lazy_static", "memchr", - "parking_lot 0.12.1", + "parking_lot 0.12.3", "thiserror", ] @@ -5758,7 +6042,7 @@ checksum = "3c99afa9a01501019ac3a14d71d9f94050346f55ca471ce90c799a15c58f61e2" dependencies = [ "dtoa", "itoa", - "parking_lot 0.12.1", + "parking_lot 0.12.3", "prometheus-client-derive-encode", ] @@ -5770,24 +6054,24 @@ checksum = "440f724eba9f6996b75d63681b0a92b06947f1457076d503a4d2e2c8f56442b8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.71", ] [[package]] name = "proptest" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31b476131c3c86cb68032fdc5cb6d5a1045e3e42d96b69fa599fd77701e1f5bf" +checksum = "b4c2511913b88df1637da85cc8d96ec8e43a3f8bb8ccb71ee1ac240d6f3df58d" dependencies = [ "bit-set", "bit-vec", - "bitflags 2.5.0", + "bitflags 2.6.0", "lazy_static", "num-traits", "rand", "rand_chacha", "rand_xorshift", - "regex-syntax 0.8.3", + "regex-syntax 0.8.4", "rusty-fork", "tempfile", "unarray", @@ -5884,17 +6168,6 @@ dependencies = [ "unsigned-varint", ] -[[package]] -name = "quicksink" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77de3c815e5a160b1539c6592796801df2043ae35e123b46d73380cfa57af858" -dependencies = [ - "futures-core", - "futures-sink", - "pin-project-lite 0.1.12", -] - [[package]] name = "quinn" version = "0.10.2" @@ -5903,11 +6176,11 @@ checksum = "8cc2c5017e4b43d5995dcea317bc46c1e09404c0a9664d2908f7f02dfe943d75" dependencies = [ "bytes", "futures-io", - "pin-project-lite 0.2.14", + "pin-project-lite", "quinn-proto", "quinn-udp", "rustc-hash", - "rustls 0.21.11", + "rustls 0.21.12", "thiserror", "tokio", "tracing", @@ -5923,7 +6196,7 @@ dependencies = [ "rand", "ring 0.16.20", "rustc-hash", - "rustls 0.21.11", + "rustls 0.21.12", "slab", "thiserror", "tinyvec", @@ -5938,7 +6211,7 @@ checksum = "055b4e778e8feb9f93c4e439f71dc2156ef13360b432b799e179a8c4cdf0b1d7" dependencies = [ "bytes", "libc", - "socket2 0.5.6", + "socket2 0.5.7", "tracing", "windows-sys 0.48.0", ] @@ -6054,15 +6327,6 @@ dependencies = [ "yasna", ] -[[package]] -name = "redox_syscall" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" -dependencies = [ - "bitflags 1.3.2", -] - [[package]] name = "redox_syscall" version = "0.4.1" @@ -6073,34 +6337,43 @@ dependencies = [ ] [[package]] -name = "redox_users" -version = "0.4.3" +name = "redox_syscall" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" +checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" +dependencies = [ + "bitflags 2.6.0", +] + +[[package]] +name = "redox_users" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891" dependencies = [ "getrandom", - "redox_syscall 0.2.16", + "libredox", "thiserror", ] [[package]] name = "ref-cast" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4846d4c50d1721b1a3bef8af76924eef20d5e723647333798c1b519b3a9473f" +checksum = "ccf0a6f84d5f1d581da8b41b47ec8600871962f2a528115b542b362d4b744931" dependencies = [ "ref-cast-impl", ] [[package]] name = "ref-cast-impl" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fddb4f8d99b0a2ebafc65a87a69a7b9875e4b1ae1f00db265d300ef7f28bccc" +checksum = "bcc303e793d3734489387d205e9b186fac9c6cfacedd98cbb2e8a5943595f3e6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.71", ] [[package]] @@ -6118,14 +6391,14 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.4" +version = "1.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" +checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.6", - "regex-syntax 0.8.3", + "regex-automata 0.4.7", + "regex-syntax 0.8.4", ] [[package]] @@ -6139,13 +6412,13 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.6" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.3", + "regex-syntax 0.8.4", ] [[package]] @@ -6156,9 +6429,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" [[package]] name = "resolv-conf" @@ -6284,9 +6557,9 @@ dependencies = [ [[package]] name = "ruint" -version = "1.12.1" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f308135fef9fc398342da5472ce7c484529df23743fb7c734e0f3d472971e62" +checksum = "2c3cc4c2511671f327125da14133d0c5c5d137f006a1017a16f557bc85b16286" dependencies = [ "alloy-rlp", "ark-ff 0.3.0", @@ -6308,15 +6581,15 @@ dependencies = [ [[package]] name = "ruint-macro" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f86854cf50259291520509879a5c294c3c9a4c334e9ff65071c51e42ef1e2343" +checksum = "48fd7bd8a6377e15ad9d42a8ec25371b94ddc67abe7c8b9127bec79bebaaae18" [[package]] name = "rustc-demangle" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustc-hash" @@ -6345,7 +6618,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" dependencies = [ - "semver 1.0.22", + "semver 1.0.23", ] [[package]] @@ -6359,11 +6632,11 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.32" +version = "0.38.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "errno", "libc", "linux-raw-sys", @@ -6372,9 +6645,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.21.11" +version = "0.21.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fecbfb7b1444f477b345853b1fce097a2c6fb637b2bfb87e6bc5db0f043fae4" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" dependencies = [ "log", "ring 0.17.8", @@ -6384,23 +6657,23 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.5" +version = "0.23.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afabcee0551bd1aa3e18e5adbf2c0544722014b899adb31bd186ec638d3da97e" +checksum = "4828ea528154ae444e5a642dbb7d5623354030dc9822b83fd9bb79683c7399d0" dependencies = [ "once_cell", "ring 0.17.8", "rustls-pki-types", - "rustls-webpki 0.102.2", + "rustls-webpki 0.102.5", "subtle", "zeroize", ] [[package]] name = "rustls-native-certs" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f1fb85efa936c42c6d5fc28d2629bb51e4b2f4b8a5211e297d599cc5a093792" +checksum = "a88d6d420651b496bdd98684116959239430022a115c1240e6c3993be0b15fba" dependencies = [ "openssl-probe", "rustls-pemfile", @@ -6415,15 +6688,15 @@ version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d" dependencies = [ - "base64 0.22.0", + "base64 0.22.1", "rustls-pki-types", ] [[package]] name = "rustls-pki-types" -version = "1.4.1" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecd36cc4259e3e4514335c4a138c6b43171a8d61d8f5c9348f9fc7529416f247" +checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d" [[package]] name = "rustls-webpki" @@ -6437,9 +6710,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.102.2" +version = "0.102.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "faaa0a62740bedb9b2ef5afa303da42764c012f743917351dc9a237ea1663610" +checksum = "f9a6fccd794a42c2c105b513a2f62bc3fd8f3ba57a4593677ceb0bd035164d78" dependencies = [ "ring 0.17.8", "rustls-pki-types", @@ -6448,9 +6721,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.15" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80af6f9131f277a45a3fba6ce8e2258037bb0477a67e610d3c1fe046ab31de47" +checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" [[package]] name = "rusty-fork" @@ -6477,15 +6750,15 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "safe_arch" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f398075ce1e6a179b46f51bd88d0598b92b00d3551f1a2d4ac49e771b56ac354" +checksum = "c3460605018fdc9612bce72735cba0d27efbcd9904780d44c7e3a9948f96148a" dependencies = [ "bytemuck", ] @@ -6603,7 +6876,7 @@ dependencies = [ "proc-macro-crate 1.3.1", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.71", ] [[package]] @@ -6654,7 +6927,7 @@ dependencies = [ "futures", "log", "parity-scale-codec", - "parking_lot 0.12.1", + "parking_lot 0.12.3", "sc-executor", "sc-transaction-pool-api", "sc-utils", @@ -6683,7 +6956,7 @@ dependencies = [ "log", "parity-db", "parity-scale-codec", - "parking_lot 0.12.1", + "parking_lot 0.12.3", "sc-client-api", "sc-state-db", "schnellru", @@ -6707,7 +6980,7 @@ dependencies = [ "libp2p-identity", "log", "mockall", - "parking_lot 0.12.1", + "parking_lot 0.12.3", "sc-client-api", "sc-utils", "serde", @@ -6734,7 +7007,7 @@ dependencies = [ "num-rational", "num-traits", "parity-scale-codec", - "parking_lot 0.12.1", + "parking_lot 0.12.3", "sc-client-api", "sc-consensus", "sc-consensus-epochs", @@ -6785,7 +7058,7 @@ dependencies = [ "futures-timer", "log", "parity-scale-codec", - "parking_lot 0.12.1", + "parking_lot 0.12.3", "rand", "sc-block-builder", "sc-chain-spec", @@ -6840,7 +7113,7 @@ version = "0.10.0-dev" source = "git+https://github.com/serai-dex/substrate#6e3f07bf5c98a6a3ec15f2b1a46148aa8c7d737a" dependencies = [ "parity-scale-codec", - "parking_lot 0.12.1", + "parking_lot 0.12.3", "sc-executor-common", "sc-executor-wasmtime", "schnellru", @@ -6907,7 +7180,7 @@ version = "4.0.0-dev" source = "git+https://github.com/serai-dex/substrate#6e3f07bf5c98a6a3ec15f2b1a46148aa8c7d737a" dependencies = [ "array-bytes", - "parking_lot 0.12.1", + "parking_lot 0.12.3", "serde_json", "sp-application-crypto", "sp-core", @@ -6935,7 +7208,7 @@ dependencies = [ "log", "mockall", "parity-scale-codec", - "parking_lot 0.12.1", + "parking_lot 0.12.3", "partial_sort", "pin-project", "rand", @@ -7095,13 +7368,13 @@ dependencies = [ "fnv", "futures", "futures-timer", - "hyper 0.14.28", + "hyper 0.14.30", "libp2p", "log", "num_cpus", "once_cell", "parity-scale-codec", - "parking_lot 0.12.1", + "parking_lot 0.12.3", "rand", "sc-client-api", "sc-network", @@ -7135,7 +7408,7 @@ dependencies = [ "jsonrpsee", "log", "parity-scale-codec", - "parking_lot 0.12.1", + "parking_lot 0.12.3", "sc-block-builder", "sc-chain-spec", "sc-client-api", @@ -7202,7 +7475,7 @@ dependencies = [ "jsonrpsee", "log", "parity-scale-codec", - "parking_lot 0.12.1", + "parking_lot 0.12.3", "sc-chain-spec", "sc-client-api", "sc-transaction-pool-api", @@ -7229,7 +7502,7 @@ dependencies = [ "jsonrpsee", "log", "parity-scale-codec", - "parking_lot 0.12.1", + "parking_lot 0.12.3", "pin-project", "rand", "sc-block-builder", @@ -7286,7 +7559,7 @@ source = "git+https://github.com/serai-dex/substrate#6e3f07bf5c98a6a3ec15f2b1a46 dependencies = [ "log", "parity-scale-codec", - "parking_lot 0.12.1", + "parking_lot 0.12.3", "sp-core", ] @@ -7318,7 +7591,7 @@ dependencies = [ "futures", "libp2p", "log", - "parking_lot 0.12.1", + "parking_lot 0.12.3", "pin-project", "rand", "sc-utils", @@ -7338,7 +7611,7 @@ dependencies = [ "lazy_static", "libc", "log", - "parking_lot 0.12.1", + "parking_lot 0.12.3", "regex", "rustc-hash", "sc-client-api", @@ -7364,7 +7637,7 @@ dependencies = [ "proc-macro-crate 1.3.1", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.71", ] [[package]] @@ -7378,7 +7651,7 @@ dependencies = [ "linked-hash-map", "log", "parity-scale-codec", - "parking_lot 0.12.1", + "parking_lot 0.12.3", "sc-client-api", "sc-transaction-pool-api", "sc-utils", @@ -7419,16 +7692,16 @@ dependencies = [ "futures-timer", "lazy_static", "log", - "parking_lot 0.12.1", + "parking_lot 0.12.3", "prometheus", "sp-arithmetic", ] [[package]] name = "scale-info" -version = "2.11.2" +version = "2.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c453e59a955f81fb62ee5d596b450383d699f152d350e9d23a0db2adb78e4c0" +checksum = "eca070c12893629e2cc820a9761bedf6ce1dcddc9852984d1dc734b8bd9bd024" dependencies = [ "bitvec", "cfg-if", @@ -7440,11 +7713,11 @@ dependencies = [ [[package]] name = "scale-info-derive" -version = "2.11.2" +version = "2.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18cf6c6447f813ef19eb450e985bcce6705f9ce7660db221b59093d15c79c4b7" +checksum = "2d35494501194174bda522a32605929eefc9ecf7e0a326c26db1fdd85881eb62" dependencies = [ - "proc-macro-crate 1.3.1", + "proc-macro-crate 3.1.0", "proc-macro2", "quote", "syn 1.0.109", @@ -7461,9 +7734,9 @@ dependencies = [ [[package]] name = "schnellru" -version = "0.2.1" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "772575a524feeb803e5b0fcbc6dd9f367e579488197c94c6e4023aad2305774d" +checksum = "c9a8ef13a93c54d20580de1e5c413e624e53121d42fc7e2c11d10ef7f8b02367" dependencies = [ "ahash", "cfg-if", @@ -7487,15 +7760,16 @@ dependencies = [ [[package]] name = "schnorrkel" -version = "0.11.3" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da18ffd9f2f5d01bc0b3050b37ce7728665f926b4dd1157fe3221b05737d924f" +checksum = "8de18f6d8ba0aad7045f5feae07ec29899c1112584a38509a84ad7b04451eaa0" dependencies = [ + "aead", "arrayref", "arrayvec", "curve25519-dalek", + "getrandom_or_panic", "merlin", - "rand", "rand_core", "serde_bytes", "sha2", @@ -7547,9 +7821,9 @@ dependencies = [ [[package]] name = "secp256k1" -version = "0.28.2" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d24b59d129cdadea20aea4fb2352fa053712e5d713eee47d700cd4b2bc002f10" +checksum = "0e0cc0f1cf93f4969faf3ea1c7d8a9faed25918d96affa959720823dfe86d4f3" dependencies = [ "bitcoin_hashes", "rand", @@ -7559,9 +7833,9 @@ dependencies = [ [[package]] name = "secp256k1-sys" -version = "0.9.2" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5d1746aae42c19d583c3c1a8c646bfad910498e2051c551a7f2e3c0c9fbb7eb" +checksum = "1433bd67156263443f14d603720b082dd3121779323fce20cba2aa07b874bc1b" dependencies = [ "cc", ] @@ -7577,11 +7851,11 @@ dependencies = [ [[package]] name = "security-framework" -version = "2.10.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "770452e37cad93e0a50d5abc3990d2bc351c36d0328f86cefec2f2fb206eaef6" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.6.0", "core-foundation", "core-foundation-sys", "libc", @@ -7590,9 +7864,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.10.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f3cc463c0ef97e11c3461a9d3787412d30e8e7eb907c79180c4a57bf7c04ef" +checksum = "75da29fe9b9b08fe9d6b22b5b4bcbc75d8db3aa31e639aa56bb62e9d46bfceaf" dependencies = [ "core-foundation-sys", "libc", @@ -7618,9 +7892,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" dependencies = [ "serde", ] @@ -7681,7 +7955,7 @@ dependencies = [ "frost-schnorrkel", "hex", "modular-frost", - "monero-serai", + "monero-wallet", "multiaddr", "parity-scale-codec", "rand_core", @@ -7842,6 +8116,17 @@ version = "0.1.0" name = "serai-env" version = "0.1.0" +[[package]] +name = "serai-ethereum-relayer" +version = "0.1.0" +dependencies = [ + "env_logger", + "log", + "serai-db", + "serai-env", + "tokio", +] + [[package]] name = "serai-full-stack-tests" version = "0.1.0" @@ -7851,7 +8136,8 @@ dependencies = [ "curve25519-dalek", "dockertest", "hex", - "monero-serai", + "monero-simple-request-rpc", + "monero-wallet", "parity-scale-codec", "rand_core", "serai-client", @@ -7878,6 +8164,7 @@ dependencies = [ "serai-dex-pallet", "serai-genesis-liquidity-primitives", "serai-primitives", + "serai-validator-sets-pallet", "serai-validator-sets-primitives", "sp-application-crypto", "sp-core", @@ -7910,6 +8197,7 @@ dependencies = [ "serai-dex-pallet", "serai-emissions-pallet", "serai-genesis-liquidity-pallet", + "serai-genesis-liquidity-primitives", "serai-in-instructions-primitives", "serai-primitives", "serai-validator-sets-pallet", @@ -7983,8 +8271,7 @@ dependencies = [ "dleq", "flexible-transcript", "minimal-ed448", - "monero-generators", - "monero-serai", + "monero-wallet-util", "multiexp", "schnorr-signatures", ] @@ -7999,6 +8286,7 @@ dependencies = [ "hex", "jsonrpsee", "libp2p", + "log", "pallet-transaction-payment-rpc", "rand_core", "sc-authority-discovery", @@ -8084,7 +8372,8 @@ dependencies = [ "k256", "log", "modular-frost", - "monero-serai", + "monero-simple-request-rpc", + "monero-wallet", "parity-scale-codec", "rand_chacha", "rand_core", @@ -8095,7 +8384,6 @@ dependencies = [ "serai-env", "serai-message-queue", "serai-processor-messages", - "serde", "serde_json", "sp-application-crypto", "thiserror", @@ -8129,7 +8417,9 @@ dependencies = [ "dockertest", "ethereum-serai", "hex", - "monero-serai", + "k256", + "monero-simple-request-rpc", + "monero-wallet", "parity-scale-codec", "rand_core", "serai-client", @@ -8165,7 +8455,7 @@ dependencies = [ "frame-support", "frame-system", "frame-system-rpc-runtime-api", - "hashbrown 0.14.3", + "hashbrown 0.14.5", "pallet-authorship", "pallet-babe", "pallet-grandpa", @@ -8174,6 +8464,7 @@ dependencies = [ "pallet-transaction-payment-rpc-runtime-api", "parity-scale-codec", "scale-info", + "serai-abi", "serai-coins-pallet", "serai-dex-pallet", "serai-emissions-pallet", @@ -8232,7 +8523,7 @@ version = "0.1.0" dependencies = [ "frame-support", "frame-system", - "hashbrown 0.14.3", + "hashbrown 0.14.5", "pallet-babe", "pallet-grandpa", "parity-scale-codec", @@ -8268,38 +8559,38 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.198" +version = "1.0.204" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9846a40c979031340571da2545a4e5b7c4163bdae79b301d5f86d03979451fcc" +checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12" dependencies = [ "serde_derive", ] [[package]] name = "serde_bytes" -version = "0.11.14" +version = "0.11.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b8497c313fd43ab992087548117643f6fcd935cbf36f176ffda0aacf9591734" +checksum = "387cc504cb06bb40a96c8e04e951fe01854cf6bc921053c954e4a606d9675c6a" dependencies = [ "serde", ] [[package]] name = "serde_derive" -version = "1.0.198" +version = "1.0.204" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e88edab869b01783ba905e7d0153f9fc1a6505a96e4ad3018011eedb838566d9" +checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.71", ] [[package]] name = "serde_json" -version = "1.0.116" +version = "1.0.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813" +checksum = "4e0d21c9a8cae1235ad58a00c11cb40d4b1e5c784f1ef2c537876ed6ffd8b7c5" dependencies = [ "itoa", "ryu", @@ -8314,14 +8605,14 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.71", ] [[package]] name = "serde_spanned" -version = "0.6.5" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" +checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0" dependencies = [ "serde", ] @@ -8340,11 +8631,11 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.7.0" +version = "3.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee80b0e361bbf88fd2f6e242ccd19cfda072cb0faa6ae694ecee08199938569a" +checksum = "69cecfa94848272156ea67b2b1a53f20fc7bc638c4a46d2f8abde08f05f4b857" dependencies = [ - "base64 0.21.7", + "base64 0.22.1", "chrono", "hex", "indexmap 1.9.3", @@ -8402,9 +8693,9 @@ dependencies = [ [[package]] name = "sha3-asm" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bac61da6b35ad76b195eb4771210f947734321a8d81d7738e1580d953bc7a15e" +checksum = "a9b57fd861253bff08bb1919e995f90ba8f4889de2726091c8876f3a4e823b40" dependencies = [ "cc", "cfg-if", @@ -8427,9 +8718,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" dependencies = [ "libc", ] @@ -8463,7 +8754,7 @@ version = "0.1.0" dependencies = [ "base64ct", "http-body-util", - "hyper 1.3.1", + "hyper 1.4.1", "hyper-rustls", "hyper-util", "tokio", @@ -8533,9 +8824,9 @@ dependencies = [ [[package]] name = "socket2" -version = "0.5.6" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" dependencies = [ "libc", "windows-sys 0.52.0", @@ -8557,6 +8848,21 @@ dependencies = [ "sha-1", ] +[[package]] +name = "soketto" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37468c595637c10857701c990f93a40ce0e357cedb0953d1c26c8d8027f9bb53" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures", + "httparse", + "log", + "rand", + "sha1", +] + [[package]] name = "sp-api" version = "4.0.0-dev" @@ -8589,7 +8895,7 @@ dependencies = [ "proc-macro-crate 1.3.1", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.71", ] [[package]] @@ -8650,7 +8956,7 @@ dependencies = [ "futures", "log", "parity-scale-codec", - "parking_lot 0.12.1", + "parking_lot 0.12.3", "schnellru", "sp-api", "sp-consensus", @@ -8743,7 +9049,7 @@ dependencies = [ "log", "merlin", "parity-scale-codec", - "parking_lot 0.12.1", + "parking_lot 0.12.3", "paste", "primitive-types", "rand", @@ -8785,7 +9091,7 @@ source = "git+https://github.com/serai-dex/substrate#6e3f07bf5c98a6a3ec15f2b1a46 dependencies = [ "quote", "sp-core-hashing", - "syn 2.0.60", + "syn 2.0.71", ] [[package]] @@ -8794,7 +9100,7 @@ version = "4.0.0-dev" source = "git+https://github.com/serai-dex/substrate#6e3f07bf5c98a6a3ec15f2b1a46148aa8c7d737a" dependencies = [ "kvdb", - "parking_lot 0.12.1", + "parking_lot 0.12.3", ] [[package]] @@ -8804,7 +9110,7 @@ source = "git+https://github.com/serai-dex/substrate#6e3f07bf5c98a6a3ec15f2b1a46 dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.71", ] [[package]] @@ -8871,7 +9177,7 @@ version = "0.27.0" source = "git+https://github.com/serai-dex/substrate#6e3f07bf5c98a6a3ec15f2b1a46148aa8c7d737a" dependencies = [ "parity-scale-codec", - "parking_lot 0.12.1", + "parking_lot 0.12.3", "sp-core", "sp-externalities", "thiserror", @@ -8976,7 +9282,7 @@ dependencies = [ "proc-macro-crate 1.3.1", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.71", ] [[package]] @@ -9016,7 +9322,7 @@ dependencies = [ "hash-db", "log", "parity-scale-codec", - "parking_lot 0.12.1", + "parking_lot 0.12.3", "rand", "smallvec", "sp-core", @@ -9088,12 +9394,12 @@ source = "git+https://github.com/serai-dex/substrate#6e3f07bf5c98a6a3ec15f2b1a46 dependencies = [ "ahash", "hash-db", - "hashbrown 0.14.3", + "hashbrown 0.14.5", "lazy_static", "memory-db", "nohash-hasher", "parity-scale-codec", - "parking_lot 0.12.1", + "parking_lot 0.12.3", "scale-info", "schnellru", "sp-core", @@ -9129,7 +9435,7 @@ dependencies = [ "parity-scale-codec", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.71", ] [[package]] @@ -9247,7 +9553,7 @@ dependencies = [ name = "std-shims" version = "0.1.1" dependencies = [ - "hashbrown 0.14.3", + "hashbrown 0.14.5", "spin 0.9.8", ] @@ -9285,6 +9591,15 @@ dependencies = [ "strum_macros 0.25.3", ] +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros 0.26.4", +] + [[package]] name = "strum_macros" version = "0.24.3" @@ -9308,7 +9623,20 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.60", + "syn 2.0.71", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.71", ] [[package]] @@ -9352,7 +9680,7 @@ name = "substrate-prometheus-endpoint" version = "0.10.0-dev" source = "git+https://github.com/serai-dex/substrate#6e3f07bf5c98a6a3ec15f2b1a46148aa8c7d737a" dependencies = [ - "hyper 0.14.28", + "hyper 0.14.30", "log", "prometheus", "thiserror", @@ -9379,9 +9707,9 @@ dependencies = [ [[package]] name = "subtle" -version = "2.5.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" @@ -9396,9 +9724,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.60" +version = "2.0.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3" +checksum = "b146dcf730474b4bcd16c311627b31ede9ab149045db4d6088b3becaea046462" dependencies = [ "proc-macro2", "quote", @@ -9407,14 +9735,14 @@ dependencies = [ [[package]] name = "syn-solidity" -version = "0.7.2" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aa0cefd02f532035d83cfec82647c6eb53140b0485220760e669f4bad489e36" +checksum = "c837dc8852cb7074e46b444afb81783140dab12c58867b49fb3898fbafedf7ea" dependencies = [ "paste", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.71", ] [[package]] @@ -9426,7 +9754,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.71", ] [[package]] @@ -9470,9 +9798,9 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "target-lexicon" -version = "0.12.14" +version = "0.12.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1fc403891a21bcfb7c37834ba66a547a8f402146eba7265b5a6d88059c9ff2f" +checksum = "4873307b7c257eddcb50c9bedf158eb669578359fb28428bef438fec8e6ba7c2" [[package]] name = "tempfile" @@ -9518,22 +9846,22 @@ checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" [[package]] name = "thiserror" -version = "1.0.59" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0126ad08bff79f29fc3ae6a55cc72352056dfff61e3ff8bb7129476d44b23aa" +checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.59" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1cd413b5d558b4c5bf3680e324a6fa5014e7b7c067a51e69dbdf47eb7148b66" +checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.71", ] [[package]] @@ -9557,12 +9885,13 @@ dependencies = [ [[package]] name = "time" -version = "0.3.31" +version = "0.3.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f657ba42c3f86e7680e53c8cd3af8abbe56b5491790b46e22e19c0d57463583e" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" dependencies = [ "deranged", "itoa", + "num-conv", "powerfmt", "serde", "time-core", @@ -9577,10 +9906,11 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.16" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26197e33420244aeb70c3e8c78376ca46571bc4e701e4791c2cd9f57dcb3a43f" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" dependencies = [ + "num-conv", "time-core", ] @@ -9614,9 +9944,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.6.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" dependencies = [ "tinyvec_macros", ] @@ -9629,32 +9959,32 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.37.0" +version = "1.38.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" +checksum = "eb2caba9f80616f438e09748d5acda951967e1ea58508ef53d9c6402485a46df" dependencies = [ "backtrace", "bytes", "libc", "mio", "num_cpus", - "parking_lot 0.12.1", - "pin-project-lite 0.2.14", + "parking_lot 0.12.3", + "pin-project-lite", "signal-hook-registry", - "socket2 0.5.6", + "socket2 0.5.7", "tokio-macros", "windows-sys 0.48.0", ] [[package]] name = "tokio-macros" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.71", ] [[package]] @@ -9663,7 +9993,7 @@ version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" dependencies = [ - "rustls 0.23.5", + "rustls 0.23.11", "rustls-pki-types", "tokio", ] @@ -9675,24 +10005,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" dependencies = [ "futures-core", - "pin-project-lite 0.2.14", + "pin-project-lite", "tokio", "tokio-util", ] [[package]] name = "tokio-util" -version = "0.7.10" +version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" +checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" dependencies = [ "bytes", "futures-core", "futures-io", "futures-sink", - "pin-project-lite 0.2.14", + "pin-project-lite", "tokio", - "tracing", ] [[package]] @@ -9718,9 +10047,9 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.5" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" dependencies = [ "serde", ] @@ -9758,7 +10087,7 @@ dependencies = [ "futures-core", "futures-util", "pin-project", - "pin-project-lite 0.2.14", + "pin-project-lite", "tokio", "tower-layer", "tower-service", @@ -9771,14 +10100,14 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61c5bb1d698276a2443e5ecfabc1008bf15a36c12e6a7176e7bf089ea9131140" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "bytes", "futures-core", "futures-util", "http 0.2.12", "http-body 0.4.6", "http-range-header", - "pin-project-lite 0.2.14", + "pin-project-lite", "tower-layer", "tower-service", ] @@ -9802,7 +10131,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ "log", - "pin-project-lite 0.2.14", + "pin-project-lite", "tracing-attributes", "tracing-core", ] @@ -9815,7 +10144,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.71", ] [[package]] @@ -10008,7 +10337,7 @@ dependencies = [ "ipconfig", "lru-cache", "once_cell", - "parking_lot 0.12.1", + "parking_lot 0.12.3", "rand", "resolv-conf", "smallvec", @@ -10095,9 +10424,9 @@ dependencies = [ [[package]] name = "unicode-width" -version = "0.1.11" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" +checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" [[package]] name = "unicode-xid" @@ -10141,9 +10470,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.0" +version = "2.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" dependencies = [ "form_urlencoded", "idna 0.5.0", @@ -10152,15 +10481,15 @@ dependencies = [ [[package]] name = "utf8parse" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.8.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" +checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" [[package]] name = "valuable" @@ -10241,7 +10570,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.71", "wasm-bindgen-shared", ] @@ -10275,7 +10604,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.71", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -10366,7 +10695,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1dfcdb72d96f01e6c85b6bf20102e7423bdbaad5c337301bab2bbf253d26413c" dependencies = [ "indexmap 2.2.6", - "semver 1.0.22", + "semver 1.0.23", ] [[package]] @@ -10584,7 +10913,7 @@ checksum = "ca7af9bb3ee875c4907835e607a275d10b04d15623d3aebe01afe8fbd3f85050" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.71", ] [[package]] @@ -10617,9 +10946,9 @@ dependencies = [ [[package]] name = "wide" -version = "0.7.16" +version = "0.7.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81a1851a719f11d1d2fea40e15c72f6c00de8c142d7ac47c1441cc7e4d0d5bc6" +checksum = "2caba658a80831539b30698ae9862a72db6697dfdd7151e46920f5f2755c3ce2" dependencies = [ "bytemuck", "safe_arch", @@ -10649,11 +10978,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" dependencies = [ - "winapi", + "windows-sys 0.52.0", ] [[package]] @@ -10664,21 +10993,22 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows" -version = "0.48.0" +version = "0.51.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +checksum = "ca229916c5ee38c2f2bc1e9d8f04df975b4bd93f9955dc69fabb5d91270045c9" dependencies = [ + "windows-core 0.51.1", "windows-targets 0.48.5", ] [[package]] name = "windows" -version = "0.51.1" +version = "0.54.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca229916c5ee38c2f2bc1e9d8f04df975b4bd93f9955dc69fabb5d91270045c9" +checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49" dependencies = [ - "windows-core", - "windows-targets 0.48.5", + "windows-core 0.54.0", + "windows-targets 0.52.6", ] [[package]] @@ -10690,6 +11020,25 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "windows-core" +version = "0.54.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65" +dependencies = [ + "windows-result", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.48.0" @@ -10705,7 +11054,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.4", + "windows-targets 0.52.6", ] [[package]] @@ -10725,17 +11074,18 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.4" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.4", - "windows_aarch64_msvc 0.52.4", - "windows_i686_gnu 0.52.4", - "windows_i686_msvc 0.52.4", - "windows_x86_64_gnu 0.52.4", - "windows_x86_64_gnullvm 0.52.4", - "windows_x86_64_msvc 0.52.4", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] [[package]] @@ -10746,9 +11096,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.4" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" @@ -10758,9 +11108,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" -version = "0.52.4" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" @@ -10770,9 +11120,15 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.52.4" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" @@ -10782,9 +11138,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.52.4" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" @@ -10794,9 +11150,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.52.4" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" @@ -10806,9 +11162,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.4" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" @@ -10818,9 +11174,9 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.52.4" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" @@ -10833,9 +11189,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.6.6" +version = "0.6.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0c976aaaa0e1f90dbb21e9587cdaf1d9679a1cde8875c0d6bd83ab96a208352" +checksum = "59b5e5f6c299a3c7890b876a2a587f3115162487e704907d9b6cd29473052ba1" [[package]] name = "winreg" @@ -10909,7 +11265,7 @@ dependencies = [ "futures", "log", "nohash-hasher", - "parking_lot 0.12.1", + "parking_lot 0.12.3", "pin-project", "rand", "static_assertions", @@ -10928,34 +11284,35 @@ dependencies = [ name = "zalloc" version = "0.1.0" dependencies = [ + "rustversion", "zeroize", ] [[package]] name = "zerocopy" -version = "0.7.32" +version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.32" +version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.71", ] [[package]] name = "zeroize" -version = "1.7.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" dependencies = [ "zeroize_derive", ] @@ -10968,14 +11325,14 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.60", + "syn 2.0.71", ] [[package]] name = "zstd" version = "0.11.2+zstd.1.5.2" dependencies = [ - "zstd 0.13.1", + "zstd 0.13.2", ] [[package]] @@ -10989,11 +11346,11 @@ dependencies = [ [[package]] name = "zstd" -version = "0.13.1" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d789b1514203a1120ad2429eae43a7bd32b90976a7bb8a05f7ec02fa88cc23a" +checksum = "fcf2b778a664581e31e389454a7072dab1647606d44f7feea22cd5abb9c9f3f9" dependencies = [ - "zstd-safe 7.1.0", + "zstd-safe 7.2.0", ] [[package]] @@ -11008,18 +11365,18 @@ dependencies = [ [[package]] name = "zstd-safe" -version = "7.1.0" +version = "7.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cd99b45c6bc03a018c8b8a86025678c87e55526064e38f9df301989dce7ec0a" +checksum = "fa556e971e7b568dc775c136fc9de8c779b1c2fc3a63defaafadffdbd3181afa" dependencies = [ "zstd-sys", ] [[package]] name = "zstd-sys" -version = "2.0.10+zstd.1.5.6" +version = "2.0.12+zstd.1.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c253a4914af5bafc8fa8c86ee400827e83cf6ec01195ec1f1ed8441bf00d65aa" +checksum = "0a4e40c320c3cb459d9a9ff6de98cff88f4751ee9275d140e2be94a2b74e4c13" dependencies = [ "cc", "pkg-config", diff --git a/Cargo.toml b/Cargo.toml index 94b52ffb..4593d40d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,8 @@ resolver = "2" members = [ # Version patches + "patches/parking_lot_core", + "patches/parking_lot", "patches/zstd", "patches/rocksdb", "patches/proc-macro-crate", @@ -35,11 +37,28 @@ members = [ "crypto/frost", "crypto/schnorrkel", - "coins/bitcoin", - "coins/ethereum/alloy-simple-request-transport", - "coins/ethereum", - "coins/monero/generators", - "coins/monero", + "networks/bitcoin", + + "networks/ethereum/alloy-simple-request-transport", + "networks/ethereum", + "networks/ethereum/relayer", + + "networks/monero/io", + "networks/monero/generators", + "networks/monero/primitives", + "networks/monero/ringct/mlsag", + "networks/monero/ringct/clsag", + "networks/monero/ringct/borromean", + "networks/monero/ringct/bulletproofs", + "networks/monero", + "networks/monero/rpc", + "networks/monero/rpc/simple-request", + "networks/monero/wallet/address", + "networks/monero/wallet", + "networks/monero/wallet/seed", + "networks/monero/wallet/polyseed", + "networks/monero/wallet/util", + "networks/monero/verify-chain", "message-queue", @@ -55,12 +74,14 @@ members = [ "substrate/coins/primitives", "substrate/coins/pallet", - "substrate/in-instructions/primitives", - "substrate/in-instructions/pallet", + "substrate/dex/pallet", "substrate/validator-sets/primitives", "substrate/validator-sets/pallet", + "substrate/in-instructions/primitives", + "substrate/in-instructions/pallet", + "substrate/signals/primitives", "substrate/signals/pallet", @@ -112,6 +133,8 @@ lazy_static = { git = "https://github.com/rust-lang-nursery/lazy-static.rs", rev # 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 diff --git a/README.md b/README.md index 4a8ac4d5..319c05db 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ wallet. 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 +- `networks`: Various libraries intended for usage in Serai yet also by the wider community. This means they will always support the functionality Serai needs, yet won't disadvantage other use cases when possible. diff --git a/audits/Cypher Stack coins bitcoin August 2023/README.md b/audits/Cypher Stack coins bitcoin August 2023/README.md deleted file mode 100644 index 97507bd3..00000000 --- a/audits/Cypher Stack coins bitcoin August 2023/README.md +++ /dev/null @@ -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. diff --git a/audits/Cypher Stack coins bitcoin August 2023/Audit.pdf b/audits/Cypher Stack networks bitcoin August 2023/Audit.pdf similarity index 100% rename from audits/Cypher Stack coins bitcoin August 2023/Audit.pdf rename to audits/Cypher Stack networks bitcoin August 2023/Audit.pdf diff --git a/audits/Cypher Stack coins bitcoin August 2023/LICENSE b/audits/Cypher Stack networks bitcoin August 2023/LICENSE similarity index 100% rename from audits/Cypher Stack coins bitcoin August 2023/LICENSE rename to audits/Cypher Stack networks bitcoin August 2023/LICENSE diff --git a/audits/Cypher Stack networks bitcoin August 2023/README.md b/audits/Cypher Stack networks bitcoin August 2023/README.md new file mode 100644 index 00000000..9ab8aa4d --- /dev/null +++ b/audits/Cypher Stack networks bitcoin August 2023/README.md @@ -0,0 +1,7 @@ +# Cypher Stack /networks/bitcoin Audit, August 2023 + +This audit was over the `/networks/bitcoin` folder (at the time located at +`/coins/bitcoin`). It is encompassing up to commit +5121ca75199dff7bd34230880a1fdd793012068c. + +Please see https://github.com/cypherstack/serai-btc-audit for provenance. diff --git a/coins/monero/Cargo.toml b/coins/monero/Cargo.toml deleted file mode 100644 index 357803c9..00000000 --- a/coins/monero/Cargo.toml +++ /dev/null @@ -1,111 +0,0 @@ -[package] -name = "monero-serai" -version = "0.1.4-alpha" -description = "A modern Monero transaction library" -license = "MIT" -repository = "https://github.com/serai-dex/serai/tree/develop/coins/monero" -authors = ["Luke Parker "] -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] -dalek-ff-group = { path = "../../crypto/dalek-ff-group", version = "0.4", default-features = false } -monero-generators = { path = "generators", version = "0.4", default-features = false } - -[dev-dependencies] -tokio = { version = "1", features = ["sync", "macros"] } - -frost = { package = "modular-frost", path = "../../crypto/frost", features = ["tests"] } - -[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 = [] - -default = ["std", "http-rpc"] diff --git a/coins/monero/README.md b/coins/monero/README.md deleted file mode 100644 index 517fb4bb..00000000 --- a/coins/monero/README.md +++ /dev/null @@ -1,49 +0,0 @@ -# monero-serai - -A modern Monero transaction library intended for usage in wallets. It prides -itself on accuracy, correctness, and removing common pit falls developers may -face. - -monero-serai also offers the following features: - -- Featured Addresses -- A FROST-based multisig orders of magnitude more performant than Monero's - -### Purpose and support - -monero-serai was written for Serai, a decentralized exchange aiming to support -Monero. Despite this, monero-serai is intended to be a widely usable library, -accurate to Monero. monero-serai guarantees the functionality needed for Serai, -yet will not deprive functionality from other users. - -Various legacy transaction formats are not currently implemented, yet we are -willing to add support for them. There aren't active development efforts around -them however. - -### Caveats - -This library DOES attempt to do the following: - -- Create on-chain transactions identical to how wallet2 would (unless told not - to) -- Not be detectable as monero-serai when scanning outputs -- Not reveal spent outputs to the connected RPC node - -This library DOES NOT attempt to do the following: - -- Have identical RPC behavior when creating transactions -- Be a wallet - -This means that monero-serai shouldn't be fingerprintable on-chain. It also -shouldn't be fingerprintable if a targeted attack occurs to detect if the -receiving wallet is monero-serai or wallet2. It also should be generally safe -for usage with remote nodes. - -It won't hide from remote nodes it's monero-serai however, potentially -allowing a remote node to profile you. The implications of this are left to the -user to consider. - -It also won't act as a wallet, just as a transaction library. wallet2 has -several *non-transaction-level* policies, such as always attempting to use two -inputs to create transactions. These are considered out of scope to -monero-serai. diff --git a/coins/monero/build.rs b/coins/monero/build.rs deleted file mode 100644 index b10e956a..00000000 --- a/coins/monero/build.rs +++ /dev/null @@ -1,67 +0,0 @@ -use std::{ - io::Write, - env, - path::Path, - fs::{File, remove_file}, -}; - -use dalek_ff_group::EdwardsPoint; - -use monero_generators::bulletproofs_generators; - -fn serialize(generators_string: &mut String, points: &[EdwardsPoint]) { - for generator in points { - generators_string.extend( - format!( - " - dalek_ff_group::EdwardsPoint( - curve25519_dalek::edwards::CompressedEdwardsY({:?}).decompress().unwrap() - ), - ", - generator.compress().to_bytes() - ) - .chars(), - ); - } -} - -fn generators(prefix: &'static str, path: &str) { - let generators = bulletproofs_generators(prefix.as_bytes()); - #[allow(non_snake_case)] - let mut G_str = String::new(); - serialize(&mut G_str, &generators.G); - #[allow(non_snake_case)] - let mut H_str = String::new(); - serialize(&mut H_str, &generators.H); - - let path = Path::new(&env::var("OUT_DIR").unwrap()).join(path); - let _ = remove_file(&path); - File::create(&path) - .unwrap() - .write_all( - format!( - " - pub(crate) static GENERATORS_CELL: OnceLock = 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() { - println!("cargo:rerun-if-changed=build.rs"); - - generators("bulletproof", "generators.rs"); - generators("bulletproof_plus", "generators_plus.rs"); -} diff --git a/coins/monero/generators/README.md b/coins/monero/generators/README.md deleted file mode 100644 index bab293c9..00000000 --- a/coins/monero/generators/README.md +++ /dev/null @@ -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. diff --git a/coins/monero/generators/src/lib.rs b/coins/monero/generators/src/lib.rs deleted file mode 100644 index c52350c2..00000000 --- a/coins/monero/generators/src/lib.rs +++ /dev/null @@ -1,79 +0,0 @@ -//! Generators used by Monero in both its Pedersen commitments and Bulletproofs(+). -//! -//! An implementation of Monero's `ge_fromfe_frombytes_vartime`, simply called -//! `hash_to_point` here, is included, as needed to generate generators. - -#![cfg_attr(not(feature = "std"), no_std)] - -use std_shims::{sync::OnceLock, vec::Vec}; - -use sha3::{Digest, Keccak256}; - -use curve25519_dalek::edwards::{EdwardsPoint as DalekPoint}; - -use group::{Group, GroupEncoding}; -use dalek_ff_group::EdwardsPoint; - -mod varint; -use varint::write_varint; - -mod hash_to_point; -pub use hash_to_point::{hash_to_point, decompress_point}; - -#[cfg(test)] -mod tests; - -fn hash(data: &[u8]) -> [u8; 32] { - Keccak256::digest(data).into() -} - -static H_CELL: OnceLock = OnceLock::new(); -/// Monero's alternate generator `H`, used for amounts in Pedersen commitments. -#[allow(non_snake_case)] -pub fn H() -> DalekPoint { - *H_CELL.get_or_init(|| { - decompress_point(hash(&EdwardsPoint::generator().to_bytes())).unwrap().mul_by_cofactor() - }) -} - -static H_POW_2_CELL: OnceLock<[DalekPoint; 64]> = OnceLock::new(); -/// Monero's alternate generator `H`, multiplied by 2**i for i in 1 ..= 64. -#[allow(non_snake_case)] -pub fn H_pow_2() -> &'static [DalekPoint; 64] { - H_POW_2_CELL.get_or_init(|| { - let mut res = [H(); 64]; - for i in 1 .. 64 { - res[i] = res[i - 1] + res[i - 1]; - } - res - }) -} - -const MAX_M: usize = 16; -const N: usize = 64; -const MAX_MN: usize = MAX_M * N; - -/// Container struct for Bulletproofs(+) generators. -#[allow(non_snake_case)] -pub struct Generators { - pub G: Vec, - pub H: Vec, -} - -/// Generate generators as needed for Bulletproofs(+), as Monero does. -pub fn bulletproofs_generators(dst: &'static [u8]) -> Generators { - let mut res = Generators { G: Vec::with_capacity(MAX_MN), H: Vec::with_capacity(MAX_MN) }; - for i in 0 .. MAX_MN { - let i = 2 * i; - - let mut even = H().compress().to_bytes().to_vec(); - even.extend(dst); - let mut odd = even.clone(); - - write_varint(&i.try_into().unwrap(), &mut even).unwrap(); - write_varint(&(i + 1).try_into().unwrap(), &mut odd).unwrap(); - res.H.push(EdwardsPoint(hash_to_point(hash(&even)))); - res.G.push(EdwardsPoint(hash_to_point(hash(&odd)))); - } - res -} diff --git a/coins/monero/generators/src/tests/mod.rs b/coins/monero/generators/src/tests/mod.rs deleted file mode 100644 index ec208e9c..00000000 --- a/coins/monero/generators/src/tests/mod.rs +++ /dev/null @@ -1 +0,0 @@ -mod hash_to_point; diff --git a/coins/monero/generators/src/varint.rs b/coins/monero/generators/src/varint.rs deleted file mode 100644 index 2e82816e..00000000 --- a/coins/monero/generators/src/varint.rs +++ /dev/null @@ -1,16 +0,0 @@ -use std_shims::io::{self, Write}; - -const VARINT_CONTINUATION_MASK: u8 = 0b1000_0000; -pub(crate) fn write_varint(varint: &u64, w: &mut W) -> io::Result<()> { - let mut varint = *varint; - while { - let mut b = u8::try_from(varint & u64::from(!VARINT_CONTINUATION_MASK)).unwrap(); - varint >>= 7; - if varint != 0 { - b |= VARINT_CONTINUATION_MASK; - } - w.write_all(&[b])?; - varint != 0 - } {} - Ok(()) -} diff --git a/coins/monero/src/bin/reserialize_chain.rs b/coins/monero/src/bin/reserialize_chain.rs deleted file mode 100644 index f2ebfcc1..00000000 --- a/coins/monero/src/bin/reserialize_chain.rs +++ /dev/null @@ -1,321 +0,0 @@ -#[cfg(feature = "binaries")] -mod binaries { - pub(crate) use std::sync::Arc; - - pub(crate) use curve25519_dalek::{scalar::Scalar, edwards::EdwardsPoint}; - - pub(crate) use multiexp::BatchVerifier; - - pub(crate) use serde::Deserialize; - pub(crate) use serde_json::json; - - pub(crate) use monero_serai::{ - Commitment, - ringct::RctPrunable, - transaction::{Input, Transaction}, - block::Block, - rpc::{RpcError, Rpc, HttpRpc}, - }; - - pub(crate) use monero_generators::decompress_point; - - pub(crate) use tokio::task::JoinHandle; - - pub(crate) async fn check_block(rpc: Arc>, block_i: usize) { - let hash = loop { - match rpc.get_block_hash(block_i).await { - Ok(hash) => break hash, - Err(RpcError::ConnectionError(e)) => { - println!("get_block_hash ConnectionError: {e}"); - continue; - } - Err(e) => panic!("couldn't get block {block_i}'s hash: {e:?}"), - } - }; - - // TODO: Grab the JSON to also check it was deserialized correctly - #[derive(Deserialize, Debug)] - struct BlockResponse { - blob: String, - } - let res: BlockResponse = loop { - match rpc.json_rpc_call("get_block", Some(json!({ "hash": hex::encode(hash) }))).await { - Ok(res) => break res, - Err(RpcError::ConnectionError(e)) => { - println!("get_block ConnectionError: {e}"); - continue; - } - Err(e) => panic!("couldn't get block {block_i} via block.hash(): {e:?}"), - } - }; - - let blob = hex::decode(res.blob).expect("node returned non-hex block"); - let block = Block::read(&mut blob.as_slice()) - .unwrap_or_else(|e| panic!("couldn't deserialize block {block_i}: {e}")); - assert_eq!(block.hash(), hash, "hash differs"); - assert_eq!(block.serialize(), blob, "serialization differs"); - - let txs_len = 1 + block.txs.len(); - - if !block.txs.is_empty() { - #[derive(Deserialize, Debug)] - struct TransactionResponse { - tx_hash: String, - as_hex: String, - } - #[derive(Deserialize, Debug)] - struct TransactionsResponse { - #[serde(default)] - missed_tx: Vec, - txs: Vec, - } - - let mut hashes_hex = block.txs.iter().map(hex::encode).collect::>(); - let mut all_txs = vec![]; - while !hashes_hex.is_empty() { - let txs: TransactionsResponse = loop { - match rpc - .rpc_call( - "get_transactions", - Some(json!({ - "txs_hashes": hashes_hex.drain(.. hashes_hex.len().min(100)).collect::>(), - })), - ) - .await - { - Ok(txs) => break txs, - Err(RpcError::ConnectionError(e)) => { - println!("get_transactions ConnectionError: {e}"); - continue; - } - Err(e) => panic!("couldn't call get_transactions: {e:?}"), - } - }; - assert!(txs.missed_tx.is_empty()); - all_txs.extend(txs.txs); - } - - let mut batch = BatchVerifier::new(block.txs.len()); - for (tx_hash, tx_res) in block.txs.into_iter().zip(all_txs) { - assert_eq!( - tx_res.tx_hash, - hex::encode(tx_hash), - "node returned a transaction with different hash" - ); - - let tx = Transaction::read( - &mut hex::decode(&tx_res.as_hex).expect("node returned non-hex transaction").as_slice(), - ) - .expect("couldn't deserialize transaction"); - - assert_eq!( - hex::encode(tx.serialize()), - tx_res.as_hex, - "Transaction serialization was different" - ); - assert_eq!(tx.hash(), tx_hash, "Transaction hash was different"); - - if matches!(tx.rct_signatures.prunable, RctPrunable::Null) { - assert_eq!(tx.prefix.version, 1); - assert!(!tx.signatures.is_empty()); - continue; - } - - let sig_hash = tx.signature_hash(); - // Verify all proofs we support proving for - // This is due to having debug_asserts calling verify within their proving, and CLSAG - // multisig explicitly calling verify as part of its signing process - // Accordingly, making sure our signature_hash algorithm is correct is great, and further - // making sure the verification functions are valid is appreciated - match tx.rct_signatures.prunable { - RctPrunable::Null | - RctPrunable::AggregateMlsagBorromean { .. } | - RctPrunable::MlsagBorromean { .. } => {} - RctPrunable::MlsagBulletproofs { bulletproofs, .. } => { - assert!(bulletproofs.batch_verify( - &mut rand_core::OsRng, - &mut batch, - (), - &tx.rct_signatures.base.commitments - )); - } - RctPrunable::Clsag { bulletproofs, clsags, pseudo_outs } => { - assert!(bulletproofs.batch_verify( - &mut rand_core::OsRng, - &mut batch, - (), - &tx.rct_signatures.base.commitments - )); - - for (i, clsag) in clsags.into_iter().enumerate() { - let (amount, key_offsets, image) = match &tx.prefix.inputs[i] { - Input::Gen(_) => panic!("Input::Gen"), - Input::ToKey { amount, key_offsets, key_image } => (amount, key_offsets, key_image), - }; - - let mut running_sum = 0; - let mut actual_indexes = vec![]; - for offset in key_offsets { - running_sum += offset; - actual_indexes.push(running_sum); - } - - async fn get_outs( - rpc: &Rpc, - amount: u64, - indexes: &[u64], - ) -> Vec<[EdwardsPoint; 2]> { - #[derive(Deserialize, Debug)] - struct Out { - key: String, - mask: String, - } - - #[derive(Deserialize, Debug)] - struct Outs { - outs: Vec, - } - - let outs: Outs = loop { - match rpc - .rpc_call( - "get_outs", - Some(json!({ - "get_txid": true, - "outputs": indexes.iter().map(|o| json!({ - "amount": amount, - "index": o - })).collect::>() - })), - ) - .await - { - Ok(outs) => break outs, - Err(RpcError::ConnectionError(e)) => { - println!("get_outs ConnectionError: {e}"); - continue; - } - Err(e) => panic!("couldn't connect to RPC to get outs: {e:?}"), - } - }; - - let rpc_point = |point: &str| { - decompress_point( - hex::decode(point) - .expect("invalid hex for ring member") - .try_into() - .expect("invalid point len for ring member"), - ) - .expect("invalid point for ring member") - }; - - outs - .outs - .iter() - .map(|out| { - let mask = rpc_point(&out.mask); - if amount != 0 { - assert_eq!(mask, Commitment::new(Scalar::from(1u8), amount).calculate()); - } - [rpc_point(&out.key), mask] - }) - .collect() - } - - clsag - .verify( - &get_outs(&rpc, amount.unwrap_or(0), &actual_indexes).await, - image, - &pseudo_outs[i], - &sig_hash, - ) - .unwrap(); - } - } - } - } - assert!(batch.verify_vartime()); - } - - println!("Deserialized, hashed, and reserialized {block_i} with {txs_len} TXs"); - } -} - -#[cfg(feature = "binaries")] -#[tokio::main] -async fn main() { - use binaries::*; - - let args = std::env::args().collect::>(); - - // Read start block as the first arg - let mut block_i = args[1].parse::().expect("invalid start block"); - - // How many blocks to work on at once - let async_parallelism: usize = - args.get(2).unwrap_or(&"8".to_string()).parse::().expect("invalid parallelism argument"); - - // Read further args as RPC URLs - let default_nodes = vec![ - "http://xmr-node.cakewallet.com:18081".to_string(), - "https://node.sethforprivacy.com".to_string(), - ]; - let mut specified_nodes = vec![]; - { - let mut i = 0; - loop { - let Some(node) = args.get(3 + i) else { break }; - specified_nodes.push(node.clone()); - i += 1; - } - } - let nodes = if specified_nodes.is_empty() { default_nodes } else { specified_nodes }; - - let rpc = |url: String| async move { - HttpRpc::new(url.clone()) - .await - .unwrap_or_else(|_| panic!("couldn't create HttpRpc connected to {url}")) - }; - let main_rpc = rpc(nodes[0].clone()).await; - let mut rpcs = vec![]; - for i in 0 .. async_parallelism { - rpcs.push(Arc::new(rpc(nodes[i % nodes.len()].clone()).await)); - } - - let mut rpc_i = 0; - let mut handles: Vec> = vec![]; - let mut height = 0; - loop { - let new_height = main_rpc.get_height().await.expect("couldn't call get_height"); - if new_height == height { - break; - } - height = new_height; - - while block_i < height { - if handles.len() >= async_parallelism { - // Guarantee one handle is complete - handles.swap_remove(0).await.unwrap(); - - // Remove all of the finished handles - let mut i = 0; - while i < handles.len() { - if handles[i].is_finished() { - handles.swap_remove(i).await.unwrap(); - continue; - } - i += 1; - } - } - - handles.push(tokio::spawn(check_block(rpcs[rpc_i].clone(), block_i))); - rpc_i = (rpc_i + 1) % rpcs.len(); - block_i += 1; - } - } -} - -#[cfg(not(feature = "binaries"))] -fn main() { - panic!("To run binaries, please build with `--feature binaries`."); -} diff --git a/coins/monero/src/block.rs b/coins/monero/src/block.rs deleted file mode 100644 index b4e97169..00000000 --- a/coins/monero/src/block.rs +++ /dev/null @@ -1,130 +0,0 @@ -use std_shims::{ - vec::Vec, - io::{self, Read, Write}, -}; - -use crate::{ - hash, - merkle::merkle_root, - serialize::*, - transaction::{Input, Transaction}, -}; - -const CORRECT_BLOCK_HASH_202612: [u8; 32] = - hex_literal::hex!("426d16cff04c71f8b16340b722dc4010a2dd3831c22041431f772547ba6e331a"); -const EXISTING_BLOCK_HASH_202612: [u8; 32] = - hex_literal::hex!("bbd604d2ba11ba27935e006ed39c9bfdd99b76bf4a50654bc1e1e61217962698"); - -#[derive(Clone, PartialEq, Eq, Debug)] -pub struct BlockHeader { - pub major_version: u8, - pub minor_version: u8, - pub timestamp: u64, - pub previous: [u8; 32], - pub nonce: u32, -} - -impl BlockHeader { - pub fn write(&self, w: &mut W) -> io::Result<()> { - write_varint(&self.major_version, w)?; - write_varint(&self.minor_version, w)?; - write_varint(&self.timestamp, w)?; - w.write_all(&self.previous)?; - w.write_all(&self.nonce.to_le_bytes()) - } - - pub fn serialize(&self) -> Vec { - let mut serialized = vec![]; - self.write(&mut serialized).unwrap(); - serialized - } - - pub fn read(r: &mut R) -> io::Result { - Ok(BlockHeader { - major_version: read_varint(r)?, - minor_version: read_varint(r)?, - timestamp: read_varint(r)?, - previous: read_bytes(r)?, - nonce: read_bytes(r).map(u32::from_le_bytes)?, - }) - } -} - -#[derive(Clone, PartialEq, Eq, Debug)] -pub struct Block { - pub header: BlockHeader, - pub miner_tx: Transaction, - pub txs: Vec<[u8; 32]>, -} - -impl Block { - pub fn number(&self) -> Option { - match self.miner_tx.prefix.inputs.first() { - Some(Input::Gen(number)) => Some(*number), - _ => None, - } - } - - pub fn write(&self, w: &mut W) -> io::Result<()> { - self.header.write(w)?; - self.miner_tx.write(w)?; - write_varint(&self.txs.len(), w)?; - for tx in &self.txs { - w.write_all(tx)?; - } - Ok(()) - } - - fn tx_merkle_root(&self) -> [u8; 32] { - merkle_root(self.miner_tx.hash(), &self.txs) - } - - /// Serialize the block as required for the proof of work hash. - /// - /// This is distinct from the serialization required for the block hash. To get the block hash, - /// use the [`Block::hash`] function. - pub fn serialize_hashable(&self) -> Vec { - let mut blob = self.header.serialize(); - blob.extend_from_slice(&self.tx_merkle_root()); - write_varint(&(1 + u64::try_from(self.txs.len()).unwrap()), &mut blob).unwrap(); - - blob - } - - pub fn hash(&self) -> [u8; 32] { - let mut hashable = self.serialize_hashable(); - // Monero pre-appends a VarInt of the block hashing blobs length before getting the block hash - // but doesn't do this when getting the proof of work hash :) - let mut hashing_blob = Vec::with_capacity(8 + hashable.len()); - write_varint(&u64::try_from(hashable.len()).unwrap(), &mut hashing_blob).unwrap(); - hashing_blob.append(&mut hashable); - - let hash = hash(&hashing_blob); - if hash == CORRECT_BLOCK_HASH_202612 { - return EXISTING_BLOCK_HASH_202612; - }; - - hash - } - - pub fn serialize(&self) -> Vec { - let mut serialized = vec![]; - self.write(&mut serialized).unwrap(); - serialized - } - - pub fn read(r: &mut R) -> io::Result { - let header = BlockHeader::read(r)?; - - let miner_tx = Transaction::read(r)?; - if !matches!(miner_tx.prefix.inputs.as_slice(), &[Input::Gen(_)]) { - Err(io::Error::other("Miner transaction has incorrect input type."))?; - } - - Ok(Block { - header, - miner_tx, - txs: (0_usize .. read_varint(r)?).map(|_| read_bytes(r)).collect::>()?, - }) - } -} diff --git a/coins/monero/src/lib.rs b/coins/monero/src/lib.rs deleted file mode 100644 index 4e6b26d1..00000000 --- a/coins/monero/src/lib.rs +++ /dev/null @@ -1,241 +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"))] -#[macro_use] -extern crate alloc; - -use std_shims::{sync::OnceLock, io}; - -use rand_core::{RngCore, CryptoRng}; - -use zeroize::{Zeroize, ZeroizeOnDrop}; - -use sha3::{Digest, Keccak256}; - -use curve25519_dalek::{ - constants::{ED25519_BASEPOINT_TABLE, ED25519_BASEPOINT_POINT}, - scalar::Scalar, - edwards::{EdwardsPoint, VartimeEdwardsPrecomputation}, - traits::VartimePrecomputedMultiscalarMul, -}; - -pub use monero_generators::{H, decompress_point}; - -mod merkle; - -mod serialize; -use serialize::{read_byte, read_u16}; - -/// UnreducedScalar struct with functionality for recovering incorrectly reduced scalars. -mod unreduced_scalar; - -/// Ring Signature structs and functionality. -pub mod ring_signatures; - -/// RingCT structs and functionality. -pub mod ringct; -use ringct::RctType; - -/// Transaction structs. -pub mod transaction; -/// Block structs. -pub mod block; - -/// Monero daemon RPC interface. -pub mod rpc; -/// Wallet functionality, enabling scanning and sending transactions. -pub mod wallet; - -#[cfg(test)] -mod tests; - -pub const DEFAULT_LOCK_WINDOW: usize = 10; -pub const COINBASE_LOCK_WINDOW: usize = 60; -pub const BLOCK_TIME: usize = 120; - -static INV_EIGHT_CELL: OnceLock = OnceLock::new(); -#[allow(non_snake_case)] -pub(crate) fn INV_EIGHT() -> Scalar { - *INV_EIGHT_CELL.get_or_init(|| Scalar::from(8u8).invert()) -} - -static BASEPOINT_PRECOMP_CELL: OnceLock = OnceLock::new(); -#[allow(non_snake_case)] -pub(crate) fn BASEPOINT_PRECOMP() -> &'static VartimeEdwardsPrecomputation { - BASEPOINT_PRECOMP_CELL - .get_or_init(|| VartimeEdwardsPrecomputation::new([ED25519_BASEPOINT_POINT])) -} - -/// Monero protocol version. -/// -/// v15 is omitted as v15 was simply v14 and v16 being active at the same time, with regards to the -/// transactions supported. Accordingly, v16 should be used during v15. -#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)] -#[allow(non_camel_case_types)] -pub enum Protocol { - v14, - v16, - Custom { - ring_len: usize, - bp_plus: bool, - optimal_rct_type: RctType, - view_tags: bool, - v16_fee: bool, - }, -} - -impl Protocol { - /// Amount of ring members under this protocol version. - pub fn ring_len(&self) -> usize { - match self { - Protocol::v14 => 11, - Protocol::v16 => 16, - Protocol::Custom { ring_len, .. } => *ring_len, - } - } - - /// Whether or not the specified version uses Bulletproofs or Bulletproofs+. - /// - /// This method will likely be reworked when versions not using Bulletproofs at all are added. - pub fn bp_plus(&self) -> bool { - match self { - Protocol::v14 => false, - Protocol::v16 => true, - Protocol::Custom { bp_plus, .. } => *bp_plus, - } - } - - // TODO: Make this an Option when we support pre-RCT protocols - pub fn optimal_rct_type(&self) -> RctType { - match self { - Protocol::v14 => RctType::Clsag, - Protocol::v16 => RctType::BulletproofsPlus, - Protocol::Custom { optimal_rct_type, .. } => *optimal_rct_type, - } - } - - /// Whether or not the specified version uses view tags. - pub fn view_tags(&self) -> bool { - match self { - Protocol::v14 => false, - Protocol::v16 => true, - Protocol::Custom { view_tags, .. } => *view_tags, - } - } - - /// Whether or not the specified version uses the fee algorithm from Monero - /// hard fork version 16 (released in v18 binaries). - pub fn v16_fee(&self) -> bool { - match self { - Protocol::v14 => false, - Protocol::v16 => true, - Protocol::Custom { v16_fee, .. } => *v16_fee, - } - } - - pub(crate) fn write(&self, w: &mut W) -> io::Result<()> { - match self { - Protocol::v14 => w.write_all(&[0, 14]), - Protocol::v16 => w.write_all(&[0, 16]), - Protocol::Custom { ring_len, bp_plus, optimal_rct_type, view_tags, v16_fee } => { - // Custom, version 0 - w.write_all(&[1, 0])?; - w.write_all(&u16::try_from(*ring_len).unwrap().to_le_bytes())?; - w.write_all(&[u8::from(*bp_plus)])?; - w.write_all(&[optimal_rct_type.to_byte()])?; - w.write_all(&[u8::from(*view_tags)])?; - w.write_all(&[u8::from(*v16_fee)]) - } - } - } - - pub(crate) fn read(r: &mut R) -> io::Result { - Ok(match read_byte(r)? { - // Monero protocol - 0 => match read_byte(r)? { - 14 => Protocol::v14, - 16 => Protocol::v16, - _ => Err(io::Error::other("unrecognized monero protocol"))?, - }, - // Custom - 1 => match read_byte(r)? { - 0 => Protocol::Custom { - ring_len: read_u16(r)?.into(), - bp_plus: match read_byte(r)? { - 0 => false, - 1 => true, - _ => Err(io::Error::other("invalid bool serialization"))?, - }, - optimal_rct_type: RctType::from_byte(read_byte(r)?) - .ok_or_else(|| io::Error::other("invalid RctType serialization"))?, - view_tags: match read_byte(r)? { - 0 => false, - 1 => true, - _ => Err(io::Error::other("invalid bool serialization"))?, - }, - v16_fee: match read_byte(r)? { - 0 => false, - 1 => true, - _ => Err(io::Error::other("invalid bool serialization"))?, - }, - }, - _ => Err(io::Error::other("unrecognized custom protocol serialization"))?, - }, - _ => Err(io::Error::other("unrecognized protocol serialization"))?, - }) - } -} - -/// Transparent structure representing a Pedersen commitment's contents. -#[allow(non_snake_case)] -#[derive(Clone, PartialEq, Eq, Zeroize, ZeroizeOnDrop)] -pub struct Commitment { - pub mask: Scalar, - pub amount: u64, -} - -impl core::fmt::Debug for Commitment { - fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> { - fmt.debug_struct("Commitment").field("amount", &self.amount).finish_non_exhaustive() - } -} - -impl Commitment { - /// A commitment to zero, defined with a mask of 1 (as to not be the identity). - pub fn zero() -> Commitment { - Commitment { mask: Scalar::ONE, amount: 0 } - } - - pub fn new(mask: Scalar, amount: u64) -> Commitment { - Commitment { mask, amount } - } - - /// Calculate a Pedersen commitment, as a point, from the transparent structure. - pub fn calculate(&self) -> EdwardsPoint { - (&self.mask * ED25519_BASEPOINT_TABLE) + (Scalar::from(self.amount) * H()) - } -} - -/// Support generating a random scalar using a modern rand, as dalek's is notoriously dated. -pub fn random_scalar(rng: &mut R) -> Scalar { - let mut r = [0; 64]; - rng.fill_bytes(&mut r); - Scalar::from_bytes_mod_order_wide(&r) -} - -pub(crate) fn hash(data: &[u8]) -> [u8; 32] { - Keccak256::digest(data).into() -} - -/// Hash the provided data to a scalar via keccak256(data) % l. -pub fn hash_to_scalar(data: &[u8]) -> Scalar { - let scalar = Scalar::from_bytes_mod_order(hash(data)); - // Monero will explicitly error in this case - // This library acknowledges its practical impossibility of it occurring, and doesn't bother to - // code in logic to handle it. That said, if it ever occurs, something must happen in order to - // not generate/verify a proof we believe to be valid when it isn't - assert!(scalar != Scalar::ZERO, "ZERO HASH: {data:?}"); - scalar -} diff --git a/coins/monero/src/ring_signatures.rs b/coins/monero/src/ring_signatures.rs deleted file mode 100644 index 72d30b0e..00000000 --- a/coins/monero/src/ring_signatures.rs +++ /dev/null @@ -1,72 +0,0 @@ -use std_shims::{ - io::{self, *}, - vec::Vec, -}; - -use zeroize::Zeroize; - -use curve25519_dalek::{EdwardsPoint, Scalar}; - -use monero_generators::hash_to_point; - -use crate::{serialize::*, hash_to_scalar}; - -#[derive(Clone, PartialEq, Eq, Debug, Zeroize)] -pub struct Signature { - c: Scalar, - r: Scalar, -} - -impl Signature { - pub fn write(&self, w: &mut W) -> io::Result<()> { - write_scalar(&self.c, w)?; - write_scalar(&self.r, w)?; - Ok(()) - } - - pub fn read(r: &mut R) -> io::Result { - Ok(Signature { c: read_scalar(r)?, r: read_scalar(r)? }) - } -} - -#[derive(Clone, PartialEq, Eq, Debug, Zeroize)] -pub struct RingSignature { - sigs: Vec, -} - -impl RingSignature { - pub fn write(&self, w: &mut W) -> io::Result<()> { - for sig in &self.sigs { - sig.write(w)?; - } - Ok(()) - } - - pub fn read(members: usize, r: &mut R) -> io::Result { - Ok(RingSignature { sigs: read_raw_vec(Signature::read, members, r)? }) - } - - pub fn verify(&self, msg: &[u8; 32], ring: &[EdwardsPoint], key_image: &EdwardsPoint) -> bool { - if ring.len() != self.sigs.len() { - return false; - } - - let mut buf = Vec::with_capacity(32 + (32 * 2 * ring.len())); - buf.extend_from_slice(msg); - - let mut sum = Scalar::ZERO; - - for (ring_member, sig) in ring.iter().zip(&self.sigs) { - #[allow(non_snake_case)] - let Li = EdwardsPoint::vartime_double_scalar_mul_basepoint(&sig.c, ring_member, &sig.r); - buf.extend_from_slice(Li.compress().as_bytes()); - #[allow(non_snake_case)] - let Ri = (sig.r * hash_to_point(ring_member.compress().to_bytes())) + (sig.c * key_image); - buf.extend_from_slice(Ri.compress().as_bytes()); - - sum += sig.c; - } - - sum == hash_to_scalar(&buf) - } -} diff --git a/coins/monero/src/ringct/bulletproofs/core.rs b/coins/monero/src/ringct/bulletproofs/core.rs deleted file mode 100644 index 6c264e00..00000000 --- a/coins/monero/src/ringct/bulletproofs/core.rs +++ /dev/null @@ -1,151 +0,0 @@ -use std_shims::{vec::Vec, sync::OnceLock}; - -use rand_core::{RngCore, CryptoRng}; - -use subtle::{Choice, ConditionallySelectable}; - -use curve25519_dalek::edwards::EdwardsPoint as DalekPoint; - -use group::{ff::Field, Group}; -use dalek_ff_group::{Scalar, EdwardsPoint}; - -use multiexp::multiexp as multiexp_const; - -pub(crate) use monero_generators::Generators; - -use crate::{INV_EIGHT as DALEK_INV_EIGHT, H as DALEK_H, Commitment, hash_to_scalar as dalek_hash}; -pub(crate) use crate::ringct::bulletproofs::scalar_vector::*; - -#[inline] -pub(crate) fn INV_EIGHT() -> Scalar { - Scalar(DALEK_INV_EIGHT()) -} - -#[inline] -pub(crate) fn H() -> EdwardsPoint { - EdwardsPoint(DALEK_H()) -} - -pub(crate) fn hash_to_scalar(data: &[u8]) -> Scalar { - Scalar(dalek_hash(data)) -} - -// Components common between variants -pub(crate) const MAX_M: usize = 16; -pub(crate) const LOG_N: usize = 6; // 2 << 6 == N -pub(crate) const N: usize = 64; - -pub(crate) fn prove_multiexp(pairs: &[(Scalar, EdwardsPoint)]) -> EdwardsPoint { - multiexp_const(pairs) * INV_EIGHT() -} - -pub(crate) fn vector_exponent( - generators: &Generators, - a: &ScalarVector, - b: &ScalarVector, -) -> EdwardsPoint { - debug_assert_eq!(a.len(), b.len()); - (a * &generators.G[.. a.len()]) + (b * &generators.H[.. b.len()]) -} - -pub(crate) fn hash_cache(cache: &mut Scalar, mash: &[[u8; 32]]) -> Scalar { - let slice = - &[cache.to_bytes().as_ref(), mash.iter().copied().flatten().collect::>().as_ref()] - .concat(); - *cache = hash_to_scalar(slice); - *cache -} - -pub(crate) fn MN(outputs: usize) -> (usize, usize, usize) { - let mut logM = 0; - let mut M; - while { - M = 1 << logM; - (M <= MAX_M) && (M < outputs) - } { - logM += 1; - } - - (logM + LOG_N, M, M * N) -} - -pub(crate) fn bit_decompose(commitments: &[Commitment]) -> (ScalarVector, ScalarVector) { - let (_, M, MN) = MN(commitments.len()); - - let sv = commitments.iter().map(|c| Scalar::from(c.amount)).collect::>(); - let mut aL = ScalarVector::new(MN); - let mut aR = ScalarVector::new(MN); - - for j in 0 .. M { - for i in (0 .. N).rev() { - let bit = - if j < sv.len() { Choice::from((sv[j][i / 8] >> (i % 8)) & 1) } else { Choice::from(0) }; - aL.0[(j * N) + i] = Scalar::conditional_select(&Scalar::ZERO, &Scalar::ONE, bit); - aR.0[(j * N) + i] = Scalar::conditional_select(&-Scalar::ONE, &Scalar::ZERO, bit); - } - } - - (aL, aR) -} - -pub(crate) fn hash_commitments>( - commitments: C, -) -> (Scalar, Vec) { - let V = commitments.into_iter().map(|c| EdwardsPoint(c) * INV_EIGHT()).collect::>(); - (hash_to_scalar(&V.iter().flat_map(|V| V.compress().to_bytes()).collect::>()), V) -} - -pub(crate) fn alpha_rho( - rng: &mut R, - generators: &Generators, - aL: &ScalarVector, - aR: &ScalarVector, -) -> (Scalar, EdwardsPoint) { - let ar = Scalar::random(rng); - (ar, (vector_exponent(generators, aL, aR) + (EdwardsPoint::generator() * ar)) * INV_EIGHT()) -} - -pub(crate) fn LR_statements( - a: &ScalarVector, - G_i: &[EdwardsPoint], - b: &ScalarVector, - H_i: &[EdwardsPoint], - cL: Scalar, - U: EdwardsPoint, -) -> Vec<(Scalar, EdwardsPoint)> { - let mut res = a - .0 - .iter() - .copied() - .zip(G_i.iter().copied()) - .chain(b.0.iter().copied().zip(H_i.iter().copied())) - .collect::>(); - res.push((cL, U)); - res -} - -static TWO_N_CELL: OnceLock = OnceLock::new(); -pub(crate) fn TWO_N() -> &'static ScalarVector { - TWO_N_CELL.get_or_init(|| ScalarVector::powers(Scalar::from(2u8), N)) -} - -pub(crate) fn challenge_products(w: &[Scalar], winv: &[Scalar]) -> Vec { - let mut products = vec![Scalar::ZERO; 1 << w.len()]; - products[0] = winv[0]; - products[1] = w[0]; - for j in 1 .. w.len() { - let mut slots = (1 << (j + 1)) - 1; - while slots > 0 { - products[slots] = products[slots / 2] * w[j]; - products[slots - 1] = products[slots / 2] * winv[j]; - slots = slots.saturating_sub(2); - } - } - - // Sanity check as if the above failed to populate, it'd be critical - for w in &products { - debug_assert!(!bool::from(w.is_zero())); - } - - products -} diff --git a/coins/monero/src/ringct/bulletproofs/mod.rs b/coins/monero/src/ringct/bulletproofs/mod.rs deleted file mode 100644 index ce9f7492..00000000 --- a/coins/monero/src/ringct/bulletproofs/mod.rs +++ /dev/null @@ -1,229 +0,0 @@ -#![allow(non_snake_case)] - -use std_shims::{ - vec::Vec, - io::{self, Read, Write}, -}; - -use rand_core::{RngCore, CryptoRng}; - -use zeroize::{Zeroize, Zeroizing}; - -use curve25519_dalek::edwards::EdwardsPoint; -use multiexp::BatchVerifier; - -use crate::{Commitment, wallet::TransactionError, serialize::*}; - -pub(crate) mod scalar_vector; -pub(crate) mod core; -use self::core::LOG_N; - -pub(crate) mod original; -use self::original::OriginalStruct; - -pub(crate) mod plus; -use self::plus::*; - -pub(crate) const MAX_OUTPUTS: usize = self::core::MAX_M; - -/// Bulletproofs enum, supporting the original and plus formulations. -#[allow(clippy::large_enum_variant)] -#[derive(Clone, PartialEq, Eq, Debug)] -pub enum Bulletproofs { - Original(OriginalStruct), - Plus(AggregateRangeProof), -} - -impl Bulletproofs { - fn bp_fields(plus: bool) -> usize { - if plus { - 6 - } else { - 9 - } - } - - // https://github.com/monero-project/monero/blob/94e67bf96bbc010241f29ada6abc89f49a81759c/ - // src/cryptonote_basic/cryptonote_format_utils.cpp#L106-L124 - pub(crate) fn calculate_bp_clawback(plus: bool, n_outputs: usize) -> (usize, usize) { - #[allow(non_snake_case)] - let mut LR_len = 0; - let mut n_padded_outputs = 1; - while n_padded_outputs < n_outputs { - LR_len += 1; - n_padded_outputs = 1 << LR_len; - } - LR_len += LOG_N; - - let mut bp_clawback = 0; - if n_padded_outputs > 2 { - let fields = Bulletproofs::bp_fields(plus); - let base = ((fields + (2 * (LOG_N + 1))) * 32) / 2; - let size = (fields + (2 * LR_len)) * 32; - bp_clawback = ((base * n_padded_outputs) - size) * 4 / 5; - } - - (bp_clawback, LR_len) - } - - pub(crate) fn fee_weight(plus: bool, outputs: usize) -> usize { - #[allow(non_snake_case)] - let (bp_clawback, LR_len) = Bulletproofs::calculate_bp_clawback(plus, outputs); - 32 * (Bulletproofs::bp_fields(plus) + (2 * LR_len)) + 2 + bp_clawback - } - - /// Prove the list of commitments are within [0 .. 2^64). - pub fn prove( - rng: &mut R, - outputs: &[Commitment], - plus: bool, - ) -> Result { - if outputs.is_empty() { - Err(TransactionError::NoOutputs)?; - } - if outputs.len() > MAX_OUTPUTS { - Err(TransactionError::TooManyOutputs)?; - } - Ok(if !plus { - Bulletproofs::Original(OriginalStruct::prove(rng, outputs)) - } else { - use dalek_ff_group::EdwardsPoint as DfgPoint; - Bulletproofs::Plus( - AggregateRangeStatement::new(outputs.iter().map(|com| DfgPoint(com.calculate())).collect()) - .unwrap() - .prove(rng, &Zeroizing::new(AggregateRangeWitness::new(outputs.to_vec()).unwrap())) - .unwrap(), - ) - }) - } - - /// Verify the given Bulletproofs. - #[must_use] - pub fn verify(&self, rng: &mut R, commitments: &[EdwardsPoint]) -> bool { - match self { - Bulletproofs::Original(bp) => bp.verify(rng, commitments), - Bulletproofs::Plus(bp) => { - let mut verifier = BatchVerifier::new(1); - // If this commitment is torsioned (which is allowed), this won't be a well-formed - // dfg::EdwardsPoint (expected to be of prime-order) - // The actual BP+ impl will perform a torsion clear though, making this safe - // TODO: Have AggregateRangeStatement take in dalek EdwardsPoint for clarity on this - let Some(statement) = AggregateRangeStatement::new( - commitments.iter().map(|c| dalek_ff_group::EdwardsPoint(*c)).collect(), - ) else { - return false; - }; - if !statement.verify(rng, &mut verifier, (), bp.clone()) { - return false; - } - verifier.verify_vartime() - } - } - } - - /// Accumulate the verification for the given Bulletproofs into the specified BatchVerifier. - /// Returns false if the Bulletproofs aren't sane, without mutating the BatchVerifier. - /// Returns true if the Bulletproofs are sane, regardless of their validity. - #[must_use] - pub fn batch_verify( - &self, - rng: &mut R, - verifier: &mut BatchVerifier, - id: ID, - commitments: &[EdwardsPoint], - ) -> bool { - match self { - Bulletproofs::Original(bp) => bp.batch_verify(rng, verifier, id, commitments), - Bulletproofs::Plus(bp) => { - let Some(statement) = AggregateRangeStatement::new( - commitments.iter().map(|c| dalek_ff_group::EdwardsPoint(*c)).collect(), - ) else { - return false; - }; - statement.verify(rng, verifier, id, bp.clone()) - } - } - } - - fn write_core io::Result<()>>( - &self, - w: &mut W, - specific_write_vec: F, - ) -> io::Result<()> { - match self { - Bulletproofs::Original(bp) => { - write_point(&bp.A, w)?; - write_point(&bp.S, w)?; - write_point(&bp.T1, w)?; - write_point(&bp.T2, w)?; - write_scalar(&bp.taux, w)?; - write_scalar(&bp.mu, w)?; - specific_write_vec(&bp.L, w)?; - specific_write_vec(&bp.R, w)?; - write_scalar(&bp.a, w)?; - write_scalar(&bp.b, w)?; - write_scalar(&bp.t, w) - } - - Bulletproofs::Plus(bp) => { - write_point(&bp.A.0, w)?; - write_point(&bp.wip.A.0, w)?; - write_point(&bp.wip.B.0, w)?; - write_scalar(&bp.wip.r_answer.0, w)?; - write_scalar(&bp.wip.s_answer.0, w)?; - write_scalar(&bp.wip.delta_answer.0, w)?; - specific_write_vec(&bp.wip.L.iter().copied().map(|L| L.0).collect::>(), w)?; - specific_write_vec(&bp.wip.R.iter().copied().map(|R| R.0).collect::>(), w) - } - } - } - - pub(crate) fn signature_write(&self, w: &mut W) -> io::Result<()> { - self.write_core(w, |points, w| write_raw_vec(write_point, points, w)) - } - - pub fn write(&self, w: &mut W) -> io::Result<()> { - self.write_core(w, |points, w| write_vec(write_point, points, w)) - } - - pub fn serialize(&self) -> Vec { - let mut serialized = vec![]; - self.write(&mut serialized).unwrap(); - serialized - } - - /// Read Bulletproofs. - pub fn read(r: &mut R) -> io::Result { - Ok(Bulletproofs::Original(OriginalStruct { - A: read_point(r)?, - S: read_point(r)?, - T1: read_point(r)?, - T2: read_point(r)?, - taux: read_scalar(r)?, - mu: read_scalar(r)?, - L: read_vec(read_point, r)?, - R: read_vec(read_point, r)?, - a: read_scalar(r)?, - b: read_scalar(r)?, - t: read_scalar(r)?, - })) - } - - /// Read Bulletproofs+. - pub fn read_plus(r: &mut R) -> io::Result { - use dalek_ff_group::{Scalar as DfgScalar, EdwardsPoint as DfgPoint}; - - Ok(Bulletproofs::Plus(AggregateRangeProof { - A: DfgPoint(read_point(r)?), - wip: WipProof { - A: DfgPoint(read_point(r)?), - B: DfgPoint(read_point(r)?), - r_answer: DfgScalar(read_scalar(r)?), - s_answer: DfgScalar(read_scalar(r)?), - delta_answer: DfgScalar(read_scalar(r)?), - L: read_vec(read_point, r)?.into_iter().map(DfgPoint).collect(), - R: read_vec(read_point, r)?.into_iter().map(DfgPoint).collect(), - }, - })) - } -} diff --git a/coins/monero/src/ringct/bulletproofs/original.rs b/coins/monero/src/ringct/bulletproofs/original.rs deleted file mode 100644 index 0e841080..00000000 --- a/coins/monero/src/ringct/bulletproofs/original.rs +++ /dev/null @@ -1,322 +0,0 @@ -use std_shims::{vec::Vec, sync::OnceLock}; - -use rand_core::{RngCore, CryptoRng}; - -use zeroize::Zeroize; - -use curve25519_dalek::{scalar::Scalar as DalekScalar, edwards::EdwardsPoint as DalekPoint}; - -use group::{ff::Field, Group}; -use dalek_ff_group::{ED25519_BASEPOINT_POINT as G, Scalar, EdwardsPoint}; - -use multiexp::{BatchVerifier, multiexp}; - -use crate::{Commitment, ringct::bulletproofs::core::*}; - -include!(concat!(env!("OUT_DIR"), "/generators.rs")); - -static IP12_CELL: OnceLock = OnceLock::new(); -pub(crate) fn IP12() -> Scalar { - *IP12_CELL.get_or_init(|| ScalarVector(vec![Scalar::ONE; N]).inner_product(TWO_N())) -} - -pub(crate) fn hadamard_fold( - l: &[EdwardsPoint], - r: &[EdwardsPoint], - a: Scalar, - b: Scalar, -) -> Vec { - let mut res = Vec::with_capacity(l.len() / 2); - for i in 0 .. l.len() { - res.push(multiexp(&[(a, l[i]), (b, r[i])])); - } - res -} - -#[derive(Clone, PartialEq, Eq, Debug)] -pub struct OriginalStruct { - pub(crate) A: DalekPoint, - pub(crate) S: DalekPoint, - pub(crate) T1: DalekPoint, - pub(crate) T2: DalekPoint, - pub(crate) taux: DalekScalar, - pub(crate) mu: DalekScalar, - pub(crate) L: Vec, - pub(crate) R: Vec, - pub(crate) a: DalekScalar, - pub(crate) b: DalekScalar, - pub(crate) t: DalekScalar, -} - -impl OriginalStruct { - pub(crate) fn prove( - rng: &mut R, - commitments: &[Commitment], - ) -> OriginalStruct { - let (logMN, M, MN) = MN(commitments.len()); - - let (aL, aR) = bit_decompose(commitments); - let commitments_points = commitments.iter().map(Commitment::calculate).collect::>(); - let (mut cache, _) = hash_commitments(commitments_points.clone()); - - let (sL, sR) = - ScalarVector((0 .. (MN * 2)).map(|_| Scalar::random(&mut *rng)).collect::>()).split(); - - let generators = GENERATORS(); - let (mut alpha, A) = alpha_rho(&mut *rng, generators, &aL, &aR); - let (mut rho, S) = alpha_rho(&mut *rng, generators, &sL, &sR); - - let y = hash_cache(&mut cache, &[A.compress().to_bytes(), S.compress().to_bytes()]); - let mut cache = hash_to_scalar(&y.to_bytes()); - let z = cache; - - let l0 = aL - z; - let l1 = sL; - - let mut zero_twos = Vec::with_capacity(MN); - let zpow = ScalarVector::powers(z, M + 2); - for j in 0 .. M { - for i in 0 .. N { - zero_twos.push(zpow[j + 2] * TWO_N()[i]); - } - } - - let yMN = ScalarVector::powers(y, MN); - let r0 = ((aR + z) * &yMN) + &ScalarVector(zero_twos); - let r1 = yMN * &sR; - - let (T1, T2, x, mut taux) = { - let t1 = l0.clone().inner_product(&r1) + r0.clone().inner_product(&l1); - let t2 = l1.clone().inner_product(&r1); - - let mut tau1 = Scalar::random(&mut *rng); - let mut tau2 = Scalar::random(&mut *rng); - - let T1 = prove_multiexp(&[(t1, H()), (tau1, EdwardsPoint::generator())]); - let T2 = prove_multiexp(&[(t2, H()), (tau2, EdwardsPoint::generator())]); - - let x = - hash_cache(&mut cache, &[z.to_bytes(), T1.compress().to_bytes(), T2.compress().to_bytes()]); - - let taux = (tau2 * (x * x)) + (tau1 * x); - - tau1.zeroize(); - tau2.zeroize(); - (T1, T2, x, taux) - }; - - let mu = (x * rho) + alpha; - alpha.zeroize(); - rho.zeroize(); - - for (i, gamma) in commitments.iter().map(|c| Scalar(c.mask)).enumerate() { - taux += zpow[i + 2] * gamma; - } - - let l = l0 + &(l1 * x); - let r = r0 + &(r1 * x); - - let t = l.clone().inner_product(&r); - - let x_ip = - hash_cache(&mut cache, &[x.to_bytes(), taux.to_bytes(), mu.to_bytes(), t.to_bytes()]); - - let mut a = l; - let mut b = r; - - let yinv = y.invert().unwrap(); - let yinvpow = ScalarVector::powers(yinv, MN); - - let mut G_proof = generators.G[.. a.len()].to_vec(); - let mut H_proof = generators.H[.. a.len()].to_vec(); - H_proof.iter_mut().zip(yinvpow.0.iter()).for_each(|(this_H, yinvpow)| *this_H *= yinvpow); - let U = H() * x_ip; - - let mut L = Vec::with_capacity(logMN); - let mut R = Vec::with_capacity(logMN); - - while a.len() != 1 { - let (aL, aR) = a.split(); - let (bL, bR) = b.split(); - - let cL = aL.clone().inner_product(&bR); - let cR = aR.clone().inner_product(&bL); - - let (G_L, G_R) = G_proof.split_at(aL.len()); - let (H_L, H_R) = H_proof.split_at(aL.len()); - - let L_i = prove_multiexp(&LR_statements(&aL, G_R, &bR, H_L, cL, U)); - let R_i = prove_multiexp(&LR_statements(&aR, G_L, &bL, H_R, cR, U)); - L.push(L_i); - R.push(R_i); - - let w = hash_cache(&mut cache, &[L_i.compress().to_bytes(), R_i.compress().to_bytes()]); - let winv = w.invert().unwrap(); - - a = (aL * w) + &(aR * winv); - b = (bL * winv) + &(bR * w); - - if a.len() != 1 { - G_proof = hadamard_fold(G_L, G_R, winv, w); - H_proof = hadamard_fold(H_L, H_R, w, winv); - } - } - - let res = OriginalStruct { - A: *A, - S: *S, - T1: *T1, - T2: *T2, - taux: *taux, - mu: *mu, - L: L.drain(..).map(|L| *L).collect(), - R: R.drain(..).map(|R| *R).collect(), - a: *a[0], - b: *b[0], - t: *t, - }; - debug_assert!(res.verify(rng, &commitments_points)); - res - } - - #[must_use] - fn verify_core( - &self, - rng: &mut R, - verifier: &mut BatchVerifier, - id: ID, - commitments: &[DalekPoint], - ) -> bool { - // Verify commitments are valid - if commitments.is_empty() || (commitments.len() > MAX_M) { - return false; - } - - // Verify L and R are properly sized - if self.L.len() != self.R.len() { - return false; - } - - let (logMN, M, MN) = MN(commitments.len()); - if self.L.len() != logMN { - return false; - } - - // Rebuild all challenges - let (mut cache, commitments) = hash_commitments(commitments.iter().copied()); - let y = hash_cache(&mut cache, &[self.A.compress().to_bytes(), self.S.compress().to_bytes()]); - - let z = hash_to_scalar(&y.to_bytes()); - cache = z; - - let x = hash_cache( - &mut cache, - &[z.to_bytes(), self.T1.compress().to_bytes(), self.T2.compress().to_bytes()], - ); - - let x_ip = hash_cache( - &mut cache, - &[x.to_bytes(), self.taux.to_bytes(), self.mu.to_bytes(), self.t.to_bytes()], - ); - - let mut w = Vec::with_capacity(logMN); - let mut winv = Vec::with_capacity(logMN); - for (L, R) in self.L.iter().zip(&self.R) { - w.push(hash_cache(&mut cache, &[L.compress().to_bytes(), R.compress().to_bytes()])); - winv.push(cache.invert().unwrap()); - } - - // Convert the proof from * INV_EIGHT to its actual form - let normalize = |point: &DalekPoint| EdwardsPoint(point.mul_by_cofactor()); - - let L = self.L.iter().map(normalize).collect::>(); - let R = self.R.iter().map(normalize).collect::>(); - let T1 = normalize(&self.T1); - let T2 = normalize(&self.T2); - let A = normalize(&self.A); - let S = normalize(&self.S); - - let commitments = commitments.iter().map(EdwardsPoint::mul_by_cofactor).collect::>(); - - // Verify it - let mut proof = Vec::with_capacity(4 + commitments.len()); - - let zpow = ScalarVector::powers(z, M + 3); - let ip1y = ScalarVector::powers(y, M * N).sum(); - let mut k = -(zpow[2] * ip1y); - for j in 1 ..= M { - k -= zpow[j + 2] * IP12(); - } - let y1 = Scalar(self.t) - ((z * ip1y) + k); - proof.push((-y1, H())); - - proof.push((-Scalar(self.taux), G)); - - for (j, commitment) in commitments.iter().enumerate() { - proof.push((zpow[j + 2], *commitment)); - } - - proof.push((x, T1)); - proof.push((x * x, T2)); - verifier.queue(&mut *rng, id, proof); - - proof = Vec::with_capacity(4 + (2 * (MN + logMN))); - let z3 = (Scalar(self.t) - (Scalar(self.a) * Scalar(self.b))) * x_ip; - proof.push((z3, H())); - proof.push((-Scalar(self.mu), G)); - - proof.push((Scalar::ONE, A)); - proof.push((x, S)); - - { - let ypow = ScalarVector::powers(y, MN); - let yinv = y.invert().unwrap(); - let yinvpow = ScalarVector::powers(yinv, MN); - - let w_cache = challenge_products(&w, &winv); - - let generators = GENERATORS(); - for i in 0 .. MN { - let g = (Scalar(self.a) * w_cache[i]) + z; - proof.push((-g, generators.G[i])); - - let mut h = Scalar(self.b) * yinvpow[i] * w_cache[(!i) & (MN - 1)]; - h -= ((zpow[(i / N) + 2] * TWO_N()[i % N]) + (z * ypow[i])) * yinvpow[i]; - proof.push((-h, generators.H[i])); - } - } - - for i in 0 .. logMN { - proof.push((w[i] * w[i], L[i])); - proof.push((winv[i] * winv[i], R[i])); - } - verifier.queue(rng, id, proof); - - true - } - - #[must_use] - pub(crate) fn verify( - &self, - rng: &mut R, - commitments: &[DalekPoint], - ) -> bool { - let mut verifier = BatchVerifier::new(1); - if self.verify_core(rng, &mut verifier, (), commitments) { - verifier.verify_vartime() - } else { - false - } - } - - #[must_use] - pub(crate) fn batch_verify( - &self, - rng: &mut R, - verifier: &mut BatchVerifier, - id: ID, - commitments: &[DalekPoint], - ) -> bool { - self.verify_core(rng, verifier, id, commitments) - } -} diff --git a/coins/monero/src/ringct/bulletproofs/plus/transcript.rs b/coins/monero/src/ringct/bulletproofs/plus/transcript.rs deleted file mode 100644 index 2108013b..00000000 --- a/coins/monero/src/ringct/bulletproofs/plus/transcript.rs +++ /dev/null @@ -1,24 +0,0 @@ -use std_shims::{sync::OnceLock, vec::Vec}; - -use dalek_ff_group::{Scalar, EdwardsPoint}; - -use monero_generators::{hash_to_point as raw_hash_to_point}; -use crate::{hash, hash_to_scalar as dalek_hash}; - -// Monero starts BP+ transcripts with the following constant. -static TRANSCRIPT_CELL: OnceLock<[u8; 32]> = OnceLock::new(); -pub(crate) fn TRANSCRIPT() -> [u8; 32] { - // Why this uses a hash_to_point is completely unknown. - *TRANSCRIPT_CELL - .get_or_init(|| raw_hash_to_point(hash(b"bulletproof_plus_transcript")).compress().to_bytes()) -} - -pub(crate) fn hash_to_scalar(data: &[u8]) -> Scalar { - Scalar(dalek_hash(data)) -} - -pub(crate) fn initial_transcript(commitments: core::slice::Iter<'_, EdwardsPoint>) -> Scalar { - let commitments_hash = - hash_to_scalar(&commitments.flat_map(|V| V.compress().to_bytes()).collect::>()); - hash_to_scalar(&[TRANSCRIPT().as_ref(), &commitments_hash.to_bytes()].concat()) -} diff --git a/coins/monero/src/ringct/hash_to_point.rs b/coins/monero/src/ringct/hash_to_point.rs deleted file mode 100644 index a36b6ee7..00000000 --- a/coins/monero/src/ringct/hash_to_point.rs +++ /dev/null @@ -1,8 +0,0 @@ -use curve25519_dalek::edwards::EdwardsPoint; - -pub use monero_generators::{hash_to_point as raw_hash_to_point}; - -/// Monero's hash to point function, as named `ge_fromfe_frombytes_vartime`. -pub fn hash_to_point(key: &EdwardsPoint) -> EdwardsPoint { - raw_hash_to_point(key.compress().to_bytes()) -} diff --git a/coins/monero/src/ringct/mod.rs b/coins/monero/src/ringct/mod.rs deleted file mode 100644 index bcd7f0c8..00000000 --- a/coins/monero/src/ringct/mod.rs +++ /dev/null @@ -1,400 +0,0 @@ -use core::ops::Deref; -use std_shims::{ - vec::Vec, - io::{self, Read, Write}, -}; - -use zeroize::{Zeroize, Zeroizing}; - -use curve25519_dalek::{constants::ED25519_BASEPOINT_TABLE, scalar::Scalar, edwards::EdwardsPoint}; - -pub(crate) mod hash_to_point; -pub use hash_to_point::{raw_hash_to_point, hash_to_point}; - -/// MLSAG struct, along with verifying functionality. -pub mod mlsag; -/// CLSAG struct, along with signing and verifying functionality. -pub mod clsag; -/// BorromeanRange struct, along with verifying functionality. -pub mod borromean; -/// Bulletproofs(+) structs, along with proving and verifying functionality. -pub mod bulletproofs; - -use crate::{ - Protocol, - serialize::*, - ringct::{mlsag::Mlsag, clsag::Clsag, borromean::BorromeanRange, bulletproofs::Bulletproofs}, -}; - -/// Generate a key image for a given key. Defined as `x * hash_to_point(xG)`. -pub fn generate_key_image(secret: &Zeroizing) -> EdwardsPoint { - hash_to_point(&(ED25519_BASEPOINT_TABLE * secret.deref())) * secret.deref() -} - -#[derive(Clone, PartialEq, Eq, Debug)] -pub enum EncryptedAmount { - Original { mask: [u8; 32], amount: [u8; 32] }, - Compact { amount: [u8; 8] }, -} - -impl EncryptedAmount { - pub fn read(compact: bool, r: &mut R) -> io::Result { - Ok(if !compact { - EncryptedAmount::Original { mask: read_bytes(r)?, amount: read_bytes(r)? } - } else { - EncryptedAmount::Compact { amount: read_bytes(r)? } - }) - } - - pub fn write(&self, w: &mut W) -> io::Result<()> { - match self { - EncryptedAmount::Original { mask, amount } => { - w.write_all(mask)?; - w.write_all(amount) - } - EncryptedAmount::Compact { amount } => w.write_all(amount), - } - } -} - -#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)] -pub enum RctType { - /// No RCT proofs. - Null, - /// One MLSAG for multiple inputs and Borromean range proofs (RCTTypeFull). - MlsagAggregate, - // One MLSAG for each input and a Borromean range proof (RCTTypeSimple). - MlsagIndividual, - // One MLSAG for each input and a Bulletproof (RCTTypeBulletproof). - Bulletproofs, - /// One MLSAG for each input and a Bulletproof, yet starting to use EncryptedAmount::Compact - /// (RCTTypeBulletproof2). - BulletproofsCompactAmount, - /// One CLSAG for each input and a Bulletproof (RCTTypeCLSAG). - Clsag, - /// One CLSAG for each input and a Bulletproof+ (RCTTypeBulletproofPlus). - BulletproofsPlus, -} - -impl RctType { - pub fn to_byte(self) -> u8 { - match self { - RctType::Null => 0, - RctType::MlsagAggregate => 1, - RctType::MlsagIndividual => 2, - RctType::Bulletproofs => 3, - RctType::BulletproofsCompactAmount => 4, - RctType::Clsag => 5, - RctType::BulletproofsPlus => 6, - } - } - - pub fn from_byte(byte: u8) -> Option { - Some(match byte { - 0 => RctType::Null, - 1 => RctType::MlsagAggregate, - 2 => RctType::MlsagIndividual, - 3 => RctType::Bulletproofs, - 4 => RctType::BulletproofsCompactAmount, - 5 => RctType::Clsag, - 6 => RctType::BulletproofsPlus, - _ => None?, - }) - } - - pub fn compact_encrypted_amounts(&self) -> bool { - match self { - RctType::Null | - RctType::MlsagAggregate | - RctType::MlsagIndividual | - RctType::Bulletproofs => false, - RctType::BulletproofsCompactAmount | RctType::Clsag | RctType::BulletproofsPlus => true, - } - } -} - -#[derive(Clone, PartialEq, Eq, Debug)] -pub struct RctBase { - pub fee: u64, - pub pseudo_outs: Vec, - pub encrypted_amounts: Vec, - pub commitments: Vec, -} - -impl RctBase { - pub(crate) fn fee_weight(outputs: usize, fee: u64) -> usize { - // 1 byte for the RCT signature type - 1 + (outputs * (8 + 32)) + varint_len(fee) - } - - pub fn write(&self, w: &mut W, rct_type: RctType) -> io::Result<()> { - w.write_all(&[rct_type.to_byte()])?; - match rct_type { - RctType::Null => Ok(()), - _ => { - write_varint(&self.fee, w)?; - if rct_type == RctType::MlsagIndividual { - write_raw_vec(write_point, &self.pseudo_outs, w)?; - } - for encrypted_amount in &self.encrypted_amounts { - encrypted_amount.write(w)?; - } - write_raw_vec(write_point, &self.commitments, w) - } - } - } - - pub fn read(inputs: usize, outputs: usize, r: &mut R) -> io::Result<(RctBase, RctType)> { - let rct_type = - RctType::from_byte(read_byte(r)?).ok_or_else(|| io::Error::other("invalid RCT type"))?; - - match rct_type { - RctType::Null | RctType::MlsagAggregate | RctType::MlsagIndividual => {} - RctType::Bulletproofs | - RctType::BulletproofsCompactAmount | - RctType::Clsag | - RctType::BulletproofsPlus => { - if outputs == 0 { - // Because the Bulletproofs(+) layout must be canonical, there must be 1 Bulletproof if - // Bulletproofs are in use - // If there are Bulletproofs, there must be a matching amount of outputs, implicitly - // banning 0 outputs - // Since HF 12 (CLSAG being 13), a 2-output minimum has also been enforced - Err(io::Error::other("RCT with Bulletproofs(+) had 0 outputs"))?; - } - } - } - - Ok(( - if rct_type == RctType::Null { - RctBase { fee: 0, pseudo_outs: vec![], encrypted_amounts: vec![], commitments: vec![] } - } else { - RctBase { - fee: read_varint(r)?, - pseudo_outs: if rct_type == RctType::MlsagIndividual { - read_raw_vec(read_point, inputs, r)? - } else { - vec![] - }, - encrypted_amounts: (0 .. outputs) - .map(|_| EncryptedAmount::read(rct_type.compact_encrypted_amounts(), r)) - .collect::>()?, - commitments: read_raw_vec(read_point, outputs, r)?, - } - }, - rct_type, - )) - } -} - -#[derive(Clone, PartialEq, Eq, Debug)] -pub enum RctPrunable { - Null, - AggregateMlsagBorromean { - borromean: Vec, - mlsag: Mlsag, - }, - MlsagBorromean { - borromean: Vec, - mlsags: Vec, - }, - MlsagBulletproofs { - bulletproofs: Bulletproofs, - mlsags: Vec, - pseudo_outs: Vec, - }, - Clsag { - bulletproofs: Bulletproofs, - clsags: Vec, - pseudo_outs: Vec, - }, -} - -impl RctPrunable { - pub(crate) fn fee_weight(protocol: Protocol, inputs: usize, outputs: usize) -> usize { - // 1 byte for number of BPs (technically a VarInt, yet there's always just zero or one) - 1 + Bulletproofs::fee_weight(protocol.bp_plus(), outputs) + - (inputs * (Clsag::fee_weight(protocol.ring_len()) + 32)) - } - - pub fn write(&self, w: &mut W, rct_type: RctType) -> io::Result<()> { - match self { - RctPrunable::Null => Ok(()), - RctPrunable::AggregateMlsagBorromean { borromean, mlsag } => { - write_raw_vec(BorromeanRange::write, borromean, w)?; - mlsag.write(w) - } - RctPrunable::MlsagBorromean { borromean, mlsags } => { - write_raw_vec(BorromeanRange::write, borromean, w)?; - write_raw_vec(Mlsag::write, mlsags, w) - } - RctPrunable::MlsagBulletproofs { bulletproofs, mlsags, pseudo_outs } => { - if rct_type == RctType::Bulletproofs { - w.write_all(&1u32.to_le_bytes())?; - } else { - w.write_all(&[1])?; - } - bulletproofs.write(w)?; - - write_raw_vec(Mlsag::write, mlsags, w)?; - write_raw_vec(write_point, pseudo_outs, w) - } - RctPrunable::Clsag { bulletproofs, clsags, pseudo_outs } => { - w.write_all(&[1])?; - bulletproofs.write(w)?; - - write_raw_vec(Clsag::write, clsags, w)?; - write_raw_vec(write_point, pseudo_outs, w) - } - } - } - - pub fn serialize(&self, rct_type: RctType) -> Vec { - let mut serialized = vec![]; - self.write(&mut serialized, rct_type).unwrap(); - serialized - } - - pub fn read( - rct_type: RctType, - ring_length: usize, - inputs: usize, - outputs: usize, - r: &mut R, - ) -> io::Result { - // While we generally don't bother with misc consensus checks, this affects the safety of - // the below defined rct_type function - // The exact line preventing zero-input transactions is: - // https://github.com/monero-project/monero/blob/00fd416a99686f0956361d1cd0337fe56e58d4a7/ - // src/ringct/rctSigs.cpp#L609 - // And then for RctNull, that's only allowed for miner TXs which require one input of - // Input::Gen - if inputs == 0 { - Err(io::Error::other("transaction had no inputs"))?; - } - - Ok(match rct_type { - RctType::Null => RctPrunable::Null, - RctType::MlsagAggregate => RctPrunable::AggregateMlsagBorromean { - borromean: read_raw_vec(BorromeanRange::read, outputs, r)?, - mlsag: Mlsag::read(ring_length, inputs + 1, r)?, - }, - RctType::MlsagIndividual => RctPrunable::MlsagBorromean { - borromean: read_raw_vec(BorromeanRange::read, outputs, r)?, - mlsags: (0 .. inputs).map(|_| Mlsag::read(ring_length, 2, r)).collect::>()?, - }, - RctType::Bulletproofs | RctType::BulletproofsCompactAmount => { - RctPrunable::MlsagBulletproofs { - bulletproofs: { - if (if rct_type == RctType::Bulletproofs { - u64::from(read_u32(r)?) - } else { - read_varint(r)? - }) != 1 - { - Err(io::Error::other("n bulletproofs instead of one"))?; - } - Bulletproofs::read(r)? - }, - mlsags: (0 .. inputs) - .map(|_| Mlsag::read(ring_length, 2, r)) - .collect::>()?, - pseudo_outs: read_raw_vec(read_point, inputs, r)?, - } - } - RctType::Clsag | RctType::BulletproofsPlus => RctPrunable::Clsag { - bulletproofs: { - if read_varint::<_, u64>(r)? != 1 { - Err(io::Error::other("n bulletproofs instead of one"))?; - } - (if rct_type == RctType::Clsag { Bulletproofs::read } else { Bulletproofs::read_plus })( - r, - )? - }, - clsags: (0 .. inputs).map(|_| Clsag::read(ring_length, r)).collect::>()?, - pseudo_outs: read_raw_vec(read_point, inputs, r)?, - }, - }) - } - - pub(crate) fn signature_write(&self, w: &mut W) -> io::Result<()> { - match self { - RctPrunable::Null => panic!("Serializing RctPrunable::Null for a signature"), - RctPrunable::AggregateMlsagBorromean { borromean, .. } | - RctPrunable::MlsagBorromean { borromean, .. } => { - borromean.iter().try_for_each(|rs| rs.write(w)) - } - RctPrunable::MlsagBulletproofs { bulletproofs, .. } | - RctPrunable::Clsag { bulletproofs, .. } => bulletproofs.signature_write(w), - } - } -} - -#[derive(Clone, PartialEq, Eq, Debug)] -pub struct RctSignatures { - pub base: RctBase, - pub prunable: RctPrunable, -} - -impl RctSignatures { - /// RctType for a given RctSignatures struct. - pub fn rct_type(&self) -> RctType { - match &self.prunable { - RctPrunable::Null => RctType::Null, - RctPrunable::AggregateMlsagBorromean { .. } => RctType::MlsagAggregate, - RctPrunable::MlsagBorromean { .. } => RctType::MlsagIndividual, - // RctBase ensures there's at least one output, making the following - // inferences guaranteed/expects impossible on any valid RctSignatures - RctPrunable::MlsagBulletproofs { .. } => { - if matches!( - self - .base - .encrypted_amounts - .first() - .expect("MLSAG with Bulletproofs didn't have any outputs"), - EncryptedAmount::Original { .. } - ) { - RctType::Bulletproofs - } else { - RctType::BulletproofsCompactAmount - } - } - RctPrunable::Clsag { bulletproofs, .. } => { - if matches!(bulletproofs, Bulletproofs::Original { .. }) { - RctType::Clsag - } else { - RctType::BulletproofsPlus - } - } - } - } - - pub(crate) fn fee_weight(protocol: Protocol, inputs: usize, outputs: usize, fee: u64) -> usize { - RctBase::fee_weight(outputs, fee) + RctPrunable::fee_weight(protocol, inputs, outputs) - } - - pub fn write(&self, w: &mut W) -> io::Result<()> { - let rct_type = self.rct_type(); - self.base.write(w, rct_type)?; - self.prunable.write(w, rct_type) - } - - pub fn serialize(&self) -> Vec { - let mut serialized = vec![]; - self.write(&mut serialized).unwrap(); - serialized - } - - pub fn read( - ring_length: usize, - inputs: usize, - outputs: usize, - r: &mut R, - ) -> io::Result { - let base = RctBase::read(inputs, outputs, r)?; - Ok(RctSignatures { - base: base.0, - prunable: RctPrunable::read(base.1, ring_length, inputs, outputs, r)?, - }) - } -} diff --git a/coins/monero/src/rpc/mod.rs b/coins/monero/src/rpc/mod.rs deleted file mode 100644 index c0a8eae2..00000000 --- a/coins/monero/src/rpc/mod.rs +++ /dev/null @@ -1,761 +0,0 @@ -use core::fmt::Debug; -#[cfg(not(feature = "std"))] -use alloc::boxed::Box; -use std_shims::{ - vec::Vec, - io, - string::{String, ToString}, -}; - -use async_trait::async_trait; - -use curve25519_dalek::edwards::EdwardsPoint; - -use monero_generators::decompress_point; - -use serde::{Serialize, Deserialize, de::DeserializeOwned}; -use serde_json::{Value, json}; - -use crate::{ - Protocol, - serialize::*, - transaction::{Input, Timelock, Transaction}, - block::Block, - wallet::{FeePriority, Fee}, -}; - -#[cfg(feature = "http-rpc")] -mod http; -#[cfg(feature = "http-rpc")] -pub use http::*; - -// Number of blocks the fee estimate will be valid for -// https://github.com/monero-project/monero/blob/94e67bf96bbc010241f29ada6abc89f49a81759c/ -// src/wallet/wallet2.cpp#L121 -const GRACE_BLOCKS_FOR_FEE_ESTIMATE: u64 = 10; - -#[derive(Deserialize, Debug)] -pub struct EmptyResponse {} -#[derive(Deserialize, Debug)] -pub struct JsonRpcResponse { - result: T, -} - -#[derive(Deserialize, Debug)] -struct TransactionResponse { - tx_hash: String, - as_hex: String, - pruned_as_hex: String, -} -#[derive(Deserialize, Debug)] -struct TransactionsResponse { - #[serde(default)] - missed_tx: Vec, - txs: Vec, -} - -#[derive(Deserialize, Debug)] -pub struct OutputResponse { - pub height: usize, - pub unlocked: bool, - key: String, - mask: String, - txid: String, -} - -#[derive(Clone, PartialEq, Eq, Debug)] -#[cfg_attr(feature = "std", derive(thiserror::Error))] -pub enum RpcError { - #[cfg_attr(feature = "std", error("internal error ({0})"))] - InternalError(&'static str), - #[cfg_attr(feature = "std", error("connection error ({0})"))] - ConnectionError(String), - #[cfg_attr(feature = "std", error("invalid node ({0})"))] - InvalidNode(String), - #[cfg_attr(feature = "std", error("unsupported protocol version ({0})"))] - UnsupportedProtocol(usize), - #[cfg_attr(feature = "std", error("transactions not found"))] - TransactionsNotFound(Vec<[u8; 32]>), - #[cfg_attr(feature = "std", error("invalid point ({0})"))] - InvalidPoint(String), - #[cfg_attr(feature = "std", error("pruned transaction"))] - PrunedTransaction, - #[cfg_attr(feature = "std", error("invalid transaction ({0:?})"))] - InvalidTransaction([u8; 32]), - #[cfg_attr(feature = "std", error("unexpected fee response"))] - InvalidFee, - #[cfg_attr(feature = "std", error("invalid priority"))] - InvalidPriority, -} - -fn rpc_hex(value: &str) -> Result, RpcError> { - hex::decode(value).map_err(|_| RpcError::InvalidNode("expected hex wasn't hex".to_string())) -} - -fn hash_hex(hash: &str) -> Result<[u8; 32], RpcError> { - rpc_hex(hash)?.try_into().map_err(|_| RpcError::InvalidNode("hash wasn't 32-bytes".to_string())) -} - -fn rpc_point(point: &str) -> Result { - decompress_point( - rpc_hex(point)?.try_into().map_err(|_| RpcError::InvalidPoint(point.to_string()))?, - ) - .ok_or_else(|| RpcError::InvalidPoint(point.to_string())) -} - -// Read an EPEE VarInt, distinct from the VarInts used throughout the rest of the protocol -fn read_epee_vi(reader: &mut R) -> io::Result { - let vi_start = read_byte(reader)?; - let len = match vi_start & 0b11 { - 0 => 1, - 1 => 2, - 2 => 4, - 3 => 8, - _ => unreachable!(), - }; - let mut vi = u64::from(vi_start >> 2); - for i in 1 .. len { - vi |= u64::from(read_byte(reader)?) << (((i - 1) * 8) + 6); - } - Ok(vi) -} - -#[async_trait] -pub trait RpcConnection: Clone + Debug { - /// Perform a POST request to the specified route with the specified body. - /// - /// The implementor is left to handle anything such as authentication. - async fn post(&self, route: &str, body: Vec) -> Result, RpcError>; -} - -// TODO: Make this provided methods for RpcConnection? -#[derive(Clone, Debug)] -pub struct Rpc(R); -impl Rpc { - /// Perform a RPC call to the specified route with the provided parameters. - /// - /// This is NOT a JSON-RPC call. They use a route of "json_rpc" and are available via - /// `json_rpc_call`. - pub async fn rpc_call( - &self, - route: &str, - params: Option, - ) -> Result { - let res = self - .0 - .post( - route, - if let Some(params) = params { - serde_json::to_string(¶ms).unwrap().into_bytes() - } else { - vec![] - }, - ) - .await?; - let res_str = std_shims::str::from_utf8(&res) - .map_err(|_| RpcError::InvalidNode("response wasn't utf-8".to_string()))?; - serde_json::from_str(res_str) - .map_err(|_| RpcError::InvalidNode(format!("response wasn't json: {res_str}"))) - } - - /// Perform a JSON-RPC call with the specified method with the provided parameters - pub async fn json_rpc_call( - &self, - method: &str, - params: Option, - ) -> Result { - let mut req = json!({ "method": method }); - if let Some(params) = params { - req.as_object_mut().unwrap().insert("params".into(), params); - } - Ok(self.rpc_call::<_, JsonRpcResponse>("json_rpc", Some(req)).await?.result) - } - - /// Perform a binary call to the specified route with the provided parameters. - pub async fn bin_call(&self, route: &str, params: Vec) -> Result, RpcError> { - self.0.post(route, params).await - } - - /// Get the active blockchain protocol version. - pub async fn get_protocol(&self) -> Result { - #[derive(Deserialize, Debug)] - struct ProtocolResponse { - major_version: usize, - } - - #[derive(Deserialize, Debug)] - struct LastHeaderResponse { - block_header: ProtocolResponse, - } - - Ok( - match self - .json_rpc_call::("get_last_block_header", None) - .await? - .block_header - .major_version - { - 13 | 14 => Protocol::v14, - 15 | 16 => Protocol::v16, - protocol => Err(RpcError::UnsupportedProtocol(protocol))?, - }, - ) - } - - pub async fn get_height(&self) -> Result { - #[derive(Deserialize, Debug)] - struct HeightResponse { - height: usize, - } - Ok(self.rpc_call::, HeightResponse>("get_height", None).await?.height) - } - - pub async fn get_transactions(&self, hashes: &[[u8; 32]]) -> Result, RpcError> { - if hashes.is_empty() { - return Ok(vec![]); - } - - let mut hashes_hex = hashes.iter().map(hex::encode).collect::>(); - let mut all_txs = Vec::with_capacity(hashes.len()); - while !hashes_hex.is_empty() { - // Monero errors if more than 100 is requested unless using a non-restricted RPC - const TXS_PER_REQUEST: usize = 100; - let this_count = TXS_PER_REQUEST.min(hashes_hex.len()); - - let txs: TransactionsResponse = self - .rpc_call( - "get_transactions", - Some(json!({ - "txs_hashes": hashes_hex.drain(.. this_count).collect::>(), - })), - ) - .await?; - - if !txs.missed_tx.is_empty() { - Err(RpcError::TransactionsNotFound( - txs.missed_tx.iter().map(|hash| hash_hex(hash)).collect::>()?, - ))?; - } - - all_txs.extend(txs.txs); - } - - all_txs - .iter() - .enumerate() - .map(|(i, res)| { - let tx = Transaction::read::<&[u8]>( - &mut rpc_hex(if !res.as_hex.is_empty() { &res.as_hex } else { &res.pruned_as_hex })? - .as_ref(), - ) - .map_err(|_| match hash_hex(&res.tx_hash) { - Ok(hash) => RpcError::InvalidTransaction(hash), - Err(err) => err, - })?; - - // https://github.com/monero-project/monero/issues/8311 - if res.as_hex.is_empty() { - match tx.prefix.inputs.first() { - Some(Input::Gen { .. }) => (), - _ => Err(RpcError::PrunedTransaction)?, - } - } - - // This does run a few keccak256 hashes, which is pointless if the node is trusted - // In exchange, this provides resilience against invalid/malicious nodes - if tx.hash() != hashes[i] { - Err(RpcError::InvalidNode( - "replied with transaction wasn't the requested transaction".to_string(), - ))?; - } - - Ok(tx) - }) - .collect() - } - - pub async fn get_transaction(&self, tx: [u8; 32]) -> Result { - self.get_transactions(&[tx]).await.map(|mut txs| txs.swap_remove(0)) - } - - /// Get the hash of a block from the node by the block's numbers. - /// This function does not verify the returned block hash is actually for the number in question. - pub async fn get_block_hash(&self, number: usize) -> Result<[u8; 32], RpcError> { - #[derive(Deserialize, Debug)] - struct BlockHeaderResponse { - hash: String, - } - #[derive(Deserialize, Debug)] - struct BlockHeaderByHeightResponse { - block_header: BlockHeaderResponse, - } - - let header: BlockHeaderByHeightResponse = - self.json_rpc_call("get_block_header_by_height", Some(json!({ "height": number }))).await?; - hash_hex(&header.block_header.hash) - } - - /// Get a block from the node by its hash. - /// This function does not verify the returned block actually has the hash in question. - pub async fn get_block(&self, hash: [u8; 32]) -> Result { - #[derive(Deserialize, Debug)] - struct BlockResponse { - blob: String, - } - - let res: BlockResponse = - self.json_rpc_call("get_block", Some(json!({ "hash": hex::encode(hash) }))).await?; - - let block = Block::read::<&[u8]>(&mut rpc_hex(&res.blob)?.as_ref()) - .map_err(|_| RpcError::InvalidNode("invalid block".to_string()))?; - if block.hash() != hash { - Err(RpcError::InvalidNode("different block than requested (hash)".to_string()))?; - } - Ok(block) - } - - pub async fn get_block_by_number(&self, number: usize) -> Result { - #[derive(Deserialize, Debug)] - struct BlockResponse { - blob: String, - } - - let res: BlockResponse = - self.json_rpc_call("get_block", Some(json!({ "height": number }))).await?; - - let block = Block::read::<&[u8]>(&mut rpc_hex(&res.blob)?.as_ref()) - .map_err(|_| RpcError::InvalidNode("invalid block".to_string()))?; - - // Make sure this is actually the block for this number - match block.miner_tx.prefix.inputs.first() { - Some(Input::Gen(actual)) => { - if usize::try_from(*actual).unwrap() == number { - Ok(block) - } else { - Err(RpcError::InvalidNode("different block than requested (number)".to_string())) - } - } - _ => Err(RpcError::InvalidNode( - "block's miner_tx didn't have an input of kind Input::Gen".to_string(), - )), - } - } - - pub async fn get_block_transactions(&self, hash: [u8; 32]) -> Result, RpcError> { - let block = self.get_block(hash).await?; - let mut res = vec![block.miner_tx]; - res.extend(self.get_transactions(&block.txs).await?); - Ok(res) - } - - pub async fn get_block_transactions_by_number( - &self, - number: usize, - ) -> Result, RpcError> { - self.get_block_transactions(self.get_block_hash(number).await?).await - } - - /// Get the output indexes of the specified transaction. - pub async fn get_o_indexes(&self, hash: [u8; 32]) -> Result, RpcError> { - /* - TODO: Use these when a suitable epee serde lib exists - - #[derive(Serialize, Debug)] - struct Request { - txid: [u8; 32], - } - - #[derive(Deserialize, Debug)] - struct OIndexes { - o_indexes: Vec, - } - */ - - // Given the immaturity of Rust epee libraries, this is a homegrown one which is only validated - // to work against this specific function - - // Header for EPEE, an 8-byte magic and a version - const EPEE_HEADER: &[u8] = b"\x01\x11\x01\x01\x01\x01\x02\x01\x01"; - - let mut request = EPEE_HEADER.to_vec(); - // Number of fields (shifted over 2 bits as the 2 LSBs are reserved for metadata) - request.push(1 << 2); - // Length of field name - request.push(4); - // Field name - request.extend(b"txid"); - // Type of field - request.push(10); - // Length of string, since this byte array is technically a string - request.push(32 << 2); - // The "string" - request.extend(hash); - - let indexes_buf = self.bin_call("get_o_indexes.bin", request).await?; - let mut indexes: &[u8] = indexes_buf.as_ref(); - - (|| { - let mut res = None; - let mut is_okay = false; - - if read_bytes::<_, { EPEE_HEADER.len() }>(&mut indexes)? != EPEE_HEADER { - Err(io::Error::other("invalid header"))?; - } - - let read_object = |reader: &mut &[u8]| -> io::Result> { - let fields = read_byte(reader)? >> 2; - - for _ in 0 .. fields { - let name_len = read_byte(reader)?; - let name = read_raw_vec(read_byte, name_len.into(), reader)?; - - let type_with_array_flag = read_byte(reader)?; - let kind = type_with_array_flag & (!0x80); - - let iters = if type_with_array_flag != kind { read_epee_vi(reader)? } else { 1 }; - - if (&name == b"o_indexes") && (kind != 5) { - Err(io::Error::other("o_indexes weren't u64s"))?; - } - - let f = match kind { - // i64 - 1 => |reader: &mut &[u8]| read_raw_vec(read_byte, 8, reader), - // i32 - 2 => |reader: &mut &[u8]| read_raw_vec(read_byte, 4, reader), - // i16 - 3 => |reader: &mut &[u8]| read_raw_vec(read_byte, 2, reader), - // i8 - 4 => |reader: &mut &[u8]| read_raw_vec(read_byte, 1, reader), - // u64 - 5 => |reader: &mut &[u8]| read_raw_vec(read_byte, 8, reader), - // u32 - 6 => |reader: &mut &[u8]| read_raw_vec(read_byte, 4, reader), - // u16 - 7 => |reader: &mut &[u8]| read_raw_vec(read_byte, 2, reader), - // u8 - 8 => |reader: &mut &[u8]| read_raw_vec(read_byte, 1, reader), - // double - 9 => |reader: &mut &[u8]| read_raw_vec(read_byte, 8, reader), - // string, or any collection of bytes - 10 => |reader: &mut &[u8]| { - let len = read_epee_vi(reader)?; - read_raw_vec( - read_byte, - len.try_into().map_err(|_| io::Error::other("u64 length exceeded usize"))?, - reader, - ) - }, - // bool - 11 => |reader: &mut &[u8]| read_raw_vec(read_byte, 1, reader), - // object, errors here as it shouldn't be used on this call - 12 => { - |_: &mut &[u8]| Err(io::Error::other("node used object in reply to get_o_indexes")) - } - // array, so far unused - 13 => |_: &mut &[u8]| Err(io::Error::other("node used the unused array type")), - _ => |_: &mut &[u8]| Err(io::Error::other("node used an invalid type")), - }; - - let mut bytes_res = vec![]; - for _ in 0 .. iters { - bytes_res.push(f(reader)?); - } - - let mut actual_res = Vec::with_capacity(bytes_res.len()); - match name.as_slice() { - b"o_indexes" => { - for o_index in bytes_res { - actual_res.push(u64::from_le_bytes( - o_index - .try_into() - .map_err(|_| io::Error::other("node didn't provide 8 bytes for a u64"))?, - )); - } - res = Some(actual_res); - } - b"status" => { - if bytes_res - .first() - .ok_or_else(|| io::Error::other("status wasn't a string"))? - .as_slice() != - b"OK" - { - // TODO: Better handle non-OK responses - Err(io::Error::other("response wasn't OK"))?; - } - is_okay = true; - } - _ => continue, - } - - if is_okay && res.is_some() { - break; - } - } - - // Didn't return a response with a status - // (if the status wasn't okay, we would've already errored) - if !is_okay { - Err(io::Error::other("response didn't contain a status"))?; - } - - // If the Vec was empty, it would've been omitted, hence the unwrap_or - // TODO: Test against a 0-output TX, such as the ones found in block 202612 - Ok(res.unwrap_or(vec![])) - }; - - read_object(&mut indexes) - })() - .map_err(|_| RpcError::InvalidNode("invalid binary response".to_string())) - } - - /// Get the output distribution, from the specified height to the specified height (both - /// inclusive). - pub async fn get_output_distribution( - &self, - from: usize, - to: usize, - ) -> Result, RpcError> { - #[derive(Deserialize, Debug)] - struct Distribution { - distribution: Vec, - } - - #[derive(Deserialize, Debug)] - struct Distributions { - distributions: Vec, - } - - let mut distributions: Distributions = self - .json_rpc_call( - "get_output_distribution", - Some(json!({ - "binary": false, - "amounts": [0], - "cumulative": true, - "from_height": from, - "to_height": to, - })), - ) - .await?; - - Ok(distributions.distributions.swap_remove(0).distribution) - } - - /// Get the specified outputs from the RingCT (zero-amount) pool - pub async fn get_outs(&self, indexes: &[u64]) -> Result, RpcError> { - #[derive(Deserialize, Debug)] - struct OutsResponse { - status: String, - outs: Vec, - } - - let res: OutsResponse = self - .rpc_call( - "get_outs", - Some(json!({ - "get_txid": true, - "outputs": indexes.iter().map(|o| json!({ - "amount": 0, - "index": o - })).collect::>() - })), - ) - .await?; - - if res.status != "OK" { - Err(RpcError::InvalidNode("bad response to get_outs".to_string()))?; - } - - Ok(res.outs) - } - - /// Get the specified outputs from the RingCT (zero-amount) pool, but only return them if their - /// timelock has been satisfied. - /// - /// The timelock being satisfied is distinct from being free of the 10-block lock applied to all - /// Monero transactions. - pub async fn get_unlocked_outputs( - &self, - indexes: &[u64], - height: usize, - fingerprintable_canonical: bool, - ) -> Result>, RpcError> { - let outs: Vec = self.get_outs(indexes).await?; - - // Only need to fetch txs to do canonical check on timelock - let txs = if fingerprintable_canonical { - self - .get_transactions( - &outs.iter().map(|out| hash_hex(&out.txid)).collect::, _>>()?, - ) - .await? - } else { - Vec::new() - }; - - // TODO: https://github.com/serai-dex/serai/issues/104 - outs - .iter() - .enumerate() - .map(|(i, out)| { - // Allow keys to be invalid, though if they are, return None to trigger selection of a new - // decoy - // Only valid keys can be used in CLSAG proofs, hence the need for re-selection, yet - // invalid keys may honestly exist on the blockchain - // Only a recent hard fork checked output keys were valid points - let Some(key) = decompress_point( - rpc_hex(&out.key)? - .try_into() - .map_err(|_| RpcError::InvalidNode("non-32-byte point".to_string()))?, - ) else { - return Ok(None); - }; - Ok(Some([key, rpc_point(&out.mask)?]).filter(|_| { - if fingerprintable_canonical { - Timelock::Block(height) >= txs[i].prefix.timelock - } else { - out.unlocked - } - })) - }) - .collect() - } - - async fn get_fee_v14(&self, priority: FeePriority) -> Result { - #[derive(Deserialize, Debug)] - struct FeeResponseV14 { - status: String, - fee: u64, - quantization_mask: u64, - } - - // https://github.com/monero-project/monero/blob/94e67bf96bbc010241f29ada6abc89f49a81759c/ - // src/wallet/wallet2.cpp#L7569-L7584 - // https://github.com/monero-project/monero/blob/94e67bf96bbc010241f29ada6abc89f49a81759c/ - // src/wallet/wallet2.cpp#L7660-L7661 - let priority_idx = - usize::try_from(if priority.fee_priority() == 0 { 1 } else { priority.fee_priority() - 1 }) - .map_err(|_| RpcError::InvalidPriority)?; - let multipliers = [1, 5, 25, 1000]; - if priority_idx >= multipliers.len() { - // though not an RPC error, it seems sensible to treat as such - Err(RpcError::InvalidPriority)?; - } - let fee_multiplier = multipliers[priority_idx]; - - let res: FeeResponseV14 = self - .json_rpc_call( - "get_fee_estimate", - Some(json!({ "grace_blocks": GRACE_BLOCKS_FOR_FEE_ESTIMATE })), - ) - .await?; - - if res.status != "OK" { - Err(RpcError::InvalidFee)?; - } - - Ok(Fee { per_weight: res.fee * fee_multiplier, mask: res.quantization_mask }) - } - - /// Get the currently estimated fee from the node. - /// - /// This may be manipulated to unsafe levels and MUST be sanity checked. - // TODO: Take a sanity check argument - pub async fn get_fee(&self, protocol: Protocol, priority: FeePriority) -> Result { - // TODO: Implement wallet2's adjust_priority which by default automatically uses a lower - // priority than provided depending on the backlog in the pool - if protocol.v16_fee() { - #[derive(Deserialize, Debug)] - struct FeeResponse { - status: String, - fees: Vec, - quantization_mask: u64, - } - - let res: FeeResponse = self - .json_rpc_call( - "get_fee_estimate", - Some(json!({ "grace_blocks": GRACE_BLOCKS_FOR_FEE_ESTIMATE })), - ) - .await?; - - // https://github.com/monero-project/monero/blob/94e67bf96bbc010241f29ada6abc89f49a81759c/ - // src/wallet/wallet2.cpp#L7615-L7620 - let priority_idx = usize::try_from(if priority.fee_priority() >= 4 { - 3 - } else { - priority.fee_priority().saturating_sub(1) - }) - .map_err(|_| RpcError::InvalidPriority)?; - - if res.status != "OK" { - Err(RpcError::InvalidFee) - } else if priority_idx >= res.fees.len() { - Err(RpcError::InvalidPriority) - } else { - Ok(Fee { per_weight: res.fees[priority_idx], mask: res.quantization_mask }) - } - } else { - self.get_fee_v14(priority).await - } - } - - pub async fn publish_transaction(&self, tx: &Transaction) -> Result<(), RpcError> { - #[allow(dead_code)] - #[derive(Deserialize, Debug)] - struct SendRawResponse { - status: String, - double_spend: bool, - fee_too_low: bool, - invalid_input: bool, - invalid_output: bool, - low_mixin: bool, - not_relayed: bool, - overspend: bool, - too_big: bool, - too_few_outputs: bool, - reason: String, - } - - let res: SendRawResponse = self - .rpc_call("send_raw_transaction", Some(json!({ "tx_as_hex": hex::encode(tx.serialize()) }))) - .await?; - - if res.status != "OK" { - Err(RpcError::InvalidTransaction(tx.hash()))?; - } - - Ok(()) - } - - // TODO: Take &Address, not &str? - pub async fn generate_blocks( - &self, - address: &str, - block_count: usize, - ) -> Result<(Vec<[u8; 32]>, usize), RpcError> { - #[derive(Debug, Deserialize)] - struct BlocksResponse { - blocks: Vec, - height: usize, - } - - let res = self - .json_rpc_call::( - "generateblocks", - Some(json!({ - "wallet_address": address, - "amount_of_blocks": block_count - })), - ) - .await?; - - let mut blocks = Vec::with_capacity(res.blocks.len()); - for block in res.blocks { - blocks.push(hash_hex(&block)?); - } - Ok((blocks, res.height)) - } -} diff --git a/coins/monero/src/serialize.rs b/coins/monero/src/serialize.rs deleted file mode 100644 index d2ae5980..00000000 --- a/coins/monero/src/serialize.rs +++ /dev/null @@ -1,172 +0,0 @@ -use core::fmt::Debug; -use std_shims::{ - vec::Vec, - io::{self, Read, Write}, -}; - -use curve25519_dalek::{scalar::Scalar, edwards::EdwardsPoint}; - -use monero_generators::decompress_point; - -const VARINT_CONTINUATION_MASK: u8 = 0b1000_0000; - -mod sealed { - pub trait VarInt: TryInto + TryFrom + Copy { - const BITS: usize; - } - impl VarInt for u8 { - const BITS: usize = 8; - } - impl VarInt for u32 { - const BITS: usize = 32; - } - impl VarInt for u64 { - const BITS: usize = 64; - } - impl VarInt for usize { - const BITS: usize = core::mem::size_of::() * 8; - } -} - -// This will panic if the VarInt exceeds u64::MAX -pub(crate) fn varint_len(varint: U) -> usize { - let varint_u64: u64 = varint.try_into().map_err(|_| "varint exceeded u64").unwrap(); - ((usize::try_from(u64::BITS - varint_u64.leading_zeros()).unwrap().saturating_sub(1)) / 7) + 1 -} - -pub(crate) fn write_byte(byte: &u8, w: &mut W) -> io::Result<()> { - w.write_all(&[*byte]) -} - -// This will panic if the VarInt exceeds u64::MAX -pub(crate) fn write_varint(varint: &U, w: &mut W) -> io::Result<()> { - let mut varint: u64 = (*varint).try_into().map_err(|_| "varint exceeded u64").unwrap(); - while { - let mut b = u8::try_from(varint & u64::from(!VARINT_CONTINUATION_MASK)).unwrap(); - varint >>= 7; - if varint != 0 { - b |= VARINT_CONTINUATION_MASK; - } - write_byte(&b, w)?; - varint != 0 - } {} - Ok(()) -} - -pub(crate) fn write_scalar(scalar: &Scalar, w: &mut W) -> io::Result<()> { - w.write_all(&scalar.to_bytes()) -} - -pub(crate) fn write_point(point: &EdwardsPoint, w: &mut W) -> io::Result<()> { - w.write_all(&point.compress().to_bytes()) -} - -pub(crate) fn write_raw_vec io::Result<()>>( - f: F, - values: &[T], - w: &mut W, -) -> io::Result<()> { - for value in values { - f(value, w)?; - } - Ok(()) -} - -pub(crate) fn write_vec io::Result<()>>( - f: F, - values: &[T], - w: &mut W, -) -> io::Result<()> { - write_varint(&values.len(), w)?; - write_raw_vec(f, values, w) -} - -pub(crate) fn read_bytes(r: &mut R) -> io::Result<[u8; N]> { - let mut res = [0; N]; - r.read_exact(&mut res)?; - Ok(res) -} - -pub(crate) fn read_byte(r: &mut R) -> io::Result { - Ok(read_bytes::<_, 1>(r)?[0]) -} - -pub(crate) fn read_u16(r: &mut R) -> io::Result { - read_bytes(r).map(u16::from_le_bytes) -} - -pub(crate) fn read_u32(r: &mut R) -> io::Result { - read_bytes(r).map(u32::from_le_bytes) -} - -pub(crate) fn read_u64(r: &mut R) -> io::Result { - read_bytes(r).map(u64::from_le_bytes) -} - -pub(crate) fn read_varint(r: &mut R) -> io::Result { - let mut bits = 0; - let mut res = 0; - while { - let b = read_byte(r)?; - if (bits != 0) && (b == 0) { - Err(io::Error::other("non-canonical varint"))?; - } - if ((bits + 7) >= U::BITS) && (b >= (1 << (U::BITS - bits))) { - Err(io::Error::other("varint overflow"))?; - } - - res += u64::from(b & (!VARINT_CONTINUATION_MASK)) << bits; - bits += 7; - b & VARINT_CONTINUATION_MASK == VARINT_CONTINUATION_MASK - } {} - res.try_into().map_err(|_| io::Error::other("VarInt does not fit into integer type")) -} - -// All scalar fields supported by monero-serai are checked to be canonical for valid transactions -// While from_bytes_mod_order would be more flexible, it's not currently needed and would be -// inaccurate to include now. While casting a wide net may be preferable, it'd also be inaccurate -// for now. There's also further edge cases as noted by -// https://github.com/monero-project/monero/issues/8438, where some scalars had an archaic -// reduction applied -pub(crate) fn read_scalar(r: &mut R) -> io::Result { - Option::from(Scalar::from_canonical_bytes(read_bytes(r)?)) - .ok_or_else(|| io::Error::other("unreduced scalar")) -} - -pub(crate) fn read_point(r: &mut R) -> io::Result { - let bytes = read_bytes(r)?; - decompress_point(bytes).ok_or_else(|| io::Error::other("invalid point")) -} - -pub(crate) fn read_torsion_free_point(r: &mut R) -> io::Result { - read_point(r) - .ok() - .filter(EdwardsPoint::is_torsion_free) - .ok_or_else(|| io::Error::other("invalid point")) -} - -pub(crate) fn read_raw_vec io::Result>( - f: F, - len: usize, - r: &mut R, -) -> io::Result> { - let mut res = vec![]; - for _ in 0 .. len { - res.push(f(r)?); - } - Ok(res) -} - -pub(crate) fn read_array io::Result, const N: usize>( - f: F, - r: &mut R, -) -> io::Result<[T; N]> { - read_raw_vec(f, N, r).map(|vec| vec.try_into().unwrap()) -} - -pub(crate) fn read_vec io::Result>( - f: F, - r: &mut R, -) -> io::Result> { - read_raw_vec(f, read_varint(r)?, r) -} diff --git a/coins/monero/src/tests/bulletproofs/mod.rs b/coins/monero/src/tests/bulletproofs/mod.rs deleted file mode 100644 index 6c276206..00000000 --- a/coins/monero/src/tests/bulletproofs/mod.rs +++ /dev/null @@ -1,95 +0,0 @@ -use hex_literal::hex; -use rand_core::OsRng; - -use curve25519_dalek::scalar::Scalar; -use monero_generators::decompress_point; -use multiexp::BatchVerifier; - -use crate::{ - Commitment, random_scalar, - ringct::bulletproofs::{Bulletproofs, original::OriginalStruct}, -}; - -mod plus; - -#[test] -fn bulletproofs_vector() { - let scalar = |scalar| Scalar::from_canonical_bytes(scalar).unwrap(); - let point = |point| decompress_point(point).unwrap(); - - // Generated from Monero - assert!(Bulletproofs::Original(OriginalStruct { - A: point(hex!("ef32c0b9551b804decdcb107eb22aa715b7ce259bf3c5cac20e24dfa6b28ac71")), - S: point(hex!("e1285960861783574ee2b689ae53622834eb0b035d6943103f960cd23e063fa0")), - T1: point(hex!("4ea07735f184ba159d0e0eb662bac8cde3eb7d39f31e567b0fbda3aa23fe5620")), - T2: point(hex!("b8390aa4b60b255630d40e592f55ec6b7ab5e3a96bfcdcd6f1cd1d2fc95f441e")), - taux: scalar(hex!("5957dba8ea9afb23d6e81cc048a92f2d502c10c749dc1b2bd148ae8d41ec7107")), - mu: scalar(hex!("923023b234c2e64774b820b4961f7181f6c1dc152c438643e5a25b0bf271bc02")), - L: vec![ - point(hex!("c45f656316b9ebf9d357fb6a9f85b5f09e0b991dd50a6e0ae9b02de3946c9d99")), - point(hex!("9304d2bf0f27183a2acc58cc755a0348da11bd345485fda41b872fee89e72aac")), - point(hex!("1bb8b71925d155dd9569f64129ea049d6149fdc4e7a42a86d9478801d922129b")), - point(hex!("5756a7bf887aa72b9a952f92f47182122e7b19d89e5dd434c747492b00e1c6b7")), - point(hex!("6e497c910d102592830555356af5ff8340e8d141e3fb60ea24cfa587e964f07d")), - point(hex!("f4fa3898e7b08e039183d444f3d55040f3c790ed806cb314de49f3068bdbb218")), - point(hex!("0bbc37597c3ead517a3841e159c8b7b79a5ceaee24b2a9a20350127aab428713")), - ], - R: vec![ - point(hex!("609420ba1702781692e84accfd225adb3d077aedc3cf8125563400466b52dbd9")), - point(hex!("fb4e1d079e7a2b0ec14f7e2a3943bf50b6d60bc346a54fcf562fb234b342abf8")), - point(hex!("6ae3ac97289c48ce95b9c557289e82a34932055f7f5e32720139824fe81b12e5")), - point(hex!("d071cc2ffbdab2d840326ad15f68c01da6482271cae3cf644670d1632f29a15c")), - point(hex!("e52a1754b95e1060589ba7ce0c43d0060820ebfc0d49dc52884bc3c65ad18af5")), - point(hex!("41573b06140108539957df71aceb4b1816d2409ce896659aa5c86f037ca5e851")), - point(hex!("a65970b2cc3c7b08b2b5b739dbc8e71e646783c41c625e2a5b1535e3d2e0f742")), - ], - a: scalar(hex!("0077c5383dea44d3cd1bc74849376bd60679612dc4b945255822457fa0c0a209")), - b: scalar(hex!("fe80cf5756473482581e1d38644007793ddc66fdeb9404ec1689a907e4863302")), - t: scalar(hex!("40dfb08e09249040df997851db311bd6827c26e87d6f0f332c55be8eef10e603")) - }) - .verify( - &mut OsRng, - &[ - // For some reason, these vectors are * INV_EIGHT - point(hex!("8e8f23f315edae4f6c2f948d9a861e0ae32d356b933cd11d2f0e031ac744c41f")) - .mul_by_cofactor(), - point(hex!("2829cbd025aa54cd6e1b59a032564f22f0b2e5627f7f2c4297f90da438b5510f")) - .mul_by_cofactor(), - ] - )); -} - -macro_rules! bulletproofs_tests { - ($name: ident, $max: ident, $plus: literal) => { - #[test] - fn $name() { - // Create Bulletproofs for all possible output quantities - let mut verifier = BatchVerifier::new(16); - for i in 1 ..= 16 { - let commitments = (1 ..= i) - .map(|i| Commitment::new(random_scalar(&mut OsRng), u64::try_from(i).unwrap())) - .collect::>(); - - let bp = Bulletproofs::prove(&mut OsRng, &commitments, $plus).unwrap(); - - let commitments = commitments.iter().map(Commitment::calculate).collect::>(); - assert!(bp.verify(&mut OsRng, &commitments)); - assert!(bp.batch_verify(&mut OsRng, &mut verifier, i, &commitments)); - } - assert!(verifier.verify_vartime()); - } - - #[test] - fn $max() { - // Check Bulletproofs errors if we try to prove for too many outputs - let mut commitments = vec![]; - for _ in 0 .. 17 { - commitments.push(Commitment::new(Scalar::ZERO, 0)); - } - assert!(Bulletproofs::prove(&mut OsRng, &commitments, $plus).is_err()); - } - }; -} - -bulletproofs_tests!(bulletproofs, bulletproofs_max, false); -bulletproofs_tests!(bulletproofs_plus, bulletproofs_plus_max, true); diff --git a/coins/monero/src/tests/bulletproofs/plus/aggregate_range_proof.rs b/coins/monero/src/tests/bulletproofs/plus/aggregate_range_proof.rs deleted file mode 100644 index 658da250..00000000 --- a/coins/monero/src/tests/bulletproofs/plus/aggregate_range_proof.rs +++ /dev/null @@ -1,30 +0,0 @@ -use rand_core::{RngCore, OsRng}; - -use multiexp::BatchVerifier; -use group::ff::Field; -use dalek_ff_group::{Scalar, EdwardsPoint}; - -use crate::{ - Commitment, - ringct::bulletproofs::plus::aggregate_range_proof::{ - AggregateRangeStatement, AggregateRangeWitness, - }, -}; - -#[test] -fn test_aggregate_range_proof() { - let mut verifier = BatchVerifier::new(16); - for m in 1 ..= 16 { - let mut commitments = vec![]; - for _ in 0 .. m { - commitments.push(Commitment::new(*Scalar::random(&mut OsRng), OsRng.next_u64())); - } - let commitment_points = commitments.iter().map(|com| EdwardsPoint(com.calculate())).collect(); - let statement = AggregateRangeStatement::new(commitment_points).unwrap(); - let witness = AggregateRangeWitness::new(commitments).unwrap(); - - let proof = statement.clone().prove(&mut OsRng, &witness).unwrap(); - statement.verify(&mut OsRng, &mut verifier, (), proof); - } - assert!(verifier.verify_vartime()); -} diff --git a/coins/monero/src/tests/mod.rs b/coins/monero/src/tests/mod.rs deleted file mode 100644 index 33d56f22..00000000 --- a/coins/monero/src/tests/mod.rs +++ /dev/null @@ -1,6 +0,0 @@ -mod unreduced_scalar; -mod clsag; -mod bulletproofs; -mod address; -mod seed; -mod extra; diff --git a/coins/monero/src/tests/seed.rs b/coins/monero/src/tests/seed.rs deleted file mode 100644 index 2c421abe..00000000 --- a/coins/monero/src/tests/seed.rs +++ /dev/null @@ -1,482 +0,0 @@ -use zeroize::Zeroizing; - -use rand_core::OsRng; - -use curve25519_dalek::scalar::Scalar; - -use crate::{ - hash, - wallet::seed::{ - Seed, SeedType, SeedError, - classic::{self, trim_by_lang}, - polyseed, - }, -}; - -#[test] -fn test_classic_seed() { - struct Vector { - language: classic::Language, - seed: String, - spend: String, - view: String, - } - - let vectors = [ - Vector { - language: classic::Language::Chinese, - seed: "摇 曲 艺 武 滴 然 效 似 赏 式 祥 歌 买 疑 小 碧 堆 博 键 房 鲜 悲 付 喷 武".into(), - spend: "a5e4fff1706ef9212993a69f246f5c95ad6d84371692d63e9bb0ea112a58340d".into(), - view: "1176c43ce541477ea2f3ef0b49b25112b084e26b8a843e1304ac4677b74cdf02".into(), - }, - Vector { - language: classic::Language::English, - seed: "washing thirsty occur lectures tuesday fainted toxic adapt \ - abnormal memoir nylon mostly building shrugged online ember northern \ - ruby woes dauntless boil family illness inroads northern" - .into(), - spend: "c0af65c0dd837e666b9d0dfed62745f4df35aed7ea619b2798a709f0fe545403".into(), - view: "513ba91c538a5a9069e0094de90e927c0cd147fa10428ce3ac1afd49f63e3b01".into(), - }, - Vector { - language: classic::Language::Dutch, - seed: "setwinst riphagen vimmetje extase blief tuitelig fuiven meifeest \ - ponywagen zesmaal ripdeal matverf codetaal leut ivoor rotten \ - wisgerhof winzucht typograaf atrium rein zilt traktaat verzaagd setwinst" - .into(), - spend: "e2d2873085c447c2bc7664222ac8f7d240df3aeac137f5ff2022eaa629e5b10a".into(), - view: "eac30b69477e3f68093d131c7fd961564458401b07f8c87ff8f6030c1a0c7301".into(), - }, - Vector { - language: classic::Language::French, - seed: "poids vaseux tarte bazar poivre effet entier nuance \ - sensuel ennui pacte osselet poudre battre alibi mouton \ - stade paquet pliage gibier type question position projet pliage" - .into(), - spend: "2dd39ff1a4628a94b5c2ec3e42fb3dfe15c2b2f010154dc3b3de6791e805b904".into(), - view: "6725b32230400a1032f31d622b44c3a227f88258939b14a7c72e00939e7bdf0e".into(), - }, - Vector { - language: classic::Language::Spanish, - seed: "minero ocupar mirar evadir octubre cal logro miope \ - opaco disco ancla litio clase cuello nasal clase \ - fiar avance deseo mente grumo negro cordón croqueta clase" - .into(), - spend: "ae2c9bebdddac067d73ec0180147fc92bdf9ac7337f1bcafbbe57dd13558eb02".into(), - view: "18deafb34d55b7a43cae2c1c1c206a3c80c12cc9d1f84640b484b95b7fec3e05".into(), - }, - Vector { - language: classic::Language::German, - seed: "Kaliber Gabelung Tapir Liveband Favorit Specht Enklave Nabel \ - Jupiter Foliant Chronik nisten löten Vase Aussage Rekord \ - Yeti Gesetz Eleganz Alraune Künstler Almweide Jahr Kastanie Almweide" - .into(), - spend: "79801b7a1b9796856e2397d862a113862e1fdc289a205e79d8d70995b276db06".into(), - view: "99f0ec556643bd9c038a4ed86edcb9c6c16032c4622ed2e000299d527a792701".into(), - }, - Vector { - language: classic::Language::Italian, - seed: "cavo pancetta auto fulmine alleanza filmato diavolo prato \ - forzare meritare litigare lezione segreto evasione votare buio \ - licenza cliente dorso natale crescere vento tutelare vetta evasione" - .into(), - spend: "5e7fd774eb00fa5877e2a8b4dc9c7ffe111008a3891220b56a6e49ac816d650a".into(), - view: "698a1dce6018aef5516e82ca0cb3e3ec7778d17dfb41a137567bfa2e55e63a03".into(), - }, - Vector { - language: classic::Language::Portuguese, - seed: "agito eventualidade onus itrio holograma sodomizar objetos dobro \ - iugoslavo bcrepuscular odalisca abjeto iuane darwinista eczema acetona \ - cibernetico hoquei gleba driver buffer azoto megera nogueira agito" - .into(), - spend: "13b3115f37e35c6aa1db97428b897e584698670c1b27854568d678e729200c0f".into(), - view: "ad1b4fd35270f5f36c4da7166672b347e75c3f4d41346ec2a06d1d0193632801".into(), - }, - Vector { - language: classic::Language::Japanese, - seed: "ぜんぶ どうぐ おたがい せんきょ おうじ そんちょう じゅしん いろえんぴつ \ - かほう つかれる えらぶ にちじょう くのう にちようび ぬまえび さんきゃく \ - おおや ちぬき うすめる いがく せつでん さうな すいえい せつだん おおや" - .into(), - spend: "c56e895cdb13007eda8399222974cdbab493640663804b93cbef3d8c3df80b0b".into(), - view: "6c3634a313ec2ee979d565c33888fd7c3502d696ce0134a8bc1a2698c7f2c508".into(), - }, - Vector { - language: classic::Language::Russian, - seed: "шатер икра нация ехать получать инерция доза реальный \ - рыжий таможня лопата душа веселый клетка атлас лекция \ - обгонять паек наивный лыжный дурак стать ежик задача паек" - .into(), - spend: "7cb5492df5eb2db4c84af20766391cd3e3662ab1a241c70fc881f3d02c381f05".into(), - view: "fcd53e41ec0df995ab43927f7c44bc3359c93523d5009fb3f5ba87431d545a03".into(), - }, - Vector { - language: classic::Language::Esperanto, - seed: "ukazo klini peco etikedo fabriko imitado onklino urino \ - pudro incidento kumuluso ikono smirgi hirundo uretro krii \ - sparkado super speciala pupo alpinisto cvana vokegi zombio fabriko" - .into(), - spend: "82ebf0336d3b152701964ed41df6b6e9a035e57fc98b84039ed0bd4611c58904".into(), - view: "cd4d120e1ea34360af528f6a3e6156063312d9cefc9aa6b5218d366c0ed6a201".into(), - }, - Vector { - language: classic::Language::Lojban, - seed: "jetnu vensa julne xrotu xamsi julne cutci dakli \ - mlatu xedja muvgau palpi xindo sfubu ciste cinri \ - blabi darno dembi janli blabi fenki bukpu burcu blabi" - .into(), - spend: "e4f8c6819ab6cf792cebb858caabac9307fd646901d72123e0367ebc0a79c200".into(), - view: "c806ce62bafaa7b2d597f1a1e2dbe4a2f96bfd804bf6f8420fc7f4a6bd700c00".into(), - }, - Vector { - language: classic::Language::EnglishOld, - seed: "glorious especially puff son moment add youth nowhere \ - throw glide grip wrong rhythm consume very swear \ - bitter heavy eventually begin reason flirt type unable" - .into(), - spend: "647f4765b66b636ff07170ab6280a9a6804dfbaf19db2ad37d23be024a18730b".into(), - view: "045da65316a906a8c30046053119c18020b07a7a3a6ef5c01ab2a8755416bd02".into(), - }, - // The following seeds require the language specification in order to calculate - // a single valid checksum - Vector { - language: classic::Language::Spanish, - seed: "pluma laico atraer pintor peor cerca balde buscar \ - lancha batir nulo reloj resto gemelo nevera poder columna gol \ - oveja latir amplio bolero feliz fuerza nevera" - .into(), - spend: "30303983fc8d215dd020cc6b8223793318d55c466a86e4390954f373fdc7200a".into(), - view: "97c649143f3c147ba59aa5506cc09c7992c5c219bb26964442142bf97980800e".into(), - }, - Vector { - language: classic::Language::Spanish, - seed: "pluma pluma pluma pluma pluma pluma pluma pluma \ - pluma pluma pluma pluma pluma pluma pluma pluma \ - pluma pluma pluma pluma pluma pluma pluma pluma pluma" - .into(), - spend: "b4050000b4050000b4050000b4050000b4050000b4050000b4050000b4050000".into(), - view: "d73534f7912b395eb70ef911791a2814eb6df7ce56528eaaa83ff2b72d9f5e0f".into(), - }, - Vector { - language: classic::Language::English, - seed: "plus plus plus plus plus plus plus plus \ - plus plus plus plus plus plus plus plus \ - plus plus plus plus plus plus plus plus plus" - .into(), - spend: "3b0400003b0400003b0400003b0400003b0400003b0400003b0400003b040000".into(), - view: "43a8a7715eed11eff145a2024ddcc39740255156da7bbd736ee66a0838053a02".into(), - }, - Vector { - language: classic::Language::Spanish, - seed: "audio audio audio audio audio audio audio audio \ - audio audio audio audio audio audio audio audio \ - audio audio audio audio audio audio audio audio audio" - .into(), - spend: "ba000000ba000000ba000000ba000000ba000000ba000000ba000000ba000000".into(), - view: "1437256da2c85d029b293d8c6b1d625d9374969301869b12f37186e3f906c708".into(), - }, - Vector { - language: classic::Language::English, - seed: "audio audio audio audio audio audio audio audio \ - audio audio audio audio audio audio audio audio \ - audio audio audio audio audio audio audio audio audio" - .into(), - spend: "7900000079000000790000007900000079000000790000007900000079000000".into(), - view: "20bec797ab96780ae6a045dd816676ca7ed1d7c6773f7022d03ad234b581d600".into(), - }, - ]; - - for vector in vectors { - let trim_seed = |seed: &str| { - seed - .split_whitespace() - .map(|word| trim_by_lang(word, vector.language)) - .collect::>() - .join(" ") - }; - - // Test against Monero - { - println!("{}. language: {:?}, seed: {}", line!(), vector.language, vector.seed.clone()); - let seed = - Seed::from_string(SeedType::Classic(vector.language), Zeroizing::new(vector.seed.clone())) - .unwrap(); - let trim = trim_seed(&vector.seed); - assert_eq!( - seed, - Seed::from_string(SeedType::Classic(vector.language), Zeroizing::new(trim)).unwrap() - ); - - let spend: [u8; 32] = hex::decode(vector.spend).unwrap().try_into().unwrap(); - // For classical seeds, Monero directly uses the entropy as a spend key - assert_eq!( - Option::::from(Scalar::from_canonical_bytes(*seed.entropy())), - Option::::from(Scalar::from_canonical_bytes(spend)), - ); - - let view: [u8; 32] = hex::decode(vector.view).unwrap().try_into().unwrap(); - // Monero then derives the view key as H(spend) - assert_eq!( - Scalar::from_bytes_mod_order(hash(&spend)), - Scalar::from_canonical_bytes(view).unwrap() - ); - - assert_eq!( - Seed::from_entropy(SeedType::Classic(vector.language), Zeroizing::new(spend), None) - .unwrap(), - seed - ); - } - - // Test against ourselves - { - let seed = Seed::new(&mut OsRng, SeedType::Classic(vector.language)); - println!("{}. seed: {}", line!(), *seed.to_string()); - let trim = trim_seed(&seed.to_string()); - assert_eq!( - seed, - Seed::from_string(SeedType::Classic(vector.language), Zeroizing::new(trim)).unwrap() - ); - assert_eq!( - seed, - Seed::from_entropy(SeedType::Classic(vector.language), seed.entropy(), None).unwrap() - ); - assert_eq!( - seed, - Seed::from_string(SeedType::Classic(vector.language), seed.to_string()).unwrap() - ); - } - } -} - -#[test] -fn test_polyseed() { - struct Vector { - language: polyseed::Language, - seed: String, - entropy: String, - birthday: u64, - has_prefix: bool, - has_accent: bool, - } - - let vectors = [ - Vector { - language: polyseed::Language::English, - seed: "raven tail swear infant grief assist regular lamp \ - duck valid someone little harsh puppy airport language" - .into(), - entropy: "dd76e7359a0ded37cd0ff0f3c829a5ae01673300000000000000000000000000".into(), - birthday: 1638446400, - has_prefix: true, - has_accent: false, - }, - Vector { - language: polyseed::Language::Spanish, - seed: "eje fin parte célebre tabú pestaña lienzo puma \ - prisión hora regalo lengua existir lápiz lote sonoro" - .into(), - entropy: "5a2b02df7db21fcbe6ec6df137d54c7b20fd2b00000000000000000000000000".into(), - birthday: 3118651200, - has_prefix: true, - has_accent: true, - }, - Vector { - language: polyseed::Language::French, - seed: "valable arracher décaler jeudi amusant dresser mener épaissir risible \ - prouesse réserve ampleur ajuster muter caméra enchère" - .into(), - entropy: "11cfd870324b26657342c37360c424a14a050b00000000000000000000000000".into(), - birthday: 1679314966, - has_prefix: true, - has_accent: true, - }, - Vector { - language: polyseed::Language::Italian, - seed: "caduco midollo copione meninge isotopo illogico riflesso tartaruga fermento \ - olandese normale tristezza episodio voragine forbito achille" - .into(), - entropy: "7ecc57c9b4652d4e31428f62bec91cfd55500600000000000000000000000000".into(), - birthday: 1679316358, - has_prefix: true, - has_accent: false, - }, - Vector { - language: polyseed::Language::Portuguese, - seed: "caverna custear azedo adeus senador apertada sedoso omitir \ - sujeito aurora videira molho cartaz gesso dentista tapar" - .into(), - entropy: "45473063711376cae38f1b3eba18c874124e1d00000000000000000000000000".into(), - birthday: 1679316657, - has_prefix: true, - has_accent: false, - }, - Vector { - language: polyseed::Language::Czech, - seed: "usmrtit nora dotaz komunita zavalit funkce mzda sotva akce \ - vesta kabel herna stodola uvolnit ustrnout email" - .into(), - entropy: "7ac8a4efd62d9c3c4c02e350d32326df37821c00000000000000000000000000".into(), - birthday: 1679316898, - has_prefix: true, - has_accent: false, - }, - Vector { - language: polyseed::Language::Korean, - seed: "전망 선풍기 국제 무궁화 설사 기름 이론적 해안 절망 예선 \ - 지우개 보관 절망 말기 시각 귀신" - .into(), - entropy: "684663fda420298f42ed94b2c512ed38ddf12b00000000000000000000000000".into(), - birthday: 1679317073, - has_prefix: false, - has_accent: false, - }, - Vector { - language: polyseed::Language::Japanese, - seed: "うちあわせ ちつじょ つごう しはい けんこう とおる てみやげ はんとし たんとう \ - といれ おさない おさえる むかう ぬぐう なふだ せまる" - .into(), - entropy: "94e6665518a6286c6e3ba508a2279eb62b771f00000000000000000000000000".into(), - birthday: 1679318722, - has_prefix: false, - has_accent: false, - }, - Vector { - language: polyseed::Language::ChineseTraditional, - seed: "亂 挖 斤 柄 代 圈 枝 轄 魯 論 函 開 勘 番 榮 壁".into(), - entropy: "b1594f585987ab0fd5a31da1f0d377dae5283f00000000000000000000000000".into(), - birthday: 1679426433, - has_prefix: false, - has_accent: false, - }, - Vector { - language: polyseed::Language::ChineseSimplified, - seed: "啊 百 族 府 票 划 伪 仓 叶 虾 借 溜 晨 左 等 鬼".into(), - entropy: "21cdd366f337b89b8d1bc1df9fe73047c22b0300000000000000000000000000".into(), - birthday: 1679426817, - has_prefix: false, - has_accent: false, - }, - // The following seed requires the language specification in order to calculate - // a single valid checksum - Vector { - language: polyseed::Language::Spanish, - seed: "impo sort usua cabi venu nobl oliv clim \ - cont barr marc auto prod vaca torn fati" - .into(), - entropy: "dbfce25fe09b68a340e01c62417eeef43ad51800000000000000000000000000".into(), - birthday: 1701511650, - has_prefix: true, - has_accent: true, - }, - ]; - - for vector in vectors { - let add_whitespace = |mut seed: String| { - seed.push(' '); - seed - }; - - let seed_without_accents = |seed: &str| { - seed - .split_whitespace() - .map(|w| w.chars().filter(char::is_ascii).collect::()) - .collect::>() - .join(" ") - }; - - let trim_seed = |seed: &str| { - let seed_to_trim = - if vector.has_accent { seed_without_accents(seed) } else { seed.to_string() }; - seed_to_trim - .split_whitespace() - .map(|w| { - let mut ascii = 0; - let mut to_take = w.len(); - for (i, char) in w.chars().enumerate() { - if char.is_ascii() { - ascii += 1; - } - if ascii == polyseed::PREFIX_LEN { - // +1 to include this character, which put us at the prefix length - to_take = i + 1; - break; - } - } - w.chars().take(to_take).collect::() - }) - .collect::>() - .join(" ") - }; - - // String -> Seed - println!("{}. language: {:?}, seed: {}", line!(), vector.language, vector.seed.clone()); - let seed = - Seed::from_string(SeedType::Polyseed(vector.language), Zeroizing::new(vector.seed.clone())) - .unwrap(); - let trim = trim_seed(&vector.seed); - let add_whitespace = add_whitespace(vector.seed.clone()); - let seed_without_accents = seed_without_accents(&vector.seed); - - // Make sure a version with added whitespace still works - let whitespaced_seed = - Seed::from_string(SeedType::Polyseed(vector.language), Zeroizing::new(add_whitespace)) - .unwrap(); - assert_eq!(seed, whitespaced_seed); - // Check trimmed versions works - if vector.has_prefix { - let trimmed_seed = - Seed::from_string(SeedType::Polyseed(vector.language), Zeroizing::new(trim)).unwrap(); - assert_eq!(seed, trimmed_seed); - } - // Check versions without accents work - if vector.has_accent { - let seed_without_accents = Seed::from_string( - SeedType::Polyseed(vector.language), - Zeroizing::new(seed_without_accents), - ) - .unwrap(); - assert_eq!(seed, seed_without_accents); - } - - let entropy = Zeroizing::new(hex::decode(vector.entropy).unwrap().try_into().unwrap()); - assert_eq!(seed.entropy(), entropy); - assert!(seed.birthday().abs_diff(vector.birthday) < polyseed::TIME_STEP); - - // Entropy -> Seed - let from_entropy = - Seed::from_entropy(SeedType::Polyseed(vector.language), entropy, Some(seed.birthday())) - .unwrap(); - assert_eq!(seed.to_string(), from_entropy.to_string()); - - // Check against ourselves - { - let seed = Seed::new(&mut OsRng, SeedType::Polyseed(vector.language)); - println!("{}. seed: {}", line!(), *seed.to_string()); - assert_eq!( - seed, - Seed::from_string(SeedType::Polyseed(vector.language), seed.to_string()).unwrap() - ); - assert_eq!( - seed, - Seed::from_entropy( - SeedType::Polyseed(vector.language), - seed.entropy(), - Some(seed.birthday()) - ) - .unwrap() - ); - } - } -} - -#[test] -fn test_invalid_polyseed() { - // This seed includes unsupported features bits and should error on decode - let seed = "include domain claim resemble urban hire lunch bird \ - crucial fire best wife ring warm ignore model" - .into(); - let res = - Seed::from_string(SeedType::Polyseed(polyseed::Language::English), Zeroizing::new(seed)); - assert_eq!(res, Err(SeedError::UnsupportedFeatures)); -} diff --git a/coins/monero/src/transaction.rs b/coins/monero/src/transaction.rs deleted file mode 100644 index 89d489fe..00000000 --- a/coins/monero/src/transaction.rs +++ /dev/null @@ -1,432 +0,0 @@ -use core::cmp::Ordering; -use std_shims::{ - vec::Vec, - io::{self, Read, Write}, -}; - -use zeroize::Zeroize; - -use curve25519_dalek::edwards::{EdwardsPoint, CompressedEdwardsY}; - -use crate::{ - Protocol, hash, - serialize::*, - ring_signatures::RingSignature, - ringct::{bulletproofs::Bulletproofs, RctType, RctBase, RctPrunable, RctSignatures}, -}; - -#[derive(Clone, PartialEq, Eq, Debug)] -pub enum Input { - Gen(u64), - ToKey { amount: Option, key_offsets: Vec, key_image: EdwardsPoint }, -} - -impl Input { - pub(crate) fn fee_weight(offsets_weight: usize) -> usize { - // Uses 1 byte for the input type - // Uses 1 byte for the VarInt amount due to amount being 0 - 1 + 1 + offsets_weight + 32 - } - - pub fn write(&self, w: &mut W) -> io::Result<()> { - match self { - Input::Gen(height) => { - w.write_all(&[255])?; - write_varint(height, w) - } - - Input::ToKey { amount, key_offsets, key_image } => { - w.write_all(&[2])?; - write_varint(&amount.unwrap_or(0), w)?; - write_vec(write_varint, key_offsets, w)?; - write_point(key_image, w) - } - } - } - - pub fn serialize(&self) -> Vec { - let mut res = vec![]; - self.write(&mut res).unwrap(); - res - } - - pub fn read(r: &mut R) -> io::Result { - Ok(match read_byte(r)? { - 255 => Input::Gen(read_varint(r)?), - 2 => { - let amount = read_varint(r)?; - // https://github.com/monero-project/monero/ - // blob/00fd416a99686f0956361d1cd0337fe56e58d4a7/ - // src/cryptonote_basic/cryptonote_format_utils.cpp#L860-L863 - // A non-RCT 0-amount input can't exist because only RCT TXs can have a 0-amount output - // That's why collapsing to None if the amount is 0 is safe, even without knowing if RCT - let amount = if amount == 0 { None } else { Some(amount) }; - Input::ToKey { - amount, - key_offsets: read_vec(read_varint, r)?, - key_image: read_torsion_free_point(r)?, - } - } - _ => Err(io::Error::other("Tried to deserialize unknown/unused input type"))?, - }) - } -} - -// Doesn't bother moving to an enum for the unused Script classes -#[derive(Clone, PartialEq, Eq, Debug)] -pub struct Output { - pub amount: Option, - pub key: CompressedEdwardsY, - pub view_tag: Option, -} - -impl Output { - pub(crate) fn fee_weight(view_tags: bool) -> usize { - // Uses 1 byte for the output type - // Uses 1 byte for the VarInt amount due to amount being 0 - 1 + 1 + 32 + if view_tags { 1 } else { 0 } - } - - pub fn write(&self, w: &mut W) -> io::Result<()> { - write_varint(&self.amount.unwrap_or(0), w)?; - w.write_all(&[2 + u8::from(self.view_tag.is_some())])?; - w.write_all(&self.key.to_bytes())?; - if let Some(view_tag) = self.view_tag { - w.write_all(&[view_tag])?; - } - Ok(()) - } - - pub fn serialize(&self) -> Vec { - let mut res = Vec::with_capacity(8 + 1 + 32); - self.write(&mut res).unwrap(); - res - } - - pub fn read(rct: bool, r: &mut R) -> io::Result { - let amount = read_varint(r)?; - let amount = if rct { - if amount != 0 { - Err(io::Error::other("RCT TX output wasn't 0"))?; - } - None - } else { - Some(amount) - }; - - let view_tag = match read_byte(r)? { - 2 => false, - 3 => true, - _ => Err(io::Error::other("Tried to deserialize unknown/unused output type"))?, - }; - - Ok(Output { - amount, - key: CompressedEdwardsY(read_bytes(r)?), - view_tag: if view_tag { Some(read_byte(r)?) } else { None }, - }) - } -} - -#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)] -pub enum Timelock { - None, - Block(usize), - Time(u64), -} - -impl Timelock { - fn from_raw(raw: u64) -> Timelock { - if raw == 0 { - Timelock::None - } else if raw < 500_000_000 { - Timelock::Block(usize::try_from(raw).unwrap()) - } else { - Timelock::Time(raw) - } - } - - fn write(&self, w: &mut W) -> io::Result<()> { - write_varint( - &match self { - Timelock::None => 0, - Timelock::Block(block) => (*block).try_into().unwrap(), - Timelock::Time(time) => *time, - }, - w, - ) - } -} - -impl PartialOrd for Timelock { - fn partial_cmp(&self, other: &Self) -> Option { - match (self, other) { - (Timelock::None, Timelock::None) => Some(Ordering::Equal), - (Timelock::None, _) => Some(Ordering::Less), - (_, Timelock::None) => Some(Ordering::Greater), - (Timelock::Block(a), Timelock::Block(b)) => a.partial_cmp(b), - (Timelock::Time(a), Timelock::Time(b)) => a.partial_cmp(b), - _ => None, - } - } -} - -#[derive(Clone, PartialEq, Eq, Debug)] -pub struct TransactionPrefix { - pub version: u64, - pub timelock: Timelock, - pub inputs: Vec, - pub outputs: Vec, - pub extra: Vec, -} - -impl TransactionPrefix { - pub(crate) fn fee_weight( - decoy_weights: &[usize], - outputs: usize, - view_tags: bool, - extra: usize, - ) -> usize { - // Assumes Timelock::None since this library won't let you create a TX with a timelock - // 1 input for every decoy weight - 1 + 1 + - varint_len(decoy_weights.len()) + - decoy_weights.iter().map(|&offsets_weight| Input::fee_weight(offsets_weight)).sum::() + - varint_len(outputs) + - (outputs * Output::fee_weight(view_tags)) + - varint_len(extra) + - extra - } - - pub fn write(&self, w: &mut W) -> io::Result<()> { - write_varint(&self.version, w)?; - self.timelock.write(w)?; - write_vec(Input::write, &self.inputs, w)?; - write_vec(Output::write, &self.outputs, w)?; - write_varint(&self.extra.len(), w)?; - w.write_all(&self.extra) - } - - pub fn serialize(&self) -> Vec { - let mut res = vec![]; - self.write(&mut res).unwrap(); - res - } - - pub fn read(r: &mut R) -> io::Result { - let version = read_varint(r)?; - // TODO: Create an enum out of version - if (version == 0) || (version > 2) { - Err(io::Error::other("unrecognized transaction version"))?; - } - - let timelock = Timelock::from_raw(read_varint(r)?); - - let inputs = read_vec(|r| Input::read(r), r)?; - if inputs.is_empty() { - Err(io::Error::other("transaction had no inputs"))?; - } - let is_miner_tx = matches!(inputs[0], Input::Gen { .. }); - - let mut prefix = TransactionPrefix { - version, - timelock, - inputs, - outputs: read_vec(|r| Output::read((!is_miner_tx) && (version == 2), r), r)?, - extra: vec![], - }; - prefix.extra = read_vec(read_byte, r)?; - Ok(prefix) - } - - pub fn hash(&self) -> [u8; 32] { - hash(&self.serialize()) - } -} - -/// Monero transaction. For version 1, rct_signatures still contains an accurate fee value. -#[derive(Clone, PartialEq, Eq, Debug)] -pub struct Transaction { - pub prefix: TransactionPrefix, - pub signatures: Vec, - pub rct_signatures: RctSignatures, -} - -impl Transaction { - pub(crate) fn fee_weight( - protocol: Protocol, - decoy_weights: &[usize], - outputs: usize, - extra: usize, - fee: u64, - ) -> usize { - TransactionPrefix::fee_weight(decoy_weights, outputs, protocol.view_tags(), extra) + - RctSignatures::fee_weight(protocol, decoy_weights.len(), outputs, fee) - } - - pub fn write(&self, w: &mut W) -> io::Result<()> { - self.prefix.write(w)?; - if self.prefix.version == 1 { - for ring_sig in &self.signatures { - ring_sig.write(w)?; - } - Ok(()) - } else if self.prefix.version == 2 { - self.rct_signatures.write(w) - } else { - panic!("Serializing a transaction with an unknown version"); - } - } - - pub fn serialize(&self) -> Vec { - let mut res = Vec::with_capacity(2048); - self.write(&mut res).unwrap(); - res - } - - pub fn read(r: &mut R) -> io::Result { - let prefix = TransactionPrefix::read(r)?; - let mut signatures = vec![]; - let mut rct_signatures = RctSignatures { - base: RctBase { fee: 0, encrypted_amounts: vec![], pseudo_outs: vec![], commitments: vec![] }, - prunable: RctPrunable::Null, - }; - - if prefix.version == 1 { - signatures = prefix - .inputs - .iter() - .filter_map(|input| match input { - Input::ToKey { key_offsets, .. } => Some(RingSignature::read(key_offsets.len(), r)), - _ => None, - }) - .collect::>()?; - - if !matches!(prefix.inputs[0], Input::Gen(..)) { - let in_amount = prefix - .inputs - .iter() - .map(|input| match input { - Input::Gen(..) => Err(io::Error::other("Input::Gen present in non-coinbase v1 TX"))?, - // v1 TXs can burn v2 outputs - // dcff3fe4f914d6b6bd4a5b800cc4cca8f2fdd1bd73352f0700d463d36812f328 is one such TX - // It includes a pre-RCT signature for a RCT output, yet if you interpret the RCT - // output as being worth 0, it passes a sum check (guaranteed since no outputs are RCT) - Input::ToKey { amount, .. } => Ok(amount.unwrap_or(0)), - }) - .collect::>>()? - .into_iter() - .sum::(); - - let mut out = 0; - for output in &prefix.outputs { - if output.amount.is_none() { - Err(io::Error::other("v1 transaction had a 0-amount output"))?; - } - out += output.amount.unwrap(); - } - - if in_amount < out { - Err(io::Error::other("transaction spent more than it had as inputs"))?; - } - rct_signatures.base.fee = in_amount - out; - } - } else if prefix.version == 2 { - rct_signatures = RctSignatures::read( - prefix.inputs.first().map_or(0, |input| match input { - Input::Gen(_) => 0, - Input::ToKey { key_offsets, .. } => key_offsets.len(), - }), - prefix.inputs.len(), - prefix.outputs.len(), - r, - )?; - } else { - Err(io::Error::other("Tried to deserialize unknown version"))?; - } - - Ok(Transaction { prefix, signatures, rct_signatures }) - } - - pub fn hash(&self) -> [u8; 32] { - let mut buf = Vec::with_capacity(2048); - if self.prefix.version == 1 { - self.write(&mut buf).unwrap(); - hash(&buf) - } else { - let mut hashes = Vec::with_capacity(96); - - hashes.extend(self.prefix.hash()); - - self.rct_signatures.base.write(&mut buf, self.rct_signatures.rct_type()).unwrap(); - hashes.extend(hash(&buf)); - buf.clear(); - - hashes.extend(&match self.rct_signatures.prunable { - RctPrunable::Null => [0; 32], - _ => { - self.rct_signatures.prunable.write(&mut buf, self.rct_signatures.rct_type()).unwrap(); - hash(&buf) - } - }); - - hash(&hashes) - } - } - - /// Calculate the hash of this transaction as needed for signing it. - pub fn signature_hash(&self) -> [u8; 32] { - if self.prefix.version == 1 { - return self.prefix.hash(); - } - - let mut buf = Vec::with_capacity(2048); - let mut sig_hash = Vec::with_capacity(96); - - sig_hash.extend(self.prefix.hash()); - - self.rct_signatures.base.write(&mut buf, self.rct_signatures.rct_type()).unwrap(); - sig_hash.extend(hash(&buf)); - buf.clear(); - - self.rct_signatures.prunable.signature_write(&mut buf).unwrap(); - sig_hash.extend(hash(&buf)); - - hash(&sig_hash) - } - - fn is_rct_bulletproof(&self) -> bool { - match &self.rct_signatures.rct_type() { - RctType::Bulletproofs | RctType::BulletproofsCompactAmount | RctType::Clsag => true, - RctType::Null | - RctType::MlsagAggregate | - RctType::MlsagIndividual | - RctType::BulletproofsPlus => false, - } - } - - fn is_rct_bulletproof_plus(&self) -> bool { - match &self.rct_signatures.rct_type() { - RctType::BulletproofsPlus => true, - RctType::Null | - RctType::MlsagAggregate | - RctType::MlsagIndividual | - RctType::Bulletproofs | - RctType::BulletproofsCompactAmount | - RctType::Clsag => false, - } - } - - /// Calculate the transaction's weight. - pub fn weight(&self) -> usize { - let blob_size = self.serialize().len(); - - let bp = self.is_rct_bulletproof(); - let bp_plus = self.is_rct_bulletproof_plus(); - if !(bp || bp_plus) { - blob_size - } else { - blob_size + Bulletproofs::calculate_bp_clawback(bp_plus, self.prefix.outputs.len()).0 - } - } -} diff --git a/coins/monero/src/wallet/address.rs b/coins/monero/src/wallet/address.rs deleted file mode 100644 index d080488d..00000000 --- a/coins/monero/src/wallet/address.rs +++ /dev/null @@ -1,325 +0,0 @@ -use core::{marker::PhantomData, fmt}; -use std_shims::string::ToString; - -use zeroize::Zeroize; - -use curve25519_dalek::edwards::EdwardsPoint; - -use monero_generators::decompress_point; - -use base58_monero::base58::{encode_check, decode_check}; - -/// The network this address is for. -#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)] -pub enum Network { - Mainnet, - Testnet, - Stagenet, -} - -/// The address type, supporting the officially documented addresses, along with -/// [Featured Addresses](https://gist.github.com/kayabaNerve/01c50bbc35441e0bbdcee63a9d823789). -#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)] -pub enum AddressType { - Standard, - Integrated([u8; 8]), - Subaddress, - Featured { subaddress: bool, payment_id: Option<[u8; 8]>, guaranteed: bool }, -} - -#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)] -pub struct SubaddressIndex { - pub(crate) account: u32, - pub(crate) address: u32, -} - -impl SubaddressIndex { - pub const fn new(account: u32, address: u32) -> Option { - if (account == 0) && (address == 0) { - return None; - } - Some(SubaddressIndex { account, address }) - } - - pub fn account(&self) -> u32 { - self.account - } - - pub fn address(&self) -> u32 { - self.address - } -} - -/// Address specification. Used internally to create addresses. -#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)] -pub enum AddressSpec { - Standard, - Integrated([u8; 8]), - Subaddress(SubaddressIndex), - Featured { subaddress: Option, payment_id: Option<[u8; 8]>, guaranteed: bool }, -} - -impl AddressType { - pub fn is_subaddress(&self) -> bool { - matches!(self, AddressType::Subaddress) || - matches!(self, AddressType::Featured { subaddress: true, .. }) - } - - pub fn payment_id(&self) -> Option<[u8; 8]> { - if let AddressType::Integrated(id) = self { - Some(*id) - } else if let AddressType::Featured { payment_id, .. } = self { - *payment_id - } else { - None - } - } - - pub fn is_guaranteed(&self) -> bool { - matches!(self, AddressType::Featured { guaranteed: true, .. }) - } -} - -/// A type which returns the byte for a given address. -pub trait AddressBytes: Clone + Copy + PartialEq + Eq + fmt::Debug { - fn network_bytes(network: Network) -> (u8, u8, u8, u8); -} - -/// Address bytes for Monero. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub struct MoneroAddressBytes; -impl AddressBytes for MoneroAddressBytes { - fn network_bytes(network: Network) -> (u8, u8, u8, u8) { - match network { - Network::Mainnet => (18, 19, 42, 70), - Network::Testnet => (53, 54, 63, 111), - Network::Stagenet => (24, 25, 36, 86), - } - } -} - -/// Address metadata. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub struct AddressMeta { - _bytes: PhantomData, - pub network: Network, - pub kind: AddressType, -} - -impl Zeroize for AddressMeta { - fn zeroize(&mut self) { - self.network.zeroize(); - self.kind.zeroize(); - } -} - -/// Error when decoding an address. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -#[cfg_attr(feature = "std", derive(thiserror::Error))] -pub enum AddressError { - #[cfg_attr(feature = "std", error("invalid address byte"))] - InvalidByte, - #[cfg_attr(feature = "std", error("invalid address encoding"))] - InvalidEncoding, - #[cfg_attr(feature = "std", error("invalid length"))] - InvalidLength, - #[cfg_attr(feature = "std", error("invalid key"))] - InvalidKey, - #[cfg_attr(feature = "std", error("unknown features"))] - UnknownFeatures, - #[cfg_attr(feature = "std", error("different network than expected"))] - DifferentNetwork, -} - -impl AddressMeta { - #[allow(clippy::wrong_self_convention)] - fn to_byte(&self) -> u8 { - let bytes = B::network_bytes(self.network); - match self.kind { - AddressType::Standard => bytes.0, - AddressType::Integrated(_) => bytes.1, - AddressType::Subaddress => bytes.2, - AddressType::Featured { .. } => bytes.3, - } - } - - /// Create an address's metadata. - pub fn new(network: Network, kind: AddressType) -> Self { - AddressMeta { _bytes: PhantomData, network, kind } - } - - // Returns an incomplete instantiation in the case of Integrated/Featured addresses - fn from_byte(byte: u8) -> Result { - let mut meta = None; - for network in [Network::Mainnet, Network::Testnet, Network::Stagenet] { - let (standard, integrated, subaddress, featured) = B::network_bytes(network); - if let Some(kind) = match byte { - _ if byte == standard => Some(AddressType::Standard), - _ if byte == integrated => Some(AddressType::Integrated([0; 8])), - _ if byte == subaddress => Some(AddressType::Subaddress), - _ if byte == featured => { - Some(AddressType::Featured { subaddress: false, payment_id: None, guaranteed: false }) - } - _ => None, - } { - meta = Some(AddressMeta::new(network, kind)); - break; - } - } - - meta.ok_or(AddressError::InvalidByte) - } - - pub fn is_subaddress(&self) -> bool { - self.kind.is_subaddress() - } - - pub fn payment_id(&self) -> Option<[u8; 8]> { - self.kind.payment_id() - } - - pub fn is_guaranteed(&self) -> bool { - self.kind.is_guaranteed() - } -} - -/// A Monero address, composed of metadata and a spend/view key. -#[derive(Clone, Copy, PartialEq, Eq)] -pub struct Address { - pub meta: AddressMeta, - pub spend: EdwardsPoint, - pub view: EdwardsPoint, -} - -impl fmt::Debug for Address { - fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { - fmt - .debug_struct("Address") - .field("meta", &self.meta) - .field("spend", &hex::encode(self.spend.compress().0)) - .field("view", &hex::encode(self.view.compress().0)) - // This is not a real field yet is the most valuable thing to know when debugging - .field("(address)", &self.to_string()) - .finish() - } -} - -impl Zeroize for Address { - fn zeroize(&mut self) { - self.meta.zeroize(); - self.spend.zeroize(); - self.view.zeroize(); - } -} - -impl fmt::Display for Address { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut data = vec![self.meta.to_byte()]; - data.extend(self.spend.compress().to_bytes()); - data.extend(self.view.compress().to_bytes()); - if let AddressType::Featured { subaddress, payment_id, guaranteed } = self.meta.kind { - // Technically should be a VarInt, yet we don't have enough features it's needed - data.push( - u8::from(subaddress) + (u8::from(payment_id.is_some()) << 1) + (u8::from(guaranteed) << 2), - ); - } - if let Some(id) = self.meta.kind.payment_id() { - data.extend(id); - } - write!(f, "{}", encode_check(&data).unwrap()) - } -} - -impl Address { - pub fn new(meta: AddressMeta, spend: EdwardsPoint, view: EdwardsPoint) -> Self { - Address { meta, spend, view } - } - - pub fn from_str_raw(s: &str) -> Result { - let raw = decode_check(s).map_err(|_| AddressError::InvalidEncoding)?; - if raw.len() < (1 + 32 + 32) { - Err(AddressError::InvalidLength)?; - } - - let mut meta = AddressMeta::from_byte(raw[0])?; - let spend = - decompress_point(raw[1 .. 33].try_into().unwrap()).ok_or(AddressError::InvalidKey)?; - let view = - decompress_point(raw[33 .. 65].try_into().unwrap()).ok_or(AddressError::InvalidKey)?; - let mut read = 65; - - if matches!(meta.kind, AddressType::Featured { .. }) { - if raw[read] >= (2 << 3) { - Err(AddressError::UnknownFeatures)?; - } - - let subaddress = (raw[read] & 1) == 1; - let integrated = ((raw[read] >> 1) & 1) == 1; - let guaranteed = ((raw[read] >> 2) & 1) == 1; - - meta.kind = AddressType::Featured { - subaddress, - payment_id: Some([0; 8]).filter(|_| integrated), - guaranteed, - }; - read += 1; - } - - // Update read early so we can verify the length - if meta.kind.payment_id().is_some() { - read += 8; - } - if raw.len() != read { - Err(AddressError::InvalidLength)?; - } - - if let AddressType::Integrated(ref mut id) = meta.kind { - id.copy_from_slice(&raw[(read - 8) .. read]); - } - if let AddressType::Featured { payment_id: Some(ref mut id), .. } = meta.kind { - id.copy_from_slice(&raw[(read - 8) .. read]); - } - - Ok(Address { meta, spend, view }) - } - - pub fn from_str(network: Network, s: &str) -> Result { - Self::from_str_raw(s).and_then(|addr| { - if addr.meta.network == network { - Ok(addr) - } else { - Err(AddressError::DifferentNetwork)? - } - }) - } - - pub fn network(&self) -> Network { - self.meta.network - } - - pub fn is_subaddress(&self) -> bool { - self.meta.is_subaddress() - } - - pub fn payment_id(&self) -> Option<[u8; 8]> { - self.meta.payment_id() - } - - pub fn is_guaranteed(&self) -> bool { - self.meta.is_guaranteed() - } -} - -/// Instantiation of the Address type with Monero's network bytes. -pub type MoneroAddress = Address; -// Allow re-interpreting of an arbitrary address as a monero address so it can be used with the -// rest of this library. Doesn't use From as it was conflicting with From for T. -impl MoneroAddress { - pub fn from(address: Address) -> MoneroAddress { - MoneroAddress::new( - AddressMeta::new(address.meta.network, address.meta.kind), - address.spend, - address.view, - ) - } -} diff --git a/coins/monero/src/wallet/decoys.rs b/coins/monero/src/wallet/decoys.rs deleted file mode 100644 index b0282f37..00000000 --- a/coins/monero/src/wallet/decoys.rs +++ /dev/null @@ -1,356 +0,0 @@ -use std_shims::{vec::Vec, collections::HashSet}; - -#[cfg(feature = "cache-distribution")] -use std_shims::sync::OnceLock; - -#[cfg(all(feature = "cache-distribution", not(feature = "std")))] -use std_shims::sync::Mutex; -#[cfg(all(feature = "cache-distribution", feature = "std"))] -use async_lock::Mutex; - -use zeroize::{Zeroize, ZeroizeOnDrop}; - -use rand_core::{RngCore, CryptoRng}; -use rand_distr::{Distribution, Gamma}; -#[cfg(not(feature = "std"))] -use rand_distr::num_traits::Float; - -use curve25519_dalek::edwards::EdwardsPoint; - -use crate::{ - serialize::varint_len, - wallet::SpendableOutput, - rpc::{RpcError, RpcConnection, Rpc}, - DEFAULT_LOCK_WINDOW, COINBASE_LOCK_WINDOW, BLOCK_TIME, -}; - -const RECENT_WINDOW: usize = 15; -const BLOCKS_PER_YEAR: usize = 365 * 24 * 60 * 60 / BLOCK_TIME; -#[allow(clippy::cast_precision_loss)] -const TIP_APPLICATION: f64 = (DEFAULT_LOCK_WINDOW * BLOCK_TIME) as f64; - -// TODO: Resolve safety of this in case a reorg occurs/the network changes -// TODO: Update this when scanning a block, as possible -#[cfg(feature = "cache-distribution")] -static DISTRIBUTION_CELL: OnceLock>> = OnceLock::new(); -#[cfg(feature = "cache-distribution")] -#[allow(non_snake_case)] -fn DISTRIBUTION() -> &'static Mutex> { - DISTRIBUTION_CELL.get_or_init(|| Mutex::new(Vec::with_capacity(3000000))) -} - -#[allow(clippy::too_many_arguments)] -async fn select_n<'a, R: RngCore + CryptoRng, RPC: RpcConnection>( - rng: &mut R, - rpc: &Rpc, - distribution: &[u64], - height: usize, - high: u64, - per_second: f64, - real: &[u64], - used: &mut HashSet, - count: usize, - fingerprintable_canonical: bool, -) -> Result, RpcError> { - // TODO: consider removing this extra RPC and expect the caller to handle it - if fingerprintable_canonical && height > rpc.get_height().await? { - // TODO: Don't use InternalError for the caller's failure - Err(RpcError::InternalError("decoys being requested from too young blocks"))?; - } - - #[cfg(test)] - let mut iters = 0; - let mut confirmed = Vec::with_capacity(count); - // Retries on failure. Retries are obvious as decoys, yet should be minimal - while confirmed.len() != count { - let remaining = count - confirmed.len(); - // TODO: over-request candidates in case some are locked to avoid needing - // round trips to the daemon (and revealing obvious decoys to the daemon) - let mut candidates = Vec::with_capacity(remaining); - while candidates.len() != remaining { - #[cfg(test)] - { - iters += 1; - // This is cheap and on fresh chains, a lot of rounds may be needed - if iters == 100 { - Err(RpcError::InternalError("hit decoy selection round limit"))?; - } - } - - // Use a gamma distribution - let mut age = Gamma::::new(19.28, 1.0 / 1.61).unwrap().sample(rng).exp(); - #[allow(clippy::cast_precision_loss)] - if age > TIP_APPLICATION { - age -= TIP_APPLICATION; - } else { - // f64 does not have try_from available, which is why these are written with `as` - age = (rng.next_u64() % u64::try_from(RECENT_WINDOW * BLOCK_TIME).unwrap()) as f64; - } - - #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)] - let o = (age * per_second) as u64; - if o < high { - let i = distribution.partition_point(|s| *s < (high - 1 - o)); - let prev = i.saturating_sub(1); - let n = distribution[i] - distribution[prev]; - if n != 0 { - let o = distribution[prev] + (rng.next_u64() % n); - if !used.contains(&o) { - // It will either actually be used, or is unusable and this prevents trying it again - used.insert(o); - candidates.push(o); - } - } - } - } - - // If this is the first time we're requesting these outputs, include the real one as well - // Prevents the node we're connected to from having a list of known decoys and then seeing a - // TX which uses all of them, with one additional output (the true spend) - let mut real_indexes = HashSet::with_capacity(real.len()); - if confirmed.is_empty() { - for real in real { - candidates.push(*real); - } - // Sort candidates so the real spends aren't the ones at the end - candidates.sort(); - for real in real { - real_indexes.insert(candidates.binary_search(real).unwrap()); - } - } - - // TODO: make sure that the real output is included in the response, and - // that mask and key are equal to expected - for (i, output) in rpc - .get_unlocked_outputs(&candidates, height, fingerprintable_canonical) - .await? - .iter_mut() - .enumerate() - { - // Don't include the real spend as a decoy, despite requesting it - if real_indexes.contains(&i) { - continue; - } - - if let Some(output) = output.take() { - confirmed.push((candidates[i], output)); - } - } - } - - Ok(confirmed) -} - -fn offset(ring: &[u64]) -> Vec { - let mut res = vec![ring[0]]; - res.resize(ring.len(), 0); - for m in (1 .. ring.len()).rev() { - res[m] = ring[m] - ring[m - 1]; - } - res -} - -async fn select_decoys( - rng: &mut R, - rpc: &Rpc, - ring_len: usize, - height: usize, - inputs: &[SpendableOutput], - fingerprintable_canonical: bool, -) -> Result, RpcError> { - #[cfg(feature = "cache-distribution")] - #[cfg(not(feature = "std"))] - let mut distribution = DISTRIBUTION().lock(); - #[cfg(feature = "cache-distribution")] - #[cfg(feature = "std")] - let mut distribution = DISTRIBUTION().lock().await; - - #[cfg(not(feature = "cache-distribution"))] - let mut distribution = vec![]; - - let decoy_count = ring_len - 1; - - // Convert the inputs in question to the raw output data - let mut real = Vec::with_capacity(inputs.len()); - let mut outputs = Vec::with_capacity(inputs.len()); - for input in inputs { - real.push(input.global_index); - outputs.push((real[real.len() - 1], [input.key(), input.commitment().calculate()])); - } - - if distribution.len() < height { - // TODO: verify distribution elems are strictly increasing - let extension = - rpc.get_output_distribution(distribution.len(), height.saturating_sub(1)).await?; - distribution.extend(extension); - } - // If asked to use an older height than previously asked, truncate to ensure accuracy - // Should never happen, yet risks desyncing if it did - distribution.truncate(height); - - if distribution.len() < DEFAULT_LOCK_WINDOW { - Err(RpcError::InternalError("not enough decoy candidates"))?; - } - - #[allow(clippy::cast_precision_loss)] - let per_second = { - let blocks = distribution.len().min(BLOCKS_PER_YEAR); - let initial = distribution[distribution.len().saturating_sub(blocks + 1)]; - let outputs = distribution[distribution.len() - 1].saturating_sub(initial); - (outputs as f64) / ((blocks * BLOCK_TIME) as f64) - }; - - let mut used = HashSet::::new(); - for o in &outputs { - used.insert(o.0); - } - - // TODO: Create a TX with less than the target amount, as allowed by the protocol - let high = distribution[distribution.len() - DEFAULT_LOCK_WINDOW]; - if high.saturating_sub(COINBASE_LOCK_WINDOW as u64) < - u64::try_from(inputs.len() * ring_len).unwrap() - { - Err(RpcError::InternalError("not enough coinbase candidates"))?; - } - - // Select all decoys for this transaction, assuming we generate a sane transaction - // We should almost never naturally generate an insane transaction, hence why this doesn't - // bother with an overage - let mut decoys = select_n( - rng, - rpc, - &distribution, - height, - high, - per_second, - &real, - &mut used, - inputs.len() * decoy_count, - fingerprintable_canonical, - ) - .await?; - real.zeroize(); - - let mut res = Vec::with_capacity(inputs.len()); - for o in outputs { - // Grab the decoys for this specific output - let mut ring = decoys.drain((decoys.len() - decoy_count) ..).collect::>(); - ring.push(o); - ring.sort_by(|a, b| a.0.cmp(&b.0)); - - // Sanity checks are only run when 1000 outputs are available in Monero - // We run this check whenever the highest output index, which we acknowledge, is > 500 - // This means we assume (for presumably test blockchains) the height being used has not had - // 500 outputs since while itself not being a sufficiently mature blockchain - // Considering Monero's p2p layer doesn't actually check transaction sanity, it should be - // fine for us to not have perfectly matching rules, especially since this code will infinite - // loop if it can't determine sanity, which is possible with sufficient inputs on - // sufficiently small chains - if high > 500 { - // Make sure the TX passes the sanity check that the median output is within the last 40% - let target_median = high * 3 / 5; - while ring[ring_len / 2].0 < target_median { - // If it's not, update the bottom half with new values to ensure the median only moves up - for removed in ring.drain(0 .. (ring_len / 2)).collect::>() { - // If we removed the real spend, add it back - if removed.0 == o.0 { - ring.push(o); - } else { - // We could not remove this, saving CPU time and removing low values as - // possibilities, yet it'd increase the amount of decoys required to create this - // transaction and some removed outputs may be the best option (as we drop the first - // half, not just the bottom n) - used.remove(&removed.0); - } - } - - // Select new outputs until we have a full sized ring again - ring.extend( - select_n( - rng, - rpc, - &distribution, - height, - high, - per_second, - &[], - &mut used, - ring_len - ring.len(), - fingerprintable_canonical, - ) - .await?, - ); - ring.sort_by(|a, b| a.0.cmp(&b.0)); - } - - // The other sanity check rule is about duplicates, yet we already enforce unique ring - // members - } - - res.push(Decoys { - // Binary searches for the real spend since we don't know where it sorted to - i: u8::try_from(ring.partition_point(|x| x.0 < o.0)).unwrap(), - offsets: offset(&ring.iter().map(|output| output.0).collect::>()), - ring: ring.iter().map(|output| output.1).collect(), - }); - } - - Ok(res) -} - -/// Decoy data, containing the actual member as well (at index `i`). -#[derive(Clone, PartialEq, Eq, Debug, Zeroize, ZeroizeOnDrop)] -pub struct Decoys { - pub(crate) i: u8, - pub(crate) offsets: Vec, - pub(crate) ring: Vec<[EdwardsPoint; 2]>, -} - -#[allow(clippy::len_without_is_empty)] -impl Decoys { - pub fn fee_weight(offsets: &[u64]) -> usize { - varint_len(offsets.len()) + offsets.iter().map(|offset| varint_len(*offset)).sum::() - } - - pub fn len(&self) -> usize { - self.offsets.len() - } - - pub fn indexes(&self) -> Vec { - let mut res = vec![self.offsets[0]; self.len()]; - for m in 1 .. res.len() { - res[m] = res[m - 1] + self.offsets[m]; - } - res - } - - /// Select decoys using the same distribution as Monero. Relies on the monerod RPC - /// response for an output's unlocked status, minimizing trips to the daemon. - pub async fn select( - rng: &mut R, - rpc: &Rpc, - ring_len: usize, - height: usize, - inputs: &[SpendableOutput], - ) -> Result, RpcError> { - select_decoys(rng, rpc, ring_len, height, inputs, false).await - } - - /// If no reorg has occurred and an honest RPC, any caller who passes the same height to this - /// function will use the same distribution to select decoys. It is fingerprintable - /// because a caller using this will not be able to select decoys that are timelocked - /// with a timestamp. Any transaction which includes timestamp timelocked decoys in its - /// rings could not be constructed using this function. - /// - /// TODO: upstream change to monerod get_outs RPC to accept a height param for checking - /// output's unlocked status and remove all usage of fingerprintable_canonical - pub async fn fingerprintable_canonical_select( - rng: &mut R, - rpc: &Rpc, - ring_len: usize, - height: usize, - inputs: &[SpendableOutput], - ) -> Result, RpcError> { - select_decoys(rng, rpc, ring_len, height, inputs, true).await - } -} diff --git a/coins/monero/src/wallet/mod.rs b/coins/monero/src/wallet/mod.rs deleted file mode 100644 index 3b08fd97..00000000 --- a/coins/monero/src/wallet/mod.rs +++ /dev/null @@ -1,268 +0,0 @@ -use core::ops::Deref; -use std_shims::collections::{HashSet, HashMap}; - -use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing}; - -use curve25519_dalek::{ - constants::ED25519_BASEPOINT_TABLE, - scalar::Scalar, - edwards::{EdwardsPoint, CompressedEdwardsY}, -}; - -use crate::{ - hash, hash_to_scalar, serialize::write_varint, ringct::EncryptedAmount, transaction::Input, -}; - -pub mod extra; -pub(crate) use extra::{PaymentId, ExtraField, Extra}; - -/// Seed creation and parsing functionality. -pub mod seed; - -/// Address encoding and decoding functionality. -pub mod address; -use address::{Network, AddressType, SubaddressIndex, AddressSpec, AddressMeta, MoneroAddress}; - -mod scan; -pub use scan::{ReceivedOutput, SpendableOutput, Timelocked}; - -pub mod decoys; -pub use decoys::Decoys; - -mod send; -pub use send::{FeePriority, Fee, TransactionError, Change, SignableTransaction, Eventuality}; -#[cfg(feature = "std")] -pub use send::SignableTransactionBuilder; -#[cfg(feature = "multisig")] -pub(crate) use send::InternalPayment; -#[cfg(feature = "multisig")] -pub use send::TransactionMachine; - -fn key_image_sort(x: &EdwardsPoint, y: &EdwardsPoint) -> core::cmp::Ordering { - x.compress().to_bytes().cmp(&y.compress().to_bytes()).reverse() -} - -// https://gist.github.com/kayabaNerve/8066c13f1fe1573286ba7a2fd79f6100 -pub(crate) fn uniqueness(inputs: &[Input]) -> [u8; 32] { - let mut u = b"uniqueness".to_vec(); - for input in inputs { - match input { - // If Gen, this should be the only input, making this loop somewhat pointless - // This works and even if there were somehow multiple inputs, it'd be a false negative - Input::Gen(height) => { - write_varint(height, &mut u).unwrap(); - } - Input::ToKey { key_image, .. } => u.extend(key_image.compress().to_bytes()), - } - } - hash(&u) -} - -// Hs("view_tag" || 8Ra || o), Hs(8Ra || o), and H(8Ra || 0x8d) with uniqueness inclusion in the -// Scalar as an option -#[allow(non_snake_case)] -pub(crate) fn shared_key( - uniqueness: Option<[u8; 32]>, - ecdh: EdwardsPoint, - o: usize, -) -> (u8, Scalar, [u8; 8]) { - // 8Ra - let mut output_derivation = ecdh.mul_by_cofactor().compress().to_bytes().to_vec(); - - let mut payment_id_xor = [0; 8]; - payment_id_xor - .copy_from_slice(&hash(&[output_derivation.as_ref(), [0x8d].as_ref()].concat())[.. 8]); - - // || o - write_varint(&o, &mut output_derivation).unwrap(); - - let view_tag = hash(&[b"view_tag".as_ref(), &output_derivation].concat())[0]; - - // uniqueness || - let shared_key = if let Some(uniqueness) = uniqueness { - [uniqueness.as_ref(), &output_derivation].concat() - } else { - output_derivation - }; - - (view_tag, hash_to_scalar(&shared_key), payment_id_xor) -} - -pub(crate) fn commitment_mask(shared_key: Scalar) -> Scalar { - let mut mask = b"commitment_mask".to_vec(); - mask.extend(shared_key.to_bytes()); - hash_to_scalar(&mask) -} - -pub(crate) fn amount_encryption(amount: u64, key: Scalar) -> [u8; 8] { - let mut amount_mask = b"amount".to_vec(); - amount_mask.extend(key.to_bytes()); - (amount ^ u64::from_le_bytes(hash(&amount_mask)[.. 8].try_into().unwrap())).to_le_bytes() -} - -// TODO: Move this under EncryptedAmount? -fn amount_decryption(amount: &EncryptedAmount, key: Scalar) -> (Scalar, u64) { - match amount { - EncryptedAmount::Original { mask, amount } => { - #[cfg(feature = "experimental")] - { - let mask_shared_sec = hash(key.as_bytes()); - let mask = - Scalar::from_bytes_mod_order(*mask) - Scalar::from_bytes_mod_order(mask_shared_sec); - - let amount_shared_sec = hash(&mask_shared_sec); - let amount_scalar = - Scalar::from_bytes_mod_order(*amount) - Scalar::from_bytes_mod_order(amount_shared_sec); - // d2b from rctTypes.cpp - let amount = u64::from_le_bytes(amount_scalar.to_bytes()[0 .. 8].try_into().unwrap()); - - (mask, amount) - } - - #[cfg(not(feature = "experimental"))] - { - let _ = mask; - let _ = amount; - todo!("decrypting a legacy monero transaction's amount") - } - } - EncryptedAmount::Compact { amount } => ( - commitment_mask(key), - u64::from_le_bytes(amount_encryption(u64::from_le_bytes(*amount), key)), - ), - } -} - -/// The private view key and public spend key, enabling scanning transactions. -#[derive(Clone, Zeroize, ZeroizeOnDrop)] -pub struct ViewPair { - spend: EdwardsPoint, - view: Zeroizing, -} - -impl ViewPair { - pub fn new(spend: EdwardsPoint, view: Zeroizing) -> ViewPair { - ViewPair { spend, view } - } - - pub fn spend(&self) -> EdwardsPoint { - self.spend - } - - pub fn view(&self) -> EdwardsPoint { - self.view.deref() * ED25519_BASEPOINT_TABLE - } - - fn subaddress_derivation(&self, index: SubaddressIndex) -> Scalar { - hash_to_scalar(&Zeroizing::new( - [ - b"SubAddr\0".as_ref(), - Zeroizing::new(self.view.to_bytes()).as_ref(), - &index.account().to_le_bytes(), - &index.address().to_le_bytes(), - ] - .concat(), - )) - } - - fn subaddress_keys(&self, index: SubaddressIndex) -> (EdwardsPoint, EdwardsPoint) { - let scalar = self.subaddress_derivation(index); - let spend = self.spend + (&scalar * ED25519_BASEPOINT_TABLE); - let view = self.view.deref() * spend; - (spend, view) - } - - /// Returns an address with the provided specification. - pub fn address(&self, network: Network, spec: AddressSpec) -> MoneroAddress { - let mut spend = self.spend; - let mut view: EdwardsPoint = self.view.deref() * ED25519_BASEPOINT_TABLE; - - // construct the address meta - let meta = match spec { - AddressSpec::Standard => AddressMeta::new(network, AddressType::Standard), - AddressSpec::Integrated(payment_id) => { - AddressMeta::new(network, AddressType::Integrated(payment_id)) - } - AddressSpec::Subaddress(index) => { - (spend, view) = self.subaddress_keys(index); - AddressMeta::new(network, AddressType::Subaddress) - } - AddressSpec::Featured { subaddress, payment_id, guaranteed } => { - if let Some(index) = subaddress { - (spend, view) = self.subaddress_keys(index); - } - AddressMeta::new( - network, - AddressType::Featured { subaddress: subaddress.is_some(), payment_id, guaranteed }, - ) - } - }; - - MoneroAddress::new(meta, spend, view) - } -} - -/// Transaction scanner. -/// This scanner is capable of generating subaddresses, additionally scanning for them once they've -/// been explicitly generated. If the burning bug is attempted, any secondary outputs will be -/// ignored. -#[derive(Clone)] -pub struct Scanner { - pair: ViewPair, - // Also contains the spend key as None - pub(crate) subaddresses: HashMap>, - pub(crate) burning_bug: Option>, -} - -impl Zeroize for Scanner { - fn zeroize(&mut self) { - self.pair.zeroize(); - - // These may not be effective, unfortunately - for (mut key, mut value) in self.subaddresses.drain() { - key.zeroize(); - value.zeroize(); - } - if let Some(ref mut burning_bug) = self.burning_bug.take() { - for mut output in burning_bug.drain() { - output.zeroize(); - } - } - } -} - -impl Drop for Scanner { - fn drop(&mut self) { - self.zeroize(); - } -} - -impl ZeroizeOnDrop for Scanner {} - -impl Scanner { - /// Create a Scanner from a ViewPair. - /// - /// burning_bug is a HashSet of used keys, intended to prevent key reuse which would burn funds. - /// - /// When an output is successfully scanned, the output key MUST be saved to disk. - /// - /// When a new scanner is created, ALL saved output keys must be passed in to be secure. - /// - /// If None is passed, a modified shared key derivation is used which is immune to the burning - /// bug (specifically the Guaranteed feature from Featured Addresses). - pub fn from_view(pair: ViewPair, burning_bug: Option>) -> Scanner { - let mut subaddresses = HashMap::new(); - subaddresses.insert(pair.spend.compress(), None); - Scanner { pair, subaddresses, burning_bug } - } - - /// Register a subaddress. - // There used to be an address function here, yet it wasn't safe. It could generate addresses - // incompatible with the Scanner. While we could return None for that, then we have the issue - // of runtime failures to generate an address. - // Removing that API was the simplest option. - pub fn register_subaddress(&mut self, subaddress: SubaddressIndex) { - let (spend, _) = self.pair.subaddress_keys(subaddress); - self.subaddresses.insert(spend.compress(), Some(subaddress)); - } -} diff --git a/coins/monero/src/wallet/scan.rs b/coins/monero/src/wallet/scan.rs deleted file mode 100644 index 0c2cebbd..00000000 --- a/coins/monero/src/wallet/scan.rs +++ /dev/null @@ -1,521 +0,0 @@ -use core::ops::Deref; -use std_shims::{ - vec::Vec, - string::ToString, - io::{self, Read, Write}, -}; - -use zeroize::{Zeroize, ZeroizeOnDrop}; - -use curve25519_dalek::{constants::ED25519_BASEPOINT_TABLE, scalar::Scalar, edwards::EdwardsPoint}; - -use monero_generators::decompress_point; - -use crate::{ - Commitment, - serialize::{read_byte, read_u32, read_u64, read_bytes, read_scalar, read_point, read_raw_vec}, - transaction::{Input, Timelock, Transaction}, - block::Block, - rpc::{RpcError, RpcConnection, Rpc}, - wallet::{ - PaymentId, Extra, address::SubaddressIndex, Scanner, uniqueness, shared_key, amount_decryption, - }, -}; - -/// An absolute output ID, defined as its transaction hash and output index. -#[derive(Clone, PartialEq, Eq, Zeroize, ZeroizeOnDrop)] -pub struct AbsoluteId { - pub tx: [u8; 32], - pub o: u8, -} - -impl core::fmt::Debug for AbsoluteId { - fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> { - fmt.debug_struct("AbsoluteId").field("tx", &hex::encode(self.tx)).field("o", &self.o).finish() - } -} - -impl AbsoluteId { - pub fn write(&self, w: &mut W) -> io::Result<()> { - w.write_all(&self.tx)?; - w.write_all(&[self.o]) - } - - pub fn serialize(&self) -> Vec { - let mut serialized = Vec::with_capacity(32 + 1); - self.write(&mut serialized).unwrap(); - serialized - } - - pub fn read(r: &mut R) -> io::Result { - Ok(AbsoluteId { tx: read_bytes(r)?, o: read_byte(r)? }) - } -} - -/// The data contained with an output. -#[derive(Clone, PartialEq, Eq, Zeroize, ZeroizeOnDrop)] -pub struct OutputData { - pub key: EdwardsPoint, - /// Absolute difference between the spend key and the key in this output - pub key_offset: Scalar, - pub commitment: Commitment, -} - -impl core::fmt::Debug for OutputData { - fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> { - fmt - .debug_struct("OutputData") - .field("key", &hex::encode(self.key.compress().0)) - .field("key_offset", &hex::encode(self.key_offset.to_bytes())) - .field("commitment", &self.commitment) - .finish() - } -} - -impl OutputData { - pub fn write(&self, w: &mut W) -> io::Result<()> { - w.write_all(&self.key.compress().to_bytes())?; - w.write_all(&self.key_offset.to_bytes())?; - w.write_all(&self.commitment.mask.to_bytes())?; - w.write_all(&self.commitment.amount.to_le_bytes()) - } - - pub fn serialize(&self) -> Vec { - let mut serialized = Vec::with_capacity(32 + 32 + 32 + 8); - self.write(&mut serialized).unwrap(); - serialized - } - - pub fn read(r: &mut R) -> io::Result { - Ok(OutputData { - key: read_point(r)?, - key_offset: read_scalar(r)?, - commitment: Commitment::new(read_scalar(r)?, read_u64(r)?), - }) - } -} - -/// The metadata for an output. -#[derive(Clone, PartialEq, Eq, Zeroize, ZeroizeOnDrop)] -pub struct Metadata { - /// The subaddress this output was sent to. - pub subaddress: Option, - /// The payment ID included with this output. - /// There are 2 circumstances in which the reference wallet2 ignores the payment ID - /// but the payment ID will be returned here anyway: - /// - /// 1) If the payment ID is tied to an output received by a subaddress account - /// that spent Monero in the transaction (the received output is considered - /// "change" and is not considered a "payment" in this case). If there are multiple - /// spending subaddress accounts in a transaction, the highest index spent key image - /// is used to determine the spending subaddress account. - /// - /// 2) If the payment ID is the unencrypted variant and the block's hf version is - /// v12 or higher (https://github.com/serai-dex/serai/issues/512) - pub payment_id: Option, - /// Arbitrary data encoded in TX extra. - pub arbitrary_data: Vec>, -} - -impl core::fmt::Debug for Metadata { - fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> { - fmt - .debug_struct("Metadata") - .field("subaddress", &self.subaddress) - .field("payment_id", &self.payment_id) - .field("arbitrary_data", &self.arbitrary_data.iter().map(hex::encode).collect::>()) - .finish() - } -} - -impl Metadata { - pub fn write(&self, w: &mut W) -> io::Result<()> { - if let Some(subaddress) = self.subaddress { - w.write_all(&[1])?; - w.write_all(&subaddress.account().to_le_bytes())?; - w.write_all(&subaddress.address().to_le_bytes())?; - } else { - w.write_all(&[0])?; - } - - if let Some(payment_id) = self.payment_id { - w.write_all(&[1])?; - payment_id.write(w)?; - } else { - w.write_all(&[0])?; - } - - w.write_all(&u32::try_from(self.arbitrary_data.len()).unwrap().to_le_bytes())?; - for part in &self.arbitrary_data { - w.write_all(&[u8::try_from(part.len()).unwrap()])?; - w.write_all(part)?; - } - Ok(()) - } - - pub fn serialize(&self) -> Vec { - let mut serialized = Vec::with_capacity(1 + 8 + 1); - self.write(&mut serialized).unwrap(); - serialized - } - - pub fn read(r: &mut R) -> io::Result { - let subaddress = if read_byte(r)? == 1 { - Some( - SubaddressIndex::new(read_u32(r)?, read_u32(r)?) - .ok_or_else(|| io::Error::other("invalid subaddress in metadata"))?, - ) - } else { - None - }; - - Ok(Metadata { - subaddress, - payment_id: if read_byte(r)? == 1 { PaymentId::read(r).ok() } else { None }, - arbitrary_data: { - let mut data = vec![]; - for _ in 0 .. read_u32(r)? { - let len = read_byte(r)?; - data.push(read_raw_vec(read_byte, usize::from(len), r)?); - } - data - }, - }) - } -} - -/// A received output, defined as its absolute ID, data, and metadara. -#[derive(Clone, PartialEq, Eq, Debug, Zeroize, ZeroizeOnDrop)] -pub struct ReceivedOutput { - pub absolute: AbsoluteId, - pub data: OutputData, - pub metadata: Metadata, -} - -impl ReceivedOutput { - pub fn key(&self) -> EdwardsPoint { - self.data.key - } - - pub fn key_offset(&self) -> Scalar { - self.data.key_offset - } - - pub fn commitment(&self) -> Commitment { - self.data.commitment.clone() - } - - pub fn arbitrary_data(&self) -> &[Vec] { - &self.metadata.arbitrary_data - } - - pub fn write(&self, w: &mut W) -> io::Result<()> { - self.absolute.write(w)?; - self.data.write(w)?; - self.metadata.write(w) - } - - pub fn serialize(&self) -> Vec { - let mut serialized = vec![]; - self.write(&mut serialized).unwrap(); - serialized - } - - pub fn read(r: &mut R) -> io::Result { - Ok(ReceivedOutput { - absolute: AbsoluteId::read(r)?, - data: OutputData::read(r)?, - metadata: Metadata::read(r)?, - }) - } -} - -/// A spendable output, defined as a received output and its index on the Monero blockchain. -/// This index is dependent on the Monero blockchain and will only be known once the output is -/// included within a block. This may change if there's a reorganization. -#[derive(Clone, PartialEq, Eq, Debug, Zeroize, ZeroizeOnDrop)] -pub struct SpendableOutput { - pub output: ReceivedOutput, - pub global_index: u64, -} - -impl SpendableOutput { - /// Update the spendable output's global index. This is intended to be called if a - /// re-organization occurred. - pub async fn refresh_global_index( - &mut self, - rpc: &Rpc, - ) -> Result<(), RpcError> { - self.global_index = *rpc - .get_o_indexes(self.output.absolute.tx) - .await? - .get(usize::from(self.output.absolute.o)) - .ok_or(RpcError::InvalidNode( - "node returned output indexes didn't include an index for this output".to_string(), - ))?; - Ok(()) - } - - pub async fn from( - rpc: &Rpc, - output: ReceivedOutput, - ) -> Result { - let mut output = SpendableOutput { output, global_index: 0 }; - output.refresh_global_index(rpc).await?; - Ok(output) - } - - pub fn key(&self) -> EdwardsPoint { - self.output.key() - } - - pub fn key_offset(&self) -> Scalar { - self.output.key_offset() - } - - pub fn commitment(&self) -> Commitment { - self.output.commitment() - } - - pub fn arbitrary_data(&self) -> &[Vec] { - self.output.arbitrary_data() - } - - pub fn write(&self, w: &mut W) -> io::Result<()> { - self.output.write(w)?; - w.write_all(&self.global_index.to_le_bytes()) - } - - pub fn serialize(&self) -> Vec { - let mut serialized = vec![]; - self.write(&mut serialized).unwrap(); - serialized - } - - pub fn read(r: &mut R) -> io::Result { - Ok(SpendableOutput { output: ReceivedOutput::read(r)?, global_index: read_u64(r)? }) - } -} - -/// A collection of timelocked outputs, either received or spendable. -#[derive(Zeroize)] -pub struct Timelocked(Timelock, Vec); -impl Drop for Timelocked { - fn drop(&mut self) { - self.zeroize(); - } -} -impl ZeroizeOnDrop for Timelocked {} - -impl Timelocked { - pub fn timelock(&self) -> Timelock { - self.0 - } - - /// Return the outputs if they're not timelocked, or an empty vector if they are. - #[must_use] - pub fn not_locked(&self) -> Vec { - if self.0 == Timelock::None { - return self.1.clone(); - } - vec![] - } - - /// Returns None if the Timelocks aren't comparable. Returns Some(vec![]) if none are unlocked. - #[must_use] - pub fn unlocked(&self, timelock: Timelock) -> Option> { - // If the Timelocks are comparable, return the outputs if they're now unlocked - if self.0 <= timelock { - Some(self.1.clone()) - } else { - None - } - } - - #[must_use] - pub fn ignore_timelock(&self) -> Vec { - self.1.clone() - } -} - -impl Scanner { - /// Scan a transaction to discover the received outputs. - pub fn scan_transaction(&mut self, tx: &Transaction) -> Timelocked { - // Only scan RCT TXs since we can only spend RCT outputs - if tx.prefix.version != 2 { - return Timelocked(tx.prefix.timelock, vec![]); - } - - let Ok(extra) = Extra::read::<&[u8]>(&mut tx.prefix.extra.as_ref()) else { - return Timelocked(tx.prefix.timelock, vec![]); - }; - - let Some((tx_keys, additional)) = extra.keys() else { - return Timelocked(tx.prefix.timelock, vec![]); - }; - - let payment_id = extra.payment_id(); - - let mut res = vec![]; - for (o, output) in tx.prefix.outputs.iter().enumerate() { - // https://github.com/serai-dex/serai/issues/106 - if let Some(burning_bug) = self.burning_bug.as_ref() { - if burning_bug.contains(&output.key) { - continue; - } - } - - let output_key = decompress_point(output.key.to_bytes()); - if output_key.is_none() { - continue; - } - let output_key = output_key.unwrap(); - - let additional = additional.as_ref().map(|additional| additional.get(o)); - - for key in tx_keys.iter().map(|key| Some(Some(key))).chain(core::iter::once(additional)) { - let key = match key { - Some(Some(key)) => key, - Some(None) => { - // This is non-standard. There were additional keys, yet not one for this output - // https://github.com/monero-project/monero/ - // blob/04a1e2875d6e35e27bb21497988a6c822d319c28/ - // src/cryptonote_basic/cryptonote_format_utils.cpp#L1062 - continue; - } - None => { - break; - } - }; - let (view_tag, shared_key, payment_id_xor) = shared_key( - if self.burning_bug.is_none() { Some(uniqueness(&tx.prefix.inputs)) } else { None }, - self.pair.view.deref() * key, - o, - ); - - let payment_id = payment_id.map(|id| id ^ payment_id_xor); - - if let Some(actual_view_tag) = output.view_tag { - if actual_view_tag != view_tag { - continue; - } - } - - // P - shared == spend - let subaddress = - self.subaddresses.get(&(output_key - (&shared_key * ED25519_BASEPOINT_TABLE)).compress()); - if subaddress.is_none() { - continue; - } - let subaddress = *subaddress.unwrap(); - - // If it has torsion, it'll subtract the non-torsioned shared key to a torsioned key - // We will not have a torsioned key in our HashMap of keys, so we wouldn't identify it as - // ours - // If we did though, it'd enable bypassing the included burning bug protection - assert!(output_key.is_torsion_free()); - - let mut key_offset = shared_key; - if let Some(subaddress) = subaddress { - key_offset += self.pair.subaddress_derivation(subaddress); - } - // Since we've found an output to us, get its amount - let mut commitment = Commitment::zero(); - - // Miner transaction - if let Some(amount) = output.amount { - commitment.amount = amount; - // Regular transaction - } else { - let (mask, amount) = match tx.rct_signatures.base.encrypted_amounts.get(o) { - Some(amount) => amount_decryption(amount, shared_key), - // This should never happen, yet it may be possible with miner transactions? - // Using get just decreases the possibility of a panic and lets us move on in that case - None => break, - }; - - // Rebuild the commitment to verify it - commitment = Commitment::new(mask, amount); - // If this is a malicious commitment, move to the next output - // Any other R value will calculate to a different spend key and are therefore ignorable - if Some(&commitment.calculate()) != tx.rct_signatures.base.commitments.get(o) { - break; - } - } - - if commitment.amount != 0 { - res.push(ReceivedOutput { - absolute: AbsoluteId { tx: tx.hash(), o: o.try_into().unwrap() }, - - data: OutputData { key: output_key, key_offset, commitment }, - - metadata: Metadata { subaddress, payment_id, arbitrary_data: extra.data() }, - }); - - if let Some(burning_bug) = self.burning_bug.as_mut() { - burning_bug.insert(output.key); - } - } - // Break to prevent public keys from being included multiple times, triggering multiple - // inclusions of the same output - break; - } - } - - Timelocked(tx.prefix.timelock, res) - } - - /// Scan a block to obtain its spendable outputs. Its the presence in a block giving these - /// transactions their global index, and this must be batched as asking for the index of specific - /// transactions is a dead giveaway for which transactions you successfully scanned. This - /// function obtains the output indexes for the miner transaction, incrementing from there - /// instead. - pub async fn scan( - &mut self, - rpc: &Rpc, - block: &Block, - ) -> Result>, RpcError> { - let mut index = rpc.get_o_indexes(block.miner_tx.hash()).await?[0]; - let mut txs = vec![block.miner_tx.clone()]; - txs.extend(rpc.get_transactions(&block.txs).await?); - - let map = |mut timelock: Timelocked, index| { - if timelock.1.is_empty() { - None - } else { - Some(Timelocked( - timelock.0, - timelock - .1 - .drain(..) - .map(|output| SpendableOutput { - global_index: index + u64::from(output.absolute.o), - output, - }) - .collect(), - )) - } - }; - - let mut res = vec![]; - for tx in txs { - if let Some(timelock) = map(self.scan_transaction(&tx), index) { - res.push(timelock); - } - index += u64::try_from( - tx.prefix - .outputs - .iter() - // Filter to v2 miner TX outputs/RCT outputs since we're tracking the RCT output index - .filter(|output| { - let is_v2_miner_tx = - (tx.prefix.version == 2) && matches!(tx.prefix.inputs.first(), Some(Input::Gen(..))); - is_v2_miner_tx || output.amount.is_none() - }) - .count(), - ) - .unwrap() - } - Ok(res) - } -} diff --git a/coins/monero/src/wallet/seed/mod.rs b/coins/monero/src/wallet/seed/mod.rs deleted file mode 100644 index 3cb2911e..00000000 --- a/coins/monero/src/wallet/seed/mod.rs +++ /dev/null @@ -1,136 +0,0 @@ -use core::fmt; -use std_shims::string::String; - -use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing}; -use rand_core::{RngCore, CryptoRng}; - -pub(crate) mod classic; -pub(crate) mod polyseed; -use classic::{CLASSIC_SEED_LENGTH, CLASSIC_SEED_LENGTH_WITH_CHECKSUM, ClassicSeed}; -use polyseed::{POLYSEED_LENGTH, Polyseed}; - -/// Error when decoding a seed. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -#[cfg_attr(feature = "std", derive(thiserror::Error))] -pub enum SeedError { - #[cfg_attr(feature = "std", error("invalid number of words in seed"))] - InvalidSeedLength, - #[cfg_attr(feature = "std", error("unknown language"))] - UnknownLanguage, - #[cfg_attr(feature = "std", error("invalid checksum"))] - InvalidChecksum, - #[cfg_attr(feature = "std", error("english old seeds don't support checksums"))] - EnglishOldWithChecksum, - #[cfg_attr(feature = "std", error("provided entropy is not valid"))] - InvalidEntropy, - #[cfg_attr(feature = "std", error("invalid seed"))] - InvalidSeed, - #[cfg_attr(feature = "std", error("provided features are not supported"))] - UnsupportedFeatures, -} - -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum SeedType { - Classic(classic::Language), - Polyseed(polyseed::Language), -} - -/// A Monero seed. -#[derive(Clone, PartialEq, Eq, Zeroize, ZeroizeOnDrop)] -pub enum Seed { - Classic(ClassicSeed), - Polyseed(Polyseed), -} - -impl fmt::Debug for Seed { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - Seed::Classic(_) => f.debug_struct("Seed::Classic").finish_non_exhaustive(), - Seed::Polyseed(_) => f.debug_struct("Seed::Polyseed").finish_non_exhaustive(), - } - } -} - -impl Seed { - /// Creates a new `Seed`. - pub fn new(rng: &mut R, seed_type: SeedType) -> Seed { - match seed_type { - SeedType::Classic(lang) => Seed::Classic(ClassicSeed::new(rng, lang)), - SeedType::Polyseed(lang) => Seed::Polyseed(Polyseed::new(rng, lang)), - } - } - - /// Parse a seed from a `String`. - pub fn from_string(seed_type: SeedType, words: Zeroizing) -> Result { - let word_count = words.split_whitespace().count(); - match seed_type { - SeedType::Classic(lang) => { - if word_count != CLASSIC_SEED_LENGTH && word_count != CLASSIC_SEED_LENGTH_WITH_CHECKSUM { - Err(SeedError::InvalidSeedLength)? - } else { - ClassicSeed::from_string(lang, words).map(Seed::Classic) - } - } - SeedType::Polyseed(lang) => { - if word_count != POLYSEED_LENGTH { - Err(SeedError::InvalidSeedLength)? - } else { - Polyseed::from_string(lang, words).map(Seed::Polyseed) - } - } - } - } - - /// Creates a `Seed` from an entropy and an optional birthday (denoted in seconds since the - /// epoch). - /// - /// For `SeedType::Classic`, the birthday is ignored. - /// - /// For `SeedType::Polyseed`, the last 13 bytes of `entropy` must be `0`. - // TODO: Return Result, not Option - pub fn from_entropy( - seed_type: SeedType, - entropy: Zeroizing<[u8; 32]>, - birthday: Option, - ) -> Option { - match seed_type { - SeedType::Classic(lang) => ClassicSeed::from_entropy(lang, entropy).map(Seed::Classic), - SeedType::Polyseed(lang) => { - Polyseed::from(lang, 0, birthday.unwrap_or(0), entropy).map(Seed::Polyseed).ok() - } - } - } - - /// Returns seed as `String`. - pub fn to_string(&self) -> Zeroizing { - match self { - Seed::Classic(seed) => seed.to_string(), - Seed::Polyseed(seed) => seed.to_string(), - } - } - - /// Returns the entropy for this seed. - pub fn entropy(&self) -> Zeroizing<[u8; 32]> { - match self { - Seed::Classic(seed) => seed.entropy(), - Seed::Polyseed(seed) => seed.entropy().clone(), - } - } - - /// Returns the key derived from this seed. - pub fn key(&self) -> Zeroizing<[u8; 32]> { - match self { - // Classic does not differentiate between its entropy and its key - Seed::Classic(seed) => seed.entropy(), - Seed::Polyseed(seed) => seed.key(), - } - } - - /// Returns the birthday of this seed. - pub fn birthday(&self) -> u64 { - match self { - Seed::Classic(_) => 0, - Seed::Polyseed(seed) => seed.birthday(), - } - } -} diff --git a/coins/monero/src/wallet/send/builder.rs b/coins/monero/src/wallet/send/builder.rs deleted file mode 100644 index 55d0fc29..00000000 --- a/coins/monero/src/wallet/send/builder.rs +++ /dev/null @@ -1,144 +0,0 @@ -use std::sync::{Arc, RwLock}; - -use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing}; - -use crate::{ - Protocol, - wallet::{ - address::MoneroAddress, Fee, SpendableOutput, Change, Decoys, SignableTransaction, - TransactionError, extra::MAX_ARBITRARY_DATA_SIZE, - }, -}; - -#[derive(Clone, PartialEq, Eq, Debug, Zeroize, ZeroizeOnDrop)] -struct SignableTransactionBuilderInternal { - protocol: Protocol, - fee_rate: Fee, - - r_seed: Option>, - inputs: Vec<(SpendableOutput, Decoys)>, - payments: Vec<(MoneroAddress, u64)>, - change_address: Change, - data: Vec>, -} - -impl SignableTransactionBuilderInternal { - // Takes in the change address so users don't miss that they have to manually set one - // If they don't, all leftover funds will become part of the fee - fn new(protocol: Protocol, fee_rate: Fee, change_address: Change) -> Self { - Self { - protocol, - fee_rate, - r_seed: None, - inputs: vec![], - payments: vec![], - change_address, - data: vec![], - } - } - - fn set_r_seed(&mut self, r_seed: Zeroizing<[u8; 32]>) { - self.r_seed = Some(r_seed); - } - - fn add_input(&mut self, input: (SpendableOutput, Decoys)) { - self.inputs.push(input); - } - fn add_inputs(&mut self, inputs: &[(SpendableOutput, Decoys)]) { - self.inputs.extend(inputs.iter().cloned()); - } - - fn add_payment(&mut self, dest: MoneroAddress, amount: u64) { - self.payments.push((dest, amount)); - } - fn add_payments(&mut self, payments: &[(MoneroAddress, u64)]) { - self.payments.extend(payments); - } - - fn add_data(&mut self, data: Vec) { - self.data.push(data); - } -} - -/// A Transaction Builder for Monero transactions. -/// All methods provided will modify self while also returning a shallow copy, enabling efficient -/// chaining with a clean API. -/// In order to fork the builder at some point, clone will still return a deep copy. -#[derive(Debug)] -pub struct SignableTransactionBuilder(Arc>); -impl Clone for SignableTransactionBuilder { - fn clone(&self) -> Self { - Self(Arc::new(RwLock::new((*self.0.read().unwrap()).clone()))) - } -} - -impl PartialEq for SignableTransactionBuilder { - fn eq(&self, other: &Self) -> bool { - *self.0.read().unwrap() == *other.0.read().unwrap() - } -} -impl Eq for SignableTransactionBuilder {} - -impl Zeroize for SignableTransactionBuilder { - fn zeroize(&mut self) { - self.0.write().unwrap().zeroize() - } -} - -impl SignableTransactionBuilder { - fn shallow_copy(&self) -> Self { - Self(self.0.clone()) - } - - pub fn new(protocol: Protocol, fee_rate: Fee, change_address: Change) -> Self { - Self(Arc::new(RwLock::new(SignableTransactionBuilderInternal::new( - protocol, - fee_rate, - change_address, - )))) - } - - pub fn set_r_seed(&mut self, r_seed: Zeroizing<[u8; 32]>) -> Self { - self.0.write().unwrap().set_r_seed(r_seed); - self.shallow_copy() - } - - pub fn add_input(&mut self, input: (SpendableOutput, Decoys)) -> Self { - self.0.write().unwrap().add_input(input); - self.shallow_copy() - } - pub fn add_inputs(&mut self, inputs: &[(SpendableOutput, Decoys)]) -> Self { - self.0.write().unwrap().add_inputs(inputs); - self.shallow_copy() - } - - pub fn add_payment(&mut self, dest: MoneroAddress, amount: u64) -> Self { - self.0.write().unwrap().add_payment(dest, amount); - self.shallow_copy() - } - pub fn add_payments(&mut self, payments: &[(MoneroAddress, u64)]) -> Self { - self.0.write().unwrap().add_payments(payments); - self.shallow_copy() - } - - pub fn add_data(&mut self, data: Vec) -> Result { - if data.len() > MAX_ARBITRARY_DATA_SIZE { - Err(TransactionError::TooMuchData)?; - } - self.0.write().unwrap().add_data(data); - Ok(self.shallow_copy()) - } - - pub fn build(self) -> Result { - let read = self.0.read().unwrap(); - SignableTransaction::new( - read.protocol, - read.r_seed.clone(), - read.inputs.clone(), - read.payments.clone(), - &read.change_address, - read.data.clone(), - read.fee_rate, - ) - } -} diff --git a/coins/monero/src/wallet/send/mod.rs b/coins/monero/src/wallet/send/mod.rs deleted file mode 100644 index f4ac208e..00000000 --- a/coins/monero/src/wallet/send/mod.rs +++ /dev/null @@ -1,1038 +0,0 @@ -use core::{ops::Deref, fmt}; -use std_shims::{ - vec::Vec, - io, - string::{String, ToString}, -}; - -use rand_core::{RngCore, CryptoRng, SeedableRng}; -use rand_chacha::ChaCha20Rng; -use rand::seq::SliceRandom; - -use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing}; - -use group::Group; -use curve25519_dalek::{ - constants::{ED25519_BASEPOINT_POINT, ED25519_BASEPOINT_TABLE}, - scalar::Scalar, - edwards::EdwardsPoint, -}; -use dalek_ff_group as dfg; - -#[cfg(feature = "multisig")] -use frost::FrostError; - -use crate::{ - Protocol, Commitment, hash, random_scalar, - serialize::{ - read_byte, read_bytes, read_u64, read_scalar, read_point, read_vec, write_byte, write_scalar, - write_point, write_raw_vec, write_vec, - }, - ringct::{ - generate_key_image, - clsag::{ClsagError, ClsagInput, Clsag}, - bulletproofs::{MAX_OUTPUTS, Bulletproofs}, - RctBase, RctPrunable, RctSignatures, - }, - transaction::{Input, Output, Timelock, TransactionPrefix, Transaction}, - rpc::RpcError, - wallet::{ - address::{Network, AddressSpec, MoneroAddress}, - ViewPair, SpendableOutput, Decoys, PaymentId, ExtraField, Extra, key_image_sort, uniqueness, - shared_key, commitment_mask, amount_encryption, - extra::{ARBITRARY_DATA_MARKER, MAX_ARBITRARY_DATA_SIZE}, - }, -}; - -#[cfg(feature = "std")] -mod builder; -#[cfg(feature = "std")] -pub use builder::SignableTransactionBuilder; - -#[cfg(feature = "multisig")] -mod multisig; -#[cfg(feature = "multisig")] -pub use multisig::TransactionMachine; -use crate::ringct::EncryptedAmount; - -#[allow(non_snake_case)] -#[derive(Clone, PartialEq, Eq, Debug, Zeroize, ZeroizeOnDrop)] -struct SendOutput { - R: EdwardsPoint, - view_tag: u8, - dest: EdwardsPoint, - commitment: Commitment, - amount: [u8; 8], -} - -impl SendOutput { - #[allow(non_snake_case)] - fn internal( - unique: [u8; 32], - output: (usize, (MoneroAddress, u64), bool), - ecdh: EdwardsPoint, - R: EdwardsPoint, - ) -> (SendOutput, Option<[u8; 8]>) { - let o = output.0; - let need_dummy_payment_id = output.2; - let output = output.1; - - let (view_tag, shared_key, payment_id_xor) = - shared_key(Some(unique).filter(|_| output.0.is_guaranteed()), ecdh, o); - - let payment_id = output - .0 - .payment_id() - .or(if need_dummy_payment_id { Some([0u8; 8]) } else { None }) - .map(|id| (u64::from_le_bytes(id) ^ u64::from_le_bytes(payment_id_xor)).to_le_bytes()); - - ( - SendOutput { - R, - view_tag, - dest: ((&shared_key * ED25519_BASEPOINT_TABLE) + output.0.spend), - commitment: Commitment::new(commitment_mask(shared_key), output.1), - amount: amount_encryption(output.1, shared_key), - }, - payment_id, - ) - } - - fn new( - r: &Zeroizing, - unique: [u8; 32], - output: (usize, (MoneroAddress, u64), bool), - ) -> (SendOutput, Option<[u8; 8]>) { - let address = output.1 .0; - SendOutput::internal( - unique, - output, - r.deref() * address.view, - if !address.is_subaddress() { - r.deref() * ED25519_BASEPOINT_TABLE - } else { - r.deref() * address.spend - }, - ) - } - - fn change( - ecdh: EdwardsPoint, - unique: [u8; 32], - output: (usize, (MoneroAddress, u64), bool), - ) -> (SendOutput, Option<[u8; 8]>) { - SendOutput::internal(unique, output, ecdh, ED25519_BASEPOINT_POINT) - } -} - -#[derive(Clone, PartialEq, Eq, Debug)] -#[cfg_attr(feature = "std", derive(thiserror::Error))] -pub enum TransactionError { - #[cfg_attr(feature = "std", error("multiple addresses with payment IDs"))] - MultiplePaymentIds, - #[cfg_attr(feature = "std", error("no inputs"))] - NoInputs, - #[cfg_attr(feature = "std", error("no outputs"))] - NoOutputs, - #[cfg_attr(feature = "std", error("invalid number of decoys"))] - InvalidDecoyQuantity, - #[cfg_attr(feature = "std", error("only one output and no change address"))] - NoChange, - #[cfg_attr(feature = "std", error("too many outputs"))] - TooManyOutputs, - #[cfg_attr(feature = "std", error("too much data"))] - TooMuchData, - #[cfg_attr(feature = "std", error("too many inputs/too much arbitrary data"))] - TooLargeTransaction, - #[cfg_attr( - feature = "std", - error("not enough funds (inputs {inputs}, outputs {outputs}, fee {fee})") - )] - NotEnoughFunds { inputs: u64, outputs: u64, fee: u64 }, - #[cfg_attr(feature = "std", error("wrong spend private key"))] - WrongPrivateKey, - #[cfg_attr(feature = "std", error("rpc error ({0})"))] - RpcError(RpcError), - #[cfg_attr(feature = "std", error("clsag error ({0})"))] - ClsagError(ClsagError), - #[cfg_attr(feature = "std", error("invalid transaction ({0})"))] - InvalidTransaction(RpcError), - #[cfg(feature = "multisig")] - #[cfg_attr(feature = "std", error("frost error {0}"))] - FrostError(FrostError), -} - -fn prepare_inputs( - inputs: &[(SpendableOutput, Decoys)], - spend: &Zeroizing, - tx: &mut Transaction, -) -> Result, EdwardsPoint, ClsagInput)>, TransactionError> { - let mut signable = Vec::with_capacity(inputs.len()); - - for (i, (input, decoys)) in inputs.iter().enumerate() { - let input_spend = Zeroizing::new(input.key_offset() + spend.deref()); - let image = generate_key_image(&input_spend); - signable.push(( - input_spend, - image, - ClsagInput::new(input.commitment().clone(), decoys.clone()) - .map_err(TransactionError::ClsagError)?, - )); - - tx.prefix.inputs.push(Input::ToKey { - amount: None, - key_offsets: decoys.offsets.clone(), - key_image: signable[i].1, - }); - } - - signable.sort_by(|x, y| x.1.compress().to_bytes().cmp(&y.1.compress().to_bytes()).reverse()); - tx.prefix.inputs.sort_by(|x, y| { - if let (Input::ToKey { key_image: x, .. }, Input::ToKey { key_image: y, .. }) = (x, y) { - x.compress().to_bytes().cmp(&y.compress().to_bytes()).reverse() - } else { - panic!("Input wasn't ToKey") - } - }); - - Ok(signable) -} - -// Deterministically calculate what the TX weight and fee will be. -fn calculate_weight_and_fee( - protocol: Protocol, - decoy_weights: &[usize], - n_outputs: usize, - extra: usize, - fee_rate: Fee, -) -> (usize, u64) { - // Starting the fee at 0 here is different than core Monero's wallet2.cpp, which starts its fee - // calculation with an estimate. - // - // This difference is okay in practice because wallet2 still ends up using a fee calculated from - // a TX's weight, as calculated later in this function. - // - // See this PR highlighting wallet2's behavior: - // https://github.com/monero-project/monero/pull/8882 - // - // Even with that PR, if the estimated fee's VarInt byte length is larger than the calculated - // fee's, the wallet can theoretically use a fee not based on the actual TX weight. This does not - // occur in practice as it's nearly impossible for wallet2 to estimate a fee that is larger - // than the calculated fee today, and on top of that, even more unlikely for that estimate's - // VarInt to be larger in byte length than the calculated fee's. - let mut weight = 0usize; - let mut fee = 0u64; - - let mut done = false; - let mut iters = 0; - let max_iters = 5; - while !done { - weight = Transaction::fee_weight(protocol, decoy_weights, n_outputs, extra, fee); - - let fee_calculated_from_weight = fee_rate.calculate_fee_from_weight(weight); - - // Continue trying to use the fee calculated from the tx's weight - done = fee_calculated_from_weight == fee; - - fee = fee_calculated_from_weight; - - #[cfg(test)] - debug_assert!(iters < max_iters, "Reached max fee calculation attempts"); - // Should never happen because the fee VarInt byte length shouldn't change *every* single iter. - // `iters` reaching `max_iters` is unexpected. - if iters >= max_iters { - // Fail-safe break to ensure funds are still spendable - break; - } - iters += 1; - } - - (weight, fee) -} - -/// Fee struct, defined as a per-unit cost and a mask for rounding purposes. -#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)] -pub struct Fee { - pub per_weight: u64, - pub mask: u64, -} - -impl Fee { - pub fn calculate_fee_from_weight(&self, weight: usize) -> u64 { - let fee = (((self.per_weight * u64::try_from(weight).unwrap()) + self.mask - 1) / self.mask) * - self.mask; - debug_assert_eq!(weight, self.calculate_weight_from_fee(fee), "Miscalculated weight from fee"); - fee - } - - pub fn calculate_weight_from_fee(&self, fee: u64) -> usize { - usize::try_from(fee / self.per_weight).unwrap() - } -} - -/// Fee priority, determining how quickly a transaction is included in a block. -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -#[allow(non_camel_case_types)] -pub enum FeePriority { - Unimportant, - Normal, - Elevated, - Priority, - Custom { priority: u32 }, -} - -/// https://github.com/monero-project/monero/blob/ac02af92867590ca80b2779a7bbeafa99ff94dcb/ -/// src/simplewallet/simplewallet.cpp#L161 -impl FeePriority { - pub(crate) fn fee_priority(&self) -> u32 { - match self { - FeePriority::Unimportant => 1, - FeePriority::Normal => 2, - FeePriority::Elevated => 3, - FeePriority::Priority => 4, - FeePriority::Custom { priority, .. } => *priority, - } - } -} - -#[derive(Clone, PartialEq, Eq, Debug, Zeroize)] -pub(crate) enum InternalPayment { - Payment((MoneroAddress, u64), bool), - Change((MoneroAddress, u64), Option>), -} - -/// The eventual output of a SignableTransaction. -/// -/// If the SignableTransaction has a Change with a view key, this will also have the view key. -/// Accordingly, it must be treated securely. -#[derive(Clone, PartialEq, Eq, Debug, Zeroize)] -pub struct Eventuality { - protocol: Protocol, - r_seed: Zeroizing<[u8; 32]>, - inputs: Vec, - payments: Vec, - extra: Vec, -} - -/// A signable transaction, either in a single-signer or multisig context. -#[derive(Clone, PartialEq, Eq, Debug, Zeroize, ZeroizeOnDrop)] -pub struct SignableTransaction { - protocol: Protocol, - r_seed: Option>, - inputs: Vec<(SpendableOutput, Decoys)>, - has_change: bool, - payments: Vec, - data: Vec>, - fee: u64, - fee_rate: Fee, -} - -/// Specification for a change output. -#[derive(Clone, PartialEq, Eq, Zeroize)] -pub struct Change { - address: Option, - view: Option>, -} - -impl fmt::Debug for Change { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.debug_struct("Change").field("address", &self.address).finish_non_exhaustive() - } -} - -impl Change { - /// Create a change output specification from a ViewPair, as needed to maintain privacy. - pub fn new(view: &ViewPair, guaranteed: bool) -> Change { - Change { - address: Some(view.address( - Network::Mainnet, - if !guaranteed { - AddressSpec::Standard - } else { - AddressSpec::Featured { subaddress: None, payment_id: None, guaranteed: true } - }, - )), - view: Some(view.view.clone()), - } - } - - /// Create a fingerprintable change output specification which will harm privacy. Only use this - /// if you know what you're doing. - /// - /// If the change address is None, there are 2 potential fingerprints: - /// - /// 1) The change in the tx is shunted to the fee (fingerprintable fee). - /// - /// 2) If there are 2 outputs in the tx, there would be no payment ID as is the case when the - /// reference wallet creates 2 output txs, since monero-serai doesn't know which output - /// to tie the dummy payment ID to. - pub fn fingerprintable(address: Option) -> Change { - Change { address, view: None } - } -} - -fn need_additional(payments: &[InternalPayment]) -> (bool, bool) { - let mut has_change_view = false; - let subaddresses = payments - .iter() - .filter(|payment| match *payment { - InternalPayment::Payment(payment, _) => payment.0.is_subaddress(), - InternalPayment::Change(change, change_view) => { - if change_view.is_some() { - has_change_view = true; - // It should not be possible to construct a change specification to a subaddress with a - // view key - debug_assert!(!change.0.is_subaddress()); - } - change.0.is_subaddress() - } - }) - .count() != - 0; - - // We need additional keys if we have any subaddresses - let mut additional = subaddresses; - // Unless the above change view key path is taken - if (payments.len() == 2) && has_change_view { - additional = false; - } - - (subaddresses, additional) -} - -fn sanity_check_change_payment_quantity(payments: &[InternalPayment], has_change_address: bool) { - debug_assert_eq!( - payments - .iter() - .filter(|payment| match *payment { - InternalPayment::Payment(_, _) => false, - InternalPayment::Change(_, _) => true, - }) - .count(), - if has_change_address { 1 } else { 0 }, - "Unexpected number of change outputs" - ); -} - -impl SignableTransaction { - /// Create a signable transaction. - /// - /// `r_seed` refers to a seed used to derive the transaction's ephemeral keys (colloquially - /// called Rs). If None is provided, one will be automatically generated. - /// - /// Up to 16 outputs may be present, including the change output. If the change address is - /// specified, leftover funds will be sent to it. - /// - /// Each chunk of data must not exceed MAX_ARBITRARY_DATA_SIZE and will be embedded in TX extra. - pub fn new( - protocol: Protocol, - r_seed: Option>, - inputs: Vec<(SpendableOutput, Decoys)>, - payments: Vec<(MoneroAddress, u64)>, - change: &Change, - data: Vec>, - fee_rate: Fee, - ) -> Result { - // Make sure there's only one payment ID - let mut has_payment_id = { - let mut payment_ids = 0; - let mut count = |addr: MoneroAddress| { - if addr.payment_id().is_some() { - payment_ids += 1 - } - }; - for payment in &payments { - count(payment.0); - } - if let Some(change_address) = change.address.as_ref() { - count(*change_address); - } - if payment_ids > 1 { - Err(TransactionError::MultiplePaymentIds)?; - } - payment_ids == 1 - }; - - if inputs.is_empty() { - Err(TransactionError::NoInputs)?; - } - if payments.is_empty() { - Err(TransactionError::NoOutputs)?; - } - - for (_, decoys) in &inputs { - if decoys.len() != protocol.ring_len() { - Err(TransactionError::InvalidDecoyQuantity)?; - } - } - - for part in &data { - if part.len() > MAX_ARBITRARY_DATA_SIZE { - Err(TransactionError::TooMuchData)?; - } - } - - // If we don't have two outputs, as required by Monero, error - if (payments.len() == 1) && change.address.is_none() { - Err(TransactionError::NoChange)?; - } - - // All 2 output txs created by the reference wallet have payment IDs to avoid - // fingerprinting integrated addresses. Note: we won't create a dummy payment - // ID if we create a 0-change 2-output tx since we don't know which output should - // receive the payment ID and such a tx is fingerprintable to monero-serai anyway - let need_dummy_payment_id = !has_payment_id && payments.len() == 1; - has_payment_id |= need_dummy_payment_id; - - // Get the outgoing amount ignoring fees - let out_amount = payments.iter().map(|payment| payment.1).sum::(); - - let outputs = payments.len() + usize::from(change.address.is_some()); - if outputs > MAX_OUTPUTS { - Err(TransactionError::TooManyOutputs)?; - } - - // Collect payments in a container that includes a change output if a change address is provided - let mut payments = payments - .into_iter() - .map(|payment| InternalPayment::Payment(payment, need_dummy_payment_id)) - .collect::>(); - debug_assert!(!need_dummy_payment_id || (payments.len() == 1 && change.address.is_some())); - - if let Some(change_address) = change.address.as_ref() { - // Push a 0 amount change output that we'll use to do fee calculations. - // We'll modify the change amount after calculating the fee - payments.push(InternalPayment::Change((*change_address, 0), change.view.clone())); - } - - // Determine if we'll need additional pub keys in tx extra - let (_, additional) = need_additional(&payments); - - // Calculate the extra length - let extra = Extra::fee_weight(outputs, additional, has_payment_id, data.as_ref()); - - // https://github.com/monero-project/monero/pull/8733 - const MAX_EXTRA_SIZE: usize = 1060; - if extra > MAX_EXTRA_SIZE { - Err(TransactionError::TooMuchData)?; - } - - // Caclculate weight of decoys - let decoy_weights = - inputs.iter().map(|(_, decoy)| Decoys::fee_weight(&decoy.offsets)).collect::>(); - - // Deterministically calculate tx weight and fee - let (weight, fee) = - calculate_weight_and_fee(protocol, &decoy_weights, outputs, extra, fee_rate); - - // The actual limit is half the block size, and for the minimum block size of 300k, that'd be - // 150k - // wallet2 will only create transactions up to 100k bytes however - const MAX_TX_SIZE: usize = 100_000; - if weight >= MAX_TX_SIZE { - Err(TransactionError::TooLargeTransaction)?; - } - - // Make sure we have enough funds - let in_amount = inputs.iter().map(|(input, _)| input.commitment().amount).sum::(); - if in_amount < (out_amount + fee) { - Err(TransactionError::NotEnoughFunds { inputs: in_amount, outputs: out_amount, fee })?; - } - - // Sanity check we have the expected number of change outputs - sanity_check_change_payment_quantity(&payments, change.address.is_some()); - - // Modify the amount of the change output - if let Some(change_address) = change.address.as_ref() { - let change_payment = payments.last_mut().unwrap(); - debug_assert!(matches!(change_payment, InternalPayment::Change(_, _))); - *change_payment = InternalPayment::Change( - (*change_address, in_amount - out_amount - fee), - change.view.clone(), - ); - } - - // Sanity check the change again after modifying - sanity_check_change_payment_quantity(&payments, change.address.is_some()); - - // Sanity check outgoing amount + fee == incoming amount - if change.address.is_some() { - debug_assert_eq!( - payments - .iter() - .map(|payment| match *payment { - InternalPayment::Payment(payment, _) => payment.1, - InternalPayment::Change(change, _) => change.1, - }) - .sum::() + - fee, - in_amount, - "Outgoing amount + fee != incoming amount" - ); - } - - Ok(SignableTransaction { - protocol, - r_seed, - inputs, - payments, - has_change: change.address.is_some(), - data, - fee, - fee_rate, - }) - } - - pub fn fee(&self) -> u64 { - self.fee - } - - pub fn fee_rate(&self) -> Fee { - self.fee_rate - } - - #[allow(clippy::type_complexity)] - fn prepare_payments( - seed: &Zeroizing<[u8; 32]>, - inputs: &[EdwardsPoint], - payments: &mut Vec, - uniqueness: [u8; 32], - ) -> (EdwardsPoint, Vec>, Vec, Option<[u8; 8]>) { - let mut rng = { - // Hash the inputs into the seed so we don't re-use Rs - // Doesn't re-use uniqueness as that's based on key images, which requires interactivity - // to generate. The output keys do not - // This remains private so long as the seed is private - let mut r_uniqueness = vec![]; - for input in inputs { - r_uniqueness.extend(input.compress().to_bytes()); - } - ChaCha20Rng::from_seed(hash( - &[b"monero-serai_outputs".as_ref(), seed.as_ref(), &r_uniqueness].concat(), - )) - }; - - // Shuffle the payments - payments.shuffle(&mut rng); - - // Used for all non-subaddress outputs, or if there's only one subaddress output and a change - let tx_key = Zeroizing::new(random_scalar(&mut rng)); - let mut tx_public_key = tx_key.deref() * ED25519_BASEPOINT_TABLE; - - // If any of these outputs are to a subaddress, we need keys distinct to them - // The only time this *does not* force having additional keys is when the only other output - // is a change output we have the view key for, enabling rewriting rA to aR - let (subaddresses, additional) = need_additional(payments); - let modified_change_ecdh = subaddresses && (!additional); - - // If we're using the aR rewrite, update tx_public_key from rG to rB - if modified_change_ecdh { - for payment in &*payments { - match payment { - InternalPayment::Payment(payment, _) => { - // This should be the only payment and it should be a subaddress - debug_assert!(payment.0.is_subaddress()); - tx_public_key = tx_key.deref() * payment.0.spend; - } - InternalPayment::Change(_, _) => {} - } - } - debug_assert!(tx_public_key != (tx_key.deref() * ED25519_BASEPOINT_TABLE)); - } - - // Actually create the outputs - let mut additional_keys = vec![]; - let mut outputs = Vec::with_capacity(payments.len()); - let mut id = None; - for (o, mut payment) in payments.drain(..).enumerate() { - // Downcast the change output to a payment output if it doesn't require special handling - // regarding it's view key - payment = if !modified_change_ecdh { - if let InternalPayment::Change(change, _) = &payment { - InternalPayment::Payment(*change, false) - } else { - payment - } - } else { - payment - }; - - let (output, payment_id) = match payment { - InternalPayment::Payment(payment, need_dummy_payment_id) => { - // If this is a subaddress, generate a dedicated r. Else, reuse the TX key - let dedicated = Zeroizing::new(random_scalar(&mut rng)); - let use_dedicated = additional && payment.0.is_subaddress(); - let r = if use_dedicated { &dedicated } else { &tx_key }; - - let (mut output, payment_id) = - SendOutput::new(r, uniqueness, (o, payment, need_dummy_payment_id)); - if modified_change_ecdh { - debug_assert_eq!(tx_public_key, output.R); - } - - if use_dedicated { - additional_keys.push(dedicated); - } else { - // If this used tx_key, randomize its R - // This is so when extra is created, there's a distinct R for it to use - output.R = dfg::EdwardsPoint::random(&mut rng).0; - } - (output, payment_id) - } - InternalPayment::Change(change, change_view) => { - // Instead of rA, use Ra, where R is r * subaddress_spend_key - // change.view must be Some as if it's None, this payment would've been downcast - let ecdh = tx_public_key * change_view.unwrap().deref(); - SendOutput::change(ecdh, uniqueness, (o, change, false)) - } - }; - - outputs.push(output); - id = id.or(payment_id); - } - - (tx_public_key, additional_keys, outputs, id) - } - - #[allow(non_snake_case)] - fn extra( - tx_key: EdwardsPoint, - additional: bool, - Rs: Vec, - id: Option<[u8; 8]>, - data: &mut Vec>, - ) -> Vec { - #[allow(non_snake_case)] - let Rs_len = Rs.len(); - let mut extra = Extra::new(tx_key, if additional { Rs } else { vec![] }); - - if let Some(id) = id { - let mut id_vec = Vec::with_capacity(1 + 8); - PaymentId::Encrypted(id).write(&mut id_vec).unwrap(); - extra.push(ExtraField::Nonce(id_vec)); - } - - // Include data if present - let extra_len = Extra::fee_weight(Rs_len, additional, id.is_some(), data.as_ref()); - for part in data.drain(..) { - let mut arb = vec![ARBITRARY_DATA_MARKER]; - arb.extend(part); - extra.push(ExtraField::Nonce(arb)); - } - - let mut serialized = Vec::with_capacity(extra_len); - extra.write(&mut serialized).unwrap(); - debug_assert_eq!(extra_len, serialized.len()); - serialized - } - - /// Returns the eventuality of this transaction. - /// - /// The eventuality is defined as the TX extra/outputs this transaction will create, if signed - /// with the specified seed. This eventuality can be compared to on-chain transactions to see - /// if the transaction has already been signed and published. - pub fn eventuality(&self) -> Option { - let inputs = self.inputs.iter().map(|(input, _)| input.key()).collect::>(); - let (tx_key, additional, outputs, id) = Self::prepare_payments( - self.r_seed.as_ref()?, - &inputs, - &mut self.payments.clone(), - // Lie about the uniqueness, used when determining output keys/commitments yet not the - // ephemeral keys, which is want we want here - // While we do still grab the outputs variable, it's so we can get its Rs - [0; 32], - ); - #[allow(non_snake_case)] - let Rs = outputs.iter().map(|output| output.R).collect(); - drop(outputs); - - let additional = !additional.is_empty(); - let extra = Self::extra(tx_key, additional, Rs, id, &mut self.data.clone()); - - Some(Eventuality { - protocol: self.protocol, - r_seed: self.r_seed.clone()?, - inputs, - payments: self.payments.clone(), - extra, - }) - } - - fn prepare_transaction( - &mut self, - rng: &mut R, - uniqueness: [u8; 32], - ) -> (Transaction, Scalar) { - // If no seed for the ephemeral keys was provided, make one - let r_seed = self.r_seed.clone().unwrap_or_else(|| { - let mut res = Zeroizing::new([0; 32]); - rng.fill_bytes(res.as_mut()); - res - }); - - let (tx_key, additional, outputs, id) = Self::prepare_payments( - &r_seed, - &self.inputs.iter().map(|(input, _)| input.key()).collect::>(), - &mut self.payments, - uniqueness, - ); - // This function only cares if additional keys were necessary, not what they were - let additional = !additional.is_empty(); - - let commitments = outputs.iter().map(|output| output.commitment.clone()).collect::>(); - let sum = commitments.iter().map(|commitment| commitment.mask).sum(); - - // Safe due to the constructor checking MAX_OUTPUTS - let bp = Bulletproofs::prove(rng, &commitments, self.protocol.bp_plus()).unwrap(); - - // Create the TX extra - let extra = Self::extra( - tx_key, - additional, - outputs.iter().map(|output| output.R).collect(), - id, - &mut self.data, - ); - - let mut fee = self.inputs.iter().map(|(input, _)| input.commitment().amount).sum::(); - let mut tx_outputs = Vec::with_capacity(outputs.len()); - let mut encrypted_amounts = Vec::with_capacity(outputs.len()); - for output in &outputs { - fee -= output.commitment.amount; - tx_outputs.push(Output { - amount: None, - key: output.dest.compress(), - view_tag: Some(output.view_tag).filter(|_| self.protocol.view_tags()), - }); - encrypted_amounts.push(EncryptedAmount::Compact { amount: output.amount }); - } - if self.has_change { - debug_assert_eq!(self.fee, fee, "transaction will use an unexpected fee"); - } - - ( - Transaction { - prefix: TransactionPrefix { - version: 2, - timelock: Timelock::None, - inputs: vec![], - outputs: tx_outputs, - extra, - }, - signatures: vec![], - rct_signatures: RctSignatures { - base: RctBase { - fee, - encrypted_amounts, - pseudo_outs: vec![], - commitments: commitments.iter().map(Commitment::calculate).collect(), - }, - prunable: RctPrunable::Clsag { bulletproofs: bp, clsags: vec![], pseudo_outs: vec![] }, - }, - }, - sum, - ) - } - - /// Sign this transaction. - pub fn sign( - mut self, - rng: &mut R, - spend: &Zeroizing, - ) -> Result { - let mut images = Vec::with_capacity(self.inputs.len()); - for (input, _) in &self.inputs { - let mut offset = Zeroizing::new(spend.deref() + input.key_offset()); - if (offset.deref() * ED25519_BASEPOINT_TABLE) != input.key() { - Err(TransactionError::WrongPrivateKey)?; - } - - images.push(generate_key_image(&offset)); - offset.zeroize(); - } - images.sort_by(key_image_sort); - - let (mut tx, mask_sum) = self.prepare_transaction( - rng, - uniqueness( - &images - .iter() - .map(|image| Input::ToKey { amount: None, key_offsets: vec![], key_image: *image }) - .collect::>(), - ), - ); - - let signable = prepare_inputs(&self.inputs, spend, &mut tx)?; - - let clsag_pairs = Clsag::sign(rng, signable, mask_sum, tx.signature_hash()); - match tx.rct_signatures.prunable { - RctPrunable::Null => panic!("Signing for RctPrunable::Null"), - RctPrunable::Clsag { ref mut clsags, ref mut pseudo_outs, .. } => { - clsags.append(&mut clsag_pairs.iter().map(|clsag| clsag.0.clone()).collect::>()); - pseudo_outs.append(&mut clsag_pairs.iter().map(|clsag| clsag.1).collect::>()); - } - _ => unreachable!("attempted to sign a TX which wasn't CLSAG"), - } - - if self.has_change { - debug_assert_eq!( - self.fee_rate.calculate_fee_from_weight(tx.weight()), - tx.rct_signatures.base.fee, - "transaction used unexpected fee", - ); - } - - Ok(tx) - } -} - -impl Eventuality { - /// Enables building a HashMap of Extra -> Eventuality for efficiently checking if an on-chain - /// transaction may match this eventuality. - /// - /// This extra is cryptographically bound to: - /// 1) A specific set of inputs (via their output key) - /// 2) A specific seed for the ephemeral keys - /// - /// This extra may be used in a transaction with a distinct set of inputs, yet no honest - /// transaction which doesn't satisfy this Eventuality will contain it. - pub fn extra(&self) -> &[u8] { - &self.extra - } - - #[must_use] - pub fn matches(&self, tx: &Transaction) -> bool { - if self.payments.len() != tx.prefix.outputs.len() { - return false; - } - - // Verify extra. - // Even if all the outputs were correct, a malicious extra could still cause a recipient to - // fail to receive their funds. - // This is the cheapest check available to perform as it does not require TX-specific ECC ops. - if self.extra != tx.prefix.extra { - return false; - } - - // Also ensure no timelock was set. - if tx.prefix.timelock != Timelock::None { - return false; - } - - // Generate the outputs. This is TX-specific due to uniqueness. - let (_, _, outputs, _) = SignableTransaction::prepare_payments( - &self.r_seed, - &self.inputs, - &mut self.payments.clone(), - uniqueness(&tx.prefix.inputs), - ); - - let rct_type = tx.rct_signatures.rct_type(); - if rct_type != self.protocol.optimal_rct_type() { - return false; - } - - // TODO: Remove this when the following for loop is updated - assert!( - rct_type.compact_encrypted_amounts(), - "created an Eventuality for a very old RctType we don't support proving for" - ); - - for (o, (expected, actual)) in outputs.iter().zip(tx.prefix.outputs.iter()).enumerate() { - // Verify the output, commitment, and encrypted amount. - if (&Output { - amount: None, - key: expected.dest.compress(), - view_tag: Some(expected.view_tag).filter(|_| self.protocol.view_tags()), - } != actual) || - (Some(&expected.commitment.calculate()) != tx.rct_signatures.base.commitments.get(o)) || - (Some(&EncryptedAmount::Compact { amount: expected.amount }) != - tx.rct_signatures.base.encrypted_amounts.get(o)) - { - return false; - } - } - - true - } - - pub fn write(&self, w: &mut W) -> io::Result<()> { - self.protocol.write(w)?; - write_raw_vec(write_byte, self.r_seed.as_ref(), w)?; - write_vec(write_point, &self.inputs, w)?; - - fn write_payment(payment: &InternalPayment, w: &mut W) -> io::Result<()> { - match payment { - InternalPayment::Payment(payment, need_dummy_payment_id) => { - w.write_all(&[0])?; - write_vec(write_byte, payment.0.to_string().as_bytes(), w)?; - w.write_all(&payment.1.to_le_bytes())?; - if *need_dummy_payment_id { - w.write_all(&[1]) - } else { - w.write_all(&[0]) - } - } - InternalPayment::Change(change, change_view) => { - w.write_all(&[1])?; - write_vec(write_byte, change.0.to_string().as_bytes(), w)?; - w.write_all(&change.1.to_le_bytes())?; - if let Some(view) = change_view.as_ref() { - w.write_all(&[1])?; - write_scalar(view, w) - } else { - w.write_all(&[0]) - } - } - } - } - write_vec(write_payment, &self.payments, w)?; - - write_vec(write_byte, &self.extra, w) - } - - pub fn serialize(&self) -> Vec { - let mut buf = Vec::with_capacity(128); - self.write(&mut buf).unwrap(); - buf - } - - pub fn read(r: &mut R) -> io::Result { - fn read_address(r: &mut R) -> io::Result { - String::from_utf8(read_vec(read_byte, r)?) - .ok() - .and_then(|str| MoneroAddress::from_str_raw(&str).ok()) - .ok_or_else(|| io::Error::other("invalid address")) - } - - fn read_payment(r: &mut R) -> io::Result { - Ok(match read_byte(r)? { - 0 => InternalPayment::Payment( - (read_address(r)?, read_u64(r)?), - match read_byte(r)? { - 0 => false, - 1 => true, - _ => Err(io::Error::other("invalid need additional"))?, - }, - ), - 1 => InternalPayment::Change( - (read_address(r)?, read_u64(r)?), - match read_byte(r)? { - 0 => None, - 1 => Some(Zeroizing::new(read_scalar(r)?)), - _ => Err(io::Error::other("invalid change view"))?, - }, - ), - _ => Err(io::Error::other("invalid payment"))?, - }) - } - - Ok(Eventuality { - protocol: Protocol::read(r)?, - r_seed: Zeroizing::new(read_bytes::<_, 32>(r)?), - inputs: read_vec(read_point, r)?, - payments: read_vec(read_payment, r)?, - extra: read_vec(read_byte, r)?, - }) - } -} diff --git a/coins/monero/src/wallet/send/multisig.rs b/coins/monero/src/wallet/send/multisig.rs deleted file mode 100644 index a5be404a..00000000 --- a/coins/monero/src/wallet/send/multisig.rs +++ /dev/null @@ -1,424 +0,0 @@ -use std_shims::{ - vec::Vec, - io::{self, Read}, - collections::HashMap, -}; -use std::sync::{Arc, RwLock}; - -use zeroize::Zeroizing; - -use rand_core::{RngCore, CryptoRng, SeedableRng}; -use rand_chacha::ChaCha20Rng; - -use group::ff::Field; -use curve25519_dalek::{traits::Identity, scalar::Scalar, edwards::EdwardsPoint}; -use dalek_ff_group as dfg; - -use transcript::{Transcript, RecommendedTranscript}; -use frost::{ - curve::Ed25519, - Participant, FrostError, ThresholdKeys, - dkg::lagrange, - sign::{ - Writable, Preprocess, CachedPreprocess, SignatureShare, PreprocessMachine, SignMachine, - SignatureMachine, AlgorithmMachine, AlgorithmSignMachine, AlgorithmSignatureMachine, - }, -}; - -use crate::{ - random_scalar, - ringct::{ - clsag::{ClsagInput, ClsagDetails, ClsagAddendum, ClsagMultisig}, - RctPrunable, - }, - transaction::{Input, Transaction}, - wallet::{TransactionError, InternalPayment, SignableTransaction, key_image_sort, uniqueness}, -}; - -/// FROST signing machine to produce a signed transaction. -pub struct TransactionMachine { - signable: SignableTransaction, - - i: Participant, - transcript: RecommendedTranscript, - - // Hashed key and scalar offset - key_images: Vec<(EdwardsPoint, Scalar)>, - inputs: Vec>>>, - clsags: Vec>, -} - -pub struct TransactionSignMachine { - signable: SignableTransaction, - - i: Participant, - transcript: RecommendedTranscript, - - key_images: Vec<(EdwardsPoint, Scalar)>, - inputs: Vec>>>, - clsags: Vec>, - - our_preprocess: Vec>, -} - -pub struct TransactionSignatureMachine { - tx: Transaction, - clsags: Vec>, -} - -impl SignableTransaction { - /// Create a FROST signing machine out of this signable transaction. - /// The height is the Monero blockchain height to synchronize around. - pub fn multisig( - self, - keys: &ThresholdKeys, - mut transcript: RecommendedTranscript, - ) -> Result { - let mut inputs = vec![]; - for _ in 0 .. self.inputs.len() { - // Doesn't resize as that will use a single Rc for the entire Vec - inputs.push(Arc::new(RwLock::new(None))); - } - let mut clsags = vec![]; - - // Create a RNG out of the input shared keys, which either requires the view key or being every - // sender, and the payments (address and amount), which a passive adversary may be able to know - // depending on how these transactions are coordinated - // Being every sender would already let you note rings which happen to use your transactions - // multiple times, already breaking privacy there - - transcript.domain_separate(b"monero_transaction"); - - // Also include the spend_key as below only the key offset is included, so this transcripts the - // sum product - // Useful as transcripting the sum product effectively transcripts the key image, further - // guaranteeing the one time properties noted below - transcript.append_message(b"spend_key", keys.group_key().0.compress().to_bytes()); - - if let Some(r_seed) = &self.r_seed { - transcript.append_message(b"r_seed", r_seed); - } - - for (input, decoys) in &self.inputs { - // These outputs can only be spent once. Therefore, it forces all RNGs derived from this - // transcript (such as the one used to create one time keys) to be unique - transcript.append_message(b"input_hash", input.output.absolute.tx); - transcript.append_message(b"input_output_index", [input.output.absolute.o]); - // Not including this, with a doxxed list of payments, would allow brute forcing the inputs - // to determine RNG seeds and therefore the true spends - transcript.append_message(b"input_shared_key", input.key_offset().to_bytes()); - - // Ensure all signers are signing the same rings - transcript.append_message(b"real_spend", [decoys.i]); - for (i, ring_member) in decoys.ring.iter().enumerate() { - transcript - .append_message(b"ring_member", [u8::try_from(i).expect("ring size exceeded 255")]); - transcript.append_message(b"ring_member_offset", decoys.offsets[i].to_le_bytes()); - transcript.append_message(b"ring_member_key", ring_member[0].compress().to_bytes()); - transcript.append_message(b"ring_member_commitment", ring_member[1].compress().to_bytes()); - } - } - - for payment in &self.payments { - match payment { - InternalPayment::Payment(payment, need_dummy_payment_id) => { - transcript.append_message(b"payment_address", payment.0.to_string().as_bytes()); - transcript.append_message(b"payment_amount", payment.1.to_le_bytes()); - transcript.append_message( - b"need_dummy_payment_id", - [if *need_dummy_payment_id { 1u8 } else { 0u8 }], - ); - } - InternalPayment::Change(change, change_view) => { - transcript.append_message(b"change_address", change.0.to_string().as_bytes()); - transcript.append_message(b"change_amount", change.1.to_le_bytes()); - if let Some(view) = change_view.as_ref() { - transcript.append_message(b"change_view_key", Zeroizing::new(view.to_bytes())); - } - } - } - } - - let mut key_images = vec![]; - for (i, (input, _)) in self.inputs.iter().enumerate() { - // Check this the right set of keys - let offset = keys.offset(dfg::Scalar(input.key_offset())); - if offset.group_key().0 != input.key() { - Err(TransactionError::WrongPrivateKey)?; - } - - let clsag = ClsagMultisig::new(transcript.clone(), input.key(), inputs[i].clone()); - key_images.push(( - clsag.H, - keys.current_offset().unwrap_or(dfg::Scalar::ZERO).0 + self.inputs[i].0.key_offset(), - )); - clsags.push(AlgorithmMachine::new(clsag, offset)); - } - - Ok(TransactionMachine { - signable: self, - - i: keys.params().i(), - transcript, - - key_images, - inputs, - clsags, - }) - } -} - -impl PreprocessMachine for TransactionMachine { - type Preprocess = Vec>; - type Signature = Transaction; - type SignMachine = TransactionSignMachine; - - fn preprocess( - mut self, - rng: &mut R, - ) -> (TransactionSignMachine, Self::Preprocess) { - // Iterate over each CLSAG calling preprocess - let mut preprocesses = Vec::with_capacity(self.clsags.len()); - let clsags = self - .clsags - .drain(..) - .map(|clsag| { - let (clsag, preprocess) = clsag.preprocess(rng); - preprocesses.push(preprocess); - clsag - }) - .collect(); - let our_preprocess = preprocesses.clone(); - - // We could add further entropy here, and previous versions of this library did so - // As of right now, the multisig's key, the inputs being spent, and the FROST data itself - // will be used for RNG seeds. In order to recreate these RNG seeds, breaking privacy, - // counterparties must have knowledge of the multisig, either the view key or access to the - // coordination layer, and then access to the actual FROST signing process - // If the commitments are sent in plain text, then entropy here also would be, making it not - // increase privacy. If they're not sent in plain text, or are otherwise inaccessible, they - // already offer sufficient entropy. That's why further entropy is not included - - ( - TransactionSignMachine { - signable: self.signable, - - i: self.i, - transcript: self.transcript, - - key_images: self.key_images, - inputs: self.inputs, - clsags, - - our_preprocess, - }, - preprocesses, - ) - } -} - -impl SignMachine for TransactionSignMachine { - type Params = (); - type Keys = ThresholdKeys; - type Preprocess = Vec>; - type SignatureShare = Vec>; - type SignatureMachine = TransactionSignatureMachine; - - fn cache(self) -> CachedPreprocess { - unimplemented!( - "Monero transactions don't support caching their preprocesses due to {}", - "being already bound to a specific transaction" - ); - } - - fn from_cache( - (): (), - _: ThresholdKeys, - _: CachedPreprocess, - ) -> (Self, Self::Preprocess) { - unimplemented!( - "Monero transactions don't support caching their preprocesses due to {}", - "being already bound to a specific transaction" - ); - } - - fn read_preprocess(&self, reader: &mut R) -> io::Result { - self.clsags.iter().map(|clsag| clsag.read_preprocess(reader)).collect() - } - - fn sign( - mut self, - mut commitments: HashMap, - msg: &[u8], - ) -> Result<(TransactionSignatureMachine, Self::SignatureShare), FrostError> { - if !msg.is_empty() { - panic!("message was passed to the TransactionMachine when it generates its own"); - } - - // Find out who's included - // This may not be a valid set of signers yet the algorithm machine will error if it's not - commitments.remove(&self.i); // Remove, if it was included for some reason - let mut included = commitments.keys().copied().collect::>(); - included.push(self.i); - included.sort_unstable(); - - // Start calculating the key images, as needed on the TX level - let mut images = vec![EdwardsPoint::identity(); self.clsags.len()]; - for (image, (generator, offset)) in images.iter_mut().zip(&self.key_images) { - *image = generator * offset; - } - - // Convert the serialized nonces commitments to a parallelized Vec - let mut commitments = (0 .. self.clsags.len()) - .map(|c| { - included - .iter() - .map(|l| { - // Add all commitments to the transcript for their entropy - // While each CLSAG will do this as they need to for security, they have their own - // transcripts cloned from this TX's initial premise's transcript. For our TX - // transcript to have the CLSAG data for entropy, it'll have to be added ourselves here - self.transcript.append_message(b"participant", (*l).to_bytes()); - - let preprocess = if *l == self.i { - self.our_preprocess[c].clone() - } else { - commitments.get_mut(l).ok_or(FrostError::MissingParticipant(*l))?[c].clone() - }; - - { - let mut buf = vec![]; - preprocess.write(&mut buf).unwrap(); - self.transcript.append_message(b"preprocess", buf); - } - - // While here, calculate the key image - // Clsag will parse/calculate/validate this as needed, yet doing so here as well - // provides the easiest API overall, as this is where the TX is (which needs the key - // images in its message), along with where the outputs are determined (where our - // outputs may need these in order to guarantee uniqueness) - images[c] += preprocess.addendum.key_image.0 * lagrange::(*l, &included).0; - - Ok((*l, preprocess)) - }) - .collect::, _>>() - }) - .collect::, _>>()?; - - // Remove our preprocess which shouldn't be here. It was just the easiest way to implement the - // above - for map in &mut commitments { - map.remove(&self.i); - } - - // Create the actual transaction - let (mut tx, output_masks) = { - let mut sorted_images = images.clone(); - sorted_images.sort_by(key_image_sort); - - self.signable.prepare_transaction( - // Technically, r_seed is used for the transaction keys if it's provided - &mut ChaCha20Rng::from_seed(self.transcript.rng_seed(b"transaction_keys_bulletproofs")), - uniqueness( - &sorted_images - .iter() - .map(|image| Input::ToKey { amount: None, key_offsets: vec![], key_image: *image }) - .collect::>(), - ), - ) - }; - - // Sort the inputs, as expected - let mut sorted = Vec::with_capacity(self.clsags.len()); - while !self.clsags.is_empty() { - let (inputs, decoys) = self.signable.inputs.swap_remove(0); - sorted.push(( - images.swap_remove(0), - inputs, - decoys, - self.inputs.swap_remove(0), - self.clsags.swap_remove(0), - commitments.swap_remove(0), - )); - } - sorted.sort_by(|x, y| key_image_sort(&x.0, &y.0)); - - let mut rng = ChaCha20Rng::from_seed(self.transcript.rng_seed(b"pseudo_out_masks")); - let mut sum_pseudo_outs = Scalar::ZERO; - while !sorted.is_empty() { - let value = sorted.remove(0); - - let mut mask = random_scalar(&mut rng); - if sorted.is_empty() { - mask = output_masks - sum_pseudo_outs; - } else { - sum_pseudo_outs += mask; - } - - tx.prefix.inputs.push(Input::ToKey { - amount: None, - key_offsets: value.2.offsets.clone(), - key_image: value.0, - }); - - *value.3.write().unwrap() = Some(ClsagDetails::new( - ClsagInput::new(value.1.commitment().clone(), value.2).map_err(|_| { - panic!("Signing an input which isn't present in the ring we created for it") - })?, - mask, - )); - - self.clsags.push(value.4); - commitments.push(value.5); - } - - let msg = tx.signature_hash(); - - // Iterate over each CLSAG calling sign - let mut shares = Vec::with_capacity(self.clsags.len()); - let clsags = self - .clsags - .drain(..) - .map(|clsag| { - let (clsag, share) = clsag.sign(commitments.remove(0), &msg)?; - shares.push(share); - Ok(clsag) - }) - .collect::>()?; - - Ok((TransactionSignatureMachine { tx, clsags }, shares)) - } -} - -impl SignatureMachine for TransactionSignatureMachine { - type SignatureShare = Vec>; - - fn read_share(&self, reader: &mut R) -> io::Result { - self.clsags.iter().map(|clsag| clsag.read_share(reader)).collect() - } - - fn complete( - mut self, - shares: HashMap, - ) -> Result { - let mut tx = self.tx; - match tx.rct_signatures.prunable { - RctPrunable::Null => panic!("Signing for RctPrunable::Null"), - RctPrunable::Clsag { ref mut clsags, ref mut pseudo_outs, .. } => { - for (c, clsag) in self.clsags.drain(..).enumerate() { - let (clsag, pseudo_out) = clsag.complete( - shares.iter().map(|(l, shares)| (*l, shares[c].clone())).collect::>(), - )?; - clsags.push(clsag); - pseudo_outs.push(pseudo_out); - } - } - RctPrunable::AggregateMlsagBorromean { .. } | - RctPrunable::MlsagBorromean { .. } | - RctPrunable::MlsagBulletproofs { .. } => { - unreachable!("attempted to sign a multisig TX which wasn't CLSAG") - } - } - Ok(tx) - } -} diff --git a/coins/monero/tests/runner.rs b/coins/monero/tests/runner.rs deleted file mode 100644 index 9cef6c21..00000000 --- a/coins/monero/tests/runner.rs +++ /dev/null @@ -1,326 +0,0 @@ -use core::ops::Deref; -use std_shims::{sync::OnceLock, collections::HashSet}; - -use zeroize::Zeroizing; -use rand_core::OsRng; - -use curve25519_dalek::{constants::ED25519_BASEPOINT_TABLE, scalar::Scalar}; - -use tokio::sync::Mutex; - -use monero_serai::{ - random_scalar, - rpc::{HttpRpc, Rpc}, - wallet::{ - ViewPair, Scanner, - address::{Network, AddressType, AddressSpec, AddressMeta, MoneroAddress}, - SpendableOutput, Fee, - }, - transaction::Transaction, - DEFAULT_LOCK_WINDOW, -}; - -pub fn random_address() -> (Scalar, ViewPair, MoneroAddress) { - let spend = random_scalar(&mut OsRng); - let spend_pub = &spend * ED25519_BASEPOINT_TABLE; - let view = Zeroizing::new(random_scalar(&mut OsRng)); - ( - spend, - ViewPair::new(spend_pub, view.clone()), - MoneroAddress { - meta: AddressMeta::new(Network::Mainnet, AddressType::Standard), - spend: spend_pub, - view: view.deref() * ED25519_BASEPOINT_TABLE, - }, - ) -} - -// TODO: Support transactions already on-chain -// TODO: Don't have a side effect of mining blocks more blocks than needed under race conditions -pub async fn mine_until_unlocked(rpc: &Rpc, addr: &str, tx_hash: [u8; 32]) { - // mine until tx is in a block - let mut height = rpc.get_height().await.unwrap(); - let mut found = false; - while !found { - let block = rpc.get_block_by_number(height - 1).await.unwrap(); - found = match block.txs.iter().find(|&&x| x == tx_hash) { - Some(_) => true, - None => { - height = rpc.generate_blocks(addr, 1).await.unwrap().1 + 1; - false - } - } - } - - // Mine until tx's outputs are unlocked - let o_indexes: Vec = rpc.get_o_indexes(tx_hash).await.unwrap(); - while rpc - .get_outs(&o_indexes) - .await - .unwrap() - .into_iter() - .all(|o| (!(o.unlocked && height >= (o.height + DEFAULT_LOCK_WINDOW)))) - { - height = rpc.generate_blocks(addr, 1).await.unwrap().1 + 1; - } -} - -// Mines 60 blocks and returns an unlocked miner TX output. -#[allow(dead_code)] -pub async fn get_miner_tx_output(rpc: &Rpc, view: &ViewPair) -> SpendableOutput { - let mut scanner = Scanner::from_view(view.clone(), Some(HashSet::new())); - - // Mine 60 blocks to unlock a miner TX - let start = rpc.get_height().await.unwrap(); - rpc - .generate_blocks(&view.address(Network::Mainnet, AddressSpec::Standard).to_string(), 60) - .await - .unwrap(); - - let block = rpc.get_block_by_number(start).await.unwrap(); - scanner.scan(rpc, &block).await.unwrap().swap_remove(0).ignore_timelock().swap_remove(0) -} - -/// Make sure the weight and fee match the expected calculation. -pub fn check_weight_and_fee(tx: &Transaction, fee_rate: Fee) { - let fee = tx.rct_signatures.base.fee; - - let weight = tx.weight(); - let expected_weight = fee_rate.calculate_weight_from_fee(fee); - assert_eq!(weight, expected_weight); - - let expected_fee = fee_rate.calculate_fee_from_weight(weight); - assert_eq!(fee, expected_fee); -} - -pub async fn rpc() -> Rpc { - let rpc = HttpRpc::new("http://serai:seraidex@127.0.0.1:18081".to_string()).await.unwrap(); - - // Only run once - if rpc.get_height().await.unwrap() != 1 { - return rpc; - } - - let addr = MoneroAddress { - meta: AddressMeta::new(Network::Mainnet, AddressType::Standard), - spend: &random_scalar(&mut OsRng) * ED25519_BASEPOINT_TABLE, - view: &random_scalar(&mut OsRng) * ED25519_BASEPOINT_TABLE, - } - .to_string(); - - // Mine 40 blocks to ensure decoy availability - rpc.generate_blocks(&addr, 40).await.unwrap(); - - // Make sure we recognize the protocol - rpc.get_protocol().await.unwrap(); - - rpc -} - -pub static SEQUENTIAL: OnceLock> = OnceLock::new(); - -#[macro_export] -macro_rules! async_sequential { - ($(async fn $name: ident() $body: block)*) => { - $( - #[tokio::test] - async fn $name() { - let guard = runner::SEQUENTIAL.get_or_init(|| tokio::sync::Mutex::new(())).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; - } - )* - } -} - -#[macro_export] -macro_rules! test { - ( - $name: ident, - ( - $first_tx: expr, - $first_checks: expr, - ), - $(( - $tx: expr, - $checks: expr, - )$(,)?),* - ) => { - async_sequential! { - async fn $name() { - use core::{ops::Deref, any::Any}; - use std::collections::HashSet; - #[cfg(feature = "multisig")] - use std::collections::HashMap; - - use zeroize::Zeroizing; - use rand_core::OsRng; - - use curve25519_dalek::constants::ED25519_BASEPOINT_TABLE; - - #[cfg(feature = "multisig")] - use transcript::{Transcript, RecommendedTranscript}; - #[cfg(feature = "multisig")] - use frost::{ - curve::Ed25519, - Participant, - tests::{THRESHOLD, key_gen}, - }; - - use monero_serai::{ - random_scalar, - wallet::{ - address::{Network, AddressSpec}, ViewPair, Scanner, Change, Decoys, FeePriority, - SignableTransaction, SignableTransactionBuilder, - }, - }; - - use runner::{ - random_address, rpc, mine_until_unlocked, get_miner_tx_output, - check_weight_and_fee, - }; - - type Builder = SignableTransactionBuilder; - - // Run each function as both a single signer and as a multisig - #[allow(clippy::redundant_closure_call)] - for multisig in [false, true] { - // Only run the multisig variant if multisig is enabled - if multisig { - #[cfg(not(feature = "multisig"))] - continue; - } - - let spend = Zeroizing::new(random_scalar(&mut OsRng)); - #[cfg(feature = "multisig")] - let keys = key_gen::<_, Ed25519>(&mut OsRng); - - let spend_pub = if !multisig { - spend.deref() * ED25519_BASEPOINT_TABLE - } else { - #[cfg(not(feature = "multisig"))] - panic!("Multisig branch called without the multisig feature"); - #[cfg(feature = "multisig")] - keys[&Participant::new(1).unwrap()].group_key().0 - }; - - let rpc = rpc().await; - - let view = ViewPair::new(spend_pub, Zeroizing::new(random_scalar(&mut OsRng))); - let addr = view.address(Network::Mainnet, AddressSpec::Standard); - - let miner_tx = get_miner_tx_output(&rpc, &view).await; - - let protocol = rpc.get_protocol().await.unwrap(); - - let builder = SignableTransactionBuilder::new( - protocol, - rpc.get_fee(protocol, FeePriority::Unimportant).await.unwrap(), - Change::new( - &ViewPair::new( - &random_scalar(&mut OsRng) * ED25519_BASEPOINT_TABLE, - Zeroizing::new(random_scalar(&mut OsRng)) - ), - false - ), - ); - - let sign = |tx: SignableTransaction| { - let spend = spend.clone(); - #[cfg(feature = "multisig")] - let keys = keys.clone(); - async move { - if !multisig { - tx.sign(&mut OsRng, &spend).unwrap() - } else { - #[cfg(not(feature = "multisig"))] - panic!("Multisig branch called without the multisig feature"); - #[cfg(feature = "multisig")] - { - let mut machines = HashMap::new(); - for i in (1 ..= THRESHOLD).map(|i| Participant::new(i).unwrap()) { - machines.insert( - i, - tx - .clone() - .multisig( - &keys[&i], - RecommendedTranscript::new(b"Monero Serai Test Transaction"), - ) - .unwrap(), - ); - } - - frost::tests::sign_without_caching(&mut OsRng, machines, &[]) - } - } - } - }; - - // TODO: Generate a distinct wallet for each transaction to prevent overlap - let next_addr = addr; - - let temp = Box::new({ - let mut builder = builder.clone(); - - let decoys = Decoys::fingerprintable_canonical_select( - &mut OsRng, - &rpc, - protocol.ring_len(), - rpc.get_height().await.unwrap(), - &[miner_tx.clone()], - ) - .await - .unwrap(); - builder.add_input((miner_tx, decoys.first().unwrap().clone())); - - let (tx, state) = ($first_tx)(rpc.clone(), builder, next_addr).await; - let fee_rate = tx.fee_rate().clone(); - let signed = sign(tx).await; - rpc.publish_transaction(&signed).await.unwrap(); - mine_until_unlocked(&rpc, &random_address().2.to_string(), signed.hash()).await; - let tx = rpc.get_transaction(signed.hash()).await.unwrap(); - check_weight_and_fee(&tx, fee_rate); - let scanner = - Scanner::from_view(view.clone(), Some(HashSet::new())); - ($first_checks)(rpc.clone(), tx, scanner, state).await - }); - #[allow(unused_variables, unused_mut, unused_assignments)] - let mut carried_state: Box = temp; - - $( - let (tx, state) = ($tx)( - protocol, - rpc.clone(), - builder.clone(), - next_addr, - *carried_state.downcast().unwrap() - ).await; - let fee_rate = tx.fee_rate().clone(); - let signed = sign(tx).await; - rpc.publish_transaction(&signed).await.unwrap(); - mine_until_unlocked(&rpc, &random_address().2.to_string(), signed.hash()).await; - let tx = rpc.get_transaction(signed.hash()).await.unwrap(); - if stringify!($name) != "spend_one_input_to_two_outputs_no_change" { - // Skip weight and fee check for the above test because when there is no change, - // the change is added to the fee - check_weight_and_fee(&tx, fee_rate); - } - #[allow(unused_assignments)] - { - let scanner = - Scanner::from_view(view.clone(), Some(HashSet::new())); - carried_state = - Box::new(($checks)(rpc.clone(), tx, scanner, state).await); - } - )* - } - } - } - } -} diff --git a/coins/monero/tests/scan.rs b/coins/monero/tests/scan.rs deleted file mode 100644 index 3e9c9069..00000000 --- a/coins/monero/tests/scan.rs +++ /dev/null @@ -1,305 +0,0 @@ -use rand::RngCore; - -use monero_serai::{ - transaction::Transaction, - wallet::{address::SubaddressIndex, extra::PaymentId}, -}; - -mod runner; - -test!( - scan_standard_address, - ( - |_, mut builder: Builder, _| async move { - let view = runner::random_address().1; - let scanner = Scanner::from_view(view.clone(), Some(HashSet::new())); - builder.add_payment(view.address(Network::Mainnet, AddressSpec::Standard), 5); - (builder.build().unwrap(), scanner) - }, - |_, tx: Transaction, _, mut state: Scanner| async move { - let output = state.scan_transaction(&tx).not_locked().swap_remove(0); - assert_eq!(output.commitment().amount, 5); - let dummy_payment_id = PaymentId::Encrypted([0u8; 8]); - assert_eq!(output.metadata.payment_id, Some(dummy_payment_id)); - }, - ), -); - -test!( - scan_subaddress, - ( - |_, mut builder: Builder, _| async move { - let subaddress = SubaddressIndex::new(0, 1).unwrap(); - - let view = runner::random_address().1; - let mut scanner = Scanner::from_view(view.clone(), Some(HashSet::new())); - scanner.register_subaddress(subaddress); - - builder.add_payment(view.address(Network::Mainnet, AddressSpec::Subaddress(subaddress)), 5); - (builder.build().unwrap(), (scanner, subaddress)) - }, - |_, tx: Transaction, _, mut state: (Scanner, SubaddressIndex)| async move { - let output = state.0.scan_transaction(&tx).not_locked().swap_remove(0); - assert_eq!(output.commitment().amount, 5); - assert_eq!(output.metadata.subaddress, Some(state.1)); - }, - ), -); - -test!( - scan_integrated_address, - ( - |_, mut builder: Builder, _| async move { - let view = runner::random_address().1; - let scanner = Scanner::from_view(view.clone(), Some(HashSet::new())); - - let mut payment_id = [0u8; 8]; - OsRng.fill_bytes(&mut payment_id); - - builder.add_payment(view.address(Network::Mainnet, AddressSpec::Integrated(payment_id)), 5); - (builder.build().unwrap(), (scanner, payment_id)) - }, - |_, tx: Transaction, _, mut state: (Scanner, [u8; 8])| async move { - let output = state.0.scan_transaction(&tx).not_locked().swap_remove(0); - assert_eq!(output.commitment().amount, 5); - assert_eq!(output.metadata.payment_id, Some(PaymentId::Encrypted(state.1))); - }, - ), -); - -test!( - scan_featured_standard, - ( - |_, mut builder: Builder, _| async move { - let view = runner::random_address().1; - let scanner = Scanner::from_view(view.clone(), Some(HashSet::new())); - builder.add_payment( - view.address( - Network::Mainnet, - AddressSpec::Featured { subaddress: None, payment_id: None, guaranteed: false }, - ), - 5, - ); - (builder.build().unwrap(), scanner) - }, - |_, tx: Transaction, _, mut state: Scanner| async move { - let output = state.scan_transaction(&tx).not_locked().swap_remove(0); - assert_eq!(output.commitment().amount, 5); - }, - ), -); - -test!( - scan_featured_subaddress, - ( - |_, mut builder: Builder, _| async move { - let subaddress = SubaddressIndex::new(0, 2).unwrap(); - - let view = runner::random_address().1; - let mut scanner = Scanner::from_view(view.clone(), Some(HashSet::new())); - scanner.register_subaddress(subaddress); - - builder.add_payment( - view.address( - Network::Mainnet, - AddressSpec::Featured { - subaddress: Some(subaddress), - payment_id: None, - guaranteed: false, - }, - ), - 5, - ); - (builder.build().unwrap(), (scanner, subaddress)) - }, - |_, tx: Transaction, _, mut state: (Scanner, SubaddressIndex)| async move { - let output = state.0.scan_transaction(&tx).not_locked().swap_remove(0); - assert_eq!(output.commitment().amount, 5); - assert_eq!(output.metadata.subaddress, Some(state.1)); - }, - ), -); - -test!( - scan_featured_integrated, - ( - |_, mut builder: Builder, _| async move { - let view = runner::random_address().1; - let scanner = Scanner::from_view(view.clone(), Some(HashSet::new())); - let mut payment_id = [0u8; 8]; - OsRng.fill_bytes(&mut payment_id); - - builder.add_payment( - view.address( - Network::Mainnet, - AddressSpec::Featured { - subaddress: None, - payment_id: Some(payment_id), - guaranteed: false, - }, - ), - 5, - ); - (builder.build().unwrap(), (scanner, payment_id)) - }, - |_, tx: Transaction, _, mut state: (Scanner, [u8; 8])| async move { - let output = state.0.scan_transaction(&tx).not_locked().swap_remove(0); - assert_eq!(output.commitment().amount, 5); - assert_eq!(output.metadata.payment_id, Some(PaymentId::Encrypted(state.1))); - }, - ), -); - -test!( - scan_featured_integrated_subaddress, - ( - |_, mut builder: Builder, _| async move { - let subaddress = SubaddressIndex::new(0, 3).unwrap(); - - let view = runner::random_address().1; - let mut scanner = Scanner::from_view(view.clone(), Some(HashSet::new())); - scanner.register_subaddress(subaddress); - - let mut payment_id = [0u8; 8]; - OsRng.fill_bytes(&mut payment_id); - - builder.add_payment( - view.address( - Network::Mainnet, - AddressSpec::Featured { - subaddress: Some(subaddress), - payment_id: Some(payment_id), - guaranteed: false, - }, - ), - 5, - ); - (builder.build().unwrap(), (scanner, payment_id, subaddress)) - }, - |_, tx: Transaction, _, mut state: (Scanner, [u8; 8], SubaddressIndex)| async move { - let output = state.0.scan_transaction(&tx).not_locked().swap_remove(0); - assert_eq!(output.commitment().amount, 5); - assert_eq!(output.metadata.payment_id, Some(PaymentId::Encrypted(state.1))); - assert_eq!(output.metadata.subaddress, Some(state.2)); - }, - ), -); - -test!( - scan_guaranteed_standard, - ( - |_, mut builder: Builder, _| async move { - let view = runner::random_address().1; - let scanner = Scanner::from_view(view.clone(), None); - - builder.add_payment( - view.address( - Network::Mainnet, - AddressSpec::Featured { subaddress: None, payment_id: None, guaranteed: true }, - ), - 5, - ); - (builder.build().unwrap(), scanner) - }, - |_, tx: Transaction, _, mut state: Scanner| async move { - let output = state.scan_transaction(&tx).not_locked().swap_remove(0); - assert_eq!(output.commitment().amount, 5); - }, - ), -); - -test!( - scan_guaranteed_subaddress, - ( - |_, mut builder: Builder, _| async move { - let subaddress = SubaddressIndex::new(1, 0).unwrap(); - - let view = runner::random_address().1; - let mut scanner = Scanner::from_view(view.clone(), None); - scanner.register_subaddress(subaddress); - - builder.add_payment( - view.address( - Network::Mainnet, - AddressSpec::Featured { - subaddress: Some(subaddress), - payment_id: None, - guaranteed: true, - }, - ), - 5, - ); - (builder.build().unwrap(), (scanner, subaddress)) - }, - |_, tx: Transaction, _, mut state: (Scanner, SubaddressIndex)| async move { - let output = state.0.scan_transaction(&tx).not_locked().swap_remove(0); - assert_eq!(output.commitment().amount, 5); - assert_eq!(output.metadata.subaddress, Some(state.1)); - }, - ), -); - -test!( - scan_guaranteed_integrated, - ( - |_, mut builder: Builder, _| async move { - let view = runner::random_address().1; - let scanner = Scanner::from_view(view.clone(), None); - let mut payment_id = [0u8; 8]; - OsRng.fill_bytes(&mut payment_id); - - builder.add_payment( - view.address( - Network::Mainnet, - AddressSpec::Featured { - subaddress: None, - payment_id: Some(payment_id), - guaranteed: true, - }, - ), - 5, - ); - (builder.build().unwrap(), (scanner, payment_id)) - }, - |_, tx: Transaction, _, mut state: (Scanner, [u8; 8])| async move { - let output = state.0.scan_transaction(&tx).not_locked().swap_remove(0); - assert_eq!(output.commitment().amount, 5); - assert_eq!(output.metadata.payment_id, Some(PaymentId::Encrypted(state.1))); - }, - ), -); - -test!( - scan_guaranteed_integrated_subaddress, - ( - |_, mut builder: Builder, _| async move { - let subaddress = SubaddressIndex::new(1, 1).unwrap(); - - let view = runner::random_address().1; - let mut scanner = Scanner::from_view(view.clone(), None); - scanner.register_subaddress(subaddress); - - let mut payment_id = [0u8; 8]; - OsRng.fill_bytes(&mut payment_id); - - builder.add_payment( - view.address( - Network::Mainnet, - AddressSpec::Featured { - subaddress: Some(subaddress), - payment_id: Some(payment_id), - guaranteed: true, - }, - ), - 5, - ); - (builder.build().unwrap(), (scanner, payment_id, subaddress)) - }, - |_, tx: Transaction, _, mut state: (Scanner, [u8; 8], SubaddressIndex)| async move { - let output = state.0.scan_transaction(&tx).not_locked().swap_remove(0); - assert_eq!(output.commitment().amount, 5); - assert_eq!(output.metadata.payment_id, Some(PaymentId::Encrypted(state.1))); - assert_eq!(output.metadata.subaddress, Some(state.2)); - }, - ), -); diff --git a/coins/monero/tests/send.rs b/coins/monero/tests/send.rs deleted file mode 100644 index 4c338eb6..00000000 --- a/coins/monero/tests/send.rs +++ /dev/null @@ -1,316 +0,0 @@ -use rand_core::OsRng; - -use monero_serai::{ - transaction::Transaction, - wallet::{ - extra::Extra, address::SubaddressIndex, ReceivedOutput, SpendableOutput, Decoys, - SignableTransactionBuilder, - }, - rpc::{Rpc, HttpRpc}, - Protocol, -}; - -mod runner; - -// Set up inputs, select decoys, then add them to the TX builder -async fn add_inputs( - protocol: Protocol, - rpc: &Rpc, - outputs: Vec, - builder: &mut SignableTransactionBuilder, -) { - let mut spendable_outputs = Vec::with_capacity(outputs.len()); - for output in outputs { - spendable_outputs.push(SpendableOutput::from(rpc, output).await.unwrap()); - } - - let decoys = Decoys::fingerprintable_canonical_select( - &mut OsRng, - rpc, - protocol.ring_len(), - rpc.get_height().await.unwrap(), - &spendable_outputs, - ) - .await - .unwrap(); - - let inputs = spendable_outputs.into_iter().zip(decoys).collect::>(); - - builder.add_inputs(&inputs); -} - -test!( - spend_miner_output, - ( - |_, mut builder: Builder, addr| async move { - builder.add_payment(addr, 5); - (builder.build().unwrap(), ()) - }, - |_, tx: Transaction, mut scanner: Scanner, ()| async move { - let output = scanner.scan_transaction(&tx).not_locked().swap_remove(0); - assert_eq!(output.commitment().amount, 5); - }, - ), -); - -test!( - spend_multiple_outputs, - ( - |_, mut builder: Builder, addr| async move { - builder.add_payment(addr, 1000000000000); - builder.add_payment(addr, 2000000000000); - (builder.build().unwrap(), ()) - }, - |_, tx: Transaction, mut scanner: Scanner, ()| async move { - let mut outputs = scanner.scan_transaction(&tx).not_locked(); - outputs.sort_by(|x, y| x.commitment().amount.cmp(&y.commitment().amount)); - assert_eq!(outputs[0].commitment().amount, 1000000000000); - assert_eq!(outputs[1].commitment().amount, 2000000000000); - outputs - }, - ), - ( - |protocol: Protocol, rpc, mut builder: Builder, addr, outputs: Vec| async move { - add_inputs(protocol, &rpc, outputs, &mut builder).await; - builder.add_payment(addr, 6); - (builder.build().unwrap(), ()) - }, - |_, tx: Transaction, mut scanner: Scanner, ()| async move { - let output = scanner.scan_transaction(&tx).not_locked().swap_remove(0); - assert_eq!(output.commitment().amount, 6); - }, - ), -); - -test!( - // Ideally, this would be single_R, yet it isn't feasible to apply allow(non_snake_case) here - single_r_subaddress_send, - ( - // Consume this builder for an output we can use in the future - // This is needed because we can't get the input from the passed in builder - |_, mut builder: Builder, addr| async move { - builder.add_payment(addr, 1000000000000); - (builder.build().unwrap(), ()) - }, - |_, tx: Transaction, mut scanner: Scanner, ()| async move { - let mut outputs = scanner.scan_transaction(&tx).not_locked(); - outputs.sort_by(|x, y| x.commitment().amount.cmp(&y.commitment().amount)); - assert_eq!(outputs[0].commitment().amount, 1000000000000); - outputs - }, - ), - ( - |protocol, rpc: Rpc<_>, _, _, outputs: Vec| async move { - use monero_serai::wallet::FeePriority; - - let change_view = ViewPair::new( - &random_scalar(&mut OsRng) * ED25519_BASEPOINT_TABLE, - Zeroizing::new(random_scalar(&mut OsRng)), - ); - - let mut builder = SignableTransactionBuilder::new( - protocol, - rpc.get_fee(protocol, FeePriority::Unimportant).await.unwrap(), - Change::new(&change_view, false), - ); - add_inputs(protocol, &rpc, vec![outputs.first().unwrap().clone()], &mut builder).await; - - // Send to a subaddress - let sub_view = ViewPair::new( - &random_scalar(&mut OsRng) * ED25519_BASEPOINT_TABLE, - Zeroizing::new(random_scalar(&mut OsRng)), - ); - builder.add_payment( - sub_view - .address(Network::Mainnet, AddressSpec::Subaddress(SubaddressIndex::new(0, 1).unwrap())), - 1, - ); - (builder.build().unwrap(), (change_view, sub_view)) - }, - |_, tx: Transaction, _, views: (ViewPair, ViewPair)| async move { - // Make sure the change can pick up its output - let mut change_scanner = Scanner::from_view(views.0, Some(HashSet::new())); - assert!(change_scanner.scan_transaction(&tx).not_locked().len() == 1); - - // Make sure the subaddress can pick up its output - let mut sub_scanner = Scanner::from_view(views.1, Some(HashSet::new())); - sub_scanner.register_subaddress(SubaddressIndex::new(0, 1).unwrap()); - let sub_outputs = sub_scanner.scan_transaction(&tx).not_locked(); - assert!(sub_outputs.len() == 1); - assert_eq!(sub_outputs[0].commitment().amount, 1); - - // Make sure only one R was included in TX extra - assert!(Extra::read::<&[u8]>(&mut tx.prefix.extra.as_ref()) - .unwrap() - .keys() - .unwrap() - .1 - .is_none()); - }, - ), -); - -test!( - spend_one_input_to_one_output_plus_change, - ( - |_, mut builder: Builder, addr| async move { - builder.add_payment(addr, 2000000000000); - (builder.build().unwrap(), ()) - }, - |_, tx: Transaction, mut scanner: Scanner, ()| async move { - let mut outputs = scanner.scan_transaction(&tx).not_locked(); - outputs.sort_by(|x, y| x.commitment().amount.cmp(&y.commitment().amount)); - assert_eq!(outputs[0].commitment().amount, 2000000000000); - outputs - }, - ), - ( - |protocol: Protocol, rpc, mut builder: Builder, addr, outputs: Vec| async move { - add_inputs(protocol, &rpc, outputs, &mut builder).await; - builder.add_payment(addr, 2); - (builder.build().unwrap(), ()) - }, - |_, tx: Transaction, mut scanner: Scanner, ()| async move { - let output = scanner.scan_transaction(&tx).not_locked().swap_remove(0); - assert_eq!(output.commitment().amount, 2); - }, - ), -); - -test!( - spend_max_outputs, - ( - |_, mut builder: Builder, addr| async move { - builder.add_payment(addr, 1000000000000); - (builder.build().unwrap(), ()) - }, - |_, tx: Transaction, mut scanner: Scanner, ()| async move { - let mut outputs = scanner.scan_transaction(&tx).not_locked(); - outputs.sort_by(|x, y| x.commitment().amount.cmp(&y.commitment().amount)); - assert_eq!(outputs[0].commitment().amount, 1000000000000); - outputs - }, - ), - ( - |protocol: Protocol, rpc, mut builder: Builder, addr, outputs: Vec| async move { - add_inputs(protocol, &rpc, outputs, &mut builder).await; - - for i in 0 .. 15 { - builder.add_payment(addr, i + 1); - } - (builder.build().unwrap(), ()) - }, - |_, tx: Transaction, mut scanner: Scanner, ()| async move { - let mut scanned_tx = scanner.scan_transaction(&tx).not_locked(); - - let mut output_amounts = HashSet::new(); - for i in 0 .. 15 { - output_amounts.insert(i + 1); - } - for _ in 0 .. 15 { - let output = scanned_tx.swap_remove(0); - let amount = output.commitment().amount; - assert!(output_amounts.contains(&amount)); - output_amounts.remove(&amount); - } - }, - ), -); - -test!( - spend_max_outputs_to_subaddresses, - ( - |_, mut builder: Builder, addr| async move { - builder.add_payment(addr, 1000000000000); - (builder.build().unwrap(), ()) - }, - |_, tx: Transaction, mut scanner: Scanner, ()| async move { - let mut outputs = scanner.scan_transaction(&tx).not_locked(); - outputs.sort_by(|x, y| x.commitment().amount.cmp(&y.commitment().amount)); - assert_eq!(outputs[0].commitment().amount, 1000000000000); - outputs - }, - ), - ( - |protocol: Protocol, rpc, mut builder: Builder, _, outputs: Vec| async move { - add_inputs(protocol, &rpc, outputs, &mut builder).await; - - let view = runner::random_address().1; - let mut scanner = Scanner::from_view(view.clone(), Some(HashSet::new())); - - let mut subaddresses = vec![]; - for i in 0 .. 15 { - let subaddress = SubaddressIndex::new(0, i + 1).unwrap(); - scanner.register_subaddress(subaddress); - - builder.add_payment( - view.address(Network::Mainnet, AddressSpec::Subaddress(subaddress)), - u64::from(i + 1), - ); - subaddresses.push(subaddress); - } - - (builder.build().unwrap(), (scanner, subaddresses)) - }, - |_, tx: Transaction, _, mut state: (Scanner, Vec)| async move { - use std::collections::HashMap; - - let mut scanned_tx = state.0.scan_transaction(&tx).not_locked(); - - let mut output_amounts_by_subaddress = HashMap::new(); - for i in 0 .. 15 { - output_amounts_by_subaddress.insert(u64::try_from(i + 1).unwrap(), state.1[i]); - } - for _ in 0 .. 15 { - let output = scanned_tx.swap_remove(0); - let amount = output.commitment().amount; - - assert!(output_amounts_by_subaddress.contains_key(&amount)); - assert_eq!(output.metadata.subaddress, Some(output_amounts_by_subaddress[&amount])); - - output_amounts_by_subaddress.remove(&amount); - } - }, - ), -); - -test!( - spend_one_input_to_two_outputs_no_change, - ( - |_, mut builder: Builder, addr| async move { - builder.add_payment(addr, 1000000000000); - (builder.build().unwrap(), ()) - }, - |_, tx: Transaction, mut scanner: Scanner, ()| async move { - let mut outputs = scanner.scan_transaction(&tx).not_locked(); - outputs.sort_by(|x, y| x.commitment().amount.cmp(&y.commitment().amount)); - assert_eq!(outputs[0].commitment().amount, 1000000000000); - outputs - }, - ), - ( - |protocol, rpc: Rpc<_>, _, addr, outputs: Vec| async move { - use monero_serai::wallet::FeePriority; - - let mut builder = SignableTransactionBuilder::new( - protocol, - rpc.get_fee(protocol, FeePriority::Unimportant).await.unwrap(), - Change::fingerprintable(None), - ); - add_inputs(protocol, &rpc, vec![outputs.first().unwrap().clone()], &mut builder).await; - builder.add_payment(addr, 10000); - builder.add_payment(addr, 50000); - - (builder.build().unwrap(), ()) - }, - |_, tx: Transaction, mut scanner: Scanner, ()| async move { - let mut outputs = scanner.scan_transaction(&tx).not_locked(); - outputs.sort_by(|x, y| x.commitment().amount.cmp(&y.commitment().amount)); - assert_eq!(outputs[0].commitment().amount, 10000); - assert_eq!(outputs[1].commitment().amount, 50000); - - // The remainder should get shunted to fee, which is fingerprintable - assert_eq!(tx.rct_signatures.base.fee, 1000000000000 - 10000 - 50000); - }, - ), -); diff --git a/common/db/src/parity_db.rs b/common/db/src/parity_db.rs index 8c913468..9ae345f6 100644 --- a/common/db/src/parity_db.rs +++ b/common/db/src/parity_db.rs @@ -4,6 +4,7 @@ pub use ::parity_db::{Options, Db as ParityDb}; use crate::*; +#[must_use] pub struct Transaction<'a>(&'a Arc, Vec<(u8, Vec, Option>)>); impl Get for Transaction<'_> { diff --git a/common/db/src/rocks.rs b/common/db/src/rocks.rs index 6a724563..1d42d902 100644 --- a/common/db/src/rocks.rs +++ b/common/db/src/rocks.rs @@ -7,6 +7,7 @@ use rocksdb::{ use crate::*; +#[must_use] pub struct Transaction<'a, T: ThreadMode>( RocksTransaction<'a, OptimisticTransactionDB>, &'a OptimisticTransactionDB, diff --git a/common/request/src/lib.rs b/common/request/src/lib.rs index 60e51019..df9689e1 100644 --- a/common/request/src/lib.rs +++ b/common/request/src/lib.rs @@ -58,6 +58,8 @@ impl Client { res.set_nodelay(true); res.set_reuse_address(true); #[cfg(feature = "tls")] + res.enforce_http(false); + #[cfg(feature = "tls")] let res = HttpsConnectorBuilder::new() .with_native_roots() .expect("couldn't fetch system's SSL roots") diff --git a/common/zalloc/Cargo.toml b/common/zalloc/Cargo.toml index 1a4a6b45..af4e7c1c 100644 --- a/common/zalloc/Cargo.toml +++ b/common/zalloc/Cargo.toml @@ -7,7 +7,7 @@ repository = "https://github.com/serai-dex/serai/tree/develop/common/zalloc" authors = ["Luke Parker "] keywords = [] edition = "2021" -rust-version = "1.60" +rust-version = "1.77.0" [package.metadata.docs.rs] all-features = true @@ -19,8 +19,10 @@ workspace = true [dependencies] zeroize = { version = "^1.5", default-features = false } +[build-dependencies] +rustversion = { version = "1", default-features = false } + [features] std = ["zeroize/std"] default = ["std"] -# Commented for now as it requires nightly and we don't use nightly -# allocator = [] +allocator = [] diff --git a/common/zalloc/build.rs b/common/zalloc/build.rs new file mode 100644 index 00000000..f3351e22 --- /dev/null +++ b/common/zalloc/build.rs @@ -0,0 +1,10 @@ +#[rustversion::nightly] +fn main() { + println!("cargo::rustc-check-cfg=cfg(zalloc_rustc_nightly)"); + println!("cargo::rustc-cfg=zalloc_rustc_nightly"); +} + +#[rustversion::not(nightly)] +fn main() { + println!("cargo::rustc-check-cfg=cfg(zalloc_rustc_nightly)"); +} diff --git a/common/zalloc/src/lib.rs b/common/zalloc/src/lib.rs index 0e4c1f75..cc5562a0 100644 --- a/common/zalloc/src/lib.rs +++ b/common/zalloc/src/lib.rs @@ -1,6 +1,6 @@ #![cfg_attr(docsrs, feature(doc_cfg))] #![cfg_attr(docsrs, feature(doc_auto_cfg))] -#![cfg_attr(feature = "allocator", feature(allocator_api))] +#![cfg_attr(all(zalloc_rustc_nightly, feature = "allocator"), feature(allocator_api))] //! Implementation of a Zeroizing Allocator, enabling zeroizing memory on deallocation. //! This can either be used with Box (requires nightly and the "allocator" feature) to provide the @@ -17,12 +17,12 @@ use zeroize::Zeroize; /// An allocator wrapper which zeroizes its memory on dealloc. pub struct ZeroizingAlloc(pub T); -#[cfg(feature = "allocator")] +#[cfg(all(zalloc_rustc_nightly, feature = "allocator"))] use core::{ ptr::NonNull, alloc::{AllocError, Allocator}, }; -#[cfg(feature = "allocator")] +#[cfg(all(zalloc_rustc_nightly, feature = "allocator"))] unsafe impl Allocator for ZeroizingAlloc { fn allocate(&self, layout: Layout) -> Result, AllocError> { self.0.allocate(layout) diff --git a/coordinator/src/db.rs b/coordinator/src/db.rs index 09eab173..04ee9d35 100644 --- a/coordinator/src/db.rs +++ b/coordinator/src/db.rs @@ -122,7 +122,7 @@ impl QueuedBatchesDb { pub fn take(txn: &mut impl DbTxn, set: ValidatorSet) -> Vec { let batches_vec = Self::get(txn, set).unwrap_or_default(); - txn.del(&Self::key(set)); + txn.del(Self::key(set)); let mut batches: &[u8] = &batches_vec; let mut res = vec![]; diff --git a/coordinator/src/p2p.rs b/coordinator/src/p2p.rs index ef876f9a..cecb3517 100644 --- a/coordinator/src/p2p.rs +++ b/coordinator/src/p2p.rs @@ -9,7 +9,7 @@ use std::{ use async_trait::async_trait; use rand_core::{RngCore, OsRng}; -use scale::Encode; +use scale::{Decode, Encode}; use borsh::{BorshSerialize, BorshDeserialize}; use serai_client::{primitives::NetworkId, validator_sets::primitives::ValidatorSet, Serai}; @@ -29,7 +29,7 @@ use libp2p::{ noise, yamux, request_response::{ Codec as RrCodecTrait, Message as RrMessage, Event as RrEvent, Config as RrConfig, - Behaviour as RrBehavior, + Behaviour as RrBehavior, ProtocolSupport, }, gossipsub::{ IdentTopic, FastMessageId, MessageId, MessageAuthenticity, ValidationMode, ConfigBuilder, @@ -45,9 +45,28 @@ pub(crate) use tributary::{ReadWrite, P2p as TributaryP2p}; use crate::{Transaction, Block, Tributary, ActiveTributary, TributaryEvent}; // Block size limit + 1 KB of space for signatures/metadata -const MAX_LIBP2P_MESSAGE_SIZE: usize = tributary::BLOCK_SIZE_LIMIT + 1024; +const MAX_LIBP2P_GOSSIP_MESSAGE_SIZE: usize = tributary::BLOCK_SIZE_LIMIT + 1024; + +const MAX_LIBP2P_REQRES_MESSAGE_SIZE: usize = + (tributary::BLOCK_SIZE_LIMIT * BLOCKS_PER_BATCH) + 1024; + +const MAX_LIBP2P_MESSAGE_SIZE: usize = { + // Manual `max` since `max` isn't a const fn + if MAX_LIBP2P_GOSSIP_MESSAGE_SIZE > MAX_LIBP2P_REQRES_MESSAGE_SIZE { + MAX_LIBP2P_GOSSIP_MESSAGE_SIZE + } else { + MAX_LIBP2P_REQRES_MESSAGE_SIZE + } +}; + const LIBP2P_TOPIC: &str = "serai-coordinator"; +// Amount of blocks in a minute +const BLOCKS_PER_MINUTE: usize = (60 / (tributary::tendermint::TARGET_BLOCK_TIME / 1000)) as usize; + +// Maximum amount of blocks to send in a batch +const BLOCKS_PER_BATCH: usize = BLOCKS_PER_MINUTE + 1; + #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, BorshSerialize, BorshDeserialize)] pub struct CosignedBlock { pub network: NetworkId, @@ -173,6 +192,18 @@ pub struct Message { pub msg: Vec, } +#[derive(Clone, Debug, Encode, Decode)] +pub struct BlockCommit { + pub block: Vec, + pub commit: Vec, +} + +#[derive(Clone, Debug, Encode, Decode)] +pub struct HeartbeatBatch { + pub blocks: Vec, + pub timestamp: u64, +} + #[async_trait] pub trait P2p: Send + Sync + Clone + fmt::Debug + TributaryP2p { type Id: Send + Sync + Clone + Copy + fmt::Debug; @@ -227,9 +258,9 @@ impl RrCodecTrait for RrCodec { ) -> io::Result> { let mut len = [0; 4]; io.read_exact(&mut len).await?; - let len = usize::try_from(u32::from_le_bytes(len)).expect("not a 32-bit platform?"); - if len > MAX_LIBP2P_MESSAGE_SIZE { - Err(io::Error::other("request length exceeded MAX_LIBP2P_MESSAGE_SIZE"))?; + let len = usize::try_from(u32::from_le_bytes(len)).expect("not at least a 32-bit platform?"); + if len > MAX_LIBP2P_REQRES_MESSAGE_SIZE { + Err(io::Error::other("request length exceeded MAX_LIBP2P_REQRES_MESSAGE_SIZE"))?; } // This may be a non-trivial allocation easily causable // While we could chunk the read, meaning we only perform the allocation as bandwidth is used, @@ -297,7 +328,7 @@ impl LibP2p { let throwaway_key_pair = Keypair::generate_ed25519(); let behavior = Behavior { - reqres: { RrBehavior::new([], RrConfig::default()) }, + reqres: { RrBehavior::new([("/coordinator", ProtocolSupport::Full)], RrConfig::default()) }, gossipsub: { let heartbeat_interval = tributary::tendermint::LATENCY_TIME / 2; let heartbeats_per_block = @@ -308,7 +339,7 @@ impl LibP2p { .heartbeat_interval(Duration::from_millis(heartbeat_interval.into())) .history_length(heartbeats_per_block * 2) .history_gossip(heartbeats_per_block) - .max_transmit_size(MAX_LIBP2P_MESSAGE_SIZE) + .max_transmit_size(MAX_LIBP2P_GOSSIP_MESSAGE_SIZE) // We send KeepAlive after 80s .idle_timeout(Duration::from_secs(85)) .validation_mode(ValidationMode::Strict) @@ -868,7 +899,7 @@ pub async fn handle_p2p_task( let p2p = p2p.clone(); async move { loop { - let Some(mut msg) = recv.recv().await else { + let Some(msg) = recv.recv().await else { // Channel closure happens when the tributary retires break; }; @@ -913,34 +944,53 @@ pub async fn handle_p2p_task( latest = next; } if to_send.len() > 3 { - for next in to_send { - let mut res = reader.block(&next).unwrap().serialize(); - res.extend(reader.commit(&next).unwrap()); - // Also include the timestamp used within the Heartbeat - res.extend(&msg.msg[32 .. 40]); - p2p.send(msg.sender, ReqResMessageKind::Block(genesis), res).await; + // prepare the batch to sends + let mut blocks = vec![]; + for (i, next) in to_send.iter().enumerate() { + if i >= BLOCKS_PER_BATCH { + break; + } + + blocks.push(BlockCommit { + block: reader.block(next).unwrap().serialize(), + commit: reader.commit(next).unwrap(), + }); } + let batch = HeartbeatBatch { blocks, timestamp: msg_time }; + + p2p + .send(msg.sender, ReqResMessageKind::Block(genesis), batch.encode()) + .await; } }); } P2pMessageKind::ReqRes(ReqResMessageKind::Block(msg_genesis)) => { assert_eq!(msg_genesis, genesis); - let mut msg_ref: &[u8] = msg.msg.as_ref(); - let Ok(block) = Block::::read(&mut msg_ref) else { - log::error!("received block message with an invalidly serialized block"); + // decode the batch + let Ok(batch) = HeartbeatBatch::decode(&mut msg.msg.as_ref()) else { + log::error!( + "received HeartBeatBatch message with an invalidly serialized batch" + ); continue; }; - // Get just the commit - msg.msg.drain(.. (msg.msg.len() - msg_ref.len())); - msg.msg.drain((msg.msg.len() - 8) ..); - let res = tributary.tributary.sync_block(block, msg.msg).await; - log::debug!( - "received block from {:?}, sync_block returned {}", - msg.sender, - res - ); + // sync blocks + for bc in batch.blocks { + // TODO: why do we use ReadWrite instead of Encode/Decode for blocks? + // Should we use the same for batches so we can read both at the same time? + let Ok(block) = Block::::read(&mut bc.block.as_slice()) else { + log::error!("received block message with an invalidly serialized block"); + continue; + }; + + let res = tributary.tributary.sync_block(block, bc.commit).await; + log::debug!( + "received block from {:?}, sync_block returned {}", + msg.sender, + res + ); + } } P2pMessageKind::Gossip(GossipMessageKind::Tributary(msg_genesis)) => { diff --git a/coordinator/src/tributary/scanner.rs b/coordinator/src/tributary/scanner.rs index 25c8b5c2..9b56e0a0 100644 --- a/coordinator/src/tributary/scanner.rs +++ b/coordinator/src/tributary/scanner.rs @@ -133,7 +133,13 @@ mod impl_pst_for_serai { key_pair: KeyPair, signature: Signature, ) { - let tx = SeraiValidatorSets::set_keys(set.network, removed, key_pair, signature); + // TODO: BoundedVec as an arg to avoid this expect + let tx = SeraiValidatorSets::set_keys( + set.network, + removed.try_into().expect("removing more than allowed"), + key_pair, + signature, + ); async fn check(serai: SeraiValidatorSets<'_>, set: ValidatorSet, (): ()) -> bool { if matches!(serai.keys(set).await, Ok(Some(_))) { log::info!("another coordinator set key pair for {:?}", set); diff --git a/coordinator/tributary/src/lib.rs b/coordinator/tributary/src/lib.rs index a4c6bfe5..0ea74bfe 100644 --- a/coordinator/tributary/src/lib.rs +++ b/coordinator/tributary/src/lib.rs @@ -1,5 +1,5 @@ use core::{marker::PhantomData, fmt::Debug}; -use std::{sync::Arc, io, collections::VecDeque}; +use std::{sync::Arc, io}; use async_trait::async_trait; @@ -154,14 +154,6 @@ pub struct Tributary { synced_block: Arc>>>, synced_block_result: Arc>, messages: Arc>>>, - - p2p_meta_task_handle: Arc, -} - -impl Drop for Tributary { - fn drop(&mut self) { - self.p2p_meta_task_handle.abort(); - } } impl Tributary { @@ -193,28 +185,7 @@ impl Tributary { ); let blockchain = Arc::new(RwLock::new(blockchain)); - let to_rebroadcast = Arc::new(RwLock::new(VecDeque::new())); - // Actively rebroadcast consensus messages to ensure they aren't prematurely dropped from the - // P2P layer - let p2p_meta_task_handle = Arc::new( - tokio::spawn({ - let to_rebroadcast = to_rebroadcast.clone(); - let p2p = p2p.clone(); - async move { - loop { - let to_rebroadcast = to_rebroadcast.read().await.clone(); - for msg in to_rebroadcast { - p2p.broadcast(genesis, msg).await; - } - tokio::time::sleep(core::time::Duration::from_secs(60)).await; - } - } - }) - .abort_handle(), - ); - - let network = - TendermintNetwork { genesis, signer, validators, blockchain, to_rebroadcast, p2p }; + let network = TendermintNetwork { genesis, signer, validators, blockchain, p2p }; let TendermintHandle { synced_block, synced_block_result, messages, machine } = TendermintMachine::new( @@ -235,7 +206,6 @@ impl Tributary { synced_block: Arc::new(RwLock::new(synced_block)), synced_block_result: Arc::new(RwLock::new(synced_block_result)), messages: Arc::new(RwLock::new(messages)), - p2p_meta_task_handle, }) } diff --git a/coordinator/tributary/src/tendermint/mod.rs b/coordinator/tributary/src/tendermint/mod.rs index e38efa5d..0ce6232c 100644 --- a/coordinator/tributary/src/tendermint/mod.rs +++ b/coordinator/tributary/src/tendermint/mod.rs @@ -1,8 +1,5 @@ use core::ops::Deref; -use std::{ - sync::Arc, - collections::{VecDeque, HashMap}, -}; +use std::{sync::Arc, collections::HashMap}; use async_trait::async_trait; @@ -270,8 +267,6 @@ pub struct TendermintNetwork { pub(crate) validators: Arc, pub(crate) blockchain: Arc>>, - pub(crate) to_rebroadcast: Arc>>>, - pub(crate) p2p: P, } @@ -308,26 +303,6 @@ impl Network for TendermintNetwork async fn broadcast(&mut self, msg: SignedMessageFor) { let mut to_broadcast = vec![TENDERMINT_MESSAGE]; to_broadcast.extend(msg.encode()); - - // Since we're broadcasting a Tendermint message, set it to be re-broadcasted every second - // until the block it's trying to build is complete - // If the P2P layer drops a message before all nodes obtained access, or a node had an - // intermittent failure, this will ensure reconcilliation - // This is atrocious if there's no content-based deduplication protocol for messages actively - // being gossiped - // LibP2p, as used by Serai, is configured to content-based deduplicate - { - let mut to_rebroadcast_lock = self.to_rebroadcast.write().await; - to_rebroadcast_lock.push_back(to_broadcast.clone()); - // We should have, ideally, 3 * validators messages within a round - // Therefore, this should keep the most recent 2-rounds - // TODO: This isn't perfect. Each participant should just rebroadcast their latest round of - // messages - while to_rebroadcast_lock.len() > (6 * self.validators.weights.len()) { - to_rebroadcast_lock.pop_front(); - } - } - self.p2p.broadcast(self.genesis, to_broadcast).await } @@ -366,7 +341,7 @@ impl Network for TendermintNetwork } } - async fn validate(&mut self, block: &Self::Block) -> Result<(), TendermintBlockError> { + async fn validate(&self, block: &Self::Block) -> Result<(), TendermintBlockError> { let block = Block::read::<&[u8]>(&mut block.0.as_ref()).map_err(|_| TendermintBlockError::Fatal)?; self @@ -428,9 +403,6 @@ impl Network for TendermintNetwork } } - // Since we've added a valid block, clear to_rebroadcast - *self.to_rebroadcast.write().await = VecDeque::new(); - Some(TendermintBlock( self.blockchain.write().await.build_block::(&self.signature_scheme()).serialize(), )) diff --git a/coordinator/tributary/src/tendermint/tx.rs b/coordinator/tributary/src/tendermint/tx.rs index 5f5102d3..8af40708 100644 --- a/coordinator/tributary/src/tendermint/tx.rs +++ b/coordinator/tributary/src/tendermint/tx.rs @@ -12,7 +12,7 @@ use crate::{ }; use tendermint::{ - verify_tendermint_evience, + verify_tendermint_evidence, ext::{Network, Commit}, }; @@ -68,7 +68,7 @@ pub(crate) fn verify_tendermint_tx( tx.verify()?; match tx { - TendermintTx::SlashEvidence(ev) => verify_tendermint_evience::(ev, schema, commit) + TendermintTx::SlashEvidence(ev) => verify_tendermint_evidence::(ev, schema, commit) .map_err(|_| TransactionError::InvalidContent)?, } diff --git a/coordinator/tributary/tendermint/src/block.rs b/coordinator/tributary/tendermint/src/block.rs index 6dfacfdb..8afe00f0 100644 --- a/coordinator/tributary/tendermint/src/block.rs +++ b/coordinator/tributary/tendermint/src/block.rs @@ -3,7 +3,6 @@ use std::{ collections::{HashSet, HashMap}, }; -use parity_scale_codec::Encode; use serai_db::{Get, DbTxn, Db}; use crate::{ @@ -20,7 +19,7 @@ pub(crate) struct BlockData { pub(crate) number: BlockNumber, pub(crate) validator_id: Option, - pub(crate) proposal: Option, + pub(crate) our_proposal: Option, pub(crate) log: MessageLog, pub(crate) slashes: HashSet, @@ -43,7 +42,7 @@ impl BlockData { weights: Arc, number: BlockNumber, validator_id: Option, - proposal: Option, + our_proposal: Option, ) -> BlockData { BlockData { db, @@ -51,7 +50,7 @@ impl BlockData { number, validator_id, - proposal, + our_proposal, log: MessageLog::new(weights), slashes: HashSet::new(), @@ -108,17 +107,17 @@ impl BlockData { self.populate_end_time(round); } - // 11-13 + // L11-13 self.round = Some(RoundData::::new( round, time.unwrap_or_else(|| self.end_time[&RoundNumber(round.0 - 1)]), )); self.end_time.insert(round, self.round().end_time()); - // 14-21 + // L14-21 if Some(proposer) == self.validator_id { let (round, block) = self.valid.clone().unzip(); - block.or_else(|| self.proposal.clone()).map(|block| Data::Proposal(round, block)) + block.or_else(|| self.our_proposal.clone()).map(|block| Data::Proposal(round, block)) } else { self.round_mut().set_timeout(Step::Propose); None @@ -177,14 +176,14 @@ impl BlockData { let new_block = last_block_or_round(&mut txn, LATEST_BLOCK_KEY, self.number.0)?; if new_block { // Delete the latest round key - txn.del(&key(LATEST_ROUND_KEY)); + txn.del(key(LATEST_ROUND_KEY)); } let new_round = last_block_or_round(&mut txn, LATEST_ROUND_KEY, round_number.0.into())?; if new_block || new_round { // Delete the messages for the old round - txn.del(&key(PROPOSE_KEY)); - txn.del(&key(PEVOTE_KEY)); - txn.del(&key(PRECOMMIT_KEY)); + txn.del(key(PROPOSE_KEY)); + txn.del(key(PEVOTE_KEY)); + txn.del(key(PRECOMMIT_KEY)); } // Check we haven't sent this message within this round @@ -198,8 +197,8 @@ impl BlockData { assert!(!new_round); None?; } - // Put this message to the DB - txn.put(&msg_key, res.encode()); + // Put that we're sending this message to the DB + txn.put(&msg_key, []); txn.commit(); } diff --git a/coordinator/tributary/tendermint/src/ext.rs b/coordinator/tributary/tendermint/src/ext.rs index b3d568a2..3869d9d9 100644 --- a/coordinator/tributary/tendermint/src/ext.rs +++ b/coordinator/tributary/tendermint/src/ext.rs @@ -288,7 +288,7 @@ pub trait Network: Sized + Send + Sync { async fn slash(&mut self, validator: Self::ValidatorId, slash_event: SlashEvent); /// Validate a block. - async fn validate(&mut self, block: &Self::Block) -> Result<(), BlockError>; + async fn validate(&self, block: &Self::Block) -> Result<(), BlockError>; /// Add a block, returning the proposal for the next one. /// diff --git a/coordinator/tributary/tendermint/src/lib.rs b/coordinator/tributary/tendermint/src/lib.rs index adc6fef7..e5193908 100644 --- a/coordinator/tributary/tendermint/src/lib.rs +++ b/coordinator/tributary/tendermint/src/lib.rs @@ -3,10 +3,10 @@ use core::fmt::Debug; use std::{ sync::Arc, time::{SystemTime, Instant, Duration}, - collections::VecDeque, + collections::{VecDeque, HashMap}, }; -use parity_scale_codec::{Encode, Decode}; +use parity_scale_codec::{Encode, Decode, IoReader}; use futures_channel::mpsc; use futures_util::{ @@ -15,6 +15,8 @@ use futures_util::{ }; use tokio::time::sleep; +use serai_db::{Get, DbTxn, Db}; + pub mod time; use time::{sys_time, CanonicalInstant}; @@ -30,6 +32,11 @@ pub(crate) mod message_log; pub mod ext; use ext::*; +const MESSAGE_TAPE_KEY: &[u8] = b"tendermint-machine-message_tape"; +fn message_tape_key(genesis: [u8; 32]) -> Vec { + [MESSAGE_TAPE_KEY, &genesis].concat() +} + pub fn commit_msg(end_time: u64, id: &[u8]) -> Vec { [&end_time.to_le_bytes(), id].concat() } @@ -62,6 +69,17 @@ impl PartialEq for Data { } } +impl core::hash::Hash for Data { + fn hash(&self, state: &mut H) { + match self { + Data::Proposal(valid_round, block) => (0, valid_round, block.id().as_ref()).hash(state), + Data::Prevote(id) => (1, id.as_ref().map(AsRef::<[u8]>::as_ref)).hash(state), + Data::Precommit(None) => (2, 0).hash(state), + Data::Precommit(Some((id, _))) => (2, 1, id.as_ref()).hash(state), + } + } +} + impl Data { pub fn step(&self) -> Step { match self { @@ -103,9 +121,23 @@ impl SignedMessage { } } +#[derive(Clone, Copy, PartialEq, Eq, Debug, Encode, Decode)] +pub enum SlashReason { + FailToPropose, + InvalidBlock, + InvalidProposer, +} + +#[derive(Clone, PartialEq, Eq, Debug, Encode, Decode)] +pub enum Evidence { + ConflictingMessages(Vec, Vec), + InvalidPrecommit(Vec), + InvalidValidRound(Vec), +} + #[derive(Clone, PartialEq, Eq, Debug)] -pub enum TendermintError { - Malicious(N::ValidatorId, Option), +pub enum TendermintError { + Malicious, Temporal, AlreadyHandled, InvalidEvidence, @@ -126,20 +158,6 @@ pub type SignedMessageFor = SignedMessage< <::SignatureScheme as SignatureScheme>::Signature, >; -#[derive(Clone, Copy, PartialEq, Eq, Debug, Encode, Decode)] -pub enum SlashReason { - FailToPropose, - InvalidBlock, - InvalidMessage, -} - -#[derive(Clone, PartialEq, Eq, Debug, Encode, Decode)] -pub enum Evidence { - ConflictingMessages(Vec, Vec), - InvalidPrecommit(Vec), - InvalidValidRound(Vec), -} - pub fn decode_signed_message(mut data: &[u8]) -> Option> { SignedMessageFor::::decode(&mut data).ok() } @@ -147,7 +165,7 @@ pub fn decode_signed_message(mut data: &[u8]) -> Option( data: &[u8], schema: &N::SignatureScheme, -) -> Result, TendermintError> { +) -> Result, TendermintError> { let msg = decode_signed_message::(data).ok_or(TendermintError::InvalidEvidence)?; // verify that evidence messages are signed correctly @@ -158,11 +176,11 @@ fn decode_and_verify_signed_message( Ok(msg) } -pub fn verify_tendermint_evience( +pub fn verify_tendermint_evidence( evidence: &Evidence, schema: &N::SignatureScheme, commit: impl Fn(u64) -> Option>, -) -> Result<(), TendermintError> { +) -> Result<(), TendermintError> { match evidence { Evidence::ConflictingMessages(first, second) => { let first = decode_and_verify_signed_message::(first, schema)?.msg; @@ -186,15 +204,16 @@ pub fn verify_tendermint_evience( }; // TODO: We need to be passed in the genesis time to handle this edge case if msg.block.0 == 0 { - todo!("invalid precommit signature on first block") + Err(TendermintError::InvalidEvidence)? + // todo!("invalid precommit signature on first block") } // get the last commit let prior_commit = match commit(msg.block.0 - 1) { Some(c) => c, - // If we have yet to sync the block in question, we will return InvalidContent based + // If we have yet to sync the block in question, we will return InvalidEvidence based // on our own temporal ambiguity - // This will also cause an InvalidContent for anything using a non-existent block, + // This will also cause an InvalidEvidence for anything using a non-existent block, // yet that's valid behavior // TODO: Double check the ramifications of this _ => Err(TendermintError::InvalidEvidence)?, @@ -229,6 +248,16 @@ pub enum SlashEvent { WithEvidence(Evidence), } +// Struct for if various upon handlers have been triggered to ensure they don't trigger multiple +// times. +#[derive(Clone, PartialEq, Eq, Debug)] +struct Upons { + upon_prevotes: bool, + upon_successful_current_round_prevotes: bool, + upon_negative_current_round_prevotes: bool, + upon_precommits: bool, +} + /// A machine executing the Tendermint protocol. pub struct TendermintMachine { db: N::Db, @@ -245,6 +274,10 @@ pub struct TendermintMachine { synced_block_result_send: mpsc::UnboundedSender, block: BlockData, + // TODO: Move this into the Block struct + round_proposals: HashMap, N::Block)>, + // TODO: Move this into the Round struct + upons: Upons, } pub struct SyncedBlock { @@ -287,6 +320,14 @@ impl TendermintMachine { // Start a new round. Returns true if we were the proposer fn round(&mut self, round: RoundNumber, time: Option) -> bool { + // Clear upons + self.upons = Upons { + upon_prevotes: false, + upon_successful_current_round_prevotes: false, + upon_negative_current_round_prevotes: false, + upon_precommits: false, + }; + let proposer = self.weights.proposer(self.block.number, round); let res = if let Some(data) = self.block.new_round(round, proposer, time) { self.broadcast(data); @@ -325,6 +366,13 @@ impl TendermintMachine { ); sleep(time_until_round_end).await; + // Clear the message tape + { + let mut txn = self.db.txn(); + txn.del(message_tape_key(self.genesis)); + txn.commit(); + } + // Clear our outbound message queue self.queue = VecDeque::new(); @@ -338,6 +386,9 @@ impl TendermintMachine { proposal, ); + // Reset the round proposals + self.round_proposals = HashMap::new(); + // Start the first round self.round(RoundNumber(0), Some(round_end)); } @@ -375,6 +426,410 @@ impl TendermintMachine { } } + fn proposal_for_round(&self, round: RoundNumber) -> Option<(Option, &N::Block)> { + self.round_proposals.get(&round).map(|(round, block)| (*round, block)) + } + + // L22-27 + fn upon_proposal_without_valid_round(&mut self) { + if self.block.round().step != Step::Propose { + return; + } + + // If we have the proposal message... + let Some((None, block)) = self.proposal_for_round(self.block.round().number) else { + return; + }; + + // There either needs to not be a locked value or it must be equivalent + #[allow(clippy::map_unwrap_or)] + if self + .block + .locked + .as_ref() + .map(|(_round, locked_block)| block.id() == *locked_block) + .unwrap_or(true) + { + self.broadcast(Data::Prevote(Some(block.id()))); + } else { + self.broadcast(Data::Prevote(None)); + } + } + + // L28-33 + fn upon_proposal_with_valid_round(&mut self) { + if self.block.round().step != Step::Propose { + return; + } + + // If we have the proposal message... + let Some((Some(proposal_valid_round), block)) = + self.proposal_for_round(self.block.round().number) + else { + return; + }; + + // Check we have the necessary prevotes + if !self.block.log.has_consensus(proposal_valid_round, &Data::Prevote(Some(block.id()))) { + return; + } + + // We don't check valid round < current round as the `message` function does + + // If locked is None, lockedRoundp is -1 and less than valid round + #[allow(clippy::map_unwrap_or)] + let locked_clause_1 = self + .block + .locked + .as_ref() + .map(|(locked_round, _block)| locked_round.0 <= proposal_valid_round.0) + .unwrap_or(true); + // The second clause is if the locked values are equivalent. If no value is locked, they aren't + #[allow(clippy::map_unwrap_or)] + let locked_clause_2 = self + .block + .locked + .as_ref() + .map(|(_round, locked_block)| block.id() == *locked_block) + .unwrap_or(false); + + if locked_clause_1 || locked_clause_2 { + self.broadcast(Data::Prevote(Some(block.id()))); + } else { + self.broadcast(Data::Prevote(None)); + } + } + + // L34-35 + fn upon_prevotes(&mut self) { + if self.upons.upon_prevotes || (self.block.round().step != Step::Prevote) { + return; + } + + if self.block.log.has_participation(self.block.round().number, Step::Prevote) { + self.block.round_mut().set_timeout(Step::Prevote); + self.upons.upon_prevotes = true; + } + } + + // L36-43 + async fn upon_successful_current_round_prevotes(&mut self) { + // Returning if `self.step == Step::Propose` is equivalent to guarding `step >= prevote` + if self.upons.upon_successful_current_round_prevotes || + (self.block.round().step == Step::Propose) + { + return; + } + + // If we have the proposal message... + let Some((_, block)) = self.proposal_for_round(self.block.round().number) else { + return; + }; + + // Check we have the necessary prevotes + if !self.block.log.has_consensus(self.block.round().number, &Data::Prevote(Some(block.id()))) { + return; + } + + let block = block.clone(); + self.upons.upon_successful_current_round_prevotes = true; + + if self.block.round().step == Step::Prevote { + self.block.locked = Some((self.block.round().number, block.id())); + let signature = self + .signer + .sign(&commit_msg( + self.block.end_time[&self.block.round().number].canonical(), + block.id().as_ref(), + )) + .await; + self.broadcast(Data::Precommit(Some((block.id(), signature)))); + } + self.block.valid = Some((self.block.round().number, block)); + } + + // L44-46 + fn upon_negative_current_round_prevotes(&mut self) { + if self.upons.upon_negative_current_round_prevotes || (self.block.round().step != Step::Prevote) + { + return; + } + + if self.block.log.has_consensus(self.block.round().number, &Data::Prevote(None)) { + self.broadcast(Data::Precommit(None)); + } + + self.upons.upon_negative_current_round_prevotes = true; + } + + // L47-48 + fn upon_precommits(&mut self) { + if self.upons.upon_precommits { + return; + } + + if self.block.log.has_participation(self.block.round().number, Step::Precommit) { + self.block.round_mut().set_timeout(Step::Precommit); + self.upons.upon_precommits = true; + } + } + + // L22-48 + async fn all_current_round_upons(&mut self) { + self.upon_proposal_without_valid_round(); + self.upon_proposal_with_valid_round(); + self.upon_prevotes(); + self.upon_successful_current_round_prevotes().await; + self.upon_negative_current_round_prevotes(); + self.upon_precommits(); + } + + // L49-54 + async fn upon_successful_precommits(&mut self, round: RoundNumber) -> bool { + // If we have the proposal message... + let Some((_, block)) = self.proposal_for_round(round) else { return false }; + + // Check we have the necessary precommits + // The precommit we check we have consensus upon uses a junk signature since message equality + // disregards the signature + if !self + .block + .log + .has_consensus(round, &Data::Precommit(Some((block.id(), self.signer.sign(&[]).await)))) + { + return false; + } + + // Get all participants in this commit + let mut validators = vec![]; + let mut sigs = vec![]; + // Get all precommits for this round + for (validator, msgs) in &self.block.log.log[&round] { + if let Some(signed) = msgs.get(&Step::Precommit) { + if let Data::Precommit(Some((id, sig))) = &signed.msg.data { + // If this precommit was for this block, include it + if *id == block.id() { + validators.push(*validator); + sigs.push(sig.clone()); + } + } + } + } + + // Form the commit itself + let commit_msg = commit_msg(self.block.end_time[&round].canonical(), block.id().as_ref()); + let commit = Commit { + end_time: self.block.end_time[&round].canonical(), + validators: validators.clone(), + signature: self.network.signature_scheme().aggregate(&validators, &commit_msg, &sigs), + }; + debug_assert!(self.network.verify_commit(block.id(), &commit)); + + // Add the block and reset the machine + log::info!( + target: "tendermint", + "TendermintMachine produced block {}", + hex::encode(block.id().as_ref()), + ); + let id = block.id(); + let proposal = self.network.add_block(block.clone(), commit).await; + log::trace!( + target: "tendermint", + "added block {} (produced by machine)", + hex::encode(id.as_ref()), + ); + self.reset(round, proposal).await; + + true + } + + // L49-54 + async fn all_any_round_upons(&mut self, round: RoundNumber) -> bool { + self.upon_successful_precommits(round).await + } + + // Returns Ok(true) if this was a Precommit which had either no signature or its signature + // validated + // Returns Ok(false) if it wasn't a Precommit or the signature wasn't validated yet + // Returns Err if the signature was invalid + async fn verify_precommit_signature( + &mut self, + signed: &SignedMessageFor, + ) -> Result { + let msg = &signed.msg; + if let Data::Precommit(precommit) = &msg.data { + let Some((id, sig)) = precommit else { return Ok(true) }; + // Also verify the end_time of the commit + // Only perform this verification if we already have the end_time + // Else, there's a DoS where we receive a precommit for some round infinitely in the future + // which forces us to calculate every end time + if let Some(end_time) = self.block.end_time.get(&msg.round) { + if !self.validators.verify(msg.sender, &commit_msg(end_time.canonical(), id.as_ref()), sig) + { + log::warn!(target: "tendermint", "validator produced an invalid commit signature"); + self + .slash( + msg.sender, + SlashEvent::WithEvidence(Evidence::InvalidPrecommit(signed.encode())), + ) + .await; + Err(TendermintError::Malicious)?; + } + return Ok(true); + } + } + Ok(false) + } + + async fn message(&mut self, signed: &SignedMessageFor) -> Result<(), TendermintError> { + let msg = &signed.msg; + if msg.block != self.block.number { + Err(TendermintError::Temporal)?; + } + + // If this is a precommit, verify its signature + self.verify_precommit_signature(signed).await?; + + // Only let the proposer propose + if matches!(msg.data, Data::Proposal(..)) && + (msg.sender != self.weights.proposer(msg.block, msg.round)) + { + log::warn!(target: "tendermint", "validator who wasn't the proposer proposed"); + // TODO: This should have evidence + self + .slash(msg.sender, SlashEvent::Id(SlashReason::InvalidProposer, msg.block.0, msg.round.0)) + .await; + Err(TendermintError::Malicious)?; + }; + + // If this is a proposal, verify the block + // If the block is invalid, drop the message, letting the timeout cover it + // This prevents needing to check if valid inside every `upon` block + if let Data::Proposal(_, block) = &msg.data { + match self.network.validate(block).await { + Ok(()) => {} + Err(BlockError::Temporal) => return Err(TendermintError::Temporal), + Err(BlockError::Fatal) => { + log::warn!(target: "tendermint", "validator proposed a fatally invalid block"); + self + .slash( + msg.sender, + SlashEvent::Id(SlashReason::InvalidBlock, self.block.number.0, msg.round.0), + ) + .await; + Err(TendermintError::Malicious)?; + } + }; + } + + // If this is a proposal, verify the valid round isn't fundamentally invalid + if let Data::Proposal(Some(valid_round), _) = msg.data { + if valid_round.0 >= msg.round.0 { + log::warn!( + target: "tendermint", + "proposed proposed with a syntactically invalid valid round", + ); + self + .slash(msg.sender, SlashEvent::WithEvidence(Evidence::InvalidValidRound(msg.encode()))) + .await; + Err(TendermintError::Malicious)?; + } + } + + // Add it to the log, returning if it was already handled + match self.block.log.log(signed.clone()) { + Ok(true) => {} + Ok(false) => Err(TendermintError::AlreadyHandled)?, + Err(evidence) => { + self.slash(msg.sender, SlashEvent::WithEvidence(evidence)).await; + Err(TendermintError::Malicious)?; + } + } + log::debug!( + target: "tendermint", + "received new tendermint message (block: {}, round: {}, step: {:?})", + msg.block.0, + msg.round.0, + msg.data.step(), + ); + + // If this is a proposal, insert it + if let Data::Proposal(vr, block) = &msg.data { + self.round_proposals.insert(msg.round, (*vr, block.clone())); + } + + // L55-56 + // Jump ahead if we should + if (msg.round.0 > self.block.round().number.0) && + (self.block.log.round_participation(msg.round) >= self.weights.fault_threshold()) + { + log::debug!( + target: "tendermint", + "jumping from round {} to round {}", + self.block.round().number.0, + msg.round.0, + ); + + // Jump to the new round. + let old_round = self.block.round().number; + self.round(msg.round, None); + + // If any jumped over/to round already has precommit messages, verify their signatures + for jumped in (old_round.0 + 1) ..= msg.round.0 { + let jumped = RoundNumber(jumped); + let round_msgs = self.block.log.log.get(&jumped).cloned().unwrap_or_default(); + for (validator, msgs) in &round_msgs { + if let Some(existing) = msgs.get(&Step::Precommit) { + if let Ok(res) = self.verify_precommit_signature(existing).await { + // Ensure this actually verified the signature instead of believing it shouldn't yet + assert!(res); + } else { + // Remove the message so it isn't counted towards forming a commit/included in one + // This won't remove the fact they precommitted for this block hash in the MessageLog + // TODO: Don't even log these in the first place until we jump, preventing needing + // to do this in the first place + self + .block + .log + .log + .get_mut(&jumped) + .unwrap() + .get_mut(validator) + .unwrap() + .remove(&Step::Precommit) + .unwrap(); + } + } + } + } + } + + // Now that we've jumped, and: + // 1) If this is a message for an old round, verified the precommit signatures + // 2) If this is a message for what was the current round, verified the precommit signatures + // 3) If this is a message for what was a future round, verified the precommit signatures if it + // has 34+% participation + // Run all `upons` run for any round, which may produce a Commit if it has 67+% participation + // (returning true if it does, letting us return now) + // It's necessary to verify the precommit signatures before Commit production is allowed, hence + // this specific flow + if self.all_any_round_upons(msg.round).await { + return Ok(()); + } + + // If this is a historic round, or a future round without sufficient participation, return + if msg.round.0 != self.block.round().number.0 { + return Ok(()); + } + // msg.round is now guaranteed to be equal to self.block.round().number + debug_assert_eq!(msg.round, self.block.round().number); + + // Run all `upons` run for the current round + self.all_current_round_upons().await; + + Ok(()) + } + /// Create a new Tendermint machine, from the specified point, with the specified block as the /// one to propose next. This will return a channel to send messages from the gossip layer and /// the machine itself. The machine should have `run` called from an asynchronous task. @@ -419,7 +874,7 @@ impl TendermintMachine { let validators = network.signature_scheme(); let weights = Arc::new(network.weights()); let validator_id = signer.validator_id().await; - // 01-10 + // L01-10 let mut machine = TendermintMachine { db: db.clone(), genesis, @@ -442,6 +897,15 @@ impl TendermintMachine { validator_id, Some(proposal), ), + + round_proposals: HashMap::new(), + + upons: Upons { + upon_prevotes: false, + upon_successful_current_round_prevotes: false, + upon_negative_current_round_prevotes: false, + upon_precommits: false, + }, }; // The end time of the last block is the start time for this one @@ -460,16 +924,16 @@ impl TendermintMachine { pub async fn run(mut self) { log::debug!(target: "tendermint", "running TendermintMachine"); + let mut rebroadcast_future = Box::pin(sleep(Duration::from_secs(60))).fuse(); loop { // Also create a future for if the queue has a message // Does not pop_front as if another message has higher priority, its future will be handled // instead in this loop, and the popped value would be dropped with the next iteration - // While no other message has a higher priority right now, this is a safer practice let mut queue_future = if self.queue.is_empty() { Fuse::terminated() } else { future::ready(()).fuse() }; if let Some((our_message, msg, mut sig)) = futures_util::select_biased! { - // Handle a new block occurring externally (an external sync loop) + // Handle a new block occurring externally (from an external sync loop) // Has the highest priority as it makes all other futures here irrelevant msg = self.synced_block_recv.next() => { if let Some(SyncedBlock { number, block, commit }) = msg { @@ -503,16 +967,19 @@ impl TendermintMachine { Some((true, self.queue.pop_front().unwrap(), None)) }, + // L57-67 // Handle any timeouts step = self.block.round().timeout_future().fuse() => { // Remove the timeout so it doesn't persist, always being the selected future due to bias // While this does enable the timeout to be entered again, the timeout setting code will // never attempt to add a timeout after its timeout has expired + // (due to it setting an `upon` boolean) self.block.round_mut().timeouts.remove(&step); - // Only run if it's still the step in question - if self.block.round().step == step { - match step { - Step::Propose => { + + match step { + Step::Propose => { + // Only run if it's still the step in question + if self.block.round().step == step { // Slash the validator for not proposing when they should've log::debug!(target: "tendermint", "validator didn't propose when they should have"); // this slash will be voted on. @@ -525,14 +992,43 @@ impl TendermintMachine { ), ).await; self.broadcast(Data::Prevote(None)); - }, - Step::Prevote => self.broadcast(Data::Precommit(None)), - Step::Precommit => { - self.round(RoundNumber(self.block.round().number.0 + 1), None); - continue; } + }, + Step::Prevote => { + // Only run if it's still the step in question + if self.block.round().step == step { + self.broadcast(Data::Precommit(None)) + } + }, + Step::Precommit => { + self.round(RoundNumber(self.block.round().number.0 + 1), None); } + }; + + // Execute the upons now that the state has changed + self.all_any_round_upons(self.block.round().number).await; + self.all_current_round_upons().await; + + None + }, + + // If it's been more than 60s, rebroadcast our own messages + () = rebroadcast_future => { + log::trace!("rebroadcast future hit within tendermint machine"); + let key = message_tape_key(self.genesis); + let messages = self.db.get(key).unwrap_or(vec![]); + let mut messages = messages.as_slice(); + + while !messages.is_empty() { + self.network.broadcast( + SignedMessageFor::::decode(&mut IoReader(&mut messages)) + .expect("saved invalid message to DB") + ).await; } + + // Reset the rebroadcast future + rebroadcast_future = Box::pin(sleep(core::time::Duration::from_secs(60))).fuse(); + None }, @@ -554,429 +1050,32 @@ impl TendermintMachine { } let sig = sig.unwrap(); - // TODO: message may internally call broadcast. We should check within broadcast it's not - // broadcasting our own message at this time. let signed_msg = SignedMessage { msg: msg.clone(), sig: sig.clone() }; let res = self.message(&signed_msg).await; + // If this is our message, and we hit an invariant, we could be slashed. + // We only broadcast our message after running it ourselves, to ensure it doesn't error, to + // ensure we don't get slashed on invariants. if res.is_err() && our_message { panic!("honest node (ourselves) had invalid behavior"); } - // Only now should we allow broadcasts since we're sure an invariant wasn't reached causing - // us to have invalid messages. + // Save this message to a linear tape of all our messages for this block, if ours + // TODO: Since we do this after we mark this message as sent to prevent equivocations, a + // precisely time reboot could cause this message marked as sent yet not added to the tape + if our_message { + let message_tape_key = message_tape_key(self.genesis); + let mut txn = self.db.txn(); + let mut message_tape = txn.get(&message_tape_key).unwrap_or(vec![]); + message_tape.extend(signed_msg.encode()); + txn.put(&message_tape_key, message_tape); + txn.commit(); + } + + // Re-broadcast this since it's an original consensus message worth handling if res.is_ok() { - // Re-broadcast this since it's an original consensus message self.network.broadcast(signed_msg).await; } - - match res { - Ok(None) => {} - Ok(Some(block)) => { - let mut validators = vec![]; - let mut sigs = vec![]; - // Get all precommits for this round - for (validator, msgs) in &self.block.log.log[&msg.round] { - if let Some(signed) = msgs.get(&Step::Precommit) { - if let Data::Precommit(Some((id, sig))) = &signed.msg.data { - // If this precommit was for this block, include it - if *id == block.id() { - validators.push(*validator); - sigs.push(sig.clone()); - } - } - } - } - - let commit_msg = - commit_msg(self.block.end_time[&msg.round].canonical(), block.id().as_ref()); - let commit = Commit { - end_time: self.block.end_time[&msg.round].canonical(), - validators: validators.clone(), - signature: self.network.signature_scheme().aggregate(&validators, &commit_msg, &sigs), - }; - debug_assert!(self.network.verify_commit(block.id(), &commit)); - - log::info!( - target: "tendermint", - "TendermintMachine produced block {}", - hex::encode(block.id().as_ref()), - ); - let id = block.id(); - let proposal = self.network.add_block(block, commit).await; - log::trace!( - target: "tendermint", - "added block {} (produced by machine)", - hex::encode(id.as_ref()), - ); - self.reset(msg.round, proposal).await; - } - Err(TendermintError::Malicious(sender, evidence)) => { - let current_msg = SignedMessage { msg: msg.clone(), sig: sig.clone() }; - - let slash = if let Some(ev) = evidence { - // if the malicious message contains a block, only vote to slash - // TODO: Should this decision be made at a higher level? - // A higher-level system may be able to verify if the contained block is fatally - // invalid - // A higher-level system may accept the bandwidth size of this, even if the issue is - // just the valid round field - if let Data::Proposal(_, _) = ¤t_msg.msg.data { - SlashEvent::Id( - SlashReason::InvalidBlock, - self.block.number.0, - self.block.round().number.0, - ) - } else { - // slash with evidence otherwise - SlashEvent::WithEvidence(ev) - } - } else { - // we don't have evidence. Slash with vote. - SlashEvent::Id( - SlashReason::InvalidMessage, - self.block.number.0, - self.block.round().number.0, - ) - }; - - // Each message that we're voting to slash over needs to be re-broadcasted so other - // validators also trigger their own votes - // TODO: should this be inside slash function? - if let SlashEvent::Id(_, _, _) = slash { - self.network.broadcast(current_msg).await; - } - - self.slash(sender, slash).await - } - Err( - TendermintError::Temporal | - TendermintError::AlreadyHandled | - TendermintError::InvalidEvidence, - ) => (), - } } } } - - // Returns Ok(true) if this was a Precommit which had either no signature or its signature - // validated - // Returns Ok(false) if it wasn't a Precommit or the signature wasn't validated yet - // Returns Err if the signature was invalid - fn verify_precommit_signature( - &self, - signed: &SignedMessageFor, - ) -> Result> { - let msg = &signed.msg; - if let Data::Precommit(precommit) = &msg.data { - let Some((id, sig)) = precommit else { return Ok(true) }; - // Also verify the end_time of the commit - // Only perform this verification if we already have the end_time - // Else, there's a DoS where we receive a precommit for some round infinitely in the future - // which forces us to calculate every end time - if let Some(end_time) = self.block.end_time.get(&msg.round) { - if !self.validators.verify(msg.sender, &commit_msg(end_time.canonical(), id.as_ref()), sig) - { - log::warn!(target: "tendermint", "Validator produced an invalid commit signature"); - Err(TendermintError::Malicious( - msg.sender, - Some(Evidence::InvalidPrecommit(signed.encode())), - ))?; - } - return Ok(true); - } - } - Ok(false) - } - - async fn message( - &mut self, - signed: &SignedMessageFor, - ) -> Result, TendermintError> { - let msg = &signed.msg; - if msg.block != self.block.number { - Err(TendermintError::Temporal)?; - } - - if (msg.block == self.block.number) && - (msg.round == self.block.round().number) && - (msg.data.step() == Step::Propose) - { - log::trace!( - target: "tendermint", - "received Propose for block {}, round {}", - msg.block.0, - msg.round.0, - ); - } - - // If this is a precommit, verify its signature - self.verify_precommit_signature(signed)?; - - // Only let the proposer propose - if matches!(msg.data, Data::Proposal(..)) && - (msg.sender != self.weights.proposer(msg.block, msg.round)) - { - log::warn!(target: "tendermint", "Validator who wasn't the proposer proposed"); - // TODO: This should have evidence - Err(TendermintError::Malicious(msg.sender, None))?; - }; - - if !self.block.log.log(signed.clone())? { - return Err(TendermintError::AlreadyHandled); - } - log::trace!( - target: "tendermint", - "received new tendermint message (block: {}, round: {}, step: {:?})", - msg.block.0, - msg.round.0, - msg.data.step(), - ); - - // All functions, except for the finalizer and the jump, are locked to the current round - - // Run the finalizer to see if it applies - // 49-52 - if matches!(msg.data, Data::Proposal(..)) || matches!(msg.data, Data::Precommit(_)) { - let proposer = self.weights.proposer(self.block.number, msg.round); - - // Get the proposal - if let Some(proposal_signed) = self.block.log.get(msg.round, proposer, Step::Propose) { - if let Data::Proposal(_, block) = &proposal_signed.msg.data { - // Check if it has gotten a sufficient amount of precommits - // Uses a junk signature since message equality disregards the signature - if self.block.log.has_consensus( - msg.round, - &Data::Precommit(Some((block.id(), self.signer.sign(&[]).await))), - ) { - // If msg.round is in the future, these Precommits won't have their inner signatures - // verified - // It should be impossible for msg.round to be in the future however, as this requires - // 67% of validators to Precommit, and we jump on 34% participating in the new round - // The one exception would be if a validator had 34%, and could cause participation to - // go from 33% (not enough to jump) to 67%, without executing the below code - // This also would require the local machine to be outside of allowed time tolerances, - // or the validator with 34% to not be publishing Prevotes (as those would cause a - // a jump) - // Both are invariants - // TODO: Replace this panic with an inner signature check - assert!(msg.round.0 <= self.block.round().number.0); - - log::debug!(target: "tendermint", "block {} has consensus", msg.block.0); - return Ok(Some(block.clone())); - } - } - } - } - - // Else, check if we need to jump ahead - #[allow(clippy::comparison_chain)] - if msg.round.0 < self.block.round().number.0 { - // Prior round, disregard if not finalizing - return Ok(None); - } else if msg.round.0 > self.block.round().number.0 { - // 55-56 - // Jump, enabling processing by the below code - if self.block.log.round_participation(msg.round) > self.weights.fault_threshold() { - log::debug!( - target: "tendermint", - "jumping from round {} to round {}", - self.block.round().number.0, - msg.round.0, - ); - - // Jump to the new round. - let proposer = self.round(msg.round, None); - - // If this round already has precommit messages, verify their signatures - let round_msgs = self.block.log.log[&msg.round].clone(); - for (validator, msgs) in &round_msgs { - if let Some(existing) = msgs.get(&Step::Precommit) { - if let Ok(res) = self.verify_precommit_signature(existing) { - // Ensure this actually verified the signature instead of believing it shouldn't yet - assert!(res); - } else { - // Remove the message so it isn't counted towards forming a commit/included in one - // This won't remove the fact they precommitted for this block hash in the MessageLog - // TODO: Don't even log these in the first place until we jump, preventing needing - // to do this in the first place - let msg = self - .block - .log - .log - .get_mut(&msg.round) - .unwrap() - .get_mut(validator) - .unwrap() - .remove(&Step::Precommit) - .unwrap(); - - // Slash the validator for publishing an invalid commit signature - self - .slash( - *validator, - SlashEvent::WithEvidence(Evidence::InvalidPrecommit(msg.encode())), - ) - .await; - } - } - } - - // If we're the proposer, return now we don't waste time on the current round - // (as it doesn't have a proposal, since we didn't propose, and cannot complete) - if proposer { - return Ok(None); - } - } else { - // Future round which we aren't ready to jump to, so return for now - return Ok(None); - } - } - - // msg.round is now guaranteed to be equal to self.block.round().number - debug_assert_eq!(msg.round, self.block.round().number); - - // The paper executes these checks when the step is prevote. Making sure this message warrants - // rerunning these checks is a sane optimization since message instances is a full iteration - // of the round map - if (self.block.round().step == Step::Prevote) && matches!(msg.data, Data::Prevote(_)) { - let (participation, weight) = - self.block.log.message_instances(self.block.round().number, &Data::Prevote(None)); - let threshold_weight = self.weights.threshold(); - if participation < threshold_weight { - log::trace!( - target: "tendermint", - "progess towards setting prevote timeout, participation: {}, needed: {}", - participation, - threshold_weight, - ); - } - // 34-35 - if participation >= threshold_weight { - log::trace!( - target: "tendermint", - "setting timeout for prevote due to sufficient participation", - ); - self.block.round_mut().set_timeout(Step::Prevote); - } - - // 44-46 - if weight >= threshold_weight { - self.broadcast(Data::Precommit(None)); - return Ok(None); - } - } - - // 47-48 - if matches!(msg.data, Data::Precommit(_)) && - self.block.log.has_participation(self.block.round().number, Step::Precommit) - { - log::trace!( - target: "tendermint", - "setting timeout for precommit due to sufficient participation", - ); - self.block.round_mut().set_timeout(Step::Precommit); - } - - // All further operations require actually having the proposal in question - let proposer = self.weights.proposer(self.block.number, self.block.round().number); - let (vr, block) = if let Some(proposal_signed) = - self.block.log.get(self.block.round().number, proposer, Step::Propose) - { - if let Data::Proposal(vr, block) = &proposal_signed.msg.data { - (vr, block) - } else { - panic!("message for Step::Propose didn't have Data::Proposal"); - } - } else { - return Ok(None); - }; - - // 22-33 - if self.block.round().step == Step::Propose { - // Delay error handling (triggering a slash) until after we vote. - let (valid, err) = match self.network.validate(block).await { - Ok(()) => (true, Ok(None)), - Err(BlockError::Temporal) => (false, Ok(None)), - Err(BlockError::Fatal) => (false, { - log::warn!(target: "tendermint", "Validator proposed a fatally invalid block"); - // TODO: Produce evidence of this for the higher level code to decide what to do with - Err(TendermintError::Malicious(proposer, None)) - }), - }; - // Create a raw vote which only requires block validity as a basis for the actual vote. - let raw_vote = Some(block.id()).filter(|_| valid); - - // If locked is none, it has a round of -1 according to the protocol. That satisfies - // 23 and 29. If it's some, both are satisfied if they're for the same ID. If it's some - // with different IDs, the function on 22 rejects yet the function on 28 has one other - // condition - let locked = self.block.locked.as_ref().map_or(true, |(_, id)| id == &block.id()); - let mut vote = raw_vote.filter(|_| locked); - - if let Some(vr) = vr { - // Malformed message - if vr.0 >= self.block.round().number.0 { - log::warn!(target: "tendermint", "Validator claimed a round from the future was valid"); - Err(TendermintError::Malicious( - msg.sender, - Some(Evidence::InvalidValidRound(signed.encode())), - ))?; - } - - if self.block.log.has_consensus(*vr, &Data::Prevote(Some(block.id()))) { - // Allow differing locked values if the proposal has a newer valid round - // This is the other condition described above - if let Some((locked_round, _)) = self.block.locked.as_ref() { - vote = vote.or_else(|| raw_vote.filter(|_| locked_round.0 <= vr.0)); - } - - self.broadcast(Data::Prevote(vote)); - return err; - } - } else { - self.broadcast(Data::Prevote(vote)); - return err; - } - - return Ok(None); - } - - if self.block.valid.as_ref().map_or(true, |(round, _)| round != &self.block.round().number) { - // 36-43 - - // The run once condition is implemented above. Since valid will always be set by this, it - // not being set, or only being set historically, means this has yet to be run - - if self.block.log.has_consensus(self.block.round().number, &Data::Prevote(Some(block.id()))) { - match self.network.validate(block).await { - // BlockError::Temporal is due to a temporal error we have, yet a supermajority of the - // network does not, Because we do not believe this block to be fatally invalid, and - // because a supermajority deems it valid, accept it. - Ok(()) | Err(BlockError::Temporal) => (), - Err(BlockError::Fatal) => { - log::warn!(target: "tendermint", "Validator proposed a fatally invalid block"); - // TODO: Produce evidence of this for the higher level code to decide what to do with - Err(TendermintError::Malicious(proposer, None))? - } - }; - - self.block.valid = Some((self.block.round().number, block.clone())); - if self.block.round().step == Step::Prevote { - self.block.locked = Some((self.block.round().number, block.id())); - self.broadcast(Data::Precommit(Some(( - block.id(), - self - .signer - .sign(&commit_msg( - self.block.end_time[&self.block.round().number].canonical(), - block.id().as_ref(), - )) - .await, - )))); - } - } - } - - Ok(None) - } } diff --git a/coordinator/tributary/tendermint/src/message_log.rs b/coordinator/tributary/tendermint/src/message_log.rs index 3959852d..c6d172c4 100644 --- a/coordinator/tributary/tendermint/src/message_log.rs +++ b/coordinator/tributary/tendermint/src/message_log.rs @@ -2,21 +2,30 @@ use std::{sync::Arc, collections::HashMap}; use parity_scale_codec::Encode; -use crate::{ext::*, RoundNumber, Step, DataFor, TendermintError, SignedMessageFor, Evidence}; +use crate::{ext::*, RoundNumber, Step, DataFor, SignedMessageFor, Evidence}; type RoundLog = HashMap<::ValidatorId, HashMap>>; pub(crate) struct MessageLog { weights: Arc, + round_participation: HashMap, + participation: HashMap<(RoundNumber, Step), u64>, + message_instances: HashMap<(RoundNumber, DataFor), u64>, pub(crate) log: HashMap>, } impl MessageLog { pub(crate) fn new(weights: Arc) -> MessageLog { - MessageLog { weights, log: HashMap::new() } + MessageLog { + weights, + round_participation: HashMap::new(), + participation: HashMap::new(), + message_instances: HashMap::new(), + log: HashMap::new(), + } } // Returns true if it's a new message - pub(crate) fn log(&mut self, signed: SignedMessageFor) -> Result> { + pub(crate) fn log(&mut self, signed: SignedMessageFor) -> Result { let msg = &signed.msg; // Clarity, and safety around default != new edge cases let round = self.log.entry(msg.round).or_insert_with(HashMap::new); @@ -30,69 +39,36 @@ impl MessageLog { target: "tendermint", "Validator sent multiple messages for the same block + round + step" ); - Err(TendermintError::Malicious( - msg.sender, - Some(Evidence::ConflictingMessages(existing.encode(), signed.encode())), - ))?; + Err(Evidence::ConflictingMessages(existing.encode(), signed.encode()))?; } return Ok(false); } + // Since we have a new message, update the participation + let sender_weight = self.weights.weight(msg.sender); + if msgs.is_empty() { + *self.round_participation.entry(msg.round).or_insert_with(|| 0) += sender_weight; + } + *self.participation.entry((msg.round, step)).or_insert_with(|| 0) += sender_weight; + *self.message_instances.entry((msg.round, msg.data.clone())).or_insert_with(|| 0) += + sender_weight; + msgs.insert(step, signed); Ok(true) } - // For a given round, return the participating weight for this step, and the weight agreeing with - // the data. - pub(crate) fn message_instances(&self, round: RoundNumber, data: &DataFor) -> (u64, u64) { - let mut participating = 0; - let mut weight = 0; - for (participant, msgs) in &self.log[&round] { - if let Some(msg) = msgs.get(&data.step()) { - let validator_weight = self.weights.weight(*participant); - participating += validator_weight; - if data == &msg.msg.data { - weight += validator_weight; - } - } - } - (participating, weight) - } - // Get the participation in a given round pub(crate) fn round_participation(&self, round: RoundNumber) -> u64 { - let mut weight = 0; - if let Some(round) = self.log.get(&round) { - for participant in round.keys() { - weight += self.weights.weight(*participant); - } - }; - weight + *self.round_participation.get(&round).unwrap_or(&0) } // Check if a supermajority of nodes have participated on a specific step pub(crate) fn has_participation(&self, round: RoundNumber, step: Step) -> bool { - let mut participating = 0; - for (participant, msgs) in &self.log[&round] { - if msgs.get(&step).is_some() { - participating += self.weights.weight(*participant); - } - } - participating >= self.weights.threshold() + *self.participation.get(&(round, step)).unwrap_or(&0) >= self.weights.threshold() } // Check if consensus has been reached on a specific piece of data pub(crate) fn has_consensus(&self, round: RoundNumber, data: &DataFor) -> bool { - let (_, weight) = self.message_instances(round, data); - weight >= self.weights.threshold() - } - - pub(crate) fn get( - &self, - round: RoundNumber, - sender: N::ValidatorId, - step: Step, - ) -> Option<&SignedMessageFor> { - self.log.get(&round).and_then(|round| round.get(&sender).and_then(|msgs| msgs.get(&step))) + *self.message_instances.get(&(round, data.clone())).unwrap_or(&0) >= self.weights.threshold() } } diff --git a/coordinator/tributary/tendermint/src/round.rs b/coordinator/tributary/tendermint/src/round.rs index 445c2784..a97e3ed1 100644 --- a/coordinator/tributary/tendermint/src/round.rs +++ b/coordinator/tributary/tendermint/src/round.rs @@ -57,6 +57,7 @@ impl RoundData { // Poll all set timeouts, returning the Step whose timeout has just expired pub(crate) async fn timeout_future(&self) -> Step { + /* let now = Instant::now(); log::trace!( target: "tendermint", @@ -64,6 +65,7 @@ impl RoundData { self.step, self.timeouts.iter().map(|(k, v)| (k, v.duration_since(now))).collect::>() ); + */ let timeout_future = |step| { let timeout = self.timeouts.get(&step).copied(); diff --git a/coordinator/tributary/tendermint/tests/ext.rs b/coordinator/tributary/tendermint/tests/ext.rs index 3b3cf7c3..bec95ddc 100644 --- a/coordinator/tributary/tendermint/tests/ext.rs +++ b/coordinator/tributary/tendermint/tests/ext.rs @@ -145,7 +145,7 @@ impl Network for TestNetwork { println!("Slash for {id} due to {event:?}"); } - async fn validate(&mut self, block: &TestBlock) -> Result<(), BlockError> { + async fn validate(&self, block: &TestBlock) -> Result<(), BlockError> { block.valid } diff --git a/crypto/dalek-ff-group/Cargo.toml b/crypto/dalek-ff-group/Cargo.toml index d8a92194..29b8806c 100644 --- a/crypto/dalek-ff-group/Cargo.toml +++ b/crypto/dalek-ff-group/Cargo.toml @@ -29,7 +29,7 @@ digest = { version = "0.10", default-features = false } ff = { version = "0.13", default-features = false, features = ["bits"] } group = { version = "0.13", default-features = false } -crypto-bigint = { version = "0.5", default-features = false } +crypto-bigint = { version = "0.5", default-features = false, features = ["zeroize"] } curve25519-dalek = { version = ">= 4.0, < 4.2", default-features = false, features = ["alloc", "zeroize", "digest", "group", "precomputed-tables"] } diff --git a/crypto/dkg/Cargo.toml b/crypto/dkg/Cargo.toml index bf308705..7ed301f5 100644 --- a/crypto/dkg/Cargo.toml +++ b/crypto/dkg/Cargo.toml @@ -7,7 +7,7 @@ repository = "https://github.com/serai-dex/serai/tree/develop/crypto/dkg" authors = ["Luke Parker "] keywords = ["dkg", "multisig", "threshold", "ff", "group"] edition = "2021" -rust-version = "1.74" +rust-version = "1.79" [package.metadata.docs.rs] all-features = true diff --git a/crypto/dkg/src/lib.rs b/crypto/dkg/src/lib.rs index 77a3bdbe..478f400f 100644 --- a/crypto/dkg/src/lib.rs +++ b/crypto/dkg/src/lib.rs @@ -16,10 +16,10 @@ pub mod musig; #[cfg(feature = "std")] pub mod encryption; -/// The distributed key generation protocol described in the -/// [FROST paper](https://eprint.iacr.org/2020/852). +/// The PedPoP distributed key generation protocol described in the +/// [FROST paper](https://eprint.iacr.org/2020/852), augmented to be verifiable. #[cfg(feature = "std")] -pub mod frost; +pub mod pedpop; /// Promote keys between ciphersuites. #[cfg(feature = "std")] diff --git a/crypto/dkg/src/frost.rs b/crypto/dkg/src/pedpop.rs similarity index 100% rename from crypto/dkg/src/frost.rs rename to crypto/dkg/src/pedpop.rs diff --git a/crypto/dkg/src/promote.rs b/crypto/dkg/src/promote.rs index 010abf80..7cad4f23 100644 --- a/crypto/dkg/src/promote.rs +++ b/crypto/dkg/src/promote.rs @@ -64,10 +64,7 @@ pub struct GeneratorPromotion { _c2: PhantomData, } -impl GeneratorPromotion -where - C2: Ciphersuite, -{ +impl> GeneratorPromotion { /// Begin promoting keys from one generator to another. Returns a proof this share was properly /// promoted. pub fn promote( diff --git a/crypto/dkg/src/tests/mod.rs b/crypto/dkg/src/tests/mod.rs index 548b2fe0..f21d7254 100644 --- a/crypto/dkg/src/tests/mod.rs +++ b/crypto/dkg/src/tests/mod.rs @@ -12,8 +12,8 @@ mod musig; pub use musig::test_musig; /// FROST key generation testing utility. -pub mod frost; -use frost::frost_gen; +pub mod pedpop; +use pedpop::pedpop_gen; // Promotion test. mod promote; @@ -53,7 +53,7 @@ pub fn recover_key(keys: &HashMap> pub fn key_gen( rng: &mut R, ) -> HashMap> { - let res = frost_gen(rng) + let res = pedpop_gen(rng) .drain() .map(|(i, core)| { assert_eq!( diff --git a/crypto/dkg/src/tests/frost.rs b/crypto/dkg/src/tests/pedpop.rs similarity index 94% rename from crypto/dkg/src/tests/frost.rs rename to crypto/dkg/src/tests/pedpop.rs index e54f60be..3ae383e3 100644 --- a/crypto/dkg/src/tests/frost.rs +++ b/crypto/dkg/src/tests/pedpop.rs @@ -6,13 +6,13 @@ use ciphersuite::Ciphersuite; use crate::{ Participant, ThresholdParams, ThresholdCore, - frost::{Commitments, KeyGenMachine, SecretShare, KeyMachine}, + pedpop::{Commitments, KeyGenMachine, SecretShare, KeyMachine}, encryption::{EncryptionKeyMessage, EncryptedMessage}, tests::{THRESHOLD, PARTICIPANTS, clone_without}, }; -type FrostEncryptedMessage = EncryptedMessage::F>>; -type FrostSecretShares = HashMap>; +type PedPoPEncryptedMessage = EncryptedMessage::F>>; +type PedPoPSecretShares = HashMap>; const CONTEXT: &str = "DKG Test Key Generation"; @@ -24,7 +24,7 @@ fn commit_enc_keys_and_shares( HashMap>, HashMap>>, HashMap, - HashMap>, + HashMap>, ) { let mut machines = HashMap::new(); let mut commitments = HashMap::new(); @@ -72,9 +72,9 @@ fn commit_enc_keys_and_shares( } fn generate_secret_shares( - shares: &HashMap>, + shares: &HashMap>, recipient: Participant, -) -> FrostSecretShares { +) -> PedPoPSecretShares { let mut our_secret_shares = HashMap::new(); for (i, shares) in shares { if recipient == *i { @@ -85,8 +85,8 @@ fn generate_secret_shares( our_secret_shares } -/// Fully perform the FROST key generation algorithm. -pub fn frost_gen( +/// Fully perform the PedPoP key generation algorithm. +pub fn pedpop_gen( rng: &mut R, ) -> HashMap> { let (mut machines, _, _, secret_shares) = commit_enc_keys_and_shares::<_, C>(rng); @@ -125,7 +125,7 @@ mod literal { use crate::{ DkgError, encryption::EncryptionKeyProof, - frost::{BlameMachine, AdditionalBlameMachine}, + pedpop::{BlameMachine, AdditionalBlameMachine}, }; use super::*; @@ -136,7 +136,7 @@ mod literal { fn test_blame( commitment_msgs: &HashMap>>, machines: Vec>, - msg: &FrostEncryptedMessage, + msg: &PedPoPEncryptedMessage, blame: &Option>, ) { for machine in machines { diff --git a/crypto/dleq/Cargo.toml b/crypto/dleq/Cargo.toml index c9d525e1..fc25899f 100644 --- a/crypto/dleq/Cargo.toml +++ b/crypto/dleq/Cargo.toml @@ -6,7 +6,7 @@ license = "MIT" repository = "https://github.com/serai-dex/serai/tree/develop/crypto/dleq" authors = ["Luke Parker "] edition = "2021" -rust-version = "1.74" +rust-version = "1.79" [package.metadata.docs.rs] all-features = true diff --git a/crypto/dleq/src/cross_group/aos.rs b/crypto/dleq/src/cross_group/aos.rs index dac3356a..b801aa3e 100644 --- a/crypto/dleq/src/cross_group/aos.rs +++ b/crypto/dleq/src/cross_group/aos.rs @@ -53,11 +53,11 @@ pub(crate) struct Aos - Aos -where - G0::Scalar: PrimeFieldBits + Zeroize, - G1::Scalar: PrimeFieldBits + Zeroize, +impl< + G0: PrimeGroup + Zeroize, + G1: PrimeGroup + Zeroize, + const RING_LEN: usize, + > Aos { #[allow(non_snake_case)] fn nonces(mut transcript: T, nonces: (G0, G1)) -> (G0::Scalar, G1::Scalar) { diff --git a/crypto/dleq/src/cross_group/bits.rs b/crypto/dleq/src/cross_group/bits.rs index a5de897a..1aeef140 100644 --- a/crypto/dleq/src/cross_group/bits.rs +++ b/crypto/dleq/src/cross_group/bits.rs @@ -76,14 +76,11 @@ pub(crate) struct Bits< } impl< - G0: PrimeGroup + Zeroize, - G1: PrimeGroup + Zeroize, + G0: PrimeGroup + Zeroize, + G1: PrimeGroup + Zeroize, const SIGNATURE: u8, const RING_LEN: usize, > Bits -where - G0::Scalar: PrimeFieldBits + Zeroize, - G1::Scalar: PrimeFieldBits + Zeroize, { fn transcript(transcript: &mut T, i: usize, commitments: (G0, G1)) { transcript.domain_separate(b"bits"); diff --git a/crypto/dleq/src/cross_group/mod.rs b/crypto/dleq/src/cross_group/mod.rs index 77569c7c..8014ea9f 100644 --- a/crypto/dleq/src/cross_group/mod.rs +++ b/crypto/dleq/src/cross_group/mod.rs @@ -112,15 +112,12 @@ pub enum DLEqError { // anyone who wants it #[derive(Clone, PartialEq, Eq, Debug)] pub struct __DLEqProof< - G0: PrimeGroup + Zeroize, - G1: PrimeGroup + Zeroize, + G0: PrimeGroup + Zeroize, + G1: PrimeGroup + Zeroize, const SIGNATURE: u8, const RING_LEN: usize, const REMAINDER_RING_LEN: usize, -> where - G0::Scalar: PrimeFieldBits, - G1::Scalar: PrimeFieldBits, -{ +> { bits: Vec>, remainder: Option>, poks: (SchnorrPoK, SchnorrPoK), @@ -200,15 +197,12 @@ dleq!( ); impl< - G0: PrimeGroup + Zeroize, - G1: PrimeGroup + Zeroize, + G0: PrimeGroup + Zeroize, + G1: PrimeGroup + Zeroize, const SIGNATURE: u8, const RING_LEN: usize, const REMAINDER_RING_LEN: usize, > __DLEqProof -where - G0::Scalar: PrimeFieldBits + Zeroize, - G1::Scalar: PrimeFieldBits + Zeroize, { pub(crate) fn transcript( transcript: &mut T, diff --git a/crypto/dleq/src/cross_group/schnorr.rs b/crypto/dleq/src/cross_group/schnorr.rs index ec560388..773af09b 100644 --- a/crypto/dleq/src/cross_group/schnorr.rs +++ b/crypto/dleq/src/cross_group/schnorr.rs @@ -28,10 +28,7 @@ pub(crate) struct SchnorrPoK { s: G::Scalar, } -impl SchnorrPoK -where - G::Scalar: PrimeFieldBits + Zeroize, -{ +impl + Zeroize> SchnorrPoK { // Not HRAm due to the lack of m #[allow(non_snake_case)] fn hra(transcript: &mut T, generator: G, R: G, A: G) -> G::Scalar { diff --git a/crypto/dleq/src/lib.rs b/crypto/dleq/src/lib.rs index 5b813b64..a8958a2e 100644 --- a/crypto/dleq/src/lib.rs +++ b/crypto/dleq/src/lib.rs @@ -105,19 +105,13 @@ pub enum DLEqError { /// A proof that points have the same discrete logarithm across generators. #[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)] -pub struct DLEqProof -where - G::Scalar: Zeroize, -{ +pub struct DLEqProof> { c: G::Scalar, s: G::Scalar, } #[allow(non_snake_case)] -impl DLEqProof -where - G::Scalar: Zeroize, -{ +impl> DLEqProof { fn transcript(transcript: &mut T, generator: G, nonce: G, point: G) { transcript.append_message(b"generator", generator.to_bytes()); transcript.append_message(b"nonce", nonce.to_bytes()); @@ -213,20 +207,14 @@ where /// across some generators, yet with a smaller overall proof size. #[cfg(feature = "std")] #[derive(Clone, PartialEq, Eq, Debug, Zeroize)] -pub struct MultiDLEqProof -where - G::Scalar: Zeroize, -{ +pub struct MultiDLEqProof> { c: G::Scalar, s: Vec, } #[cfg(feature = "std")] #[allow(non_snake_case)] -impl MultiDLEqProof -where - G::Scalar: Zeroize, -{ +impl> MultiDLEqProof { /// Prove for each scalar that the series of points created by multiplying it against its /// matching generators share a discrete logarithm. /// This function panics if `generators.len() != scalars.len()`. diff --git a/crypto/dleq/src/tests/cross_group/schnorr.rs b/crypto/dleq/src/tests/cross_group/schnorr.rs index e7039e00..14e1e84b 100644 --- a/crypto/dleq/src/tests/cross_group/schnorr.rs +++ b/crypto/dleq/src/tests/cross_group/schnorr.rs @@ -14,10 +14,7 @@ use transcript::{Transcript, RecommendedTranscript}; use crate::cross_group::schnorr::SchnorrPoK; -fn test_schnorr() -where - G::Scalar: PrimeFieldBits + Zeroize, -{ +fn test_schnorr + Zeroize>() { let transcript = RecommendedTranscript::new(b"Schnorr Test"); let mut batch = BatchVerifier::new(10); diff --git a/crypto/ed448/src/backend.rs b/crypto/ed448/src/backend.rs index 83dc3fca..db41e811 100644 --- a/crypto/ed448/src/backend.rs +++ b/crypto/ed448/src/backend.rs @@ -34,7 +34,7 @@ macro_rules! math_op { impl $Op<$Other> for $Value { type Output = $Value; fn $op_fn(self, other: $Other) -> Self::Output { - Self($function(self.0, other.0)) + $Value($function(self.0, other.0)) } } impl $Assign<$Other> for $Value { @@ -45,7 +45,7 @@ macro_rules! math_op { impl<'a> $Op<&'a $Other> for $Value { type Output = $Value; fn $op_fn(self, other: &'a $Other) -> Self::Output { - Self($function(self.0, other.0)) + $Value($function(self.0, other.0)) } } impl<'a> $Assign<&'a $Other> for $Value { @@ -60,7 +60,7 @@ macro_rules! from_wrapper { ($wrapper: ident, $inner: ident, $uint: ident) => { impl From<$uint> for $wrapper { fn from(a: $uint) -> $wrapper { - Self(Residue::new(&$inner::from(a))) + $wrapper(Residue::new(&$inner::from(a))) } } }; @@ -127,7 +127,7 @@ macro_rules! field { impl Neg for $FieldName { type Output = $FieldName; fn neg(self) -> $FieldName { - Self(self.0.neg()) + $FieldName(self.0.neg()) } } @@ -141,13 +141,13 @@ macro_rules! field { impl $FieldName { /// Perform an exponentiation. pub fn pow(&self, other: $FieldName) -> $FieldName { - let mut table = [Self(Residue::ONE); 16]; + let mut table = [$FieldName(Residue::ONE); 16]; table[1] = *self; for i in 2 .. 16 { table[i] = table[i - 1] * self; } - let mut res = Self(Residue::ONE); + let mut res = $FieldName(Residue::ONE); let mut bits = 0; for (i, mut bit) in other.to_le_bits().iter_mut().rev().enumerate() { bits <<= 1; @@ -170,8 +170,8 @@ macro_rules! field { } impl Field for $FieldName { - const ZERO: Self = Self(Residue::ZERO); - const ONE: Self = Self(Residue::ONE); + const ZERO: Self = $FieldName(Residue::ZERO); + const ONE: Self = $FieldName(Residue::ONE); fn random(mut rng: impl RngCore) -> Self { let mut bytes = [0; 112]; @@ -188,12 +188,12 @@ macro_rules! field { fn invert(&self) -> CtOption { const NEG_2: $FieldName = - Self($ResidueType::sub(&$ResidueType::ZERO, &$ResidueType::new(&U448::from_u8(2)))); + $FieldName($ResidueType::sub(&$ResidueType::ZERO, &$ResidueType::new(&U448::from_u8(2)))); CtOption::new(self.pow(NEG_2), !self.is_zero()) } fn sqrt(&self) -> CtOption { - const MOD_1_4: $FieldName = Self($ResidueType::new( + const MOD_1_4: $FieldName = $FieldName($ResidueType::new( &$MODULUS.saturating_add(&U448::ONE).wrapping_div(&U448::from_u8(4)), )); @@ -217,14 +217,14 @@ macro_rules! field { const TWO_INV: Self = $FieldName($ResidueType::new(&U448::from_u8(2)).invert().0); const MULTIPLICATIVE_GENERATOR: Self = - Self(Residue::new(&U448::from_u8($MULTIPLICATIVE_GENERATOR))); + $FieldName(Residue::new(&U448::from_u8($MULTIPLICATIVE_GENERATOR))); // True for both the Ed448 Scalar field and FieldElement field const S: u32 = 1; // Both fields have their root of unity as -1 const ROOT_OF_UNITY: Self = - Self($ResidueType::sub(&$ResidueType::ZERO, &$ResidueType::new(&U448::ONE))); - const ROOT_OF_UNITY_INV: Self = Self(Self::ROOT_OF_UNITY.0.invert().0); + $FieldName($ResidueType::sub(&$ResidueType::ZERO, &$ResidueType::new(&U448::ONE))); + const ROOT_OF_UNITY_INV: Self = $FieldName(Self::ROOT_OF_UNITY.0.invert().0); const DELTA: Self = $FieldName(Residue::new(&U448::from_le_hex($DELTA))); diff --git a/crypto/ff-group-tests/Cargo.toml b/crypto/ff-group-tests/Cargo.toml index bb55d5a1..aa328fa1 100644 --- a/crypto/ff-group-tests/Cargo.toml +++ b/crypto/ff-group-tests/Cargo.toml @@ -7,7 +7,7 @@ repository = "https://github.com/serai-dex/serai/tree/develop/crypto/ff-group-te authors = ["Luke Parker "] keywords = ["ff", "group", "ecc"] edition = "2021" -rust-version = "1.60" +rust-version = "1.79" [package.metadata.docs.rs] all-features = true diff --git a/crypto/ff-group-tests/src/group.rs b/crypto/ff-group-tests/src/group.rs index b3f7a155..0f0aab4e 100644 --- a/crypto/ff-group-tests/src/group.rs +++ b/crypto/ff-group-tests/src/group.rs @@ -178,10 +178,7 @@ pub fn test_prime_group(rng: &mut R) { } /// Run all tests offered by this crate on the group. -pub fn test_prime_group_bits(rng: &mut R) -where - G::Scalar: PrimeFieldBits, -{ +pub fn test_prime_group_bits>(rng: &mut R) { test_prime_field_bits::(rng); test_prime_group::(rng); } diff --git a/crypto/frost/Cargo.toml b/crypto/frost/Cargo.toml index b89d5290..7c32b6f0 100644 --- a/crypto/frost/Cargo.toml +++ b/crypto/frost/Cargo.toml @@ -7,7 +7,7 @@ repository = "https://github.com/serai-dex/serai/tree/develop/crypto/frost" authors = ["Luke Parker "] keywords = ["frost", "multisig", "threshold"] edition = "2021" -rust-version = "1.74" +rust-version = "1.79" [package.metadata.docs.rs] all-features = true diff --git a/crypto/frost/README.md b/crypto/frost/README.md index 27845844..e6ed2b0a 100644 --- a/crypto/frost/README.md +++ b/crypto/frost/README.md @@ -10,7 +10,7 @@ integrating with existing systems. This library offers ciphersuites compatible with the [IETF draft](https://github.com/cfrg/draft-irtf-cfrg-frost). Currently, version -11 is supported. +15 is supported. This library was [audited by Cypher Stack in March 2023](https://github.com/serai-dex/serai/raw/e1bb2c191b7123fd260d008e31656d090d559d21/audits/Cypher%20Stack%20crypto%20March%202023/Audit.pdf), diff --git a/crypto/frost/src/sign.rs b/crypto/frost/src/sign.rs index 73ea0a7d..5115244f 100644 --- a/crypto/frost/src/sign.rs +++ b/crypto/frost/src/sign.rs @@ -362,9 +362,7 @@ impl> SignMachine for AlgorithmSignMachi rho_transcript.append_message(b"message", C::hash_msg(msg)); rho_transcript.append_message( b"preprocesses", - &C::hash_commitments( - self.params.algorithm.transcript().challenge(b"preprocesses").as_ref(), - ), + C::hash_commitments(self.params.algorithm.transcript().challenge(b"preprocesses").as_ref()), ); // Generate the per-signer binding factors diff --git a/crypto/multiexp/Cargo.toml b/crypto/multiexp/Cargo.toml index 27b47ea9..228b85ab 100644 --- a/crypto/multiexp/Cargo.toml +++ b/crypto/multiexp/Cargo.toml @@ -7,7 +7,7 @@ repository = "https://github.com/serai-dex/serai/tree/develop/crypto/multiexp" authors = ["Luke Parker "] keywords = ["multiexp", "ff", "group"] edition = "2021" -rust-version = "1.70" +rust-version = "1.79" [package.metadata.docs.rs] all-features = true diff --git a/crypto/multiexp/src/batch.rs b/crypto/multiexp/src/batch.rs index 1cc48349..8016047d 100644 --- a/crypto/multiexp/src/batch.rs +++ b/crypto/multiexp/src/batch.rs @@ -12,27 +12,21 @@ use crate::{multiexp, multiexp_vartime}; // Flatten the contained statements to a single Vec. // Wrapped in Zeroizing in case any of the included statements contain private values. #[allow(clippy::type_complexity)] -fn flat( +fn flat + Zeroize>( slice: &[(Id, Vec<(G::Scalar, G)>)], -) -> Zeroizing> -where - ::Scalar: PrimeFieldBits + Zeroize, -{ +) -> Zeroizing> { Zeroizing::new(slice.iter().flat_map(|pairs| pairs.1.iter()).copied().collect::>()) } /// A batch verifier intended to verify a series of statements are each equivalent to zero. #[allow(clippy::type_complexity)] #[derive(Clone, Zeroize)] -pub struct BatchVerifier( +pub struct BatchVerifier + Zeroize>( Zeroizing)>>, -) -where - ::Scalar: PrimeFieldBits + Zeroize; +); -impl BatchVerifier -where - ::Scalar: PrimeFieldBits + Zeroize, +impl + Zeroize> + BatchVerifier { /// Create a new batch verifier, expected to verify the following amount of statements. /// diff --git a/crypto/multiexp/src/lib.rs b/crypto/multiexp/src/lib.rs index cf0133fc..dfd8e033 100644 --- a/crypto/multiexp/src/lib.rs +++ b/crypto/multiexp/src/lib.rs @@ -49,10 +49,10 @@ fn u8_from_bool(bit_ref: &mut bool) -> u8 { // Convert scalars to `window`-sized bit groups, as needed to index a table // This algorithm works for `window <= 8` -pub(crate) fn prep_bits(pairs: &[(G::Scalar, G)], window: u8) -> Vec> -where - G::Scalar: PrimeFieldBits, -{ +pub(crate) fn prep_bits>( + pairs: &[(G::Scalar, G)], + window: u8, +) -> Vec> { let w_usize = usize::from(window); let mut groupings = vec![]; @@ -175,10 +175,7 @@ fn algorithm(len: usize) -> Algorithm { /// Performs a multiexponentiation, automatically selecting the optimal algorithm based on the /// amount of pairs. -pub fn multiexp(pairs: &[(G::Scalar, G)]) -> G -where - G::Scalar: PrimeFieldBits + Zeroize, -{ +pub fn multiexp>(pairs: &[(G::Scalar, G)]) -> G { match algorithm(pairs.len()) { Algorithm::Null => Group::identity(), Algorithm::Single => pairs[0].1 * pairs[0].0, @@ -190,10 +187,7 @@ where /// Performs a multiexponentiation in variable time, automatically selecting the optimal algorithm /// based on the amount of pairs. -pub fn multiexp_vartime(pairs: &[(G::Scalar, G)]) -> G -where - G::Scalar: PrimeFieldBits, -{ +pub fn multiexp_vartime>(pairs: &[(G::Scalar, G)]) -> G { match algorithm(pairs.len()) { Algorithm::Null => Group::identity(), Algorithm::Single => pairs[0].1 * pairs[0].0, diff --git a/crypto/multiexp/src/pippenger.rs b/crypto/multiexp/src/pippenger.rs index 10d7d141..3660b7b2 100644 --- a/crypto/multiexp/src/pippenger.rs +++ b/crypto/multiexp/src/pippenger.rs @@ -7,10 +7,10 @@ use crate::prep_bits; // Pippenger's algorithm for multiexponentiation, as published in the SIAM Journal on Computing // DOI: 10.1137/0209022 -pub(crate) fn pippenger(pairs: &[(G::Scalar, G)], window: u8) -> G -where - G::Scalar: PrimeFieldBits, -{ +pub(crate) fn pippenger>( + pairs: &[(G::Scalar, G)], + window: u8, +) -> G { let mut bits = prep_bits(pairs, window); let mut res = G::identity(); @@ -37,10 +37,10 @@ where res } -pub(crate) fn pippenger_vartime(pairs: &[(G::Scalar, G)], window: u8) -> G -where - G::Scalar: PrimeFieldBits, -{ +pub(crate) fn pippenger_vartime>( + pairs: &[(G::Scalar, G)], + window: u8, +) -> G { let bits = prep_bits(pairs, window); let mut res = G::identity(); diff --git a/crypto/multiexp/src/straus.rs b/crypto/multiexp/src/straus.rs index 6f472c05..f576c973 100644 --- a/crypto/multiexp/src/straus.rs +++ b/crypto/multiexp/src/straus.rs @@ -24,10 +24,10 @@ fn prep_tables(pairs: &[(G::Scalar, G)], window: u8) -> Vec> { // Straus's algorithm for multiexponentiation, as published in The American Mathematical Monthly // DOI: 10.2307/2310929 -pub(crate) fn straus(pairs: &[(G::Scalar, G)], window: u8) -> G -where - G::Scalar: PrimeFieldBits + Zeroize, -{ +pub(crate) fn straus>( + pairs: &[(G::Scalar, G)], + window: u8, +) -> G { let mut groupings = prep_bits(pairs, window); let tables = prep_tables(pairs, window); @@ -48,10 +48,10 @@ where res } -pub(crate) fn straus_vartime(pairs: &[(G::Scalar, G)], window: u8) -> G -where - G::Scalar: PrimeFieldBits, -{ +pub(crate) fn straus_vartime>( + pairs: &[(G::Scalar, G)], + window: u8, +) -> G { let groupings = prep_bits(pairs, window); let tables = prep_tables(pairs, window); diff --git a/crypto/multiexp/src/tests/batch.rs b/crypto/multiexp/src/tests/batch.rs index 5c6cd581..2e78a5dc 100644 --- a/crypto/multiexp/src/tests/batch.rs +++ b/crypto/multiexp/src/tests/batch.rs @@ -9,10 +9,7 @@ use group::Group; use crate::BatchVerifier; -pub(crate) fn test_batch() -where - G::Scalar: PrimeFieldBits + Zeroize, -{ +pub(crate) fn test_batch + Zeroize>() { let valid = |batch: BatchVerifier<_, G>| { assert!(batch.verify()); assert!(batch.verify_vartime()); diff --git a/crypto/multiexp/src/tests/mod.rs b/crypto/multiexp/src/tests/mod.rs index 4a5b4ca9..3050c96e 100644 --- a/crypto/multiexp/src/tests/mod.rs +++ b/crypto/multiexp/src/tests/mod.rs @@ -18,10 +18,7 @@ mod batch; use batch::test_batch; #[allow(dead_code)] -fn benchmark_internal(straus_bool: bool) -where - G::Scalar: PrimeFieldBits + Zeroize, -{ +fn benchmark_internal>(straus_bool: bool) { let runs: usize = 20; let mut start = 0; @@ -86,10 +83,7 @@ where } } -fn test_multiexp() -where - G::Scalar: PrimeFieldBits + Zeroize, -{ +fn test_multiexp>() { let test = |pairs: &[_], sum| { // These should automatically determine the best algorithm assert_eq!(multiexp(pairs), sum); diff --git a/crypto/schnorr/Cargo.toml b/crypto/schnorr/Cargo.toml index 91f8722b..2ea04f5b 100644 --- a/crypto/schnorr/Cargo.toml +++ b/crypto/schnorr/Cargo.toml @@ -7,7 +7,7 @@ repository = "https://github.com/serai-dex/serai/tree/develop/crypto/schnorr" authors = ["Luke Parker "] keywords = ["schnorr", "ff", "group"] edition = "2021" -rust-version = "1.74" +rust-version = "1.79" [package.metadata.docs.rs] all-features = true diff --git a/crypto/schnorr/src/tests/rfc8032.rs b/crypto/schnorr/src/tests/rfc8032.rs index 991cf450..418f4c0e 100644 --- a/crypto/schnorr/src/tests/rfc8032.rs +++ b/crypto/schnorr/src/tests/rfc8032.rs @@ -52,7 +52,7 @@ fn test_rfc8032() { SchnorrSignature::::read::<&[u8]>(&mut hex::decode(vector.2).unwrap().as_ref()) .unwrap(); let hram = Sha512::new_with_prefix( - &[sig.R.to_bytes().as_ref(), &key.to_bytes(), &hex::decode(vector.1).unwrap()].concat(), + [sig.R.to_bytes().as_ref(), &key.to_bytes(), &hex::decode(vector.1).unwrap()].concat(), ); assert!(sig.verify(key, Scalar::from_hash(hram))); } diff --git a/crypto/schnorrkel/Cargo.toml b/crypto/schnorrkel/Cargo.toml index f5819070..2508bef0 100644 --- a/crypto/schnorrkel/Cargo.toml +++ b/crypto/schnorrkel/Cargo.toml @@ -7,7 +7,7 @@ repository = "https://github.com/serai-dex/serai/tree/develop/crypto/schnorrkel" authors = ["Luke Parker "] keywords = ["frost", "multisig", "threshold", "schnorrkel"] edition = "2021" -rust-version = "1.74" +rust-version = "1.79" [package.metadata.docs.rs] all-features = true diff --git a/crypto/transcript/Cargo.toml b/crypto/transcript/Cargo.toml index 566ad56b..84e08abf 100644 --- a/crypto/transcript/Cargo.toml +++ b/crypto/transcript/Cargo.toml @@ -7,7 +7,7 @@ repository = "https://github.com/serai-dex/serai/tree/develop/crypto/transcript" authors = ["Luke Parker "] keywords = ["transcript"] edition = "2021" -rust-version = "1.73" +rust-version = "1.79" [package.metadata.docs.rs] all-features = true diff --git a/crypto/transcript/README.md b/crypto/transcript/README.md index a8772a0a..17124693 100644 --- a/crypto/transcript/README.md +++ b/crypto/transcript/README.md @@ -3,9 +3,9 @@ Flexible Transcript is a crate offering: - `Transcript`, a trait offering functions transcripts should implement. - `DigestTranscript`, a competent transcript format instantiated against a -provided hash function. + provided hash function. - `MerlinTranscript`, a wrapper of `merlin` into the trait (available via the -`merlin` feature). + `merlin` feature). - `RecommendedTranscript`, a transcript recommended for usage in applications. Currently, this is `DigestTranscript` (available via the `recommended` feature). diff --git a/crypto/transcript/src/tests.rs b/crypto/transcript/src/tests.rs index 93651b03..ce5a0a1c 100644 --- a/crypto/transcript/src/tests.rs +++ b/crypto/transcript/src/tests.rs @@ -1,10 +1,7 @@ use crate::Transcript; /// Test the sanity of a transcript. -pub fn test_transcript() -where - T::Challenge: PartialEq, -{ +pub fn test_transcript>() { // Ensure distinct names cause distinct challenges { let mut t1 = T::new(b"1"); diff --git a/deny.toml b/deny.toml index 218062b1..6cb9ceea 100644 --- a/deny.toml +++ b/deny.toml @@ -1,19 +1,19 @@ [advisories] +version = 2 + db-path = "~/.cargo/advisory-db" db-urls = ["https://github.com/rustsec/advisory-db"] -vulnerability = "deny" yanked = "deny" -notice = "warn" -unmaintained = "warn" ignore = [ + "RUSTSEC-2020-0168", # mach is unmaintained "RUSTSEC-2021-0139", # https://github.com/serai-dex/serai/228 "RUSTSEC-2022-0061", # https://github.com/serai-dex/serai/227 ] [licenses] -unlicensed = "deny" +version = 2 allow = [ # Effective public domain @@ -36,14 +36,11 @@ allow = [ "GPL-3.0 WITH Classpath-exception-2.0", ] -copyleft = "deny" -allow-osi-fsf-free = "neither" -default = "deny" - exceptions = [ { allow = ["AGPL-3.0"], name = "serai-env" }, { allow = ["AGPL-3.0"], name = "ethereum-serai" }, + { allow = ["AGPL-3.0"], name = "serai-ethereum-relayer" }, { allow = ["AGPL-3.0"], name = "serai-message-queue" }, @@ -59,6 +56,8 @@ exceptions = [ { allow = ["AGPL-3.0"], name = "serai-genesis-liquidity-pallet" }, { allow = ["AGPL-3.0"], name = "serai-emissions-pallet" }, + { allow = ["AGPL-3.0"], name = "serai-genesis-liquidity-pallet" }, + { allow = ["AGPL-3.0"], name = "serai-in-instructions-pallet" }, { allow = ["AGPL-3.0"], name = "serai-validator-sets-pallet" }, @@ -102,7 +101,6 @@ allow-git = [ "https://github.com/rust-lang-nursery/lazy-static.rs", "https://github.com/serai-dex/substrate-bip39", "https://github.com/serai-dex/substrate", - "https://github.com/alloy-rs/alloy", "https://github.com/monero-rs/base58-monero", "https://github.com/orcalabs/dockertest-rs", ] diff --git a/docs/.ruby-version b/docs/.ruby-version index 8c50098d..a0891f56 100644 --- a/docs/.ruby-version +++ b/docs/.ruby-version @@ -1 +1 @@ -3.1 +3.3.4 diff --git a/docs/Gemfile b/docs/Gemfile index 0b800b1f..0f76691f 100644 --- a/docs/Gemfile +++ b/docs/Gemfile @@ -1,4 +1,4 @@ source 'https://rubygems.org' gem "jekyll", "~> 4.3.3" -gem "just-the-docs", "0.8.1" +gem "just-the-docs", "0.8.2" diff --git a/docs/Gemfile.lock b/docs/Gemfile.lock index 34e40cd9..9231036f 100644 --- a/docs/Gemfile.lock +++ b/docs/Gemfile.lock @@ -1,19 +1,22 @@ GEM remote: https://rubygems.org/ specs: - addressable (2.8.6) - public_suffix (>= 2.0.2, < 6.0) + addressable (2.8.7) + public_suffix (>= 2.0.2, < 7.0) + bigdecimal (3.1.8) colorator (1.1.0) - concurrent-ruby (1.2.3) + concurrent-ruby (1.3.3) em-websocket (0.5.3) eventmachine (>= 0.12.9) http_parser.rb (~> 0) eventmachine (1.2.7) - ffi (1.16.3) + ffi (1.17.0-x86_64-linux-gnu) forwardable-extended (2.6.0) - google-protobuf (3.25.3-x86_64-linux) + google-protobuf (4.27.2-x86_64-linux) + bigdecimal + rake (>= 13) http_parser.rb (0.8.0) - i18n (1.14.4) + i18n (1.14.5) concurrent-ruby (~> 1.0) jekyll (4.3.3) addressable (~> 2.4) @@ -39,7 +42,7 @@ GEM jekyll (>= 3.8, < 5.0) jekyll-watch (2.2.1) listen (~> 3.0) - just-the-docs (0.8.1) + just-the-docs (0.8.2) jekyll (>= 3.8.5) jekyll-include-cache jekyll-seo-tag (>= 2.0) @@ -55,17 +58,18 @@ GEM mercenary (0.4.0) pathutil (0.16.2) forwardable-extended (~> 2.6) - public_suffix (5.0.4) - rake (13.1.0) + public_suffix (6.0.0) + rake (13.2.1) rb-fsevent (0.11.2) - rb-inotify (0.10.1) + rb-inotify (0.11.1) ffi (~> 1.0) - rexml (3.2.6) - rouge (4.2.0) + rexml (3.3.2) + strscan + rouge (4.3.0) safe_yaml (1.0.5) - sass-embedded (1.63.6) - google-protobuf (~> 3.23) - rake (>= 13.0.0) + sass-embedded (1.77.8-x86_64-linux-gnu) + google-protobuf (~> 4.26) + strscan (3.1.0) terminal-table (3.0.2) unicode-display_width (>= 1.1.1, < 3) unicode-display_width (2.5.0) @@ -76,7 +80,7 @@ PLATFORMS DEPENDENCIES jekyll (~> 4.3.3) - just-the-docs (= 0.8.1) + just-the-docs (= 0.8.2) BUNDLED WITH - 2.2.5 + 2.5.11 diff --git a/coins/bitcoin/Cargo.toml b/networks/bitcoin/Cargo.toml similarity index 88% rename from coins/bitcoin/Cargo.toml rename to networks/bitcoin/Cargo.toml index 4ff0f79a..7d6c9412 100644 --- a/coins/bitcoin/Cargo.toml +++ b/networks/bitcoin/Cargo.toml @@ -3,10 +3,10 @@ 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" +repository = "https://github.com/serai-dex/serai/tree/develop/networks/bitcoin" authors = ["Luke Parker ", "Vrx "] edition = "2021" -rust-version = "1.74" +rust-version = "1.79" [package.metadata.docs.rs] all-features = true @@ -23,7 +23,7 @@ thiserror = { version = "1", default-features = false, optional = true } zeroize = { version = "^1.5", default-features = false } rand_core = { version = "0.6", default-features = false } -bitcoin = { version = "0.31", default-features = false, features = ["no-std"] } +bitcoin = { version = "0.32", default-features = false } k256 = { version = "^0.13.1", default-features = false, features = ["arithmetic", "bits"] } @@ -36,7 +36,7 @@ serde_json = { version = "1", default-features = false, optional = true } simple-request = { path = "../../common/request", version = "0.1", default-features = false, features = ["tls", "basic-auth"], optional = true } [dev-dependencies] -secp256k1 = { version = "0.28", default-features = false, features = ["std"] } +secp256k1 = { version = "0.29", default-features = false, features = ["std"] } frost = { package = "modular-frost", path = "../../crypto/frost", features = ["tests"] } diff --git a/coins/bitcoin/LICENSE b/networks/bitcoin/LICENSE similarity index 100% rename from coins/bitcoin/LICENSE rename to networks/bitcoin/LICENSE diff --git a/coins/bitcoin/README.md b/networks/bitcoin/README.md similarity index 100% rename from coins/bitcoin/README.md rename to networks/bitcoin/README.md diff --git a/coins/bitcoin/src/crypto.rs b/networks/bitcoin/src/crypto.rs similarity index 100% rename from coins/bitcoin/src/crypto.rs rename to networks/bitcoin/src/crypto.rs diff --git a/coins/bitcoin/src/lib.rs b/networks/bitcoin/src/lib.rs similarity index 100% rename from coins/bitcoin/src/lib.rs rename to networks/bitcoin/src/lib.rs diff --git a/coins/bitcoin/src/rpc.rs b/networks/bitcoin/src/rpc.rs similarity index 98% rename from coins/bitcoin/src/rpc.rs rename to networks/bitcoin/src/rpc.rs index 6778636b..fb1c35d6 100644 --- a/coins/bitcoin/src/rpc.rs +++ b/networks/bitcoin/src/rpc.rs @@ -195,13 +195,13 @@ impl Rpc { // If this was already successfully published, consider this having succeeded if let RpcError::RequestError(Error { code, .. }) = e { if code == RPC_VERIFY_ALREADY_IN_CHAIN { - return Ok(tx.txid()); + return Ok(tx.compute_txid()); } } Err(e)? } }; - if txid != tx.txid() { + if txid != tx.compute_txid() { Err(RpcError::InvalidResponse("returned TX ID inequals calculated TX ID"))?; } Ok(txid) @@ -215,7 +215,7 @@ impl Rpc { let tx: Transaction = encode::deserialize(&bytes) .map_err(|_| RpcError::InvalidResponse("node sent an improperly serialized transaction"))?; - let mut tx_hash = *tx.txid().as_raw_hash().as_byte_array(); + 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"))?; diff --git a/coins/bitcoin/src/tests/crypto.rs b/networks/bitcoin/src/tests/crypto.rs similarity index 93% rename from coins/bitcoin/src/tests/crypto.rs rename to networks/bitcoin/src/tests/crypto.rs index 2170219c..cfc694f4 100644 --- a/coins/bitcoin/src/tests/crypto.rs +++ b/networks/bitcoin/src/tests/crypto.rs @@ -39,7 +39,7 @@ fn test_algorithm() { .verify_schnorr( &Signature::from_slice(&sig) .expect("couldn't convert produced signature to secp256k1::Signature"), - &Message::from(Hash::hash(MESSAGE)), + &Message::from_digest_slice(Hash::hash(MESSAGE).as_ref()).unwrap(), &x_only(&keys[&Participant::new(1).unwrap()].group_key()), ) .unwrap() diff --git a/coins/bitcoin/src/tests/mod.rs b/networks/bitcoin/src/tests/mod.rs similarity index 100% rename from coins/bitcoin/src/tests/mod.rs rename to networks/bitcoin/src/tests/mod.rs diff --git a/coins/bitcoin/src/wallet/mod.rs b/networks/bitcoin/src/wallet/mod.rs similarity index 83% rename from coins/bitcoin/src/wallet/mod.rs rename to networks/bitcoin/src/wallet/mod.rs index 3f099faa..195182ff 100644 --- a/coins/bitcoin/src/wallet/mod.rs +++ b/networks/bitcoin/src/wallet/mod.rs @@ -4,7 +4,7 @@ use std_shims::{ io::{self, Write}, }; #[cfg(feature = "std")] -use std_shims::io::Read; +use std::io::{Read, BufReader}; use k256::{ elliptic_curve::sec1::{Tag, ToEncodedPoint}, @@ -18,8 +18,8 @@ use frost::{ }; use bitcoin::{ - consensus::encode::serialize, key::TweakedPublicKey, address::Payload, OutPoint, ScriptBuf, - TxOut, Transaction, Block, + consensus::encode::serialize, key::TweakedPublicKey, OutPoint, ScriptBuf, TxOut, Transaction, + Block, }; #[cfg(feature = "std")] use bitcoin::consensus::encode::Decodable; @@ -46,12 +46,12 @@ pub fn tweak_keys(keys: &ThresholdKeys) -> ThresholdKeys { /// Return the Taproot address payload for a public key. /// /// If the key is odd, this will return None. -pub fn address_payload(key: ProjectivePoint) -> Option { +pub fn p2tr_script_buf(key: ProjectivePoint) -> Option { if key.to_encoded_point(true).tag() != Tag::CompressedEvenY { return None; } - Some(Payload::p2tr_tweaked(TweakedPublicKey::dangerous_assume_tweaked(x_only(&key)))) + Some(ScriptBuf::new_p2tr_tweaked(TweakedPublicKey::dangerous_assume_tweaked(x_only(&key)))) } /// A spendable output. @@ -89,11 +89,17 @@ impl ReceivedOutput { /// Read a ReceivedOutput from a generic satisfying Read. #[cfg(feature = "std")] pub fn read(r: &mut R) -> io::Result { - Ok(ReceivedOutput { - offset: Secp256k1::read_F(r)?, - output: TxOut::consensus_decode(r).map_err(|_| io::Error::other("invalid TxOut"))?, - outpoint: OutPoint::consensus_decode(r).map_err(|_| io::Error::other("invalid OutPoint"))?, - }) + 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. @@ -124,7 +130,7 @@ impl Scanner { /// Returns None if this key can't be scanned for. pub fn new(key: ProjectivePoint) -> Option { let mut scripts = HashMap::new(); - scripts.insert(address_payload(key)?.script_pubkey(), Scalar::ZERO); + scripts.insert(p2tr_script_buf(key)?, Scalar::ZERO); Some(Scanner { key, scripts }) } @@ -141,9 +147,8 @@ impl Scanner { // chance of being even // That means this should terminate within a very small amount of iterations loop { - match address_payload(self.key + (ProjectivePoint::GENERATOR * offset)) { - Some(address) => { - let script = address.script_pubkey(); + match p2tr_script_buf(self.key + (ProjectivePoint::GENERATOR * offset)) { + Some(script) => { if self.scripts.contains_key(&script) { None?; } @@ -166,7 +171,7 @@ impl Scanner { res.push(ReceivedOutput { offset: *offset, output: output.clone(), - outpoint: OutPoint::new(tx.txid(), vout), + outpoint: OutPoint::new(tx.compute_txid(), vout), }); } } diff --git a/coins/bitcoin/src/wallet/send.rs b/networks/bitcoin/src/wallet/send.rs similarity index 87% rename from coins/bitcoin/src/wallet/send.rs rename to networks/bitcoin/src/wallet/send.rs index 24594ab4..9a723523 100644 --- a/coins/bitcoin/src/wallet/send.rs +++ b/networks/bitcoin/src/wallet/send.rs @@ -18,12 +18,12 @@ use bitcoin::{ absolute::LockTime, script::{PushBytesBuf, ScriptBuf}, transaction::{Version, Transaction}, - OutPoint, Sequence, Witness, TxIn, Amount, TxOut, Address, + OutPoint, Sequence, Witness, TxIn, Amount, TxOut, }; use crate::{ crypto::Schnorr, - wallet::{ReceivedOutput, address_payload}, + wallet::{ReceivedOutput, p2tr_script_buf}, }; #[rustfmt::skip] @@ -61,7 +61,11 @@ pub struct SignableTransaction { } impl SignableTransaction { - fn calculate_weight(inputs: usize, payments: &[(Address, u64)], change: Option<&Address>) -> u64 { + fn calculate_weight_vbytes( + inputs: usize, + payments: &[(ScriptBuf, u64)], + change: Option<&ScriptBuf>, + ) -> (u64, u64) { // Expand this a full transaction in order to use the bitcoin library's weight function let mut tx = Transaction { version: Version(2), @@ -86,16 +90,42 @@ impl SignableTransaction { // The script pub key is not of a fixed size and does have to be used here .map(|payment| TxOut { value: Amount::from_sat(payment.1), - script_pubkey: payment.0.script_pubkey(), + 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.script_pubkey() }); + tx.output.push(TxOut { value: Amount::ZERO, script_pubkey: change.clone() }); } - u64::from(tx.weight()) + + let weight = tx.weight(); + + // Now calculate the size in vbytes + + /* + "Virtual transaction size" is weight ceildiv 4 per + https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki + + https://github.com/bitcoin/bitcoin/blob/306ccd4927a2efe325c8d84be1bdb79edeb29b04 + /src/policy/policy.cpp#L295-L298 + implements this almost as expected, with an additional consideration to signature operations + + Signature operations (the second argument of the following call) do not count Taproot + signatures per https://github.com/bitcoin/bips/blob/master/bip-0342.mediawiki#cite_ref-11-0 + + We don't risk running afoul of the Taproot signature limit as it allows at least one per + input, which is all we use + */ + ( + weight.to_wu(), + u64::try_from(bitcoin::policy::get_virtual_tx_size( + i64::try_from(weight.to_wu()).unwrap(), + 0i64, + )) + .unwrap(), + ) } /// Returns the fee necessary for this transaction to achieve the fee rate specified at @@ -121,10 +151,10 @@ impl SignableTransaction { /// If data is specified, an OP_RETURN output will be added with it. pub fn new( mut inputs: Vec, - payments: &[(Address, u64)], - change: Option<&Address>, + payments: &[(ScriptBuf, u64)], + change: Option, data: Option>, - fee_per_weight: u64, + fee_per_vbyte: u64, ) -> Result { if inputs.is_empty() { Err(TransactionError::NoInputs)?; @@ -159,10 +189,7 @@ impl SignableTransaction { let payment_sat = payments.iter().map(|payment| payment.1).sum::(); let mut tx_outs = payments .iter() - .map(|payment| TxOut { - value: Amount::from_sat(payment.1), - script_pubkey: payment.0.script_pubkey(), - }) + .map(|payment| TxOut { value: Amount::from_sat(payment.1), script_pubkey: payment.0.clone() }) .collect::>(); // Add the OP_RETURN output @@ -176,34 +203,14 @@ impl SignableTransaction { }) } - let mut weight = Self::calculate_weight(tx_ins.len(), payments, None); - let mut needed_fee = fee_per_weight * weight; + let (mut weight, vbytes) = Self::calculate_weight_vbytes(tx_ins.len(), payments, None); - // "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 - ); + let mut needed_fee = fee_per_vbyte * vbytes; // Technically, if there isn't change, this TX may still pay enough of a fee to pass the // minimum fee. Such edge cases aren't worth programming when they go against intent, as the // specified fee rate is too low to be valid // bitcoin::policy::DEFAULT_MIN_RELAY_TX_FEE is in sats/kilo-vbyte - if needed_fee < ((u64::from(bitcoin::policy::DEFAULT_MIN_RELAY_TX_FEE) * vsize) / 1000) { + if needed_fee < ((u64::from(bitcoin::policy::DEFAULT_MIN_RELAY_TX_FEE) * vbytes) / 1000) { Err(TransactionError::TooLowFee)?; } @@ -213,12 +220,12 @@ impl SignableTransaction { // 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; + let (weight_with_change, vbytes_with_change) = + Self::calculate_weight_vbytes(tx_ins.len(), payments, Some(&change)); + let fee_with_change = fee_per_vbyte * vbytes_with_change; if let Some(value) = input_sat.checked_sub(payment_sat + fee_with_change) { if value >= DUST { - tx_outs - .push(TxOut { value: Amount::from_sat(value), script_pubkey: change.script_pubkey() }); + tx_outs.push(TxOut { value: Amount::from_sat(value), script_pubkey: change }); weight = weight_with_change; needed_fee = fee_with_change; } @@ -248,7 +255,7 @@ impl SignableTransaction { /// Returns the TX ID of the transaction this will create. pub fn txid(&self) -> [u8; 32] { - let mut res = self.tx.txid().to_byte_array(); + let mut res = self.tx.compute_txid().to_byte_array(); res.reverse(); res } @@ -288,7 +295,7 @@ impl SignableTransaction { transcript.append_message(b"signing_input", u32::try_from(i).unwrap().to_le_bytes()); let offset = keys.clone().offset(self.offsets[i]); - if address_payload(offset.group_key())?.script_pubkey() != self.prevouts[i].script_pubkey { + if p2tr_script_buf(offset.group_key())? != self.prevouts[i].script_pubkey { None?; } diff --git a/coins/bitcoin/tests/rpc.rs b/networks/bitcoin/tests/rpc.rs similarity index 100% rename from coins/bitcoin/tests/rpc.rs rename to networks/bitcoin/tests/rpc.rs diff --git a/coins/bitcoin/tests/runner.rs b/networks/bitcoin/tests/runner.rs similarity index 100% rename from coins/bitcoin/tests/runner.rs rename to networks/bitcoin/tests/runner.rs diff --git a/coins/bitcoin/tests/wallet.rs b/networks/bitcoin/tests/wallet.rs similarity index 88% rename from coins/bitcoin/tests/wallet.rs rename to networks/bitcoin/tests/wallet.rs index 9eca20c7..4b77e61a 100644 --- a/coins/bitcoin/tests/wallet.rs +++ b/networks/bitcoin/tests/wallet.rs @@ -22,11 +22,10 @@ use bitcoin_serai::{ hashes::Hash as HashTrait, blockdata::opcodes::all::OP_RETURN, script::{PushBytesBuf, Instruction, Instructions, Script}, - address::NetworkChecked, OutPoint, Amount, TxOut, Transaction, Network, Address, }, wallet::{ - tweak_keys, address_payload, ReceivedOutput, Scanner, TransactionError, SignableTransaction, + tweak_keys, p2tr_script_buf, ReceivedOutput, Scanner, TransactionError, SignableTransaction, }, rpc::Rpc, }; @@ -48,7 +47,7 @@ async fn send_and_get_output(rpc: &Rpc, scanner: &Scanner, key: ProjectivePoint) "generatetoaddress", serde_json::json!([ 1, - Address::::new(Network::Regtest, address_payload(key).unwrap()) + Address::from_script(&p2tr_script_buf(key).unwrap(), Network::Regtest).unwrap() ]), ) .await @@ -69,7 +68,7 @@ async fn send_and_get_output(rpc: &Rpc, scanner: &Scanner, key: ProjectivePoint) assert_eq!(outputs, scanner.scan_transaction(&block.txdata[0])); assert_eq!(outputs.len(), 1); - assert_eq!(outputs[0].outpoint(), &OutPoint::new(block.txdata[0].txid(), 0)); + assert_eq!(outputs[0].outpoint(), &OutPoint::new(block.txdata[0].compute_txid(), 0)); assert_eq!(outputs[0].value(), block.txdata[0].output[0].value.to_sat()); assert_eq!( @@ -193,7 +192,7 @@ async_sequential! { assert_eq!(output.offset(), Scalar::ZERO); let inputs = vec![output]; - let addr = || Address::::new(Network::Regtest, address_payload(key).unwrap()); + let addr = || p2tr_script_buf(key).unwrap(); let payments = vec![(addr(), 1000)]; assert!(SignableTransaction::new(inputs.clone(), &payments, None, None, FEE).is_ok()); @@ -206,7 +205,7 @@ async_sequential! { // 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()); + 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 @@ -229,7 +228,7 @@ async_sequential! { ); assert_eq!( - SignableTransaction::new(inputs.clone(), &[], Some(&addr()), None, 0), + SignableTransaction::new(inputs.clone(), &[], Some(addr()), None, 0), Err(TransactionError::TooLowFee), ); @@ -261,20 +260,19 @@ async_sequential! { // Declare payments, change, fee let payments = [ - (Address::::new(Network::Regtest, address_payload(key).unwrap()), 1005), - (Address::::new(Network::Regtest, address_payload(offset_key).unwrap()), 1007) + (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 = - Address::::new(Network::Regtest, address_payload(change_key).unwrap()); + 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), + Some(change_addr.clone()), None, FEE ).unwrap(); @@ -287,7 +285,7 @@ async_sequential! { // Ensure we can scan it let outputs = scanner.scan_transaction(&tx); for (o, output) in outputs.iter().enumerate() { - assert_eq!(output.outpoint(), &OutPoint::new(tx.txid(), u32::try_from(o).unwrap())); + assert_eq!(output.outpoint(), &OutPoint::new(tx.compute_txid(), u32::try_from(o).unwrap())); assert_eq!(&ReceivedOutput::read::<&[u8]>(&mut output.serialize().as_ref()).unwrap(), output); } @@ -299,13 +297,13 @@ async_sequential! { for ((output, scanned), payment) in tx.output.iter().zip(outputs.iter()).zip(payments.iter()) { assert_eq!( output, - &TxOut { script_pubkey: payment.0.script_pubkey(), value: Amount::from_sat(payment.1) }, + &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); + assert_eq!(needed_fee, u64::try_from(tx.vsize()).unwrap() * FEE); let input_value = output.value() + offset_output.value(); let output_value = tx.output.iter().map(|output| output.value.to_sat()).sum::(); assert_eq!(input_value - output_value, needed_fee); @@ -314,13 +312,13 @@ async_sequential! { input_value - payments.iter().map(|payment| payment.1).sum::() - needed_fee; assert_eq!( tx.output[2], - TxOut { script_pubkey: change_addr.script_pubkey(), value: Amount::from_sat(change_amount) }, + 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.txid().as_raw_hash().as_byte_array(); + 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); @@ -344,7 +342,7 @@ async_sequential! { &SignableTransaction::new( vec![output], &[], - Some(&Address::::new(Network::Regtest, address_payload(key).unwrap())), + Some(p2tr_script_buf(key).unwrap()), Some(data.clone()), FEE ).unwrap() diff --git a/coins/ethereum/.gitignore b/networks/ethereum/.gitignore similarity index 100% rename from coins/ethereum/.gitignore rename to networks/ethereum/.gitignore diff --git a/coins/ethereum/Cargo.toml b/networks/ethereum/Cargo.toml similarity index 55% rename from coins/ethereum/Cargo.toml rename to networks/ethereum/Cargo.toml index dc30764e..a700d117 100644 --- a/coins/ethereum/Cargo.toml +++ b/networks/ethereum/Cargo.toml @@ -3,11 +3,11 @@ 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" +repository = "https://github.com/serai-dex/serai/tree/develop/networks/ethereum" authors = ["Luke Parker ", "Elizabeth Binks "] edition = "2021" publish = false -rust-version = "1.74" +rust-version = "1.79" [package.metadata.docs.rs] all-features = true @@ -29,21 +29,21 @@ frost = { package = "modular-frost", path = "../../crypto/frost", default-featur 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 = "b79db21734cffddc11753fe62ba571565c896f42", default-features = false, features = ["k256"] } -alloy-network = { git = "https://github.com/alloy-rs/alloy", rev = "b79db21734cffddc11753fe62ba571565c896f42", default-features = false } -alloy-rpc-types = { git = "https://github.com/alloy-rs/alloy", rev = "b79db21734cffddc11753fe62ba571565c896f42", default-features = false } -alloy-rpc-client = { git = "https://github.com/alloy-rs/alloy", rev = "b79db21734cffddc11753fe62ba571565c896f42", default-features = false } +alloy-consensus = { version = "0.1", default-features = false, features = ["k256"] } +alloy-network = { version = "0.1", default-features = false } +alloy-rpc-types-eth = { version = "0.1", default-features = false } +alloy-rpc-client = { version = "0.1", 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 = "b79db21734cffddc11753fe62ba571565c896f42", default-features = false } +alloy-provider = { version = "0.1", default-features = false } -alloy-node-bindings = { git = "https://github.com/alloy-rs/alloy", rev = "b79db21734cffddc11753fe62ba571565c896f42", default-features = false, optional = true } +alloy-node-bindings = { version = "0.1", 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 = "b79db21734cffddc11753fe62ba571565c896f42", default-features = false } +alloy-node-bindings = { version = "0.1", default-features = false } [features] -tests = ["alloy-node-bindings"] +tests = ["alloy-node-bindings", "frost/tests"] diff --git a/coins/ethereum/LICENSE b/networks/ethereum/LICENSE similarity index 100% rename from coins/ethereum/LICENSE rename to networks/ethereum/LICENSE diff --git a/coins/ethereum/README.md b/networks/ethereum/README.md similarity index 100% rename from coins/ethereum/README.md rename to networks/ethereum/README.md diff --git a/coins/ethereum/alloy-simple-request-transport/Cargo.toml b/networks/ethereum/alloy-simple-request-transport/Cargo.toml similarity index 59% rename from coins/ethereum/alloy-simple-request-transport/Cargo.toml rename to networks/ethereum/alloy-simple-request-transport/Cargo.toml index 0d9ea6b8..973888dc 100644 --- a/coins/ethereum/alloy-simple-request-transport/Cargo.toml +++ b/networks/ethereum/alloy-simple-request-transport/Cargo.toml @@ -3,7 +3,7 @@ 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" +repository = "https://github.com/serai-dex/serai/tree/develop/networks/ethereum/alloy-simple-request-transport" authors = ["Luke Parker "] edition = "2021" rust-version = "1.74" @@ -21,8 +21,8 @@ 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 = "b79db21734cffddc11753fe62ba571565c896f42", default-features = false } -alloy-transport = { git = "https://github.com/alloy-rs/alloy", rev = "b79db21734cffddc11753fe62ba571565c896f42", default-features = false } +alloy-json-rpc = { version = "0.1", default-features = false } +alloy-transport = { version = "0.1", default-features = false } [features] default = ["tls"] diff --git a/coins/ethereum/alloy-simple-request-transport/LICENSE b/networks/ethereum/alloy-simple-request-transport/LICENSE similarity index 100% rename from coins/ethereum/alloy-simple-request-transport/LICENSE rename to networks/ethereum/alloy-simple-request-transport/LICENSE diff --git a/coins/ethereum/alloy-simple-request-transport/README.md b/networks/ethereum/alloy-simple-request-transport/README.md similarity index 100% rename from coins/ethereum/alloy-simple-request-transport/README.md rename to networks/ethereum/alloy-simple-request-transport/README.md diff --git a/coins/ethereum/alloy-simple-request-transport/src/lib.rs b/networks/ethereum/alloy-simple-request-transport/src/lib.rs similarity index 100% rename from coins/ethereum/alloy-simple-request-transport/src/lib.rs rename to networks/ethereum/alloy-simple-request-transport/src/lib.rs diff --git a/coins/ethereum/build.rs b/networks/ethereum/build.rs similarity index 100% rename from coins/ethereum/build.rs rename to networks/ethereum/build.rs diff --git a/coins/ethereum/contracts/Deployer.sol b/networks/ethereum/contracts/Deployer.sol similarity index 100% rename from coins/ethereum/contracts/Deployer.sol rename to networks/ethereum/contracts/Deployer.sol diff --git a/coins/ethereum/contracts/IERC20.sol b/networks/ethereum/contracts/IERC20.sol similarity index 100% rename from coins/ethereum/contracts/IERC20.sol rename to networks/ethereum/contracts/IERC20.sol diff --git a/coins/ethereum/contracts/Router.sol b/networks/ethereum/contracts/Router.sol similarity index 100% rename from coins/ethereum/contracts/Router.sol rename to networks/ethereum/contracts/Router.sol diff --git a/coins/ethereum/contracts/Sandbox.sol b/networks/ethereum/contracts/Sandbox.sol similarity index 100% rename from coins/ethereum/contracts/Sandbox.sol rename to networks/ethereum/contracts/Sandbox.sol diff --git a/coins/ethereum/contracts/Schnorr.sol b/networks/ethereum/contracts/Schnorr.sol similarity index 100% rename from coins/ethereum/contracts/Schnorr.sol rename to networks/ethereum/contracts/Schnorr.sol diff --git a/networks/ethereum/relayer/Cargo.toml b/networks/ethereum/relayer/Cargo.toml new file mode 100644 index 00000000..89d8e99e --- /dev/null +++ b/networks/ethereum/relayer/Cargo.toml @@ -0,0 +1,30 @@ +[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/networks/ethereum/relayer" +authors = ["Luke Parker "] +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"] diff --git a/networks/ethereum/relayer/LICENSE b/networks/ethereum/relayer/LICENSE new file mode 100644 index 00000000..26d57cbb --- /dev/null +++ b/networks/ethereum/relayer/LICENSE @@ -0,0 +1,15 @@ +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 . diff --git a/networks/ethereum/relayer/README.md b/networks/ethereum/relayer/README.md new file mode 100644 index 00000000..beed4b72 --- /dev/null +++ b/networks/ethereum/relayer/README.md @@ -0,0 +1,4 @@ +# Ethereum Transaction Relayer + +This server collects Ethereum router commands to be published, offering an RPC +to fetch them. diff --git a/networks/ethereum/relayer/src/main.rs b/networks/ethereum/relayer/src/main.rs new file mode 100644 index 00000000..54593004 --- /dev/null +++ b/networks/ethereum/relayer/src/main.rs @@ -0,0 +1,100 @@ +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 }; + } + }); + } +} diff --git a/coins/ethereum/src/abi/mod.rs b/networks/ethereum/src/abi/mod.rs similarity index 100% rename from coins/ethereum/src/abi/mod.rs rename to networks/ethereum/src/abi/mod.rs diff --git a/coins/ethereum/src/abi/router.rs b/networks/ethereum/src/abi/router.rs similarity index 100% rename from coins/ethereum/src/abi/router.rs rename to networks/ethereum/src/abi/router.rs diff --git a/coins/ethereum/src/abi/schnorr.rs b/networks/ethereum/src/abi/schnorr.rs similarity index 100% rename from coins/ethereum/src/abi/schnorr.rs rename to networks/ethereum/src/abi/schnorr.rs diff --git a/coins/ethereum/src/crypto.rs b/networks/ethereum/src/crypto.rs similarity index 100% rename from coins/ethereum/src/crypto.rs rename to networks/ethereum/src/crypto.rs diff --git a/coins/ethereum/src/deployer.rs b/networks/ethereum/src/deployer.rs similarity index 92% rename from coins/ethereum/src/deployer.rs rename to networks/ethereum/src/deployer.rs index 1a16664c..19aa328d 100644 --- a/coins/ethereum/src/deployer.rs +++ b/networks/ethereum/src/deployer.rs @@ -5,7 +5,7 @@ use alloy_consensus::{Signed, TxLegacy}; use alloy_sol_types::{SolCall, SolEvent}; -use alloy_rpc_types::{BlockNumberOrTag, Filter}; +use alloy_rpc_types_eth::{BlockNumberOrTag, Filter}; use alloy_simple_request_transport::SimpleRequest; use alloy_provider::{Provider, RootProvider}; @@ -58,14 +58,7 @@ impl Deployer { /// Construct a new view of the `Deployer`. pub async fn new(provider: Arc>) -> Result, Error> { let address = Self::address(); - #[cfg(not(test))] - let required_block = BlockNumberOrTag::Finalized; - #[cfg(test)] - let required_block = BlockNumberOrTag::Latest; - let code = provider - .get_code_at(address.into(), required_block.into()) - .await - .map_err(|_| Error::ConnectionError)?; + 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); diff --git a/coins/ethereum/src/erc20.rs b/networks/ethereum/src/erc20.rs similarity index 99% rename from coins/ethereum/src/erc20.rs rename to networks/ethereum/src/erc20.rs index 1d874403..6a32f7cc 100644 --- a/coins/ethereum/src/erc20.rs +++ b/networks/ethereum/src/erc20.rs @@ -4,7 +4,7 @@ use alloy_core::primitives::{Address, B256, U256}; use alloy_sol_types::{SolInterface, SolEvent}; -use alloy_rpc_types::Filter; +use alloy_rpc_types_eth::Filter; use alloy_simple_request_transport::SimpleRequest; use alloy_provider::{Provider, RootProvider}; diff --git a/coins/ethereum/src/lib.rs b/networks/ethereum/src/lib.rs similarity index 94% rename from coins/ethereum/src/lib.rs rename to networks/ethereum/src/lib.rs index 322b5f83..38bd79e7 100644 --- a/coins/ethereum/src/lib.rs +++ b/networks/ethereum/src/lib.rs @@ -7,7 +7,7 @@ pub mod alloy { pub use alloy_consensus as consensus; pub use alloy_network as network; - pub use alloy_rpc_types as rpc_types; + pub use alloy_rpc_types_eth 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; diff --git a/coins/ethereum/src/machine.rs b/networks/ethereum/src/machine.rs similarity index 100% rename from coins/ethereum/src/machine.rs rename to networks/ethereum/src/machine.rs diff --git a/coins/ethereum/src/router.rs b/networks/ethereum/src/router.rs similarity index 99% rename from coins/ethereum/src/router.rs rename to networks/ethereum/src/router.rs index 8d46b24f..c569d409 100644 --- a/coins/ethereum/src/router.rs +++ b/networks/ethereum/src/router.rs @@ -12,9 +12,9 @@ use alloy_consensus::TxLegacy; use alloy_sol_types::{SolValue, SolConstructor, SolCall, SolEvent}; -use alloy_rpc_types::Filter; +use alloy_rpc_types_eth::Filter; #[cfg(test)] -use alloy_rpc_types::{BlockId, TransactionRequest, TransactionInput}; +use alloy_rpc_types_eth::{BlockId, TransactionRequest, TransactionInput}; use alloy_simple_request_transport::SimpleRequest; use alloy_provider::{Provider, RootProvider}; diff --git a/coins/ethereum/src/tests/abi/mod.rs b/networks/ethereum/src/tests/abi/mod.rs similarity index 100% rename from coins/ethereum/src/tests/abi/mod.rs rename to networks/ethereum/src/tests/abi/mod.rs diff --git a/coins/ethereum/src/tests/contracts/ERC20.sol b/networks/ethereum/src/tests/contracts/ERC20.sol similarity index 100% rename from coins/ethereum/src/tests/contracts/ERC20.sol rename to networks/ethereum/src/tests/contracts/ERC20.sol diff --git a/coins/ethereum/src/tests/contracts/Schnorr.sol b/networks/ethereum/src/tests/contracts/Schnorr.sol similarity index 100% rename from coins/ethereum/src/tests/contracts/Schnorr.sol rename to networks/ethereum/src/tests/contracts/Schnorr.sol diff --git a/coins/ethereum/src/tests/crypto.rs b/networks/ethereum/src/tests/crypto.rs similarity index 100% rename from coins/ethereum/src/tests/crypto.rs rename to networks/ethereum/src/tests/crypto.rs diff --git a/coins/ethereum/src/tests/mod.rs b/networks/ethereum/src/tests/mod.rs similarity index 93% rename from coins/ethereum/src/tests/mod.rs rename to networks/ethereum/src/tests/mod.rs index 085ef3a2..912513ff 100644 --- a/coins/ethereum/src/tests/mod.rs +++ b/networks/ethereum/src/tests/mod.rs @@ -11,7 +11,7 @@ use alloy_core::{ }; use alloy_consensus::{SignableTransaction, TxLegacy}; -use alloy_rpc_types::{BlockNumberOrTag, TransactionReceipt}; +use alloy_rpc_types_eth::TransactionReceipt; use alloy_simple_request_transport::SimpleRequest; use alloy_provider::{Provider, RootProvider}; @@ -57,15 +57,14 @@ pub async fn send( // 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, BlockNumberOrTag::Latest.into()).await.unwrap(); + 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, BlockNumberOrTag::Latest.into()).await.unwrap() > + provider.get_balance(address).await.unwrap() > ((U256::from(tx.gas_price) * U256::from(tx.gas_limit)) + tx.value) ); diff --git a/coins/ethereum/src/tests/router.rs b/networks/ethereum/src/tests/router.rs similarity index 98% rename from coins/ethereum/src/tests/router.rs rename to networks/ethereum/src/tests/router.rs index 39a865bd..e3804d7e 100644 --- a/coins/ethereum/src/tests/router.rs +++ b/networks/ethereum/src/tests/router.rs @@ -14,6 +14,7 @@ use frost::{ use alloy_core::primitives::{Address, U256}; use alloy_simple_request_transport::SimpleRequest; +use alloy_rpc_types_eth::BlockTransactionsKind; use alloy_rpc_client::ClientBuilder; use alloy_provider::{Provider, RootProvider}; @@ -84,7 +85,7 @@ async fn setup_test() -> ( async fn latest_block_hash(client: &RootProvider) -> [u8; 32] { client - .get_block(client.get_block_number().await.unwrap().into(), false) + .get_block(client.get_block_number().await.unwrap().into(), BlockTransactionsKind::Hashes) .await .unwrap() .unwrap() diff --git a/coins/ethereum/src/tests/schnorr.rs b/networks/ethereum/src/tests/schnorr.rs similarity index 97% rename from coins/ethereum/src/tests/schnorr.rs rename to networks/ethereum/src/tests/schnorr.rs index 21d8b45a..2c72ed19 100644 --- a/coins/ethereum/src/tests/schnorr.rs +++ b/networks/ethereum/src/tests/schnorr.rs @@ -15,7 +15,7 @@ use alloy_core::primitives::Address; use alloy_sol_types::SolCall; -use alloy_rpc_types::{TransactionInput, TransactionRequest}; +use alloy_rpc_types_eth::{TransactionInput, TransactionRequest}; use alloy_simple_request_transport::SimpleRequest; use alloy_rpc_client::ClientBuilder; use alloy_provider::{Provider, RootProvider}; diff --git a/networks/monero/Cargo.toml b/networks/monero/Cargo.toml new file mode 100644 index 00000000..73ce850c --- /dev/null +++ b/networks/monero/Cargo.toml @@ -0,0 +1,57 @@ +[package] +name = "monero-serai" +version = "0.1.4-alpha" +description = "A modern Monero transaction library" +license = "MIT" +repository = "https://github.com/serai-dex/serai/tree/develop/networks/monero" +authors = ["Luke Parker "] +edition = "2021" +rust-version = "1.79" + +[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 } + +zeroize = { version = "^1.5", default-features = false, features = ["zeroize_derive"] } + +curve25519-dalek = { version = "4", default-features = false, features = ["alloc", "zeroize"] } + +monero-io = { path = "io", version = "0.1", default-features = false } +monero-generators = { path = "generators", version = "0.4", default-features = false } +monero-primitives = { path = "primitives", version = "0.1", default-features = false } +monero-mlsag = { path = "ringct/mlsag", version = "0.1", default-features = false } +monero-clsag = { path = "ringct/clsag", version = "0.1", default-features = false } +monero-borromean = { path = "ringct/borromean", version = "0.1", default-features = false } +monero-bulletproofs = { path = "ringct/bulletproofs", version = "0.1", default-features = false } + +hex-literal = "0.4" + +[dev-dependencies] +hex = { version = "0.4", default-features = false, features = ["std"] } +serde = { version = "1", default-features = false, features = ["std", "derive"] } +serde_json = { version = "1", default-features = false, features = ["std"] } + +[features] +std = [ + "std-shims/std", + + "zeroize/std", + + "monero-io/std", + "monero-generators/std", + "monero-primitives/std", + "monero-mlsag/std", + "monero-clsag/std", + "monero-borromean/std", + "monero-bulletproofs/std", +] + +compile-time-generators = ["curve25519-dalek/precomputed-tables", "monero-bulletproofs/compile-time-generators"] +multisig = ["monero-clsag/multisig", "std"] +default = ["std", "compile-time-generators"] diff --git a/coins/monero/generators/LICENSE b/networks/monero/LICENSE similarity index 96% rename from coins/monero/generators/LICENSE rename to networks/monero/LICENSE index 6779f0ec..91d893c1 100644 --- a/coins/monero/generators/LICENSE +++ b/networks/monero/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2022-2023 Luke Parker +Copyright (c) 2022-2024 Luke Parker Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/networks/monero/README.md b/networks/monero/README.md new file mode 100644 index 00000000..50146e2e --- /dev/null +++ b/networks/monero/README.md @@ -0,0 +1,28 @@ +# monero-serai + +A modern Monero transaction library. It provides a modern, Rust-friendly view of +the Monero protocol. + +This library is usable under no-std when the `std` feature (on by default) is +disabled. + +### Wallet Functionality + +monero-serai originally included wallet functionality. That has been moved to +monero-wallet. + +### 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 does not include any functionality specific to Serai. + +### Cargo Features + +- `std` (on by default): Enables `std` (and with it, more efficient internal + implementations). +- `compile-time-generators` (on by default): Derives the generators at + compile-time so they don't need to be derived at runtime. This is recommended + if program size doesn't need to be kept minimal. +- `multisig`: Enables the `multisig` feature for all dependencies. diff --git a/coins/monero/generators/Cargo.toml b/networks/monero/generators/Cargo.toml similarity index 66% rename from coins/monero/generators/Cargo.toml rename to networks/monero/generators/Cargo.toml index 22df2ae3..af8cbcd9 100644 --- a/coins/monero/generators/Cargo.toml +++ b/networks/monero/generators/Cargo.toml @@ -1,9 +1,9 @@ [package] name = "monero-generators" version = "0.4.0" -description = "Monero's hash_to_point and generators" +description = "Monero's hash to point function and generators" license = "MIT" -repository = "https://github.com/serai-dex/serai/tree/develop/coins/monero/generators" +repository = "https://github.com/serai-dex/serai/tree/develop/networks/monero/generators" authors = ["Luke Parker "] edition = "2021" @@ -20,15 +20,27 @@ std-shims = { path = "../../../common/std-shims", version = "^0.1.1", default-fe 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"] } +curve25519-dalek = { version = "4", default-features = false, features = ["alloc", "zeroize"] } group = { version = "0.13", default-features = false } dalek-ff-group = { path = "../../../crypto/dalek-ff-group", version = "0.4", default-features = false } +monero-io = { path = "../io", version = "0.1", default-features = false } + [dev-dependencies] hex = "0.4" [features] -std = ["std-shims/std", "subtle/std", "sha3/std", "dalek-ff-group/std"] +std = [ + "std-shims/std", + + "subtle/std", + + "sha3/std", + + "group/alloc", + "dalek-ff-group/std", + + "monero-io/std" +] default = ["std"] diff --git a/coins/monero/LICENSE b/networks/monero/generators/LICENSE similarity index 96% rename from coins/monero/LICENSE rename to networks/monero/generators/LICENSE index 6779f0ec..91d893c1 100644 --- a/coins/monero/LICENSE +++ b/networks/monero/generators/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2022-2023 Luke Parker +Copyright (c) 2022-2024 Luke Parker Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/networks/monero/generators/README.md b/networks/monero/generators/README.md new file mode 100644 index 00000000..e9ac925b --- /dev/null +++ b/networks/monero/generators/README.md @@ -0,0 +1,13 @@ +# Monero Generators + +Generators used by Monero in both its Pedersen commitments and Bulletproofs(+). +An implementation of Monero's `hash_to_ec` is included, as needed to generate +the generators. + +This library is usable under no-std when the `std` feature (on by default) is +disabled. + +### Cargo Features + +- `std` (on by default): Enables `std` (and with it, more efficient internal + implementations). diff --git a/coins/monero/generators/src/hash_to_point.rs b/networks/monero/generators/src/hash_to_point.rs similarity index 70% rename from coins/monero/generators/src/hash_to_point.rs rename to networks/monero/generators/src/hash_to_point.rs index 6a76207d..23b3a086 100644 --- a/coins/monero/generators/src/hash_to_point.rs +++ b/networks/monero/generators/src/hash_to_point.rs @@ -1,27 +1,20 @@ use subtle::ConditionallySelectable; -use curve25519_dalek::edwards::{EdwardsPoint, CompressedEdwardsY}; +use curve25519_dalek::edwards::EdwardsPoint; use group::ff::{Field, PrimeField}; use dalek_ff_group::FieldElement; -use crate::hash; +use monero_io::decompress_point; -/// Decompress canonically encoded ed25519 point -/// It does not check if the point is in the prime order subgroup -pub fn decompress_point(bytes: [u8; 32]) -> Option { - CompressedEdwardsY(bytes) - .decompress() - // Ban points which are either unreduced or -0 - .filter(|point| point.compress().to_bytes() == bytes) -} +use crate::keccak256; -/// Monero's hash to point function, as named `hash_to_ec`. +/// Monero's `hash_to_ec` function. pub fn hash_to_point(bytes: [u8; 32]) -> EdwardsPoint { #[allow(non_snake_case)] let A = FieldElement::from(486662u64); - let v = FieldElement::from_square(hash(&bytes)).double(); + let v = FieldElement::from_square(keccak256(&bytes)).double(); let w = v + FieldElement::ONE; let x = w.square() + (-A.square() * v); diff --git a/networks/monero/generators/src/lib.rs b/networks/monero/generators/src/lib.rs new file mode 100644 index 00000000..1fc7c099 --- /dev/null +++ b/networks/monero/generators/src/lib.rs @@ -0,0 +1,94 @@ +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![doc = include_str!("../README.md")] +#![deny(missing_docs)] +#![cfg_attr(not(feature = "std"), no_std)] + +use std_shims::{sync::OnceLock, vec::Vec}; + +use sha3::{Digest, Keccak256}; + +use curve25519_dalek::{constants::ED25519_BASEPOINT_POINT, edwards::EdwardsPoint}; + +use monero_io::{write_varint, decompress_point}; + +mod hash_to_point; +pub use hash_to_point::hash_to_point; + +#[cfg(test)] +mod tests; + +fn keccak256(data: &[u8]) -> [u8; 32] { + Keccak256::digest(data).into() +} + +static H_CELL: OnceLock = OnceLock::new(); +/// Monero's `H` generator. +/// +/// Contrary to convention (`G` for values, `H` for randomness), `H` is used by Monero for amounts +/// within Pedersen commitments. +#[allow(non_snake_case)] +pub fn H() -> EdwardsPoint { + *H_CELL.get_or_init(|| { + decompress_point(keccak256(&ED25519_BASEPOINT_POINT.compress().to_bytes())) + .unwrap() + .mul_by_cofactor() + }) +} + +static H_POW_2_CELL: OnceLock<[EdwardsPoint; 64]> = OnceLock::new(); +/// Monero's `H` generator, multiplied by 2**i for i in 1 ..= 64. +/// +/// This table is useful when working with amounts, which are u64s. +#[allow(non_snake_case)] +pub fn H_pow_2() -> &'static [EdwardsPoint; 64] { + H_POW_2_CELL.get_or_init(|| { + let mut res = [H(); 64]; + for i in 1 .. 64 { + res[i] = res[i - 1] + res[i - 1]; + } + res + }) +} + +/// The maximum amount of commitments provable for within a single range proof. +pub const MAX_COMMITMENTS: usize = 16; +/// The amount of bits a value within a commitment may use. +pub const COMMITMENT_BITS: usize = 64; +/// The logarithm (over 2) of the amount of bits a value within a commitment may use. +pub const LOG_COMMITMENT_BITS: usize = 6; // 2 ** 6 == N + +/// Container struct for Bulletproofs(+) generators. +#[allow(non_snake_case)] +pub struct Generators { + /// The G (bold) vector of generators. + pub G: Vec, + /// The H (bold) vector of generators. + pub H: Vec, +} + +/// Generate generators as needed for Bulletproofs(+), as Monero does. +/// +/// Consumers should not call this function ad-hoc, yet call it within a build script or use a +/// once-initialized static. +pub fn bulletproofs_generators(dst: &'static [u8]) -> Generators { + // The maximum amount of bits used within a single range proof. + const MAX_MN: usize = MAX_COMMITMENTS * COMMITMENT_BITS; + + let mut preimage = H().compress().to_bytes().to_vec(); + preimage.extend(dst); + + let mut res = Generators { G: Vec::with_capacity(MAX_MN), H: Vec::with_capacity(MAX_MN) }; + for i in 0 .. MAX_MN { + // We generate a pair of generators per iteration + let i = 2 * i; + + let mut even = preimage.clone(); + write_varint(&i, &mut even).unwrap(); + res.H.push(hash_to_point(keccak256(&even))); + + let mut odd = preimage.clone(); + write_varint(&(i + 1), &mut odd).unwrap(); + res.G.push(hash_to_point(keccak256(&odd))); + } + res +} diff --git a/coins/monero/generators/src/tests/hash_to_point.rs b/networks/monero/generators/src/tests/mod.rs similarity index 98% rename from coins/monero/generators/src/tests/hash_to_point.rs rename to networks/monero/generators/src/tests/mod.rs index c4535e08..3ab9449f 100644 --- a/coins/monero/generators/src/tests/hash_to_point.rs +++ b/networks/monero/generators/src/tests/mod.rs @@ -1,7 +1,7 @@ use crate::{decompress_point, hash_to_point}; #[test] -fn crypto_tests() { +fn test_vectors() { // tests.txt file copied from monero repo // https://github.com/monero-project/monero/ // blob/ac02af92867590ca80b2779a7bbeafa99ff94dcb/tests/crypto/tests.txt @@ -21,7 +21,6 @@ fn crypto_tests() { }; let actual = decompress_point(hex::decode(key).unwrap().try_into().unwrap()); - assert_eq!(actual.is_some(), expected); } "hash_to_ec" => { @@ -29,7 +28,6 @@ fn crypto_tests() { let expected = words.next().unwrap(); let actual = hash_to_point(hex::decode(bytes).unwrap().try_into().unwrap()); - assert_eq!(hex::encode(actual.compress().to_bytes()), expected); } _ => unreachable!("unknown command"), diff --git a/coins/monero/generators/src/tests/tests.txt b/networks/monero/generators/src/tests/tests.txt similarity index 100% rename from coins/monero/generators/src/tests/tests.txt rename to networks/monero/generators/src/tests/tests.txt diff --git a/networks/monero/io/Cargo.toml b/networks/monero/io/Cargo.toml new file mode 100644 index 00000000..a4270738 --- /dev/null +++ b/networks/monero/io/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "monero-io" +version = "0.1.0" +description = "Serialization functions, as within the Monero protocol" +license = "MIT" +repository = "https://github.com/serai-dex/serai/tree/develop/networks/monero/io" +authors = ["Luke Parker "] +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 } + +curve25519-dalek = { version = "4", default-features = false, features = ["alloc"] } + +[features] +std = ["std-shims/std"] +default = ["std"] diff --git a/networks/monero/io/LICENSE b/networks/monero/io/LICENSE new file mode 100644 index 00000000..91d893c1 --- /dev/null +++ b/networks/monero/io/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022-2024 Luke Parker + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/networks/monero/io/README.md b/networks/monero/io/README.md new file mode 100644 index 00000000..536b72dd --- /dev/null +++ b/networks/monero/io/README.md @@ -0,0 +1,11 @@ +# Monero IO + +Serialization functions, as within the Monero protocol. + +This library is usable under no-std when the `std` feature (on by default) is +disabled. + +### Cargo Features + +- `std` (on by default): Enables `std` (and with it, more efficient internal + implementations). diff --git a/networks/monero/io/src/lib.rs b/networks/monero/io/src/lib.rs new file mode 100644 index 00000000..68acbe80 --- /dev/null +++ b/networks/monero/io/src/lib.rs @@ -0,0 +1,219 @@ +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![doc = include_str!("../README.md")] +#![deny(missing_docs)] +#![cfg_attr(not(feature = "std"), no_std)] + +use core::fmt::Debug; +use std_shims::{ + vec, + vec::Vec, + io::{self, Read, Write}, +}; + +use curve25519_dalek::{ + scalar::Scalar, + edwards::{EdwardsPoint, CompressedEdwardsY}, +}; + +const VARINT_CONTINUATION_MASK: u8 = 0b1000_0000; + +mod sealed { + /// A trait for a number readable/writable as a VarInt. + /// + /// This is sealed to prevent unintended implementations. + pub trait VarInt: TryInto + TryFrom + Copy { + const BITS: usize; + } + + impl VarInt for u8 { + const BITS: usize = 8; + } + impl VarInt for u32 { + const BITS: usize = 32; + } + impl VarInt for u64 { + const BITS: usize = 64; + } + impl VarInt for usize { + const BITS: usize = core::mem::size_of::() * 8; + } +} + +/// The amount of bytes this number will take when serialized as a VarInt. +/// +/// This function will panic if the VarInt exceeds u64::MAX. +pub fn varint_len(varint: V) -> usize { + let varint_u64: u64 = varint.try_into().map_err(|_| "varint exceeded u64").unwrap(); + ((usize::try_from(u64::BITS - varint_u64.leading_zeros()).unwrap().saturating_sub(1)) / 7) + 1 +} + +/// Write a byte. +/// +/// This is used as a building block within generic functions. +pub fn write_byte(byte: &u8, w: &mut W) -> io::Result<()> { + w.write_all(&[*byte]) +} + +/// Write a number, VarInt-encoded. +/// +/// This will panic if the VarInt exceeds u64::MAX. +pub fn write_varint(varint: &U, w: &mut W) -> io::Result<()> { + let mut varint: u64 = (*varint).try_into().map_err(|_| "varint exceeded u64").unwrap(); + while { + let mut b = u8::try_from(varint & u64::from(!VARINT_CONTINUATION_MASK)).unwrap(); + varint >>= 7; + if varint != 0 { + b |= VARINT_CONTINUATION_MASK; + } + write_byte(&b, w)?; + varint != 0 + } {} + Ok(()) +} + +/// Write a scalar. +pub fn write_scalar(scalar: &Scalar, w: &mut W) -> io::Result<()> { + w.write_all(&scalar.to_bytes()) +} + +/// Write a point. +pub fn write_point(point: &EdwardsPoint, w: &mut W) -> io::Result<()> { + w.write_all(&point.compress().to_bytes()) +} + +/// Write a list of elements, without length-prefixing. +pub fn write_raw_vec io::Result<()>>( + f: F, + values: &[T], + w: &mut W, +) -> io::Result<()> { + for value in values { + f(value, w)?; + } + Ok(()) +} + +/// Write a list of elements, with length-prefixing. +pub fn write_vec io::Result<()>>( + f: F, + values: &[T], + w: &mut W, +) -> io::Result<()> { + write_varint(&values.len(), w)?; + write_raw_vec(f, values, w) +} + +/// Read a constant amount of bytes. +pub fn read_bytes(r: &mut R) -> io::Result<[u8; N]> { + let mut res = [0; N]; + r.read_exact(&mut res)?; + Ok(res) +} + +/// Read a single byte. +pub fn read_byte(r: &mut R) -> io::Result { + Ok(read_bytes::<_, 1>(r)?[0]) +} + +/// Read a u16, little-endian encoded. +pub fn read_u16(r: &mut R) -> io::Result { + read_bytes(r).map(u16::from_le_bytes) +} + +/// Read a u32, little-endian encoded. +pub fn read_u32(r: &mut R) -> io::Result { + read_bytes(r).map(u32::from_le_bytes) +} + +/// Read a u64, little-endian encoded. +pub fn read_u64(r: &mut R) -> io::Result { + read_bytes(r).map(u64::from_le_bytes) +} + +/// Read a canonically-encoded VarInt. +pub fn read_varint(r: &mut R) -> io::Result { + let mut bits = 0; + let mut res = 0; + while { + let b = read_byte(r)?; + if (bits != 0) && (b == 0) { + Err(io::Error::other("non-canonical varint"))?; + } + if ((bits + 7) >= U::BITS) && (b >= (1 << (U::BITS - bits))) { + Err(io::Error::other("varint overflow"))?; + } + + res += u64::from(b & (!VARINT_CONTINUATION_MASK)) << bits; + bits += 7; + b & VARINT_CONTINUATION_MASK == VARINT_CONTINUATION_MASK + } {} + res.try_into().map_err(|_| io::Error::other("VarInt does not fit into integer type")) +} + +/// Read a canonically-encoded scalar. +/// +/// Some scalars within the Monero protocol are not enforced to be canonically encoded. For such +/// scalars, they should be represented as `[u8; 32]` and later converted to scalars as relevant. +pub fn read_scalar(r: &mut R) -> io::Result { + Option::from(Scalar::from_canonical_bytes(read_bytes(r)?)) + .ok_or_else(|| io::Error::other("unreduced scalar")) +} + +/// Decompress a canonically-encoded Ed25519 point. +/// +/// Ed25519 is of order `8 * l`. This function ensures each of those `8 * l` points have a singular +/// encoding by checking points aren't encoded with an unreduced field element, and aren't negative +/// when the negative is equivalent (0 == -0). +/// +/// Since this decodes an Ed25519 point, it does not check the point is in the prime-order +/// subgroup. Torsioned points do have a canonical encoding, and only aren't canonical when +/// considered in relation to the prime-order subgroup. +pub fn decompress_point(bytes: [u8; 32]) -> Option { + CompressedEdwardsY(bytes) + .decompress() + // Ban points which are either unreduced or -0 + .filter(|point| point.compress().to_bytes() == bytes) +} + +/// Read a canonically-encoded Ed25519 point. +/// +/// This internally calls `decompress_point` and has the same definition of canonicity. This +/// function does not check the resulting point is within the prime-order subgroup. +pub fn read_point(r: &mut R) -> io::Result { + let bytes = read_bytes(r)?; + decompress_point(bytes).ok_or_else(|| io::Error::other("invalid point")) +} + +/// Read a canonically-encoded Ed25519 point, within the prime-order subgroup. +pub fn read_torsion_free_point(r: &mut R) -> io::Result { + read_point(r) + .ok() + .filter(EdwardsPoint::is_torsion_free) + .ok_or_else(|| io::Error::other("invalid point")) +} + +/// Read a variable-length list of elements, without length-prefixing. +pub fn read_raw_vec io::Result>( + f: F, + len: usize, + r: &mut R, +) -> io::Result> { + let mut res = vec![]; + for _ in 0 .. len { + res.push(f(r)?); + } + Ok(res) +} + +/// Read a constant-length list of elements. +pub fn read_array io::Result, const N: usize>( + f: F, + r: &mut R, +) -> io::Result<[T; N]> { + read_raw_vec(f, N, r).map(|vec| vec.try_into().unwrap()) +} + +/// Read a length-prefixed variable-length list of elements. +pub fn read_vec io::Result>(f: F, r: &mut R) -> io::Result> { + read_raw_vec(f, read_varint(r)?, r) +} diff --git a/networks/monero/primitives/Cargo.toml b/networks/monero/primitives/Cargo.toml new file mode 100644 index 00000000..2e0bb5cd --- /dev/null +++ b/networks/monero/primitives/Cargo.toml @@ -0,0 +1,44 @@ +[package] +name = "monero-primitives" +version = "0.1.0" +description = "Primitives for the Monero protocol" +license = "MIT" +repository = "https://github.com/serai-dex/serai/tree/develop/networks/monero/primitives" +authors = ["Luke Parker "] +edition = "2021" +rust-version = "1.79" + +[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 } + +zeroize = { version = "^1.5", default-features = false, features = ["zeroize_derive"] } + +# Cryptographic dependencies +sha3 = { version = "0.10", default-features = false } +curve25519-dalek = { version = "4", default-features = false, features = ["alloc", "zeroize"] } + +# Other Monero dependencies +monero-io = { path = "../io", version = "0.1", default-features = false } +monero-generators = { path = "../generators", version = "0.4", default-features = false } + +[dev-dependencies] +hex = { version = "0.4", default-features = false, features = ["alloc"] } + +[features] +std = [ + "std-shims/std", + + "zeroize/std", + + "sha3/std", + + "monero-generators/std", +] +default = ["std"] diff --git a/networks/monero/primitives/LICENSE b/networks/monero/primitives/LICENSE new file mode 100644 index 00000000..91d893c1 --- /dev/null +++ b/networks/monero/primitives/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022-2024 Luke Parker + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/networks/monero/primitives/README.md b/networks/monero/primitives/README.md new file mode 100644 index 00000000..c866193b --- /dev/null +++ b/networks/monero/primitives/README.md @@ -0,0 +1,11 @@ +# Monero Primitives + +Primitive structures and functions for the Monero protocol. + +This library is usable under no-std when the `std` feature (on by default) is +disabled. + +### Cargo Features + +- `std` (on by default): Enables `std` (and with it, more efficient internal + implementations). diff --git a/networks/monero/primitives/src/lib.rs b/networks/monero/primitives/src/lib.rs new file mode 100644 index 00000000..5d8a2336 --- /dev/null +++ b/networks/monero/primitives/src/lib.rs @@ -0,0 +1,248 @@ +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![doc = include_str!("../README.md")] +#![deny(missing_docs)] +#![cfg_attr(not(feature = "std"), no_std)] + +use std_shims::{io, vec::Vec}; +#[cfg(feature = "std")] +use std_shims::sync::OnceLock; + +use zeroize::{Zeroize, ZeroizeOnDrop}; + +use sha3::{Digest, Keccak256}; +use curve25519_dalek::{ + constants::ED25519_BASEPOINT_POINT, + traits::VartimePrecomputedMultiscalarMul, + scalar::Scalar, + edwards::{EdwardsPoint, VartimeEdwardsPrecomputation}, +}; + +use monero_io::*; +use monero_generators::H; + +mod unreduced_scalar; +pub use unreduced_scalar::UnreducedScalar; + +#[cfg(test)] +mod tests; + +// On std, we cache some variables in statics. +#[cfg(feature = "std")] +static INV_EIGHT_CELL: OnceLock = OnceLock::new(); +/// The inverse of 8 over l. +#[cfg(feature = "std")] +#[allow(non_snake_case)] +pub fn INV_EIGHT() -> Scalar { + *INV_EIGHT_CELL.get_or_init(|| Scalar::from(8u8).invert()) +} +// In no-std environments, we prefer the reduced memory use and calculate it ad-hoc. +/// The inverse of 8 over l. +#[cfg(not(feature = "std"))] +#[allow(non_snake_case)] +pub fn INV_EIGHT() -> Scalar { + Scalar::from(8u8).invert() +} + +#[cfg(feature = "std")] +static G_PRECOMP_CELL: OnceLock = OnceLock::new(); +/// A cached (if std) pre-computation of the Ed25519 generator, G. +#[cfg(feature = "std")] +#[allow(non_snake_case)] +pub fn G_PRECOMP() -> &'static VartimeEdwardsPrecomputation { + G_PRECOMP_CELL.get_or_init(|| VartimeEdwardsPrecomputation::new([ED25519_BASEPOINT_POINT])) +} +/// A cached (if std) pre-computation of the Ed25519 generator, G. +#[cfg(not(feature = "std"))] +#[allow(non_snake_case)] +pub fn G_PRECOMP() -> VartimeEdwardsPrecomputation { + VartimeEdwardsPrecomputation::new([ED25519_BASEPOINT_POINT]) +} + +/// The Keccak-256 hash function. +pub fn keccak256(data: impl AsRef<[u8]>) -> [u8; 32] { + Keccak256::digest(data.as_ref()).into() +} + +/// Hash the provided data to a scalar via keccak256(data) % l. +/// +/// This function panics if it finds the Keccak-256 preimage for [0; 32]. +pub fn keccak256_to_scalar(data: impl AsRef<[u8]>) -> Scalar { + let scalar = Scalar::from_bytes_mod_order(keccak256(data.as_ref())); + // Monero will explicitly error in this case + // This library acknowledges its practical impossibility of it occurring, and doesn't bother to + // code in logic to handle it. That said, if it ever occurs, something must happen in order to + // not generate/verify a proof we believe to be valid when it isn't + assert!(scalar != Scalar::ZERO, "ZERO HASH: {:?}", data.as_ref()); + scalar +} + +/// Transparent structure representing a Pedersen commitment's contents. +#[allow(non_snake_case)] +#[derive(Clone, PartialEq, Eq, Zeroize, ZeroizeOnDrop)] +pub struct Commitment { + /// The mask for this commitment. + pub mask: Scalar, + /// The amount committed to by this commitment. + pub amount: u64, +} + +impl core::fmt::Debug for Commitment { + fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> { + fmt.debug_struct("Commitment").field("amount", &self.amount).finish_non_exhaustive() + } +} + +impl Commitment { + /// A commitment to zero, defined with a mask of 1 (as to not be the identity). + pub fn zero() -> Commitment { + Commitment { mask: Scalar::ONE, amount: 0 } + } + + /// Create a new Commitment. + pub fn new(mask: Scalar, amount: u64) -> Commitment { + Commitment { mask, amount } + } + + /// Calculate the Pedersen commitment, as a point, from this transparent structure. + pub fn calculate(&self) -> EdwardsPoint { + EdwardsPoint::vartime_double_scalar_mul_basepoint(&Scalar::from(self.amount), &H(), &self.mask) + } + + /// Write the Commitment. + /// + /// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol + /// defined serialization. + pub fn write(&self, w: &mut W) -> io::Result<()> { + w.write_all(&self.mask.to_bytes())?; + w.write_all(&self.amount.to_le_bytes()) + } + + /// Serialize the Commitment to a `Vec`. + /// + /// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol + /// defined serialization. + pub fn serialize(&self) -> Vec { + let mut res = Vec::with_capacity(32 + 8); + self.write(&mut res).unwrap(); + res + } + + /// Read a Commitment. + /// + /// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol + /// defined serialization. + pub fn read(r: &mut R) -> io::Result { + Ok(Commitment::new(read_scalar(r)?, read_u64(r)?)) + } +} + +/// Decoy data, as used for producing Monero's ring signatures. +#[derive(Clone, PartialEq, Eq, Zeroize, ZeroizeOnDrop)] +pub struct Decoys { + offsets: Vec, + signer_index: u8, + ring: Vec<[EdwardsPoint; 2]>, +} + +impl core::fmt::Debug for Decoys { + fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> { + fmt + .debug_struct("Decoys") + .field("offsets", &self.offsets) + .field("ring", &self.ring) + .finish_non_exhaustive() + } +} + +#[allow(clippy::len_without_is_empty)] +impl Decoys { + /// Create a new instance of decoy data. + /// + /// `offsets` are the positions of each ring member within the Monero blockchain, offset from the + /// prior member's position (with the initial ring member offset from 0). + pub fn new(offsets: Vec, signer_index: u8, ring: Vec<[EdwardsPoint; 2]>) -> Option { + if (offsets.len() != ring.len()) || (usize::from(signer_index) >= ring.len()) { + None?; + } + Some(Decoys { offsets, signer_index, ring }) + } + + /// The length of the ring. + pub fn len(&self) -> usize { + self.offsets.len() + } + + /// The positions of the ring members within the Monero blockchain, as their offsets. + /// + /// The list is formatted as the position of the first ring member, then the offset from each + /// ring member to its prior. + pub fn offsets(&self) -> &[u64] { + &self.offsets + } + + /// The positions of the ring members within the Monero blockchain. + pub fn positions(&self) -> Vec { + let mut res = Vec::with_capacity(self.len()); + res.push(self.offsets[0]); + for m in 1 .. self.len() { + res.push(res[m - 1] + self.offsets[m]); + } + res + } + + /// The index of the signer within the ring. + pub fn signer_index(&self) -> u8 { + self.signer_index + } + + /// The ring. + pub fn ring(&self) -> &[[EdwardsPoint; 2]] { + &self.ring + } + + /// The [key, commitment] pair of the signer. + pub fn signer_ring_members(&self) -> [EdwardsPoint; 2] { + self.ring[usize::from(self.signer_index)] + } + + /// Write the Decoys. + /// + /// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol + /// defined serialization. + pub fn write(&self, w: &mut impl io::Write) -> io::Result<()> { + write_vec(write_varint, &self.offsets, w)?; + w.write_all(&[self.signer_index])?; + write_vec( + |pair, w| { + write_point(&pair[0], w)?; + write_point(&pair[1], w) + }, + &self.ring, + w, + ) + } + + /// Serialize the Decoys to a `Vec`. + /// + /// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol + /// defined serialization. + pub fn serialize(&self) -> Vec { + let mut res = + Vec::with_capacity((1 + (2 * self.offsets.len())) + 1 + 1 + (self.ring.len() * 64)); + self.write(&mut res).unwrap(); + res + } + + /// Read a set of Decoys. + /// + /// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol + /// defined serialization. + pub fn read(r: &mut impl io::Read) -> io::Result { + Decoys::new( + read_vec(read_varint, r)?, + read_byte(r)?, + read_vec(|r| Ok([read_point(r)?, read_point(r)?]), r)?, + ) + .ok_or_else(|| io::Error::other("invalid Decoys")) + } +} diff --git a/coins/monero/src/tests/unreduced_scalar.rs b/networks/monero/primitives/src/tests.rs similarity index 97% rename from coins/monero/src/tests/unreduced_scalar.rs rename to networks/monero/primitives/src/tests.rs index 1816991d..a14d1cd5 100644 --- a/coins/monero/src/tests/unreduced_scalar.rs +++ b/networks/monero/primitives/src/tests.rs @@ -1,6 +1,6 @@ use curve25519_dalek::scalar::Scalar; -use crate::unreduced_scalar::*; +use crate::UnreducedScalar; #[test] fn recover_scalars() { diff --git a/coins/monero/src/unreduced_scalar.rs b/networks/monero/primitives/src/unreduced_scalar.rs similarity index 76% rename from coins/monero/src/unreduced_scalar.rs rename to networks/monero/primitives/src/unreduced_scalar.rs index d0baa681..8b75a4f7 100644 --- a/coins/monero/src/unreduced_scalar.rs +++ b/networks/monero/primitives/src/unreduced_scalar.rs @@ -1,18 +1,19 @@ use core::cmp::Ordering; - use std_shims::{ sync::OnceLock, io::{self, *}, }; +use zeroize::Zeroize; + use curve25519_dalek::scalar::Scalar; -use crate::serialize::*; +use monero_io::*; static PRECOMPUTED_SCALARS_CELL: OnceLock<[Scalar; 8]> = OnceLock::new(); -/// Precomputed scalars used to recover an incorrectly reduced scalar. +// Precomputed scalars used to recover an incorrectly reduced scalar. #[allow(non_snake_case)] -pub(crate) fn PRECOMPUTED_SCALARS() -> [Scalar; 8] { +fn PRECOMPUTED_SCALARS() -> [Scalar; 8] { *PRECOMPUTED_SCALARS_CELL.get_or_init(|| { let mut precomputed_scalars = [Scalar::ONE; 8]; for (i, scalar) in precomputed_scalars.iter_mut().enumerate().skip(1) { @@ -22,22 +23,27 @@ pub(crate) fn PRECOMPUTED_SCALARS() -> [Scalar; 8] { }) } -#[derive(Clone, PartialEq, Eq, Debug)] +/// An unreduced scalar. +/// +/// While most of modern Monero enforces scalars be reduced, certain legacy parts of the code did +/// not. These section can generally simply be read as a scalar/reduced into a scalar when the time +/// comes, yet a couple have non-standard reductions performed. +/// +/// This struct delays scalar conversions and offers the non-standard reduction. +#[derive(Clone, PartialEq, Eq, Debug, Zeroize)] pub struct UnreducedScalar(pub [u8; 32]); impl UnreducedScalar { + /// Write an UnreducedScalar. pub fn write(&self, w: &mut W) -> io::Result<()> { w.write_all(&self.0) } + /// Read an UnreducedScalar. pub fn read(r: &mut R) -> io::Result { Ok(UnreducedScalar(read_bytes(r)?)) } - pub fn as_bytes(&self) -> &[u8; 32] { - &self.0 - } - fn as_bits(&self) -> [u8; 256] { let mut bits = [0; 256]; for (i, bit) in bits.iter_mut().enumerate() { @@ -47,12 +53,12 @@ impl UnreducedScalar { bits } - /// Computes the non-adjacent form of this scalar with width 5. - /// - /// This matches Monero's `slide` function and intentionally gives incorrect outputs under - /// certain conditions in order to match Monero. - /// - /// This function does not execute in constant time. + // Computes the non-adjacent form of this scalar with width 5. + // + // This matches Monero's `slide` function and intentionally gives incorrect outputs under + // certain conditions in order to match Monero. + // + // This function does not execute in constant time. fn non_adjacent_form(&self) -> [i8; 256] { let bits = self.as_bits(); let mut naf = [0i8; 256]; @@ -108,11 +114,11 @@ impl UnreducedScalar { /// Recover the scalar that an array of bytes was incorrectly interpreted as by Monero's `slide` /// function. /// - /// In Borromean range proofs Monero was not checking that the scalars used were - /// reduced. This lead to the scalar stored being interpreted as a different scalar, - /// this function recovers that scalar. + /// In Borromean range proofs, Monero was not checking that the scalars used were + /// reduced. This lead to the scalar stored being interpreted as a different scalar. + /// This function recovers that scalar. /// - /// See: https://github.com/monero-project/monero/issues/8438 + /// See for more info. pub fn recover_monero_slide_scalar(&self) -> Scalar { if self.0[31] & 128 == 0 { // Computing the w-NAF of a number can only give an output with 1 more bit than diff --git a/networks/monero/ringct/borromean/Cargo.toml b/networks/monero/ringct/borromean/Cargo.toml new file mode 100644 index 00000000..b720c0e2 --- /dev/null +++ b/networks/monero/ringct/borromean/Cargo.toml @@ -0,0 +1,41 @@ +[package] +name = "monero-borromean" +version = "0.1.0" +description = "Borromean ring signatures arranged into a range proof, as done by the Monero protocol" +license = "MIT" +repository = "https://github.com/serai-dex/serai/tree/develop/networks/monero/ringct/borromean" +authors = ["Luke Parker "] +edition = "2021" +rust-version = "1.79" + +[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 } + +zeroize = { version = "^1.5", default-features = false, features = ["zeroize_derive"] } + +# Cryptographic dependencies +curve25519-dalek = { version = "4", default-features = false, features = ["alloc", "zeroize"] } + +# Other Monero dependencies +monero-io = { path = "../../io", version = "0.1", default-features = false } +monero-generators = { path = "../../generators", version = "0.4", default-features = false } +monero-primitives = { path = "../../primitives", version = "0.1", default-features = false } + +[features] +std = [ + "std-shims/std", + + "zeroize/std", + + "monero-io/std", + "monero-generators/std", + "monero-primitives/std", +] +default = ["std"] diff --git a/networks/monero/ringct/borromean/LICENSE b/networks/monero/ringct/borromean/LICENSE new file mode 100644 index 00000000..91d893c1 --- /dev/null +++ b/networks/monero/ringct/borromean/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022-2024 Luke Parker + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/networks/monero/ringct/borromean/README.md b/networks/monero/ringct/borromean/README.md new file mode 100644 index 00000000..3b836804 --- /dev/null +++ b/networks/monero/ringct/borromean/README.md @@ -0,0 +1,12 @@ +# Monero Borromean + +Borromean ring signatures arranged into a range proof, as done by the Monero +protocol. + +This library is usable under no-std when the `std` feature (on by default) is +disabled. + +### Cargo Features + +- `std` (on by default): Enables `std` (and with it, more efficient internal + implementations). diff --git a/coins/monero/src/ringct/borromean.rs b/networks/monero/ringct/borromean/src/lib.rs similarity index 61% rename from coins/monero/src/ringct/borromean.rs rename to networks/monero/ringct/borromean/src/lib.rs index 215b3394..5e105142 100644 --- a/coins/monero/src/ringct/borromean.rs +++ b/networks/monero/ringct/borromean/src/lib.rs @@ -1,26 +1,35 @@ +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![doc = include_str!("../README.md")] +#![deny(missing_docs)] +#![cfg_attr(not(feature = "std"), no_std)] +#![allow(non_snake_case)] + use core::fmt::Debug; use std_shims::io::{self, Read, Write}; +use zeroize::Zeroize; + use curve25519_dalek::{traits::Identity, Scalar, EdwardsPoint}; +use monero_io::*; use monero_generators::H_pow_2; +use monero_primitives::{keccak256_to_scalar, UnreducedScalar}; -use crate::{hash_to_scalar, unreduced_scalar::UnreducedScalar, serialize::*}; - -/// 64 Borromean ring signatures. -/// -/// s0 and s1 are stored as `UnreducedScalar`s due to Monero not requiring they were reduced. -/// `UnreducedScalar` preserves their original byte encoding and implements a custom reduction -/// algorithm which was in use. -#[derive(Clone, PartialEq, Eq, Debug)] -pub struct BorromeanSignatures { - pub s0: [UnreducedScalar; 64], - pub s1: [UnreducedScalar; 64], - pub ee: Scalar, +// 64 Borromean ring signatures, as needed for a 64-bit range proof. +// +// s0 and s1 are stored as `UnreducedScalar`s due to Monero not requiring they were reduced. +// `UnreducedScalar` preserves their original byte encoding and implements a custom reduction +// algorithm which was in use. +#[derive(Clone, PartialEq, Eq, Debug, Zeroize)] +struct BorromeanSignatures { + s0: [UnreducedScalar; 64], + s1: [UnreducedScalar; 64], + ee: Scalar, } impl BorromeanSignatures { - pub fn read(r: &mut R) -> io::Result { + // Read a set of BorromeanSignatures. + fn read(r: &mut R) -> io::Result { Ok(BorromeanSignatures { s0: read_array(UnreducedScalar::read, r)?, s1: read_array(UnreducedScalar::read, r)?, @@ -28,7 +37,8 @@ impl BorromeanSignatures { }) } - pub fn write(&self, w: &mut W) -> io::Result<()> { + // Write the set of BorromeanSignatures. + fn write(&self, w: &mut W) -> io::Result<()> { for s0 in &self.s0 { s0.write(w)?; } @@ -50,36 +60,41 @@ impl BorromeanSignatures { ); #[allow(non_snake_case)] let LV = EdwardsPoint::vartime_double_scalar_mul_basepoint( - &hash_to_scalar(LL.compress().as_bytes()), + &keccak256_to_scalar(LL.compress().as_bytes()), &keys_b[i], &self.s1[i].recover_monero_slide_scalar(), ); transcript[(i * 32) .. ((i + 1) * 32)].copy_from_slice(LV.compress().as_bytes()); } - hash_to_scalar(&transcript) == self.ee + keccak256_to_scalar(transcript) == self.ee } } /// A range proof premised on Borromean ring signatures. -#[derive(Clone, PartialEq, Eq, Debug)] +#[derive(Clone, PartialEq, Eq, Debug, Zeroize)] pub struct BorromeanRange { - pub sigs: BorromeanSignatures, - pub bit_commitments: [EdwardsPoint; 64], + sigs: BorromeanSignatures, + bit_commitments: [EdwardsPoint; 64], } impl BorromeanRange { + /// Read a BorromeanRange proof. pub fn read(r: &mut R) -> io::Result { Ok(BorromeanRange { sigs: BorromeanSignatures::read(r)?, bit_commitments: read_array(read_point, r)?, }) } + + /// Write the BorromeanRange proof. pub fn write(&self, w: &mut W) -> io::Result<()> { self.sigs.write(w)?; write_raw_vec(write_point, &self.bit_commitments, w) } + /// Verify the commitment contains a 64-bit value. + #[must_use] pub fn verify(&self, commitment: &EdwardsPoint) -> bool { if &self.bit_commitments.iter().sum::() != commitment { return false; diff --git a/networks/monero/ringct/bulletproofs/Cargo.toml b/networks/monero/ringct/bulletproofs/Cargo.toml new file mode 100644 index 00000000..a4d395e0 --- /dev/null +++ b/networks/monero/ringct/bulletproofs/Cargo.toml @@ -0,0 +1,55 @@ +[package] +name = "monero-bulletproofs" +version = "0.1.0" +description = "Bulletproofs(+) range proofs, as defined by the Monero protocol" +license = "MIT" +repository = "https://github.com/serai-dex/serai/tree/develop/networks/monero/ringct/bulletproofs" +authors = ["Luke Parker "] +edition = "2021" +rust-version = "1.79" + +[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 } + +thiserror = { version = "1", default-features = false, optional = true } + +rand_core = { version = "0.6", default-features = false } +zeroize = { version = "^1.5", default-features = false, features = ["zeroize_derive"] } + +# Cryptographic dependencies +curve25519-dalek = { version = "4", default-features = false, features = ["alloc", "zeroize"] } + +# Other Monero dependencies +monero-io = { path = "../../io", version = "0.1", default-features = false } +monero-generators = { path = "../../generators", version = "0.4", default-features = false } +monero-primitives = { path = "../../primitives", version = "0.1", default-features = false } + +[build-dependencies] +curve25519-dalek = { version = "4", default-features = false, features = ["alloc", "zeroize"] } +monero-generators = { path = "../../generators", version = "0.4", default-features = false } + +[dev-dependencies] +hex-literal = "0.4" + +[features] +std = [ + "std-shims/std", + + "thiserror", + + "rand_core/std", + "zeroize/std", + + "monero-io/std", + "monero-generators/std", + "monero-primitives/std", +] +compile-time-generators = ["curve25519-dalek/precomputed-tables"] +default = ["std", "compile-time-generators"] diff --git a/networks/monero/ringct/bulletproofs/LICENSE b/networks/monero/ringct/bulletproofs/LICENSE new file mode 100644 index 00000000..91d893c1 --- /dev/null +++ b/networks/monero/ringct/bulletproofs/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022-2024 Luke Parker + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/networks/monero/ringct/bulletproofs/README.md b/networks/monero/ringct/bulletproofs/README.md new file mode 100644 index 00000000..8f407fef --- /dev/null +++ b/networks/monero/ringct/bulletproofs/README.md @@ -0,0 +1,14 @@ +# Monero Bulletproofs(+) + +Bulletproofs(+) range proofs, as defined by the Monero protocol. + +This library is usable under no-std when the `std` feature (on by default) is +disabled. + +### Cargo Features + +- `std` (on by default): Enables `std` (and with it, more efficient internal + implementations). +- `compile-time-generators` (on by default): Derives the generators at + compile-time so they don't need to be derived at runtime. This is recommended + if program size doesn't need to be kept minimal. diff --git a/networks/monero/ringct/bulletproofs/build.rs b/networks/monero/ringct/bulletproofs/build.rs new file mode 100644 index 00000000..6ef1bb54 --- /dev/null +++ b/networks/monero/ringct/bulletproofs/build.rs @@ -0,0 +1,88 @@ +use std::{ + io::Write, + env, + path::Path, + fs::{File, remove_file}, +}; + +#[cfg(feature = "compile-time-generators")] +fn generators(prefix: &'static str, path: &str) { + use curve25519_dalek::EdwardsPoint; + + use monero_generators::bulletproofs_generators; + + fn serialize(generators_string: &mut String, points: &[EdwardsPoint]) { + for generator in points { + generators_string.extend( + format!( + " + curve25519_dalek::edwards::CompressedEdwardsY({:?}).decompress().unwrap(), + ", + generator.compress().to_bytes() + ) + .chars(), + ); + } + } + + 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!( + " + static GENERATORS_CELL: OnceLock = OnceLock::new(); + pub(crate) fn GENERATORS() -> &'static Generators {{ + GENERATORS_CELL.get_or_init(|| Generators {{ + G: std_shims::vec![ + {G_str} + ], + H: std_shims::vec![ + {H_str} + ], + }}) + }} + ", + ) + .as_bytes(), + ) + .unwrap(); +} + +#[cfg(not(feature = "compile-time-generators"))] +fn generators(prefix: &'static str, path: &str) { + let path = Path::new(&env::var("OUT_DIR").unwrap()).join(path); + let _ = remove_file(&path); + File::create(&path) + .unwrap() + .write_all( + format!( + r#" + static GENERATORS_CELL: OnceLock = OnceLock::new(); + pub(crate) fn GENERATORS() -> &'static Generators {{ + GENERATORS_CELL.get_or_init(|| {{ + monero_generators::bulletproofs_generators(b"{prefix}") + }}) + }} + "#, + ) + .as_bytes(), + ) + .unwrap(); +} + +fn main() { + println!("cargo:rerun-if-changed=build.rs"); + + generators("bulletproof", "generators.rs"); + generators("bulletproof_plus", "generators_plus.rs"); +} diff --git a/networks/monero/ringct/bulletproofs/src/batch_verifier.rs b/networks/monero/ringct/bulletproofs/src/batch_verifier.rs new file mode 100644 index 00000000..1bf9fb8d --- /dev/null +++ b/networks/monero/ringct/bulletproofs/src/batch_verifier.rs @@ -0,0 +1,101 @@ +use std_shims::vec::Vec; + +use curve25519_dalek::{ + constants::ED25519_BASEPOINT_POINT, + traits::{IsIdentity, VartimeMultiscalarMul}, + scalar::Scalar, + edwards::EdwardsPoint, +}; + +use monero_generators::{H, Generators}; + +use crate::{original, plus}; + +#[derive(Default)] +pub(crate) struct InternalBatchVerifier { + pub(crate) g: Scalar, + pub(crate) h: Scalar, + pub(crate) g_bold: Vec, + pub(crate) h_bold: Vec, + pub(crate) other: Vec<(Scalar, EdwardsPoint)>, +} + +impl InternalBatchVerifier { + #[must_use] + fn verify(self, G: EdwardsPoint, H: EdwardsPoint, generators: &Generators) -> bool { + let capacity = 2 + self.g_bold.len() + self.h_bold.len() + self.other.len(); + let mut scalars = Vec::with_capacity(capacity); + let mut points = Vec::with_capacity(capacity); + + scalars.push(self.g); + points.push(G); + + scalars.push(self.h); + points.push(H); + + for (i, g_bold) in self.g_bold.into_iter().enumerate() { + scalars.push(g_bold); + points.push(generators.G[i]); + } + + for (i, h_bold) in self.h_bold.into_iter().enumerate() { + scalars.push(h_bold); + points.push(generators.H[i]); + } + + for (scalar, point) in self.other { + scalars.push(scalar); + points.push(point); + } + + EdwardsPoint::vartime_multiscalar_mul(scalars, points).is_identity() + } +} + +#[derive(Default)] +pub(crate) struct BulletproofsBatchVerifier(pub(crate) InternalBatchVerifier); +impl BulletproofsBatchVerifier { + #[must_use] + pub(crate) fn verify(self) -> bool { + self.0.verify(ED25519_BASEPOINT_POINT, H(), original::GENERATORS()) + } +} + +#[derive(Default)] +pub(crate) struct BulletproofsPlusBatchVerifier(pub(crate) InternalBatchVerifier); +impl BulletproofsPlusBatchVerifier { + #[must_use] + pub(crate) fn verify(self) -> bool { + // Bulletproofs+ is written as per the paper, with G for the value and H for the mask + // Monero uses H for the value and G for the mask + self.0.verify(H(), ED25519_BASEPOINT_POINT, plus::GENERATORS()) + } +} + +/// A batch verifier for Bulletproofs(+). +/// +/// This uses a fixed layout such that all fixed points only incur a single point scaling, +/// regardless of the amounts of proofs verified. For all variable points (commitments), they're +/// accumulated with the fixed points into a single multiscalar multiplication. +#[derive(Default)] +pub struct BatchVerifier { + pub(crate) original: BulletproofsBatchVerifier, + pub(crate) plus: BulletproofsPlusBatchVerifier, +} +impl BatchVerifier { + /// Create a new batch verifier. + pub fn new() -> Self { + Self { + original: BulletproofsBatchVerifier(InternalBatchVerifier::default()), + plus: BulletproofsPlusBatchVerifier(InternalBatchVerifier::default()), + } + } + + /// Verify all of the proofs queued within this batch verifier. + /// + /// This uses a variable-time multiscalar multiplication internally. + #[must_use] + pub fn verify(self) -> bool { + self.original.verify() && self.plus.verify() + } +} diff --git a/networks/monero/ringct/bulletproofs/src/core.rs b/networks/monero/ringct/bulletproofs/src/core.rs new file mode 100644 index 00000000..09112670 --- /dev/null +++ b/networks/monero/ringct/bulletproofs/src/core.rs @@ -0,0 +1,74 @@ +use std_shims::{vec, vec::Vec}; + +use curve25519_dalek::{ + traits::{MultiscalarMul, VartimeMultiscalarMul}, + scalar::Scalar, + edwards::EdwardsPoint, +}; + +pub(crate) use monero_generators::{MAX_COMMITMENTS, COMMITMENT_BITS, LOG_COMMITMENT_BITS}; + +pub(crate) fn multiexp(pairs: &[(Scalar, EdwardsPoint)]) -> EdwardsPoint { + let mut buf_scalars = Vec::with_capacity(pairs.len()); + let mut buf_points = Vec::with_capacity(pairs.len()); + for (scalar, point) in pairs { + buf_scalars.push(scalar); + buf_points.push(point); + } + EdwardsPoint::multiscalar_mul(buf_scalars, buf_points) +} + +pub(crate) fn multiexp_vartime(pairs: &[(Scalar, EdwardsPoint)]) -> EdwardsPoint { + let mut buf_scalars = Vec::with_capacity(pairs.len()); + let mut buf_points = Vec::with_capacity(pairs.len()); + for (scalar, point) in pairs { + buf_scalars.push(scalar); + buf_points.push(point); + } + EdwardsPoint::vartime_multiscalar_mul(buf_scalars, buf_points) +} + +/* +This has room for optimization worth investigating further. It currently takes +an iterative approach. It can be optimized further via divide and conquer. + +Assume there are 4 challenges. + +Iterative approach (current): + 1. Do the optimal multiplications across challenge column 0 and 1. + 2. Do the optimal multiplications across that result and column 2. + 3. Do the optimal multiplications across that result and column 3. + +Divide and conquer (worth investigating further): + 1. Do the optimal multiplications across challenge column 0 and 1. + 2. Do the optimal multiplications across challenge column 2 and 3. + 3. Multiply both results together. + +When there are 4 challenges (n=16), the iterative approach does 28 multiplications +versus divide and conquer's 24. +*/ +pub(crate) fn challenge_products(challenges: &[(Scalar, Scalar)]) -> Vec { + let mut products = vec![Scalar::ONE; 1 << challenges.len()]; + + if !challenges.is_empty() { + products[0] = challenges[0].1; + products[1] = challenges[0].0; + + for (j, challenge) in challenges.iter().enumerate().skip(1) { + let mut slots = (1 << (j + 1)) - 1; + while slots > 0 { + products[slots] = products[slots / 2] * challenge.0; + products[slots - 1] = products[slots / 2] * challenge.1; + + slots = slots.saturating_sub(2); + } + } + + // Sanity check since if the above failed to populate, it'd be critical + for product in &products { + debug_assert!(*product != Scalar::ZERO); + } + } + + products +} diff --git a/networks/monero/ringct/bulletproofs/src/lib.rs b/networks/monero/ringct/bulletproofs/src/lib.rs new file mode 100644 index 00000000..2a789575 --- /dev/null +++ b/networks/monero/ringct/bulletproofs/src/lib.rs @@ -0,0 +1,292 @@ +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![doc = include_str!("../README.md")] +#![deny(missing_docs)] +#![cfg_attr(not(feature = "std"), no_std)] +#![allow(non_snake_case)] + +use std_shims::{ + vec, + vec::Vec, + io::{self, Read, Write}, +}; + +use rand_core::{RngCore, CryptoRng}; +use zeroize::Zeroizing; + +use curve25519_dalek::edwards::EdwardsPoint; + +use monero_io::*; +pub use monero_generators::MAX_COMMITMENTS; +use monero_primitives::Commitment; + +pub(crate) mod scalar_vector; +pub(crate) mod point_vector; + +pub(crate) mod core; +use crate::core::LOG_COMMITMENT_BITS; + +pub(crate) mod batch_verifier; +use batch_verifier::{BulletproofsBatchVerifier, BulletproofsPlusBatchVerifier}; +pub use batch_verifier::BatchVerifier; + +pub(crate) mod original; +use crate::original::{ + IpProof, AggregateRangeStatement as OriginalStatement, AggregateRangeWitness as OriginalWitness, + AggregateRangeProof as OriginalProof, +}; + +pub(crate) mod plus; +use crate::plus::{ + WipProof, AggregateRangeStatement as PlusStatement, AggregateRangeWitness as PlusWitness, + AggregateRangeProof as PlusProof, +}; + +#[cfg(test)] +mod tests; + +/// An error from proving/verifying Bulletproofs(+). +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +#[cfg_attr(feature = "std", derive(thiserror::Error))] +pub enum BulletproofError { + /// Proving/verifying a Bulletproof(+) range proof with no commitments. + #[cfg_attr(feature = "std", error("no commitments to prove the range for"))] + NoCommitments, + /// Proving/verifying a Bulletproof(+) range proof with more commitments than supported. + #[cfg_attr(feature = "std", error("too many commitments to prove the range for"))] + TooManyCommitments, +} + +/// A Bulletproof(+). +/// +/// This encapsulates either a Bulletproof or a Bulletproof+. +#[allow(clippy::large_enum_variant)] +#[derive(Clone, PartialEq, Eq, Debug)] +pub enum Bulletproof { + /// A Bulletproof. + Original(OriginalProof), + /// A Bulletproof+. + Plus(PlusProof), +} + +impl Bulletproof { + fn bp_fields(plus: bool) -> usize { + if plus { + 6 + } else { + 9 + } + } + + /// Calculate the weight penalty for the Bulletproof(+). + /// + /// Bulletproofs(+) are logarithmically sized yet linearly timed. Evaluating by their size alone + /// accordingly doesn't properly represent the burden of the proof. Monero 'claws back' some of + /// the weight lost by using a proof smaller than it is fast to compensate for this. + // https://github.com/monero-project/monero/blob/94e67bf96bbc010241f29ada6abc89f49a81759c/ + // src/cryptonote_basic/cryptonote_format_utils.cpp#L106-L124 + pub fn calculate_bp_clawback(plus: bool, n_outputs: usize) -> (usize, usize) { + #[allow(non_snake_case)] + let mut LR_len = 0; + let mut n_padded_outputs = 1; + while n_padded_outputs < n_outputs { + LR_len += 1; + n_padded_outputs = 1 << LR_len; + } + LR_len += LOG_COMMITMENT_BITS; + + let mut bp_clawback = 0; + if n_padded_outputs > 2 { + let fields = Bulletproof::bp_fields(plus); + let base = ((fields + (2 * (LOG_COMMITMENT_BITS + 1))) * 32) / 2; + let size = (fields + (2 * LR_len)) * 32; + bp_clawback = ((base * n_padded_outputs) - size) * 4 / 5; + } + + (bp_clawback, LR_len) + } + + /// Prove the list of commitments are within [0 .. 2^64) with an aggregate Bulletproof. + pub fn prove( + rng: &mut R, + outputs: Vec, + ) -> Result { + if outputs.is_empty() { + Err(BulletproofError::NoCommitments)?; + } + if outputs.len() > MAX_COMMITMENTS { + Err(BulletproofError::TooManyCommitments)?; + } + let commitments = outputs.iter().map(Commitment::calculate).collect::>(); + Ok(Bulletproof::Original( + OriginalStatement::new(&commitments) + .unwrap() + .prove(rng, OriginalWitness::new(outputs).unwrap()) + .unwrap(), + )) + } + + /// Prove the list of commitments are within [0 .. 2^64) with an aggregate Bulletproof+. + pub fn prove_plus( + rng: &mut R, + outputs: Vec, + ) -> Result { + if outputs.is_empty() { + Err(BulletproofError::NoCommitments)?; + } + if outputs.len() > MAX_COMMITMENTS { + Err(BulletproofError::TooManyCommitments)?; + } + let commitments = outputs.iter().map(Commitment::calculate).collect::>(); + Ok(Bulletproof::Plus( + PlusStatement::new(&commitments) + .unwrap() + .prove(rng, &Zeroizing::new(PlusWitness::new(outputs).unwrap())) + .unwrap(), + )) + } + + /// Verify the given Bulletproof(+). + #[must_use] + pub fn verify(&self, rng: &mut R, commitments: &[EdwardsPoint]) -> bool { + match self { + Bulletproof::Original(bp) => { + let mut verifier = BulletproofsBatchVerifier::default(); + let Some(statement) = OriginalStatement::new(commitments) else { + return false; + }; + if !statement.verify(rng, &mut verifier, bp.clone()) { + return false; + } + verifier.verify() + } + Bulletproof::Plus(bp) => { + let mut verifier = BulletproofsPlusBatchVerifier::default(); + let Some(statement) = PlusStatement::new(commitments) else { + return false; + }; + if !statement.verify(rng, &mut verifier, bp.clone()) { + return false; + } + verifier.verify() + } + } + } + + /// Accumulate the verification for the given Bulletproof(+) into the specified BatchVerifier. + /// + /// Returns false if the Bulletproof(+) isn't sane, leaving the BatchVerifier in an undefined + /// state. + /// + /// Returns true if the Bulletproof(+) is sane, regardless of its validity. + /// + /// The BatchVerifier must have its verification function executed to actually verify this proof. + #[must_use] + pub fn batch_verify( + &self, + rng: &mut R, + verifier: &mut BatchVerifier, + commitments: &[EdwardsPoint], + ) -> bool { + match self { + Bulletproof::Original(bp) => { + let Some(statement) = OriginalStatement::new(commitments) else { + return false; + }; + statement.verify(rng, &mut verifier.original, bp.clone()) + } + Bulletproof::Plus(bp) => { + let Some(statement) = PlusStatement::new(commitments) else { + return false; + }; + statement.verify(rng, &mut verifier.plus, bp.clone()) + } + } + } + + fn write_core io::Result<()>>( + &self, + w: &mut W, + specific_write_vec: F, + ) -> io::Result<()> { + match self { + Bulletproof::Original(bp) => { + write_point(&bp.A, w)?; + write_point(&bp.S, w)?; + write_point(&bp.T1, w)?; + write_point(&bp.T2, w)?; + write_scalar(&bp.tau_x, w)?; + write_scalar(&bp.mu, w)?; + specific_write_vec(&bp.ip.L, w)?; + specific_write_vec(&bp.ip.R, w)?; + write_scalar(&bp.ip.a, w)?; + write_scalar(&bp.ip.b, w)?; + write_scalar(&bp.t_hat, w) + } + + Bulletproof::Plus(bp) => { + write_point(&bp.A, w)?; + write_point(&bp.wip.A, w)?; + write_point(&bp.wip.B, w)?; + write_scalar(&bp.wip.r_answer, w)?; + write_scalar(&bp.wip.s_answer, w)?; + write_scalar(&bp.wip.delta_answer, w)?; + specific_write_vec(&bp.wip.L, w)?; + specific_write_vec(&bp.wip.R, w) + } + } + } + + /// Write a Bulletproof(+) for the message signed by a transaction's signature. + /// + /// This has a distinct encoding from the standard encoding. + pub fn signature_write(&self, w: &mut W) -> io::Result<()> { + self.write_core(w, |points, w| write_raw_vec(write_point, points, w)) + } + + /// Write a Bulletproof(+). + pub fn write(&self, w: &mut W) -> io::Result<()> { + self.write_core(w, |points, w| write_vec(write_point, points, w)) + } + + /// Serialize a Bulletproof(+) to a `Vec`. + pub fn serialize(&self) -> Vec { + let mut serialized = vec![]; + self.write(&mut serialized).unwrap(); + serialized + } + + /// Read a Bulletproof. + pub fn read(r: &mut R) -> io::Result { + Ok(Bulletproof::Original(OriginalProof { + A: read_point(r)?, + S: read_point(r)?, + T1: read_point(r)?, + T2: read_point(r)?, + tau_x: read_scalar(r)?, + mu: read_scalar(r)?, + ip: IpProof { + L: read_vec(read_point, r)?, + R: read_vec(read_point, r)?, + a: read_scalar(r)?, + b: read_scalar(r)?, + }, + t_hat: read_scalar(r)?, + })) + } + + /// Read a Bulletproof+. + pub fn read_plus(r: &mut R) -> io::Result { + Ok(Bulletproof::Plus(PlusProof { + A: read_point(r)?, + wip: WipProof { + A: read_point(r)?, + B: read_point(r)?, + r_answer: read_scalar(r)?, + s_answer: read_scalar(r)?, + delta_answer: read_scalar(r)?, + L: read_vec(read_point, r)?.into_iter().collect(), + R: read_vec(read_point, r)?.into_iter().collect(), + }, + })) + } +} diff --git a/networks/monero/ringct/bulletproofs/src/original/inner_product.rs b/networks/monero/ringct/bulletproofs/src/original/inner_product.rs new file mode 100644 index 00000000..be8f3a83 --- /dev/null +++ b/networks/monero/ringct/bulletproofs/src/original/inner_product.rs @@ -0,0 +1,303 @@ +use std_shims::{vec, vec::Vec}; + +use zeroize::Zeroize; + +use curve25519_dalek::{Scalar, EdwardsPoint}; + +use monero_generators::H; +use monero_primitives::{INV_EIGHT, keccak256_to_scalar}; +use crate::{ + core::{multiexp_vartime, challenge_products}, + scalar_vector::ScalarVector, + point_vector::PointVector, + BulletproofsBatchVerifier, +}; + +/// An error from proving/verifying Inner-Product statements. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub(crate) enum IpError { + IncorrectAmountOfGenerators, + DifferingLrLengths, +} + +/// The Bulletproofs Inner-Product statement. +/// +/// This is for usage with Protocol 2 from the Bulletproofs paper. +#[derive(Clone, Debug)] +pub(crate) struct IpStatement { + // Weights for h_bold + h_bold_weights: ScalarVector, + // u as the discrete logarithm of G + u: Scalar, +} + +/// The witness for the Bulletproofs Inner-Product statement. +#[derive(Clone, Debug)] +pub(crate) struct IpWitness { + // a + a: ScalarVector, + // b + b: ScalarVector, +} + +impl IpWitness { + /// Construct a new witness for an Inner-Product statement. + /// + /// This functions return None if the lengths of a, b are mismatched, not a power of two, or are + /// empty. + pub(crate) fn new(a: ScalarVector, b: ScalarVector) -> Option { + if a.0.is_empty() || (a.len() != b.len()) { + None?; + } + + let mut power_of_2 = 1; + while power_of_2 < a.len() { + power_of_2 <<= 1; + } + if power_of_2 != a.len() { + None?; + } + + Some(Self { a, b }) + } +} + +/// A proof for the Bulletproofs Inner-Product statement. +#[derive(Clone, PartialEq, Eq, Debug, Zeroize)] +pub(crate) struct IpProof { + pub(crate) L: Vec, + pub(crate) R: Vec, + pub(crate) a: Scalar, + pub(crate) b: Scalar, +} + +impl IpStatement { + /// Create a new Inner-Product statement which won't transcript P. + /// + /// This MUST only be called when P is deterministic to already transcripted elements. + pub(crate) fn new_without_P_transcript(h_bold_weights: ScalarVector, u: Scalar) -> Self { + Self { h_bold_weights, u } + } + + // Transcript a round of the protocol + fn transcript_L_R(transcript: Scalar, L: EdwardsPoint, R: EdwardsPoint) -> Scalar { + let mut transcript = transcript.to_bytes().to_vec(); + transcript.extend(L.compress().to_bytes()); + transcript.extend(R.compress().to_bytes()); + keccak256_to_scalar(transcript) + } + + /// Prove for this Inner-Product statement. + /// + /// Returns an error if this statement couldn't be proven for (such as if the witness isn't + /// consistent). + pub(crate) fn prove( + self, + mut transcript: Scalar, + witness: IpWitness, + ) -> Result { + let generators = crate::original::GENERATORS(); + let g_bold_slice = &generators.G[.. witness.a.len()]; + let h_bold_slice = &generators.H[.. witness.a.len()]; + + let (mut g_bold, mut h_bold, u, mut a, mut b) = { + let IpStatement { h_bold_weights, u } = self; + let u = H() * u; + + // Ensure we have the exact amount of weights + if h_bold_weights.len() != g_bold_slice.len() { + Err(IpError::IncorrectAmountOfGenerators)?; + } + // Acquire a local copy of the generators + let g_bold = PointVector(g_bold_slice.to_vec()); + let h_bold = PointVector(h_bold_slice.to_vec()).mul_vec(&h_bold_weights); + + let IpWitness { a, b } = witness; + + (g_bold, h_bold, u, a, b) + }; + + let mut L_vec = vec![]; + let mut R_vec = vec![]; + + // `else: (n > 1)` case, lines 18-35 of the Bulletproofs paper + // This interprets `g_bold.len()` as `n` + while g_bold.len() > 1 { + // Split a, b, g_bold, h_bold as needed for lines 20-24 + let (a1, a2) = a.clone().split(); + let (b1, b2) = b.clone().split(); + + let (g_bold1, g_bold2) = g_bold.split(); + let (h_bold1, h_bold2) = h_bold.split(); + + let n_hat = g_bold1.len(); + + // Sanity + debug_assert_eq!(a1.len(), n_hat); + debug_assert_eq!(a2.len(), n_hat); + debug_assert_eq!(b1.len(), n_hat); + debug_assert_eq!(b2.len(), n_hat); + debug_assert_eq!(g_bold1.len(), n_hat); + debug_assert_eq!(g_bold2.len(), n_hat); + debug_assert_eq!(h_bold1.len(), n_hat); + debug_assert_eq!(h_bold2.len(), n_hat); + + // cl, cr, lines 21-22 + let cl = a1.clone().inner_product(&b2); + let cr = a2.clone().inner_product(&b1); + + let L = { + let mut L_terms = Vec::with_capacity(1 + (2 * g_bold1.len())); + for (a, g) in a1.0.iter().zip(g_bold2.0.iter()) { + L_terms.push((*a, *g)); + } + for (b, h) in b2.0.iter().zip(h_bold1.0.iter()) { + L_terms.push((*b, *h)); + } + L_terms.push((cl, u)); + // Uses vartime since this isn't a ZK proof + multiexp_vartime(&L_terms) + }; + L_vec.push(L * INV_EIGHT()); + + let R = { + let mut R_terms = Vec::with_capacity(1 + (2 * g_bold1.len())); + for (a, g) in a2.0.iter().zip(g_bold1.0.iter()) { + R_terms.push((*a, *g)); + } + for (b, h) in b1.0.iter().zip(h_bold2.0.iter()) { + R_terms.push((*b, *h)); + } + R_terms.push((cr, u)); + multiexp_vartime(&R_terms) + }; + R_vec.push(R * INV_EIGHT()); + + // Now that we've calculate L, R, transcript them to receive x (26-27) + transcript = Self::transcript_L_R(transcript, *L_vec.last().unwrap(), *R_vec.last().unwrap()); + let x = transcript; + let x_inv = x.invert(); + + // The prover and verifier now calculate the following (28-31) + g_bold = PointVector(Vec::with_capacity(g_bold1.len())); + for (a, b) in g_bold1.0.into_iter().zip(g_bold2.0.into_iter()) { + g_bold.0.push(multiexp_vartime(&[(x_inv, a), (x, b)])); + } + h_bold = PointVector(Vec::with_capacity(h_bold1.len())); + for (a, b) in h_bold1.0.into_iter().zip(h_bold2.0.into_iter()) { + h_bold.0.push(multiexp_vartime(&[(x, a), (x_inv, b)])); + } + + // 32-34 + a = (a1 * x) + &(a2 * x_inv); + b = (b1 * x_inv) + &(b2 * x); + } + + // `if n = 1` case from line 14-17 + + // Sanity + debug_assert_eq!(g_bold.len(), 1); + debug_assert_eq!(h_bold.len(), 1); + debug_assert_eq!(a.len(), 1); + debug_assert_eq!(b.len(), 1); + + // We simply send a/b + Ok(IpProof { L: L_vec, R: R_vec, a: a[0], b: b[0] }) + } + + /// Queue an Inner-Product proof for batch verification. + /// + /// This will return Err if there is an error. This will return Ok if the proof was successfully + /// queued for batch verification. The caller is required to verify the batch in order to ensure + /// the proof is actually correct. + pub(crate) fn verify( + self, + verifier: &mut BulletproofsBatchVerifier, + ip_rows: usize, + mut transcript: Scalar, + verifier_weight: Scalar, + proof: IpProof, + ) -> Result<(), IpError> { + let generators = crate::original::GENERATORS(); + let g_bold_slice = &generators.G[.. ip_rows]; + let h_bold_slice = &generators.H[.. ip_rows]; + + let IpStatement { h_bold_weights, u } = self; + + // Verify the L/R lengths + { + // Calculate the discrete log w.r.t. 2 for the amount of generators present + let mut lr_len = 0; + while (1 << lr_len) < g_bold_slice.len() { + lr_len += 1; + } + + // This proof has less/more terms than the passed in generators are for + if proof.L.len() != lr_len { + Err(IpError::IncorrectAmountOfGenerators)?; + } + if proof.L.len() != proof.R.len() { + Err(IpError::DifferingLrLengths)?; + } + } + + // Again, we start with the `else: (n > 1)` case + + // We need x, x_inv per lines 25-27 for lines 28-31 + let mut xs = Vec::with_capacity(proof.L.len()); + for (L, R) in proof.L.iter().zip(proof.R.iter()) { + transcript = Self::transcript_L_R(transcript, *L, *R); + xs.push(transcript); + } + + // We calculate their inverse in batch + let mut x_invs = xs.clone(); + Scalar::batch_invert(&mut x_invs); + + // Now, with x and x_inv, we need to calculate g_bold', h_bold', P' + // + // For the sake of performance, we solely want to calculate all of these in terms of scalings + // for g_bold, h_bold, P, and don't want to actually perform intermediary scalings of the + // points + // + // L and R are easy, as it's simply x**2, x**-2 + // + // For the series of g_bold, h_bold, we use the `challenge_products` function + // For how that works, please see its own documentation + let product_cache = { + let mut challenges = Vec::with_capacity(proof.L.len()); + + let x_iter = xs.into_iter().zip(x_invs); + let lr_iter = proof.L.into_iter().zip(proof.R); + for ((x, x_inv), (L, R)) in x_iter.zip(lr_iter) { + challenges.push((x, x_inv)); + verifier.0.other.push((verifier_weight * (x * x), L.mul_by_cofactor())); + verifier.0.other.push((verifier_weight * (x_inv * x_inv), R.mul_by_cofactor())); + } + + challenge_products(&challenges) + }; + + // And now for the `if n = 1` case + let c = proof.a * proof.b; + + // The multiexp of these terms equate to the final permutation of P + // We now add terms for a * g_bold' + b * h_bold' b + c * u, with the scalars negative such + // that the terms sum to 0 for an honest prover + + // The g_bold * a term case from line 16 + #[allow(clippy::needless_range_loop)] + for i in 0 .. g_bold_slice.len() { + verifier.0.g_bold[i] -= verifier_weight * product_cache[i] * proof.a; + } + // The h_bold * b term case from line 16 + for i in 0 .. h_bold_slice.len() { + verifier.0.h_bold[i] -= + verifier_weight * product_cache[product_cache.len() - 1 - i] * proof.b * h_bold_weights[i]; + } + // The c * u term case from line 16 + verifier.0.h -= verifier_weight * c * u; + + Ok(()) + } +} diff --git a/networks/monero/ringct/bulletproofs/src/original/mod.rs b/networks/monero/ringct/bulletproofs/src/original/mod.rs new file mode 100644 index 00000000..18fac4d6 --- /dev/null +++ b/networks/monero/ringct/bulletproofs/src/original/mod.rs @@ -0,0 +1,339 @@ +use std_shims::{sync::OnceLock, vec::Vec}; + +use rand_core::{RngCore, CryptoRng}; + +use zeroize::Zeroize; + +use curve25519_dalek::{constants::ED25519_BASEPOINT_POINT, Scalar, EdwardsPoint}; + +use monero_generators::{H, Generators, MAX_COMMITMENTS, COMMITMENT_BITS}; +use monero_primitives::{Commitment, INV_EIGHT, keccak256_to_scalar}; +use crate::{core::multiexp, scalar_vector::ScalarVector, BulletproofsBatchVerifier}; + +pub(crate) mod inner_product; +use inner_product::*; +pub(crate) use inner_product::IpProof; + +include!(concat!(env!("OUT_DIR"), "/generators.rs")); + +#[derive(Clone, Debug)] +pub(crate) struct AggregateRangeStatement<'a> { + commitments: &'a [EdwardsPoint], +} + +#[derive(Clone, Debug)] +pub(crate) struct AggregateRangeWitness { + commitments: Vec, +} + +#[derive(Clone, PartialEq, Eq, Debug, Zeroize)] +pub struct AggregateRangeProof { + pub(crate) A: EdwardsPoint, + pub(crate) S: EdwardsPoint, + pub(crate) T1: EdwardsPoint, + pub(crate) T2: EdwardsPoint, + pub(crate) tau_x: Scalar, + pub(crate) mu: Scalar, + pub(crate) t_hat: Scalar, + pub(crate) ip: IpProof, +} + +impl<'a> AggregateRangeStatement<'a> { + pub(crate) fn new(commitments: &'a [EdwardsPoint]) -> Option { + if commitments.is_empty() || (commitments.len() > MAX_COMMITMENTS) { + None?; + } + Some(Self { commitments }) + } +} + +impl AggregateRangeWitness { + pub(crate) fn new(commitments: Vec) -> Option { + if commitments.is_empty() || (commitments.len() > MAX_COMMITMENTS) { + None?; + } + Some(Self { commitments }) + } +} + +impl<'a> AggregateRangeStatement<'a> { + fn initial_transcript(&self) -> (Scalar, Vec) { + let V = self.commitments.iter().map(|c| c * INV_EIGHT()).collect::>(); + (keccak256_to_scalar(V.iter().flat_map(|V| V.compress().to_bytes()).collect::>()), V) + } + + fn transcript_A_S(transcript: Scalar, A: EdwardsPoint, S: EdwardsPoint) -> (Scalar, Scalar) { + let mut buf = Vec::with_capacity(96); + buf.extend(transcript.to_bytes()); + buf.extend(A.compress().to_bytes()); + buf.extend(S.compress().to_bytes()); + let y = keccak256_to_scalar(buf); + let z = keccak256_to_scalar(y.to_bytes()); + (y, z) + } + + fn transcript_T12(transcript: Scalar, T1: EdwardsPoint, T2: EdwardsPoint) -> Scalar { + let mut buf = Vec::with_capacity(128); + buf.extend(transcript.to_bytes()); + buf.extend(transcript.to_bytes()); + buf.extend(T1.compress().to_bytes()); + buf.extend(T2.compress().to_bytes()); + keccak256_to_scalar(buf) + } + + fn transcript_tau_x_mu_t_hat( + transcript: Scalar, + tau_x: Scalar, + mu: Scalar, + t_hat: Scalar, + ) -> Scalar { + let mut buf = Vec::with_capacity(128); + buf.extend(transcript.to_bytes()); + buf.extend(transcript.to_bytes()); + buf.extend(tau_x.to_bytes()); + buf.extend(mu.to_bytes()); + buf.extend(t_hat.to_bytes()); + keccak256_to_scalar(buf) + } + + #[allow(clippy::needless_pass_by_value)] + pub(crate) fn prove( + self, + rng: &mut (impl RngCore + CryptoRng), + witness: AggregateRangeWitness, + ) -> Option { + if self.commitments != witness.commitments.iter().map(Commitment::calculate).collect::>() + { + None? + }; + + let generators = GENERATORS(); + + let (mut transcript, _) = self.initial_transcript(); + + // Find out the padded amount of commitments + let mut padded_pow_of_2 = 1; + while padded_pow_of_2 < witness.commitments.len() { + padded_pow_of_2 <<= 1; + } + + let mut aL = ScalarVector::new(padded_pow_of_2 * COMMITMENT_BITS); + for (i, commitment) in witness.commitments.iter().enumerate() { + let mut amount = commitment.amount; + for j in 0 .. COMMITMENT_BITS { + aL[(i * COMMITMENT_BITS) + j] = Scalar::from(amount & 1); + amount >>= 1; + } + } + let aR = aL.clone() - Scalar::ONE; + + let alpha = Scalar::random(&mut *rng); + + let A = { + let mut terms = Vec::with_capacity(1 + (2 * aL.len())); + terms.push((alpha, ED25519_BASEPOINT_POINT)); + for (aL, G) in aL.0.iter().zip(&generators.G) { + terms.push((*aL, *G)); + } + for (aR, H) in aR.0.iter().zip(&generators.H) { + terms.push((*aR, *H)); + } + let res = multiexp(&terms) * INV_EIGHT(); + terms.zeroize(); + res + }; + + let mut sL = ScalarVector::new(padded_pow_of_2 * COMMITMENT_BITS); + let mut sR = ScalarVector::new(padded_pow_of_2 * COMMITMENT_BITS); + for i in 0 .. (padded_pow_of_2 * COMMITMENT_BITS) { + sL[i] = Scalar::random(&mut *rng); + sR[i] = Scalar::random(&mut *rng); + } + let rho = Scalar::random(&mut *rng); + + let S = { + let mut terms = Vec::with_capacity(1 + (2 * sL.len())); + terms.push((rho, ED25519_BASEPOINT_POINT)); + for (sL, G) in sL.0.iter().zip(&generators.G) { + terms.push((*sL, *G)); + } + for (sR, H) in sR.0.iter().zip(&generators.H) { + terms.push((*sR, *H)); + } + let res = multiexp(&terms) * INV_EIGHT(); + terms.zeroize(); + res + }; + + let (y, z) = Self::transcript_A_S(transcript, A, S); + transcript = z; + let z = ScalarVector::powers(z, 3 + padded_pow_of_2); + + let twos = ScalarVector::powers(Scalar::from(2u8), COMMITMENT_BITS); + + let l = [aL - z[1], sL]; + let y_pow_n = ScalarVector::powers(y, aR.len()); + let mut r = [((aR + z[1]) * &y_pow_n), sR * &y_pow_n]; + { + for j in 0 .. padded_pow_of_2 { + for i in 0 .. COMMITMENT_BITS { + r[0].0[(j * COMMITMENT_BITS) + i] += z[2 + j] * twos[i]; + } + } + } + let t1 = (l[0].clone().inner_product(&r[1])) + (r[0].clone().inner_product(&l[1])); + let t2 = l[1].clone().inner_product(&r[1]); + + let tau_1 = Scalar::random(&mut *rng); + let T1 = { + let mut T1_terms = [(t1, H()), (tau_1, ED25519_BASEPOINT_POINT)]; + for term in &mut T1_terms { + term.0 *= INV_EIGHT(); + } + let T1 = multiexp(&T1_terms); + T1_terms.zeroize(); + T1 + }; + let tau_2 = Scalar::random(&mut *rng); + let T2 = { + let mut T2_terms = [(t2, H()), (tau_2, ED25519_BASEPOINT_POINT)]; + for term in &mut T2_terms { + term.0 *= INV_EIGHT(); + } + let T2 = multiexp(&T2_terms); + T2_terms.zeroize(); + T2 + }; + + transcript = Self::transcript_T12(transcript, T1, T2); + let x = transcript; + + let [l0, l1] = l; + let l = l0 + &(l1 * x); + let [r0, r1] = r; + let r = r0 + &(r1 * x); + let t_hat = l.clone().inner_product(&r); + let mut tau_x = ((tau_2 * x) + tau_1) * x; + { + for (i, commitment) in witness.commitments.iter().enumerate() { + tau_x += z[2 + i] * commitment.mask; + } + } + let mu = alpha + (rho * x); + + let y_inv_pow_n = ScalarVector::powers(y.invert(), l.len()); + + transcript = Self::transcript_tau_x_mu_t_hat(transcript, tau_x, mu, t_hat); + let x_ip = transcript; + + let ip = IpStatement::new_without_P_transcript(y_inv_pow_n, x_ip) + .prove(transcript, IpWitness::new(l, r).unwrap()) + .unwrap(); + + let res = AggregateRangeProof { A, S, T1, T2, tau_x, mu, t_hat, ip }; + #[cfg(debug_assertions)] + { + let mut verifier = BulletproofsBatchVerifier::default(); + debug_assert!(self.verify(rng, &mut verifier, res.clone())); + debug_assert!(verifier.verify()); + } + Some(res) + } + + #[must_use] + pub(crate) fn verify( + self, + rng: &mut (impl RngCore + CryptoRng), + verifier: &mut BulletproofsBatchVerifier, + mut proof: AggregateRangeProof, + ) -> bool { + let mut padded_pow_of_2 = 1; + while padded_pow_of_2 < self.commitments.len() { + padded_pow_of_2 <<= 1; + } + let ip_rows = padded_pow_of_2 * COMMITMENT_BITS; + + while verifier.0.g_bold.len() < ip_rows { + verifier.0.g_bold.push(Scalar::ZERO); + verifier.0.h_bold.push(Scalar::ZERO); + } + + let (mut transcript, mut commitments) = self.initial_transcript(); + for commitment in &mut commitments { + *commitment = commitment.mul_by_cofactor(); + } + + let (y, z) = Self::transcript_A_S(transcript, proof.A, proof.S); + transcript = z; + let z = ScalarVector::powers(z, 3 + padded_pow_of_2); + transcript = Self::transcript_T12(transcript, proof.T1, proof.T2); + let x = transcript; + transcript = Self::transcript_tau_x_mu_t_hat(transcript, proof.tau_x, proof.mu, proof.t_hat); + let x_ip = transcript; + + proof.A = proof.A.mul_by_cofactor(); + proof.S = proof.S.mul_by_cofactor(); + proof.T1 = proof.T1.mul_by_cofactor(); + proof.T2 = proof.T2.mul_by_cofactor(); + + let y_pow_n = ScalarVector::powers(y, ip_rows); + let y_inv_pow_n = ScalarVector::powers(y.invert(), ip_rows); + + let twos = ScalarVector::powers(Scalar::from(2u8), COMMITMENT_BITS); + + // 65 + { + let weight = Scalar::random(&mut *rng); + verifier.0.h += weight * proof.t_hat; + verifier.0.g += weight * proof.tau_x; + + // Now that we've accumulated the lhs, negate the weight and accumulate the rhs + // These will now sum to 0 if equal + let weight = -weight; + + verifier.0.h += weight * (z[1] - (z[2])) * y_pow_n.sum(); + + for (i, commitment) in commitments.iter().enumerate() { + verifier.0.other.push((weight * z[2 + i], *commitment)); + } + + for i in 0 .. padded_pow_of_2 { + verifier.0.h -= weight * z[3 + i] * twos.clone().sum(); + } + verifier.0.other.push((weight * x, proof.T1)); + verifier.0.other.push((weight * (x * x), proof.T2)); + } + + let ip_weight = Scalar::random(&mut *rng); + + // 66 + verifier.0.other.push((ip_weight, proof.A)); + verifier.0.other.push((ip_weight * x, proof.S)); + // We can replace these with a g_sum, h_sum scalar in the batch verifier + // It'd trade `2 * ip_rows` scalar additions (per proof) for one scalar addition and an + // additional term in the MSM + let ip_z = ip_weight * z[1]; + for i in 0 .. ip_rows { + verifier.0.h_bold[i] += ip_z; + } + let neg_ip_z = -ip_z; + for i in 0 .. ip_rows { + verifier.0.g_bold[i] += neg_ip_z; + } + { + for j in 0 .. padded_pow_of_2 { + for i in 0 .. COMMITMENT_BITS { + let full_i = (j * COMMITMENT_BITS) + i; + verifier.0.h_bold[full_i] += ip_weight * y_inv_pow_n[full_i] * z[2 + j] * twos[i]; + } + } + } + verifier.0.h += ip_weight * x_ip * proof.t_hat; + + // 67, 68 + verifier.0.g += ip_weight * -proof.mu; + let res = IpStatement::new_without_P_transcript(y_inv_pow_n, x_ip) + .verify(verifier, ip_rows, transcript, ip_weight, proof.ip); + res.is_ok() + } +} diff --git a/coins/monero/src/ringct/bulletproofs/plus/aggregate_range_proof.rs b/networks/monero/ringct/bulletproofs/src/plus/aggregate_range_proof.rs similarity index 63% rename from coins/monero/src/ringct/bulletproofs/plus/aggregate_range_proof.rs rename to networks/monero/ringct/bulletproofs/src/plus/aggregate_range_proof.rs index cba95014..e3d4bc92 100644 --- a/coins/monero/src/ringct/bulletproofs/plus/aggregate_range_proof.rs +++ b/networks/monero/ringct/bulletproofs/src/plus/aggregate_range_proof.rs @@ -1,40 +1,28 @@ -use std_shims::vec::Vec; +use std_shims::{vec, vec::Vec}; use rand_core::{RngCore, CryptoRng}; - use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing}; -use multiexp::{multiexp, multiexp_vartime, BatchVerifier}; -use group::{ - ff::{Field, PrimeField}, - Group, GroupEncoding, -}; -use dalek_ff_group::{Scalar, EdwardsPoint}; +use curve25519_dalek::{traits::Identity, scalar::Scalar, edwards::EdwardsPoint}; + +use monero_primitives::{INV_EIGHT, Commitment, keccak256_to_scalar}; use crate::{ - Commitment, - ringct::{ - bulletproofs::core::{MAX_M, N}, - bulletproofs::plus::{ - ScalarVector, PointVector, GeneratorsList, Generators, - transcript::*, - weighted_inner_product::{WipStatement, WipWitness, WipProof}, - padded_pow_of_2, u64_decompose, - }, + batch_verifier::BulletproofsPlusBatchVerifier, + core::{MAX_COMMITMENTS, COMMITMENT_BITS, multiexp, multiexp_vartime}, + plus::{ + ScalarVector, PointVector, GeneratorsList, BpPlusGenerators, + transcript::*, + weighted_inner_product::{WipStatement, WipWitness, WipProof}, + padded_pow_of_2, u64_decompose, }, }; // Figure 3 of the Bulletproofs+ Paper #[derive(Clone, Debug)] -pub(crate) struct AggregateRangeStatement { - generators: Generators, - V: Vec, -} - -impl Zeroize for AggregateRangeStatement { - fn zeroize(&mut self) { - self.V.zeroize(); - } +pub(crate) struct AggregateRangeStatement<'a> { + generators: BpPlusGenerators, + V: &'a [EdwardsPoint], } #[derive(Clone, Debug, Zeroize, ZeroizeOnDrop)] @@ -42,7 +30,7 @@ pub(crate) struct AggregateRangeWitness(Vec); impl AggregateRangeWitness { pub(crate) fn new(commitments: Vec) -> Option { - if commitments.is_empty() || (commitments.len() > MAX_M) { + if commitments.is_empty() || (commitments.len() > MAX_COMMITMENTS) { return None; } @@ -50,35 +38,48 @@ impl AggregateRangeWitness { } } +/// Internal structure representing a Bulletproof+, as defined by Monero.. +#[doc(hidden)] #[derive(Clone, PartialEq, Eq, Debug, Zeroize)] pub struct AggregateRangeProof { pub(crate) A: EdwardsPoint, pub(crate) wip: WipProof, } -impl AggregateRangeStatement { - pub(crate) fn new(V: Vec) -> Option { - if V.is_empty() || (V.len() > MAX_M) { +struct AHatComputation { + y: Scalar, + d_descending_y_plus_z: ScalarVector, + y_mn_plus_one: Scalar, + z: Scalar, + z_pow: ScalarVector, + A_hat: EdwardsPoint, +} + +impl<'a> AggregateRangeStatement<'a> { + pub(crate) fn new(V: &'a [EdwardsPoint]) -> Option { + if V.is_empty() || (V.len() > MAX_COMMITMENTS) { return None; } - Some(Self { generators: Generators::new(), V }) + Some(Self { generators: BpPlusGenerators::new(), V }) } fn transcript_A(transcript: &mut Scalar, A: EdwardsPoint) -> (Scalar, Scalar) { - let y = hash_to_scalar(&[transcript.to_repr().as_ref(), A.to_bytes().as_ref()].concat()); - let z = hash_to_scalar(y.to_bytes().as_ref()); + let y = keccak256_to_scalar( + [transcript.to_bytes().as_ref(), A.compress().to_bytes().as_ref()].concat(), + ); + let z = keccak256_to_scalar(y.to_bytes().as_ref()); *transcript = z; (y, z) } fn d_j(j: usize, m: usize) -> ScalarVector { - let mut d_j = Vec::with_capacity(m * N); - for _ in 0 .. (j - 1) * N { + let mut d_j = Vec::with_capacity(m * COMMITMENT_BITS); + for _ in 0 .. (j - 1) * COMMITMENT_BITS { d_j.push(Scalar::ZERO); } - d_j.append(&mut ScalarVector::powers(Scalar::from(2u8), N).0); - for _ in 0 .. (m - j) * N { + d_j.append(&mut ScalarVector::powers(Scalar::from(2u8), COMMITMENT_BITS).0); + for _ in 0 .. (m - j) * COMMITMENT_BITS { d_j.push(Scalar::ZERO); } ScalarVector(d_j) @@ -86,23 +87,26 @@ impl AggregateRangeStatement { fn compute_A_hat( mut V: PointVector, - generators: &Generators, + generators: &BpPlusGenerators, transcript: &mut Scalar, mut A: EdwardsPoint, - ) -> (Scalar, ScalarVector, Scalar, Scalar, ScalarVector, EdwardsPoint) { + ) -> AHatComputation { let (y, z) = Self::transcript_A(transcript, A); A = A.mul_by_cofactor(); while V.len() < padded_pow_of_2(V.len()) { V.0.push(EdwardsPoint::identity()); } - let mn = V.len() * N; + let mn = V.len() * COMMITMENT_BITS; + // 2, 4, 6, 8... powers of z, of length equivalent to the amount of commitments let mut z_pow = Vec::with_capacity(V.len()); + // z**2 + z_pow.push(z * z); let mut d = ScalarVector::new(mn); for j in 1 ..= V.len() { - z_pow.push(z.pow(Scalar::from(2 * u64::try_from(j).unwrap()))); // TODO: Optimize this + z_pow.push(*z_pow.last().unwrap() * z_pow[0]); d = d + &(Self::d_j(j, V.len()) * (z_pow[j - 1])); } @@ -128,23 +132,23 @@ impl AggregateRangeStatement { let neg_z = -z; let mut A_terms = Vec::with_capacity((generators.len() * 2) + 2); for (i, d_y_z) in d_descending_y_plus_z.0.iter().enumerate() { - A_terms.push((neg_z, generators.generator(GeneratorsList::GBold1, i))); - A_terms.push((*d_y_z, generators.generator(GeneratorsList::HBold1, i))); + A_terms.push((neg_z, generators.generator(GeneratorsList::GBold, i))); + A_terms.push((*d_y_z, generators.generator(GeneratorsList::HBold, i))); } A_terms.push((y_mn_plus_one, commitment_accum)); A_terms.push(( - ((y_pows * z) - (d.sum() * y_mn_plus_one * z) - (y_pows * z.square())), - Generators::g(), + ((y_pows * z) - (d.sum() * y_mn_plus_one * z) - (y_pows * (z * z))), + BpPlusGenerators::g(), )); - ( + AHatComputation { y, d_descending_y_plus_z, y_mn_plus_one, z, - ScalarVector(z_pow), - A + multiexp_vartime(&A_terms), - ) + z_pow: ScalarVector(z_pow), + A_hat: A + multiexp_vartime(&A_terms), + } } pub(crate) fn prove( @@ -157,7 +161,7 @@ impl AggregateRangeStatement { return None; } for (commitment, witness) in self.V.iter().zip(witness.0.iter()) { - if witness.calculate() != **commitment { + if witness.calculate() != *commitment { return None; } } @@ -170,19 +174,19 @@ impl AggregateRangeStatement { // Commitments aren't transmitted INV_EIGHT though, so this multiplies by INV_EIGHT to enable // clearing its cofactor without mutating the value // For some reason, these values are transcripted * INV_EIGHT, not as transmitted - let mut V = V.into_iter().map(|V| EdwardsPoint(V.0 * crate::INV_EIGHT())).collect::>(); + let V = V.iter().map(|V| V * INV_EIGHT()).collect::>(); let mut transcript = initial_transcript(V.iter()); - V.iter_mut().for_each(|V| *V = V.mul_by_cofactor()); + let mut V = V.iter().map(EdwardsPoint::mul_by_cofactor).collect::>(); // Pad V while V.len() < padded_pow_of_2(V.len()) { V.push(EdwardsPoint::identity()); } - let generators = generators.reduce(V.len() * N); + let generators = generators.reduce(V.len() * COMMITMENT_BITS); let mut d_js = Vec::with_capacity(V.len()); - let mut a_l = ScalarVector(Vec::with_capacity(V.len() * N)); + let mut a_l = ScalarVector(Vec::with_capacity(V.len() * COMMITMENT_BITS)); for j in 1 ..= V.len() { d_js.push(Self::d_j(j, V.len())); #[allow(clippy::map_unwrap_or)] @@ -200,26 +204,26 @@ impl AggregateRangeStatement { let mut A_terms = Vec::with_capacity((generators.len() * 2) + 1); for (i, a_l) in a_l.0.iter().enumerate() { - A_terms.push((*a_l, generators.generator(GeneratorsList::GBold1, i))); + A_terms.push((*a_l, generators.generator(GeneratorsList::GBold, i))); } for (i, a_r) in a_r.0.iter().enumerate() { - A_terms.push((*a_r, generators.generator(GeneratorsList::HBold1, i))); + A_terms.push((*a_r, generators.generator(GeneratorsList::HBold, i))); } - A_terms.push((alpha, Generators::h())); + A_terms.push((alpha, BpPlusGenerators::h())); let mut A = multiexp(&A_terms); A_terms.zeroize(); // Multiply by INV_EIGHT per earlier commentary - A.0 *= crate::INV_EIGHT(); + A *= INV_EIGHT(); - let (y, d_descending_y_plus_z, y_mn_plus_one, z, z_pow, A_hat) = + let AHatComputation { y, d_descending_y_plus_z, y_mn_plus_one, z, z_pow, A_hat } = Self::compute_A_hat(PointVector(V), &generators, &mut transcript, A); let a_l = a_l - z; let a_r = a_r + &d_descending_y_plus_z; let mut alpha = alpha; for j in 1 ..= witness.0.len() { - alpha += z_pow[j - 1] * Scalar(witness.0[j - 1].mask) * y_mn_plus_one; + alpha += z_pow[j - 1] * witness.0[j - 1].mask * y_mn_plus_one; } Some(AggregateRangeProof { @@ -230,23 +234,22 @@ impl AggregateRangeStatement { }) } - pub(crate) fn verify( + pub(crate) fn verify( self, rng: &mut R, - verifier: &mut BatchVerifier, - id: Id, + verifier: &mut BulletproofsPlusBatchVerifier, proof: AggregateRangeProof, ) -> bool { let Self { generators, V } = self; - let mut V = V.into_iter().map(|V| EdwardsPoint(V.0 * crate::INV_EIGHT())).collect::>(); + let V = V.iter().map(|V| V * INV_EIGHT()).collect::>(); let mut transcript = initial_transcript(V.iter()); - V.iter_mut().for_each(|V| *V = V.mul_by_cofactor()); + let V = V.iter().map(EdwardsPoint::mul_by_cofactor).collect::>(); - let generators = generators.reduce(V.len() * N); + let generators = generators.reduce(V.len() * COMMITMENT_BITS); - let (y, _, _, _, _, A_hat) = + let AHatComputation { y, A_hat, .. } = Self::compute_A_hat(PointVector(V), &generators, &mut transcript, proof.A); - WipStatement::new(generators, A_hat, y).verify(rng, verifier, id, transcript, proof.wip) + WipStatement::new(generators, A_hat, y).verify(rng, verifier, transcript, proof.wip) } } diff --git a/coins/monero/src/ringct/bulletproofs/plus/mod.rs b/networks/monero/ringct/bulletproofs/src/plus/mod.rs similarity index 55% rename from coins/monero/src/ringct/bulletproofs/plus/mod.rs rename to networks/monero/ringct/bulletproofs/src/plus/mod.rs index 30417821..92bff236 100644 --- a/coins/monero/src/ringct/bulletproofs/plus/mod.rs +++ b/networks/monero/ringct/bulletproofs/src/plus/mod.rs @@ -1,11 +1,12 @@ #![allow(non_snake_case)] -use group::Group; -use dalek_ff_group::{Scalar, EdwardsPoint}; +use std_shims::sync::OnceLock; -pub(crate) use crate::ringct::bulletproofs::scalar_vector::ScalarVector; -mod point_vector; -pub(crate) use point_vector::PointVector; +use curve25519_dalek::{constants::ED25519_BASEPOINT_POINT, scalar::Scalar, edwards::EdwardsPoint}; + +use monero_generators::{H, Generators}; + +pub(crate) use crate::{scalar_vector::ScalarVector, point_vector::PointVector}; pub(crate) mod transcript; pub(crate) mod weighted_inner_product; @@ -23,55 +24,50 @@ pub(crate) fn padded_pow_of_2(i: usize) -> usize { #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] pub(crate) enum GeneratorsList { - GBold1, - HBold1, + GBold, + HBold, } -// TODO: Table these #[derive(Clone, Debug)] -pub(crate) struct Generators { - g_bold1: &'static [EdwardsPoint], - h_bold1: &'static [EdwardsPoint], +pub(crate) struct BpPlusGenerators { + g_bold: &'static [EdwardsPoint], + h_bold: &'static [EdwardsPoint], } -mod generators { - use std_shims::sync::OnceLock; - use monero_generators::Generators; - include!(concat!(env!("OUT_DIR"), "/generators_plus.rs")); -} +include!(concat!(env!("OUT_DIR"), "/generators_plus.rs")); -impl Generators { +impl BpPlusGenerators { #[allow(clippy::new_without_default)] pub(crate) fn new() -> Self { - let gens = generators::GENERATORS(); - Generators { g_bold1: &gens.G, h_bold1: &gens.H } + let gens = GENERATORS(); + BpPlusGenerators { g_bold: &gens.G, h_bold: &gens.H } } pub(crate) fn len(&self) -> usize { - self.g_bold1.len() + self.g_bold.len() } pub(crate) fn g() -> EdwardsPoint { - dalek_ff_group::EdwardsPoint(crate::H()) + H() } pub(crate) fn h() -> EdwardsPoint { - EdwardsPoint::generator() + ED25519_BASEPOINT_POINT } pub(crate) fn generator(&self, list: GeneratorsList, i: usize) -> EdwardsPoint { match list { - GeneratorsList::GBold1 => self.g_bold1[i], - GeneratorsList::HBold1 => self.h_bold1[i], + GeneratorsList::GBold => self.g_bold[i], + GeneratorsList::HBold => self.h_bold[i], } } pub(crate) fn reduce(&self, generators: usize) -> Self { // Round to the nearest power of 2 let generators = padded_pow_of_2(generators); - assert!(generators <= self.g_bold1.len()); + assert!(generators <= self.g_bold.len()); - Generators { g_bold1: &self.g_bold1[.. generators], h_bold1: &self.h_bold1[.. generators] } + BpPlusGenerators { g_bold: &self.g_bold[.. generators], h_bold: &self.h_bold[.. generators] } } } diff --git a/networks/monero/ringct/bulletproofs/src/plus/transcript.rs b/networks/monero/ringct/bulletproofs/src/plus/transcript.rs new file mode 100644 index 00000000..3e43a239 --- /dev/null +++ b/networks/monero/ringct/bulletproofs/src/plus/transcript.rs @@ -0,0 +1,20 @@ +use std_shims::{sync::OnceLock, vec::Vec}; + +use curve25519_dalek::{scalar::Scalar, edwards::EdwardsPoint}; + +use monero_generators::hash_to_point; +use monero_primitives::{keccak256, keccak256_to_scalar}; + +// Monero starts BP+ transcripts with the following constant. +static TRANSCRIPT_CELL: OnceLock<[u8; 32]> = OnceLock::new(); +pub(crate) fn TRANSCRIPT() -> [u8; 32] { + // Why this uses a hash_to_point is completely unknown. + *TRANSCRIPT_CELL + .get_or_init(|| hash_to_point(keccak256(b"bulletproof_plus_transcript")).compress().to_bytes()) +} + +pub(crate) fn initial_transcript(commitments: core::slice::Iter<'_, EdwardsPoint>) -> Scalar { + let commitments_hash = + keccak256_to_scalar(commitments.flat_map(|V| V.compress().to_bytes()).collect::>()); + keccak256_to_scalar([TRANSCRIPT().as_ref(), &commitments_hash.to_bytes()].concat()) +} diff --git a/coins/monero/src/ringct/bulletproofs/plus/weighted_inner_product.rs b/networks/monero/ringct/bulletproofs/src/plus/weighted_inner_product.rs similarity index 62% rename from coins/monero/src/ringct/bulletproofs/plus/weighted_inner_product.rs rename to networks/monero/ringct/bulletproofs/src/plus/weighted_inner_product.rs index 7cb9a4df..4c838840 100644 --- a/coins/monero/src/ringct/bulletproofs/plus/weighted_inner_product.rs +++ b/networks/monero/ringct/bulletproofs/src/plus/weighted_inner_product.rs @@ -1,24 +1,21 @@ -use std_shims::vec::Vec; +use std_shims::{vec, vec::Vec}; use rand_core::{RngCore, CryptoRng}; - use zeroize::{Zeroize, ZeroizeOnDrop}; -use multiexp::{BatchVerifier, multiexp, multiexp_vartime}; -use group::{ - ff::{Field, PrimeField}, - GroupEncoding, -}; -use dalek_ff_group::{Scalar, EdwardsPoint}; +use curve25519_dalek::{scalar::Scalar, edwards::EdwardsPoint}; -use crate::ringct::bulletproofs::plus::{ - ScalarVector, PointVector, GeneratorsList, Generators, padded_pow_of_2, transcript::*, +use monero_primitives::{INV_EIGHT, keccak256_to_scalar}; +use crate::{ + core::{multiexp, multiexp_vartime, challenge_products}, + batch_verifier::BulletproofsPlusBatchVerifier, + plus::{ScalarVector, PointVector, GeneratorsList, BpPlusGenerators, padded_pow_of_2}, }; // Figure 1 of the Bulletproofs+ paper #[derive(Clone, Debug)] pub(crate) struct WipStatement { - generators: Generators, + generators: BpPlusGenerators, P: EdwardsPoint, y: ScalarVector, } @@ -68,7 +65,7 @@ pub(crate) struct WipProof { } impl WipStatement { - pub(crate) fn new(generators: Generators, P: EdwardsPoint, y: Scalar) -> Self { + pub(crate) fn new(generators: BpPlusGenerators, P: EdwardsPoint, y: Scalar) -> Self { debug_assert_eq!(generators.len(), padded_pow_of_2(generators.len())); // y ** n @@ -82,16 +79,26 @@ impl WipStatement { } fn transcript_L_R(transcript: &mut Scalar, L: EdwardsPoint, R: EdwardsPoint) -> Scalar { - let e = hash_to_scalar( - &[transcript.to_repr().as_ref(), L.to_bytes().as_ref(), R.to_bytes().as_ref()].concat(), + let e = keccak256_to_scalar( + [ + transcript.to_bytes().as_ref(), + L.compress().to_bytes().as_ref(), + R.compress().to_bytes().as_ref(), + ] + .concat(), ); *transcript = e; e } fn transcript_A_B(transcript: &mut Scalar, A: EdwardsPoint, B: EdwardsPoint) -> Scalar { - let e = hash_to_scalar( - &[transcript.to_repr().as_ref(), A.to_bytes().as_ref(), B.to_bytes().as_ref()].concat(), + let e = keccak256_to_scalar( + [ + transcript.to_bytes().as_ref(), + A.compress().to_bytes().as_ref(), + B.compress().to_bytes().as_ref(), + ] + .concat(), ); *transcript = e; e @@ -100,9 +107,6 @@ impl WipStatement { // Prover's variant of the shared code block to calculate G/H/P when n > 1 // Returns each permutation of G/H since the prover needs to do operation on each permutation // P is dropped as it's unused in the prover's path - // TODO: It'd still probably be faster to keep in terms of the original generators, both between - // the reduced amount of group operations and the potential tabling of the generators under - // multiexp #[allow(clippy::too_many_arguments)] fn next_G_H( transcript: &mut Scalar, @@ -119,7 +123,7 @@ impl WipStatement { debug_assert_eq!(g_bold1.len(), h_bold1.len()); let e = Self::transcript_L_R(transcript, L, R); - let inv_e = e.invert().unwrap(); + let inv_e = e.invert(); // This vartime is safe as all of these arguments are public let mut new_g_bold = Vec::with_capacity(g_bold1.len()); @@ -133,57 +137,12 @@ impl WipStatement { new_h_bold.push(multiexp_vartime(&[(e, h_bold.0), (inv_e, h_bold.1)])); } - let e_square = e.square(); - let inv_e_square = inv_e.square(); + let e_square = e * e; + let inv_e_square = inv_e * inv_e; (e, inv_e, e_square, inv_e_square, PointVector(new_g_bold), PointVector(new_h_bold)) } - /* - This has room for optimization worth investigating further. It currently takes - an iterative approach. It can be optimized further via divide and conquer. - - Assume there are 4 challenges. - - Iterative approach (current): - 1. Do the optimal multiplications across challenge column 0 and 1. - 2. Do the optimal multiplications across that result and column 2. - 3. Do the optimal multiplications across that result and column 3. - - Divide and conquer (worth investigating further): - 1. Do the optimal multiplications across challenge column 0 and 1. - 2. Do the optimal multiplications across challenge column 2 and 3. - 3. Multiply both results together. - - When there are 4 challenges (n=16), the iterative approach does 28 multiplications - versus divide and conquer's 24. - */ - fn challenge_products(challenges: &[(Scalar, Scalar)]) -> Vec { - let mut products = vec![Scalar::ONE; 1 << challenges.len()]; - - if !challenges.is_empty() { - products[0] = challenges[0].1; - products[1] = challenges[0].0; - - for (j, challenge) in challenges.iter().enumerate().skip(1) { - let mut slots = (1 << (j + 1)) - 1; - while slots > 0 { - products[slots] = products[slots / 2] * challenge.0; - products[slots - 1] = products[slots / 2] * challenge.1; - - slots = slots.saturating_sub(2); - } - } - - // Sanity check since if the above failed to populate, it'd be critical - for product in &products { - debug_assert!(!bool::from(product.is_zero())); - } - } - - products - } - pub(crate) fn prove( self, rng: &mut R, @@ -197,16 +156,27 @@ impl WipStatement { if generators.len() != witness.a.len() { return None; } - let (g, h) = (Generators::g(), Generators::h()); + let (g, h) = (BpPlusGenerators::g(), BpPlusGenerators::h()); let mut g_bold = vec![]; let mut h_bold = vec![]; for i in 0 .. generators.len() { - g_bold.push(generators.generator(GeneratorsList::GBold1, i)); - h_bold.push(generators.generator(GeneratorsList::HBold1, i)); + g_bold.push(generators.generator(GeneratorsList::GBold, i)); + h_bold.push(generators.generator(GeneratorsList::HBold, i)); } let mut g_bold = PointVector(g_bold); let mut h_bold = PointVector(h_bold); + let mut y_inv = { + let mut i = 1; + let mut to_invert = vec![]; + while i < g_bold.len() { + to_invert.push(y[i - 1]); + i *= 2; + } + Scalar::batch_invert(&mut to_invert); + to_invert + }; + // Check P has the expected relationship #[cfg(debug_assertions)] { @@ -260,8 +230,7 @@ impl WipStatement { let c_l = a1.clone().weighted_inner_product(&b2, &y); let c_r = (a2.clone() * y_n_hat).weighted_inner_product(&b1, &y); - // TODO: Calculate these with a batch inversion - let y_inv_n_hat = y_n_hat.invert().unwrap(); + let y_inv_n_hat = y_inv.pop().unwrap(); let mut L_terms = (a1.clone() * y_inv_n_hat) .0 @@ -271,7 +240,7 @@ impl WipStatement { .collect::>(); L_terms.push((c_l, g)); L_terms.push((d_l, h)); - let L = multiexp(&L_terms) * Scalar(crate::INV_EIGHT()); + let L = multiexp(&L_terms) * INV_EIGHT(); L_vec.push(L); L_terms.zeroize(); @@ -283,7 +252,7 @@ impl WipStatement { .collect::>(); R_terms.push((c_r, g)); R_terms.push((d_r, h)); - let R = multiexp(&R_terms) * Scalar(crate::INV_EIGHT()); + let R = multiexp(&R_terms) * INV_EIGHT(); R_vec.push(R); R_terms.zeroize(); @@ -316,33 +285,32 @@ impl WipStatement { let mut A_terms = vec![(r, g_bold[0]), (s, h_bold[0]), ((ry * b[0]) + (s * y[0] * a[0]), g), (delta, h)]; - let A = multiexp(&A_terms) * Scalar(crate::INV_EIGHT()); + let A = multiexp(&A_terms) * INV_EIGHT(); A_terms.zeroize(); let mut B_terms = vec![(ry * s, g), (eta, h)]; - let B = multiexp(&B_terms) * Scalar(crate::INV_EIGHT()); + let B = multiexp(&B_terms) * INV_EIGHT(); B_terms.zeroize(); let e = Self::transcript_A_B(&mut transcript, A, B); let r_answer = r + (a[0] * e); let s_answer = s + (b[0] * e); - let delta_answer = eta + (delta * e) + (alpha * e.square()); + let delta_answer = eta + (delta * e) + (alpha * (e * e)); Some(WipProof { L: L_vec, R: R_vec, A, B, r_answer, s_answer, delta_answer }) } - pub(crate) fn verify( + pub(crate) fn verify( self, rng: &mut R, - verifier: &mut BatchVerifier, - id: Id, + verifier: &mut BulletproofsPlusBatchVerifier, mut transcript: Scalar, mut proof: WipProof, ) -> bool { - let WipStatement { generators, P, y } = self; + let verifier_weight = Scalar::random(rng); - let (g, h) = (Generators::g(), Generators::h()); + let WipStatement { generators, P, y } = self; // Verify the L/R lengths { @@ -359,7 +327,7 @@ impl WipStatement { } let inv_y = { - let inv_y = y[0].invert().unwrap(); + let inv_y = y[0].invert(); let mut res = Vec::with_capacity(y.len()); res.push(inv_y); while res.len() < y.len() { @@ -368,51 +336,49 @@ impl WipStatement { res }; - let mut P_terms = vec![(Scalar::ONE, P)]; - P_terms.reserve(6 + (2 * generators.len()) + proof.L.len()); - - let mut challenges = Vec::with_capacity(proof.L.len()); - let product_cache = { - let mut es = Vec::with_capacity(proof.L.len()); - for (L, R) in proof.L.iter_mut().zip(proof.R.iter_mut()) { - es.push(Self::transcript_L_R(&mut transcript, *L, *R)); - *L = L.mul_by_cofactor(); - *R = R.mul_by_cofactor(); - } - - let mut inv_es = es.clone(); - let mut scratch = vec![Scalar::ZERO; es.len()]; - group::ff::BatchInverter::invert_with_external_scratch(&mut inv_es, &mut scratch); - drop(scratch); - - debug_assert_eq!(es.len(), inv_es.len()); - debug_assert_eq!(es.len(), proof.L.len()); - debug_assert_eq!(es.len(), proof.R.len()); - for ((e, inv_e), (L, R)) in - es.drain(..).zip(inv_es.drain(..)).zip(proof.L.iter().zip(proof.R.iter())) - { - debug_assert_eq!(e.invert().unwrap(), inv_e); - - challenges.push((e, inv_e)); - - let e_square = e.square(); - let inv_e_square = inv_e.square(); - P_terms.push((e_square, *L)); - P_terms.push((inv_e_square, *R)); - } - - Self::challenge_products(&challenges) - }; + let mut e_is = Vec::with_capacity(proof.L.len()); + for (L, R) in proof.L.iter_mut().zip(proof.R.iter_mut()) { + e_is.push(Self::transcript_L_R(&mut transcript, *L, *R)); + *L = L.mul_by_cofactor(); + *R = R.mul_by_cofactor(); + } let e = Self::transcript_A_B(&mut transcript, proof.A, proof.B); proof.A = proof.A.mul_by_cofactor(); proof.B = proof.B.mul_by_cofactor(); - let neg_e_square = -e.square(); + let neg_e_square = verifier_weight * -(e * e); - let mut multiexp = P_terms; - multiexp.reserve(4 + (2 * generators.len())); - for (scalar, _) in &mut multiexp { - *scalar *= neg_e_square; + verifier.0.other.push((neg_e_square, P)); + + let mut challenges = Vec::with_capacity(proof.L.len()); + let product_cache = { + let mut inv_e_is = e_is.clone(); + Scalar::batch_invert(&mut inv_e_is); + + debug_assert_eq!(e_is.len(), inv_e_is.len()); + debug_assert_eq!(e_is.len(), proof.L.len()); + debug_assert_eq!(e_is.len(), proof.R.len()); + for ((e_i, inv_e_i), (L, R)) in + e_is.drain(..).zip(inv_e_is.drain(..)).zip(proof.L.iter().zip(proof.R.iter())) + { + debug_assert_eq!(e_i.invert(), inv_e_i); + + challenges.push((e_i, inv_e_i)); + + let e_i_square = e_i * e_i; + let inv_e_i_square = inv_e_i * inv_e_i; + verifier.0.other.push((neg_e_square * e_i_square, *L)); + verifier.0.other.push((neg_e_square * inv_e_i_square, *R)); + } + + challenge_products(&challenges) + }; + + while verifier.0.g_bold.len() < generators.len() { + verifier.0.g_bold.push(Scalar::ZERO); + } + while verifier.0.h_bold.len() < generators.len() { + verifier.0.h_bold.push(Scalar::ZERO); } let re = proof.r_answer * e; @@ -421,23 +387,18 @@ impl WipStatement { if i > 0 { scalar *= inv_y[i - 1]; } - multiexp.push((scalar, generators.generator(GeneratorsList::GBold1, i))); + verifier.0.g_bold[i] += verifier_weight * scalar; } let se = proof.s_answer * e; for i in 0 .. generators.len() { - multiexp.push(( - se * product_cache[product_cache.len() - 1 - i], - generators.generator(GeneratorsList::HBold1, i), - )); + verifier.0.h_bold[i] += verifier_weight * (se * product_cache[product_cache.len() - 1 - i]); } - multiexp.push((-e, proof.A)); - multiexp.push((proof.r_answer * y[0] * proof.s_answer, g)); - multiexp.push((proof.delta_answer, h)); - multiexp.push((-Scalar::ONE, proof.B)); - - verifier.queue(rng, id, multiexp); + verifier.0.other.push((verifier_weight * -e, proof.A)); + verifier.0.g += verifier_weight * (proof.r_answer * y[0] * proof.s_answer); + verifier.0.h += verifier_weight * proof.delta_answer; + verifier.0.other.push((-verifier_weight, proof.B)); true } diff --git a/coins/monero/src/ringct/bulletproofs/plus/point_vector.rs b/networks/monero/ringct/bulletproofs/src/point_vector.rs similarity index 71% rename from coins/monero/src/ringct/bulletproofs/plus/point_vector.rs rename to networks/monero/ringct/bulletproofs/src/point_vector.rs index ac753a01..c2635038 100644 --- a/coins/monero/src/ringct/bulletproofs/plus/point_vector.rs +++ b/networks/monero/ringct/bulletproofs/src/point_vector.rs @@ -1,16 +1,16 @@ use core::ops::{Index, IndexMut}; use std_shims::vec::Vec; -use zeroize::{Zeroize, ZeroizeOnDrop}; +use zeroize::Zeroize; -use dalek_ff_group::EdwardsPoint; +use curve25519_dalek::edwards::EdwardsPoint; + +use crate::scalar_vector::ScalarVector; #[cfg(test)] -use multiexp::multiexp; -#[cfg(test)] -use crate::ringct::bulletproofs::plus::ScalarVector; +use crate::core::multiexp; -#[derive(Clone, PartialEq, Eq, Debug, Zeroize, ZeroizeOnDrop)] +#[derive(Clone, PartialEq, Eq, Debug, Zeroize)] pub(crate) struct PointVector(pub(crate) Vec); impl Index for PointVector { @@ -27,6 +27,15 @@ impl IndexMut for PointVector { } impl PointVector { + pub(crate) fn mul_vec(&self, vector: &ScalarVector) -> Self { + assert_eq!(self.len(), vector.len()); + let mut res = self.clone(); + for (i, val) in res.0.iter_mut().enumerate() { + *val *= vector.0[i]; + } + res + } + #[cfg(test)] pub(crate) fn multiexp(&self, vector: &ScalarVector) -> EdwardsPoint { debug_assert_eq!(self.len(), vector.len()); diff --git a/coins/monero/src/ringct/bulletproofs/scalar_vector.rs b/networks/monero/ringct/bulletproofs/src/scalar_vector.rs similarity index 96% rename from coins/monero/src/ringct/bulletproofs/scalar_vector.rs rename to networks/monero/ringct/bulletproofs/src/scalar_vector.rs index e6288367..ae723a42 100644 --- a/coins/monero/src/ringct/bulletproofs/scalar_vector.rs +++ b/networks/monero/ringct/bulletproofs/src/scalar_vector.rs @@ -2,13 +2,13 @@ use core::{ borrow::Borrow, ops::{Index, IndexMut, Add, Sub, Mul}, }; -use std_shims::vec::Vec; +use std_shims::{vec, vec::Vec}; use zeroize::{Zeroize, ZeroizeOnDrop}; -use group::ff::Field; -use dalek_ff_group::{Scalar, EdwardsPoint}; -use multiexp::multiexp; +use curve25519_dalek::{scalar::Scalar, edwards::EdwardsPoint}; + +use crate::core::multiexp; #[derive(Clone, PartialEq, Eq, Debug, Zeroize, ZeroizeOnDrop)] pub(crate) struct ScalarVector(pub(crate) Vec); diff --git a/networks/monero/ringct/bulletproofs/src/tests/mod.rs b/networks/monero/ringct/bulletproofs/src/tests/mod.rs new file mode 100644 index 00000000..fa4c8939 --- /dev/null +++ b/networks/monero/ringct/bulletproofs/src/tests/mod.rs @@ -0,0 +1,56 @@ +use rand_core::{RngCore, OsRng}; + +use curve25519_dalek::scalar::Scalar; + +use monero_primitives::Commitment; +use crate::{batch_verifier::BatchVerifier, Bulletproof, BulletproofError}; + +mod original; +mod plus; + +macro_rules! bulletproofs_tests { + ($name: ident, $max: ident, $plus: literal) => { + #[test] + fn $name() { + // Create Bulletproofs for all possible output quantities + let mut verifier = BatchVerifier::new(); + for i in 1 ..= 16 { + let commitments = (1 ..= i) + .map(|_| Commitment::new(Scalar::random(&mut OsRng), OsRng.next_u64())) + .collect::>(); + + let bp = if $plus { + Bulletproof::prove_plus(&mut OsRng, commitments.clone()).unwrap() + } else { + Bulletproof::prove(&mut OsRng, commitments.clone()).unwrap() + }; + + let commitments = commitments.iter().map(Commitment::calculate).collect::>(); + assert!(bp.verify(&mut OsRng, &commitments)); + assert!(bp.batch_verify(&mut OsRng, &mut verifier, &commitments)); + } + assert!(verifier.verify()); + } + + #[test] + fn $max() { + // Check Bulletproofs errors if we try to prove for too many outputs + let mut commitments = vec![]; + for _ in 0 .. 17 { + commitments.push(Commitment::new(Scalar::ZERO, 0)); + } + assert_eq!( + (if $plus { + Bulletproof::prove_plus(&mut OsRng, commitments) + } else { + Bulletproof::prove(&mut OsRng, commitments) + }) + .unwrap_err(), + BulletproofError::TooManyCommitments, + ); + } + }; +} + +bulletproofs_tests!(bulletproofs, bulletproofs_max, false); +bulletproofs_tests!(bulletproofs_plus, bulletproofs_plus_max, true); diff --git a/networks/monero/ringct/bulletproofs/src/tests/original/inner_product.rs b/networks/monero/ringct/bulletproofs/src/tests/original/inner_product.rs new file mode 100644 index 00000000..98aa842f --- /dev/null +++ b/networks/monero/ringct/bulletproofs/src/tests/original/inner_product.rs @@ -0,0 +1,75 @@ +// The inner product relation is P = sum(g_bold * a, h_bold * b, g * (a * b)) + +use rand_core::OsRng; + +use curve25519_dalek::Scalar; + +use monero_generators::H; + +use crate::{ + scalar_vector::ScalarVector, + point_vector::PointVector, + original::{ + GENERATORS, + inner_product::{IpStatement, IpWitness}, + }, + BulletproofsBatchVerifier, +}; + +#[test] +fn test_zero_inner_product() { + let statement = + IpStatement::new_without_P_transcript(ScalarVector(vec![Scalar::ONE; 1]), Scalar::ONE); + let witness = IpWitness::new(ScalarVector::new(1), ScalarVector::new(1)).unwrap(); + + let transcript = Scalar::random(&mut OsRng); + let proof = statement.clone().prove(transcript, witness).unwrap(); + + let mut verifier = BulletproofsBatchVerifier::default(); + verifier.0.g_bold = vec![Scalar::ZERO; 1]; + verifier.0.h_bold = vec![Scalar::ZERO; 1]; + statement.verify(&mut verifier, 1, transcript, Scalar::random(&mut OsRng), proof).unwrap(); + assert!(verifier.verify()); +} + +#[test] +fn test_inner_product() { + // P = sum(g_bold * a, h_bold * b, g * u * ) + let generators = GENERATORS(); + let mut verifier = BulletproofsBatchVerifier::default(); + verifier.0.g_bold = vec![Scalar::ZERO; 32]; + verifier.0.h_bold = vec![Scalar::ZERO; 32]; + for i in [1, 2, 4, 8, 16, 32] { + let g = H(); + let mut g_bold = vec![]; + let mut h_bold = vec![]; + for i in 0 .. i { + g_bold.push(generators.G[i]); + h_bold.push(generators.H[i]); + } + let g_bold = PointVector(g_bold); + let h_bold = PointVector(h_bold); + + let mut a = ScalarVector::new(i); + let mut b = ScalarVector::new(i); + + for i in 0 .. i { + a[i] = Scalar::random(&mut OsRng); + b[i] = Scalar::random(&mut OsRng); + } + + let P = g_bold.multiexp(&a) + h_bold.multiexp(&b) + (g * a.clone().inner_product(&b)); + + let statement = + IpStatement::new_without_P_transcript(ScalarVector(vec![Scalar::ONE; i]), Scalar::ONE); + let witness = IpWitness::new(a, b).unwrap(); + + let transcript = Scalar::random(&mut OsRng); + let proof = statement.clone().prove(transcript, witness).unwrap(); + + let weight = Scalar::random(&mut OsRng); + verifier.0.other.push((weight, P)); + statement.verify(&mut verifier, i, transcript, weight, proof).unwrap(); + } + assert!(verifier.verify()); +} diff --git a/networks/monero/ringct/bulletproofs/src/tests/original/mod.rs b/networks/monero/ringct/bulletproofs/src/tests/original/mod.rs new file mode 100644 index 00000000..c0010b4f --- /dev/null +++ b/networks/monero/ringct/bulletproofs/src/tests/original/mod.rs @@ -0,0 +1,62 @@ +use hex_literal::hex; +use rand_core::OsRng; + +use curve25519_dalek::scalar::Scalar; + +use monero_io::decompress_point; + +use crate::{ + original::{IpProof, AggregateRangeProof as OriginalProof}, + Bulletproof, +}; + +mod inner_product; + +#[test] +fn bulletproofs_vector() { + let scalar = |scalar| Scalar::from_canonical_bytes(scalar).unwrap(); + let point = |point| decompress_point(point).unwrap(); + + // Generated from Monero + assert!(Bulletproof::Original(OriginalProof { + A: point(hex!("ef32c0b9551b804decdcb107eb22aa715b7ce259bf3c5cac20e24dfa6b28ac71")), + S: point(hex!("e1285960861783574ee2b689ae53622834eb0b035d6943103f960cd23e063fa0")), + T1: point(hex!("4ea07735f184ba159d0e0eb662bac8cde3eb7d39f31e567b0fbda3aa23fe5620")), + T2: point(hex!("b8390aa4b60b255630d40e592f55ec6b7ab5e3a96bfcdcd6f1cd1d2fc95f441e")), + tau_x: scalar(hex!("5957dba8ea9afb23d6e81cc048a92f2d502c10c749dc1b2bd148ae8d41ec7107")), + mu: scalar(hex!("923023b234c2e64774b820b4961f7181f6c1dc152c438643e5a25b0bf271bc02")), + ip: IpProof { + L: vec![ + point(hex!("c45f656316b9ebf9d357fb6a9f85b5f09e0b991dd50a6e0ae9b02de3946c9d99")), + point(hex!("9304d2bf0f27183a2acc58cc755a0348da11bd345485fda41b872fee89e72aac")), + point(hex!("1bb8b71925d155dd9569f64129ea049d6149fdc4e7a42a86d9478801d922129b")), + point(hex!("5756a7bf887aa72b9a952f92f47182122e7b19d89e5dd434c747492b00e1c6b7")), + point(hex!("6e497c910d102592830555356af5ff8340e8d141e3fb60ea24cfa587e964f07d")), + point(hex!("f4fa3898e7b08e039183d444f3d55040f3c790ed806cb314de49f3068bdbb218")), + point(hex!("0bbc37597c3ead517a3841e159c8b7b79a5ceaee24b2a9a20350127aab428713")), + ], + R: vec![ + point(hex!("609420ba1702781692e84accfd225adb3d077aedc3cf8125563400466b52dbd9")), + point(hex!("fb4e1d079e7a2b0ec14f7e2a3943bf50b6d60bc346a54fcf562fb234b342abf8")), + point(hex!("6ae3ac97289c48ce95b9c557289e82a34932055f7f5e32720139824fe81b12e5")), + point(hex!("d071cc2ffbdab2d840326ad15f68c01da6482271cae3cf644670d1632f29a15c")), + point(hex!("e52a1754b95e1060589ba7ce0c43d0060820ebfc0d49dc52884bc3c65ad18af5")), + point(hex!("41573b06140108539957df71aceb4b1816d2409ce896659aa5c86f037ca5e851")), + point(hex!("a65970b2cc3c7b08b2b5b739dbc8e71e646783c41c625e2a5b1535e3d2e0f742")), + ], + a: scalar(hex!("0077c5383dea44d3cd1bc74849376bd60679612dc4b945255822457fa0c0a209")), + b: scalar(hex!("fe80cf5756473482581e1d38644007793ddc66fdeb9404ec1689a907e4863302")), + }, + t_hat: scalar(hex!("40dfb08e09249040df997851db311bd6827c26e87d6f0f332c55be8eef10e603")) + }) + .verify( + &mut OsRng, + &[ + // For some reason, these vectors are * INV_EIGHT + point(hex!("8e8f23f315edae4f6c2f948d9a861e0ae32d356b933cd11d2f0e031ac744c41f")) + .mul_by_cofactor(), + point(hex!("2829cbd025aa54cd6e1b59a032564f22f0b2e5627f7f2c4297f90da438b5510f")) + .mul_by_cofactor(), + ] + )); +} diff --git a/networks/monero/ringct/bulletproofs/src/tests/plus/aggregate_range_proof.rs b/networks/monero/ringct/bulletproofs/src/tests/plus/aggregate_range_proof.rs new file mode 100644 index 00000000..ba5d0543 --- /dev/null +++ b/networks/monero/ringct/bulletproofs/src/tests/plus/aggregate_range_proof.rs @@ -0,0 +1,28 @@ +use rand_core::{RngCore, OsRng}; + +use curve25519_dalek::Scalar; + +use monero_primitives::Commitment; + +use crate::{ + batch_verifier::BulletproofsPlusBatchVerifier, + plus::aggregate_range_proof::{AggregateRangeStatement, AggregateRangeWitness}, +}; + +#[test] +fn test_aggregate_range_proof() { + let mut verifier = BulletproofsPlusBatchVerifier::default(); + for m in 1 ..= 16 { + let mut commitments = vec![]; + for _ in 0 .. m { + commitments.push(Commitment::new(Scalar::random(&mut OsRng), OsRng.next_u64())); + } + let commitment_points = commitments.iter().map(Commitment::calculate).collect::>(); + let statement = AggregateRangeStatement::new(&commitment_points).unwrap(); + let witness = AggregateRangeWitness::new(commitments).unwrap(); + + let proof = statement.clone().prove(&mut OsRng, &witness).unwrap(); + statement.verify(&mut OsRng, &mut verifier, proof); + } + assert!(verifier.verify()); +} diff --git a/coins/monero/src/tests/bulletproofs/plus/mod.rs b/networks/monero/ringct/bulletproofs/src/tests/plus/mod.rs similarity index 100% rename from coins/monero/src/tests/bulletproofs/plus/mod.rs rename to networks/monero/ringct/bulletproofs/src/tests/plus/mod.rs diff --git a/coins/monero/src/tests/bulletproofs/plus/weighted_inner_product.rs b/networks/monero/ringct/bulletproofs/src/tests/plus/weighted_inner_product.rs similarity index 66% rename from coins/monero/src/tests/bulletproofs/plus/weighted_inner_product.rs rename to networks/monero/ringct/bulletproofs/src/tests/plus/weighted_inner_product.rs index b0890cf8..eaa00cd3 100644 --- a/coins/monero/src/tests/bulletproofs/plus/weighted_inner_product.rs +++ b/networks/monero/ringct/bulletproofs/src/tests/plus/weighted_inner_product.rs @@ -2,13 +2,14 @@ use rand_core::OsRng; -use multiexp::BatchVerifier; -use group::{ff::Field, Group}; -use dalek_ff_group::{Scalar, EdwardsPoint}; +use curve25519_dalek::{traits::Identity, scalar::Scalar, edwards::EdwardsPoint}; -use crate::ringct::bulletproofs::plus::{ - ScalarVector, PointVector, GeneratorsList, Generators, - weighted_inner_product::{WipStatement, WipWitness}, +use crate::{ + batch_verifier::BulletproofsPlusBatchVerifier, + plus::{ + ScalarVector, PointVector, GeneratorsList, BpPlusGenerators, + weighted_inner_product::{WipStatement, WipWitness}, + }, }; #[test] @@ -17,33 +18,33 @@ fn test_zero_weighted_inner_product() { let P = EdwardsPoint::identity(); let y = Scalar::random(&mut OsRng); - let generators = Generators::new().reduce(1); + let generators = BpPlusGenerators::new().reduce(1); let statement = WipStatement::new(generators, P, y); let witness = WipWitness::new(ScalarVector::new(1), ScalarVector::new(1), Scalar::ZERO).unwrap(); let transcript = Scalar::random(&mut OsRng); let proof = statement.clone().prove(&mut OsRng, transcript, &witness).unwrap(); - let mut verifier = BatchVerifier::new(1); - statement.verify(&mut OsRng, &mut verifier, (), transcript, proof); - assert!(verifier.verify_vartime()); + let mut verifier = BulletproofsPlusBatchVerifier::default(); + statement.verify(&mut OsRng, &mut verifier, transcript, proof); + assert!(verifier.verify()); } #[test] fn test_weighted_inner_product() { // P = sum(g_bold * a, h_bold * b, g * (a * y * b), h * alpha) - let mut verifier = BatchVerifier::new(6); - let generators = Generators::new(); + let mut verifier = BulletproofsPlusBatchVerifier::default(); + let generators = BpPlusGenerators::new(); for i in [1, 2, 4, 8, 16, 32] { let generators = generators.reduce(i); - let g = Generators::g(); - let h = Generators::h(); + let g = BpPlusGenerators::g(); + let h = BpPlusGenerators::h(); assert_eq!(generators.len(), i); let mut g_bold = vec![]; let mut h_bold = vec![]; for i in 0 .. i { - g_bold.push(generators.generator(GeneratorsList::GBold1, i)); - h_bold.push(generators.generator(GeneratorsList::HBold1, i)); + g_bold.push(generators.generator(GeneratorsList::GBold, i)); + h_bold.push(generators.generator(GeneratorsList::HBold, i)); } let g_bold = PointVector(g_bold); let h_bold = PointVector(h_bold); @@ -75,7 +76,7 @@ fn test_weighted_inner_product() { let transcript = Scalar::random(&mut OsRng); let proof = statement.clone().prove(&mut OsRng, transcript, &witness).unwrap(); - statement.verify(&mut OsRng, &mut verifier, (), transcript, proof); + statement.verify(&mut OsRng, &mut verifier, transcript, proof); } - assert!(verifier.verify_vartime()); + assert!(verifier.verify()); } diff --git a/networks/monero/ringct/clsag/Cargo.toml b/networks/monero/ringct/clsag/Cargo.toml new file mode 100644 index 00000000..27e100ee --- /dev/null +++ b/networks/monero/ringct/clsag/Cargo.toml @@ -0,0 +1,65 @@ +[package] +name = "monero-clsag" +version = "0.1.0" +description = "The CLSAG linkable ring signature, as defined by the Monero protocol" +license = "MIT" +repository = "https://github.com/serai-dex/serai/tree/develop/networks/monero/ringct/clsag" +authors = ["Luke Parker "] +edition = "2021" +rust-version = "1.79" + +[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 } + +thiserror = { version = "1", default-features = false, optional = true } + +rand_core = { version = "0.6", default-features = false } +zeroize = { version = "^1.5", default-features = false, features = ["zeroize_derive"] } +subtle = { version = "^2.4", default-features = false } + +# Cryptographic dependencies +curve25519-dalek = { version = "4", default-features = false, features = ["alloc", "zeroize"] } + +# Multisig dependencies +rand_chacha = { version = "0.3", default-features = false, optional = true } +transcript = { package = "flexible-transcript", path = "../../../../crypto/transcript", version = "0.3", default-features = false, features = ["recommended"], optional = true } +group = { version = "0.13", default-features = false, optional = true } +dalek-ff-group = { path = "../../../../crypto/dalek-ff-group", version = "0.4", default-features = false, optional = true } +frost = { package = "modular-frost", path = "../../../../crypto/frost", default-features = false, features = ["ed25519"], optional = true } + +# Other Monero dependencies +monero-io = { path = "../../io", version = "0.1", default-features = false } +monero-generators = { path = "../../generators", version = "0.4", default-features = false } +monero-primitives = { path = "../../primitives", version = "0.1", default-features = false } + +[dev-dependencies] +frost = { package = "modular-frost", path = "../../../../crypto/frost", default-features = false, features = ["ed25519", "tests"] } + +[features] +std = [ + "std-shims/std", + + "thiserror", + + "rand_core/std", + "zeroize/std", + "subtle/std", + + "rand_chacha?/std", + "transcript?/std", + "group?/alloc", + "dalek-ff-group?/std", + + "monero-io/std", + "monero-generators/std", + "monero-primitives/std", +] +multisig = ["rand_chacha", "transcript", "group", "dalek-ff-group", "frost", "std"] +default = ["std"] diff --git a/networks/monero/ringct/clsag/LICENSE b/networks/monero/ringct/clsag/LICENSE new file mode 100644 index 00000000..91d893c1 --- /dev/null +++ b/networks/monero/ringct/clsag/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022-2024 Luke Parker + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/networks/monero/ringct/clsag/README.md b/networks/monero/ringct/clsag/README.md new file mode 100644 index 00000000..4b90c86c --- /dev/null +++ b/networks/monero/ringct/clsag/README.md @@ -0,0 +1,15 @@ +# Monero CLSAG + +The CLSAG linkable ring signature, as defined by the Monero protocol. + +Additionally included is a FROST-inspired threshold multisignature algorithm. + +This library is usable under no-std when the `std` feature (on by default) is +disabled. + +### Cargo Features + +- `std` (on by default): Enables `std` (and with it, more efficient internal + implementations). +- `multisig`: Provides a FROST-inspired threshold multisignature algorithm for + use. diff --git a/coins/monero/src/ringct/clsag/mod.rs b/networks/monero/ringct/clsag/src/lib.rs similarity index 54% rename from coins/monero/src/ringct/clsag/mod.rs rename to networks/monero/ringct/clsag/src/lib.rs index 042d964a..0aab537b 100644 --- a/coins/monero/src/ringct/clsag/mod.rs +++ b/networks/monero/ringct/clsag/src/lib.rs @@ -1,7 +1,12 @@ +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![doc = include_str!("../README.md")] +#![deny(missing_docs)] +#![cfg_attr(not(feature = "std"), no_std)] #![allow(non_snake_case)] use core::ops::Deref; use std_shims::{ + vec, vec::Vec, io::{self, Read, Write}, }; @@ -18,64 +23,67 @@ use curve25519_dalek::{ edwards::{EdwardsPoint, VartimeEdwardsPrecomputation}, }; -use crate::{ - INV_EIGHT, BASEPOINT_PRECOMP, Commitment, random_scalar, hash_to_scalar, wallet::decoys::Decoys, - ringct::hash_to_point, serialize::*, -}; +use monero_io::*; +use monero_generators::hash_to_point; +use monero_primitives::{INV_EIGHT, G_PRECOMP, Commitment, Decoys, keccak256_to_scalar}; #[cfg(feature = "multisig")] mod multisig; #[cfg(feature = "multisig")] -pub use multisig::{ClsagDetails, ClsagAddendum, ClsagMultisig}; +pub use multisig::{ClsagMultisigMaskSender, ClsagAddendum, ClsagMultisig}; -/// Errors returned when CLSAG signing fails. +#[cfg(all(feature = "std", test))] +mod tests; + +/// Errors when working with CLSAGs. #[derive(Clone, Copy, PartialEq, Eq, Debug)] #[cfg_attr(feature = "std", derive(thiserror::Error))] pub enum ClsagError { - #[cfg_attr(feature = "std", error("internal error ({0})"))] - InternalError(&'static str), + /// The ring was invalid (such as being too small or too large). #[cfg_attr(feature = "std", error("invalid ring"))] InvalidRing, - #[cfg_attr(feature = "std", error("invalid ring member (member {0}, ring size {1})"))] - InvalidRingMember(u8, u8), + /// The discrete logarithm of the key, scaling G, wasn't equivalent to the signing ring member. + #[cfg_attr(feature = "std", error("invalid commitment"))] + InvalidKey, + /// The commitment opening provided did not match the ring member's. #[cfg_attr(feature = "std", error("invalid commitment"))] InvalidCommitment, + /// The key image was invalid (such as being identity or torsioned) #[cfg_attr(feature = "std", error("invalid key image"))] InvalidImage, + /// The `D` component was invalid. #[cfg_attr(feature = "std", error("invalid D"))] InvalidD, + /// The `s` vector was invalid. #[cfg_attr(feature = "std", error("invalid s"))] InvalidS, + /// The `c1` variable was invalid. #[cfg_attr(feature = "std", error("invalid c1"))] InvalidC1, } -/// Input being signed for. +/// Context on the input being signed for. #[derive(Clone, PartialEq, Eq, Debug, Zeroize, ZeroizeOnDrop)] -pub struct ClsagInput { - // The actual commitment for the true spend - pub(crate) commitment: Commitment, - // True spend index, offsets, and ring - pub(crate) decoys: Decoys, +pub struct ClsagContext { + // The opening for the commitment of the signing ring member + commitment: Commitment, + // Selected ring members' positions, signer index, and ring + decoys: Decoys, } -impl ClsagInput { - pub fn new(commitment: Commitment, decoys: Decoys) -> Result { - let n = decoys.len(); - if n > u8::MAX.into() { - Err(ClsagError::InternalError("max ring size in this library is u8 max"))?; - } - let n = u8::try_from(n).unwrap(); - if decoys.i >= n { - Err(ClsagError::InvalidRingMember(decoys.i, n))?; +impl ClsagContext { + /// Create a new context, as necessary for signing. + pub fn new(decoys: Decoys, commitment: Commitment) -> Result { + if decoys.len() > u8::MAX.into() { + Err(ClsagError::InvalidRing)?; } // Validate the commitment matches - if decoys.ring[usize::from(decoys.i)][1] != commitment.calculate() { + if decoys.signer_ring_members()[1] != commitment.calculate() { Err(ClsagError::InvalidCommitment)?; } - Ok(ClsagInput { commitment, decoys }) + Ok(ClsagContext { commitment, decoys }) } } @@ -86,6 +94,7 @@ enum Mode { } // Core of the CLSAG algorithm, applicable to both sign and verify with minimal differences +// // Said differences are covered via the above Mode fn core( ring: &[[EdwardsPoint; 2]], @@ -134,10 +143,10 @@ fn core( to_hash.extend(D_INV_EIGHT.compress().to_bytes()); to_hash.extend(pseudo_out.compress().to_bytes()); // mu_P with agg_0 - let mu_P = hash_to_scalar(&to_hash); + let mu_P = keccak256_to_scalar(&to_hash); // mu_C with agg_1 to_hash[PREFIX_AGG_0_LEN - 1] = b'1'; - let mu_C = hash_to_scalar(&to_hash); + let mu_C = keccak256_to_scalar(&to_hash); // Truncate it for the round transcript, altering the DST as needed to_hash.truncate(((2 * n) + 1) * 32); @@ -159,7 +168,7 @@ fn core( end = r + n; to_hash.extend(A.compress().to_bytes()); to_hash.extend(AH.compress().to_bytes()); - c = hash_to_scalar(&to_hash); + c = keccak256_to_scalar(&to_hash); } Mode::Verify(c1) => { @@ -181,11 +190,11 @@ fn core( EdwardsPoint::multiscalar_mul([s[i], c_p, c_c], [ED25519_BASEPOINT_POINT, P[i], C[i]]) } Mode::Verify(..) => { - BASEPOINT_PRECOMP().vartime_mixed_multiscalar_mul([s[i]], [c_p, c_c], [P[i], C[i]]) + G_PRECOMP().vartime_mixed_multiscalar_mul([s[i]], [c_p, c_c], [P[i], C[i]]) } }; - let PH = hash_to_point(&P[i]); + let PH = hash_to_point(P[i].compress().0); // (c_p * I) + (c_c * D) + (s_i * PH) let R = match A_c1 { @@ -198,7 +207,7 @@ fn core( to_hash.truncate(((2 * n) + 3) * 32); to_hash.extend(L.compress().to_bytes()); to_hash.extend(R.compress().to_bytes()); - c = hash_to_scalar(&to_hash); + c = keccak256_to_scalar(&to_hash); // This will only execute once and shouldn't need to be constant time. Making it constant time // removes the risk of branch prediction creating timing differences depending on ring index @@ -210,91 +219,142 @@ fn core( ((D_INV_EIGHT, c * mu_P, c * mu_C), c1) } -/// CLSAG signature, as used in Monero. +/// The CLSAG signature, as used in Monero. #[derive(Clone, PartialEq, Eq, Debug)] pub struct Clsag { + /// The difference of the commitment randomnesses, scaling the key image generator. pub D: EdwardsPoint, + /// The responses for each ring member. pub s: Vec, + /// The first challenge in the ring. pub c1: Scalar, } +struct ClsagSignCore { + incomplete_clsag: Clsag, + pseudo_out: EdwardsPoint, + key_challenge: Scalar, + challenged_mask: Scalar, +} + impl Clsag { // Sign core is the extension of core as needed for signing, yet is shared between single signer // and multisig, hence why it's still core - pub(crate) fn sign_core( + fn sign_core( rng: &mut R, I: &EdwardsPoint, - input: &ClsagInput, + input: &ClsagContext, mask: Scalar, msg: &[u8; 32], A: EdwardsPoint, AH: EdwardsPoint, - ) -> (Clsag, EdwardsPoint, Scalar, Scalar) { - let r: usize = input.decoys.i.into(); + ) -> ClsagSignCore { + let r: usize = input.decoys.signer_index().into(); let pseudo_out = Commitment::new(mask, input.commitment.amount).calculate(); - let z = input.commitment.mask - mask; + let mask_delta = input.commitment.mask - mask; - let H = hash_to_point(&input.decoys.ring[r][0]); - let D = H * z; - let mut s = Vec::with_capacity(input.decoys.ring.len()); - for _ in 0 .. input.decoys.ring.len() { - s.push(random_scalar(rng)); + let H = hash_to_point(input.decoys.ring()[r][0].compress().0); + let D = H * mask_delta; + let mut s = Vec::with_capacity(input.decoys.ring().len()); + for _ in 0 .. input.decoys.ring().len() { + s.push(Scalar::random(rng)); } - let ((D, p, c), c1) = - core(&input.decoys.ring, I, &pseudo_out, msg, &D, &s, &Mode::Sign(r, A, AH)); + let ((D, c_p, c_c), c1) = + core(input.decoys.ring(), I, &pseudo_out, msg, &D, &s, &Mode::Sign(r, A, AH)); - (Clsag { D, s, c1 }, pseudo_out, p, c * z) + ClsagSignCore { + incomplete_clsag: Clsag { D, s, c1 }, + pseudo_out, + key_challenge: c_p, + challenged_mask: c_c * mask_delta, + } } - /// Generate CLSAG signatures for the given inputs. - /// inputs is of the form (private key, key image, input). - /// sum_outputs is for the sum of the outputs' commitment masks. + /// Sign CLSAG signatures for the provided inputs. + /// + /// Monero ensures the rerandomized input commitments have the same value as the outputs by + /// checking `sum(rerandomized_input_commitments) - sum(output_commitments) == 0`. This requires + /// not only the amounts balance, yet also + /// `sum(input_commitment_masks) - sum(output_commitment_masks)`. + /// + /// Monero solves this by following the wallet protocol to determine each output commitment's + /// randomness, then using random masks for all but the last input. The last input is + /// rerandomized to the necessary mask for the equation to balance. + /// + /// Due to Monero having this behavior, it only makes sense to sign CLSAGs as a list, hence this + /// API being the way it is. + /// + /// `inputs` is of the form (discrete logarithm of the key, context). + /// + /// `sum_outputs` is for the sum of the output commitments' masks. pub fn sign( rng: &mut R, - mut inputs: Vec<(Zeroizing, EdwardsPoint, ClsagInput)>, + mut inputs: Vec<(Zeroizing, ClsagContext)>, sum_outputs: Scalar, msg: [u8; 32], - ) -> Vec<(Clsag, EdwardsPoint)> { + ) -> Result, ClsagError> { + // Create the key images + let mut key_image_generators = vec![]; + let mut key_images = vec![]; + for input in &inputs { + let key = input.1.decoys.signer_ring_members()[0]; + + // Check the key is consistent + if (ED25519_BASEPOINT_TABLE * input.0.deref()) != key { + Err(ClsagError::InvalidKey)?; + } + + let key_image_generator = hash_to_point(key.compress().0); + key_image_generators.push(key_image_generator); + key_images.push(key_image_generator * input.0.deref()); + } + let mut res = Vec::with_capacity(inputs.len()); let mut sum_pseudo_outs = Scalar::ZERO; for i in 0 .. inputs.len() { - let mut mask = random_scalar(rng); + let mask; + // If this is the last input, set the mask as described above if i == (inputs.len() - 1) { mask = sum_outputs - sum_pseudo_outs; } else { + mask = Scalar::random(rng); sum_pseudo_outs += mask; } - let mut nonce = Zeroizing::new(random_scalar(rng)); - let (mut clsag, pseudo_out, p, c) = Clsag::sign_core( - rng, - &inputs[i].1, - &inputs[i].2, - mask, - &msg, - nonce.deref() * ED25519_BASEPOINT_TABLE, - nonce.deref() * - hash_to_point(&inputs[i].2.decoys.ring[usize::from(inputs[i].2.decoys.i)][0]), - ); - // Effectively r - cx, except cx is (c_p x) + (c_c z), where z is the delta between a ring - // member's commitment and our input commitment (which will only have a known discrete log - // over G if the amounts cancel out) - clsag.s[usize::from(inputs[i].2.decoys.i)] = nonce.deref() - ((p * inputs[i].0.deref()) + c); + let mut nonce = Zeroizing::new(Scalar::random(rng)); + let ClsagSignCore { mut incomplete_clsag, pseudo_out, key_challenge, challenged_mask } = + Clsag::sign_core( + rng, + &key_images[i], + &inputs[i].1, + mask, + &msg, + nonce.deref() * ED25519_BASEPOINT_TABLE, + nonce.deref() * key_image_generators[i], + ); + // Effectively r - c x, except c x is (c_p x) + (c_c z), where z is the delta between the + // ring member's commitment and our pseudo-out commitment (which will only have a known + // discrete log over G if the amounts cancel out) + incomplete_clsag.s[usize::from(inputs[i].1.decoys.signer_index())] = + nonce.deref() - ((key_challenge * inputs[i].0.deref()) + challenged_mask); + let clsag = incomplete_clsag; + + // Zeroize private keys and nonces. inputs[i].0.zeroize(); nonce.zeroize(); debug_assert!(clsag - .verify(&inputs[i].2.decoys.ring, &inputs[i].1, &pseudo_out, &msg) + .verify(inputs[i].1.decoys.ring(), &key_images[i], &pseudo_out, &msg) .is_ok()); res.push((clsag, pseudo_out)); } - res + Ok(res) } - /// Verify the CLSAG signature against the given Transaction data. + /// Verify a CLSAG signature for the provided context. pub fn verify( &self, ring: &[[EdwardsPoint; 2]], @@ -302,15 +362,15 @@ impl Clsag { pseudo_out: &EdwardsPoint, msg: &[u8; 32], ) -> Result<(), ClsagError> { - // Preliminary checks. s, c1, and points must also be encoded canonically, which isn't checked - // here + // Preliminary checks + // s, c1, and points must also be encoded canonically, which is checked at time of decode if ring.is_empty() { Err(ClsagError::InvalidRing)?; } if ring.len() != self.s.len() { Err(ClsagError::InvalidS)?; } - if I.is_identity() { + if I.is_identity() || (!I.is_torsion_free()) { Err(ClsagError::InvalidImage)?; } @@ -326,16 +386,14 @@ impl Clsag { Ok(()) } - pub(crate) fn fee_weight(ring_len: usize) -> usize { - (ring_len * 32) + 32 + 32 - } - + /// Write a CLSAG. pub fn write(&self, w: &mut W) -> io::Result<()> { write_raw_vec(write_scalar, &self.s, w)?; w.write_all(&self.c1.to_bytes())?; write_point(&self.D, w) } + /// Read a CLSAG. pub fn read(decoys: usize, r: &mut R) -> io::Result { Ok(Clsag { s: read_raw_vec(read_scalar, decoys, r)?, c1: read_scalar(r)?, D: read_point(r)? }) } diff --git a/coins/monero/src/ringct/clsag/multisig.rs b/networks/monero/ringct/clsag/src/multisig.rs similarity index 61% rename from coins/monero/src/ringct/clsag/multisig.rs rename to networks/monero/ringct/clsag/src/multisig.rs index e9234979..bfbb8fc5 100644 --- a/coins/monero/src/ringct/clsag/multisig.rs +++ b/networks/monero/ringct/clsag/src/multisig.rs @@ -1,14 +1,14 @@ use core::{ops::Deref, fmt::Debug}; use std_shims::{ + sync::{Arc, Mutex}, io::{self, Read, Write}, collections::HashMap, }; -use std::sync::{Arc, RwLock}; use rand_core::{RngCore, CryptoRng, SeedableRng}; use rand_chacha::ChaCha20Rng; -use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing}; +use zeroize::{Zeroize, Zeroizing}; use curve25519_dalek::{scalar::Scalar, edwards::EdwardsPoint}; @@ -26,23 +26,22 @@ use frost::{ algorithm::{WriteAddendum, Algorithm}, }; -use crate::ringct::{ - hash_to_point, - clsag::{ClsagInput, Clsag}, -}; +use monero_generators::hash_to_point; -impl ClsagInput { +use crate::{ClsagContext, Clsag}; + +impl ClsagContext { fn transcript(&self, transcript: &mut T) { // Doesn't domain separate as this is considered part of the larger CLSAG proof // Ring index - transcript.append_message(b"real_spend", [self.decoys.i]); + transcript.append_message(b"signer_index", [self.decoys.signer_index()]); // Ring - for (i, pair) in self.decoys.ring.iter().enumerate() { - // Doesn't include global output indexes as CLSAG doesn't care and won't be affected by it + for (i, pair) in self.decoys.ring().iter().enumerate() { + // Doesn't include global output indexes as CLSAG doesn't care/won't be affected by it // They're just a unreliable reference to this data which will be included in the message - // if in use + // if somehow relevant transcript.append_message(b"member", [u8::try_from(i).expect("ring size exceeded 255")]); // This also transcripts the key image generator since it's derived from this key transcript.append_message(b"key", pair[0].compress().to_bytes()); @@ -50,33 +49,56 @@ impl ClsagInput { } // Doesn't include the commitment's parts as the above ring + index includes the commitment - // The only potential malleability would be if the G/H relationship is known breaking the + // The only potential malleability would be if the G/H relationship is known, breaking the // discrete log problem, which breaks everything already } } -/// CLSAG input and the mask to use for it. -#[derive(Clone, Debug, Zeroize, ZeroizeOnDrop)] -pub struct ClsagDetails { - input: ClsagInput, - mask: Scalar, +/// A channel to send the mask to use for the pseudo-out (rerandomized commitment) with. +/// +/// A mask must be sent along this channel before any preprocess addendums are handled. Breaking +/// this rule will cause a panic. +#[derive(Clone, Debug)] +pub struct ClsagMultisigMaskSender { + buf: Arc>>, } +#[derive(Clone, Debug)] +struct ClsagMultisigMaskReceiver { + buf: Arc>>, +} +impl ClsagMultisigMaskSender { + fn new() -> (ClsagMultisigMaskSender, ClsagMultisigMaskReceiver) { + let buf = Arc::new(Mutex::new(None)); + (ClsagMultisigMaskSender { buf: buf.clone() }, ClsagMultisigMaskReceiver { buf }) + } -impl ClsagDetails { - pub fn new(input: ClsagInput, mask: Scalar) -> ClsagDetails { - ClsagDetails { input, mask } + /// Send a mask to a CLSAG multisig instance. + pub fn send(self, mask: Scalar) { + *self.buf.lock() = Some(mask); + } +} +impl ClsagMultisigMaskReceiver { + fn recv(self) -> Scalar { + self.buf.lock().unwrap() } } -/// Addendum produced during the FROST signing process with relevant data. +/// Addendum produced during the signing process. #[derive(Clone, PartialEq, Eq, Zeroize, Debug)] pub struct ClsagAddendum { - pub(crate) key_image: dfg::EdwardsPoint, + key_image_share: dfg::EdwardsPoint, +} + +impl ClsagAddendum { + /// The key image share within this addendum. + pub fn key_image_share(&self) -> dfg::EdwardsPoint { + self.key_image_share + } } impl WriteAddendum for ClsagAddendum { fn write(&self, writer: &mut W) -> io::Result<()> { - writer.write_all(self.key_image.compress().to_bytes().as_ref()) + writer.write_all(self.key_image_share.compress().to_bytes().as_ref()) } } @@ -90,66 +112,83 @@ struct Interim { pseudo_out: EdwardsPoint, } -/// FROST algorithm for producing a CLSAG signature. +/// FROST-inspired algorithm for producing a CLSAG signature. +/// +/// Before this has its `process_addendum` called, a mask must be set. Else this will panic. +/// +/// The message signed is expected to be a 32-byte value. Per Monero, it's the keccak256 hash of +/// the transaction data which is signed. This will panic if the message is not a 32-byte value. #[allow(non_snake_case)] #[derive(Clone, Debug)] pub struct ClsagMultisig { transcript: RecommendedTranscript, - pub(crate) H: EdwardsPoint, + key_image_generator: EdwardsPoint, key_image_shares: HashMap<[u8; 32], dfg::EdwardsPoint>, image: Option, - details: Arc>>, + context: ClsagContext, + + mask_recv: Option, + mask: Option, msg: Option<[u8; 32]>, interim: Option, } impl ClsagMultisig { + /// Construct a new instance of multisignature CLSAG signing. pub fn new( transcript: RecommendedTranscript, - output_key: EdwardsPoint, - details: Arc>>, - ) -> ClsagMultisig { - ClsagMultisig { - transcript, + context: ClsagContext, + ) -> (ClsagMultisig, ClsagMultisigMaskSender) { + let (mask_send, mask_recv) = ClsagMultisigMaskSender::new(); + ( + ClsagMultisig { + transcript, - H: hash_to_point(&output_key), - key_image_shares: HashMap::new(), - image: None, + key_image_generator: hash_to_point(context.decoys.signer_ring_members()[0].compress().0), + key_image_shares: HashMap::new(), + image: None, - details, + context, - msg: None, - interim: None, - } + mask_recv: Some(mask_recv), + mask: None, + + msg: None, + interim: None, + }, + mask_send, + ) } - fn input(&self) -> ClsagInput { - (*self.details.read().unwrap()).as_ref().unwrap().input.clone() - } - - fn mask(&self) -> Scalar { - (*self.details.read().unwrap()).as_ref().unwrap().mask + /// The key image generator used by the signer. + pub fn key_image_generator(&self) -> EdwardsPoint { + self.key_image_generator } } impl Algorithm for ClsagMultisig { type Transcript = RecommendedTranscript; type Addendum = ClsagAddendum; + // We output the CLSAG and the key image, which requires an interactive protocol to obtain type Signature = (Clsag, EdwardsPoint); + // We need the nonce represented against both G and the key image generator fn nonces(&self) -> Vec> { - vec![vec![dfg::EdwardsPoint::generator(), dfg::EdwardsPoint(self.H)]] + vec![vec![dfg::EdwardsPoint::generator(), dfg::EdwardsPoint(self.key_image_generator)]] } + // We also publish our share of the key image fn preprocess_addendum( &mut self, _rng: &mut R, keys: &ThresholdKeys, ) -> ClsagAddendum { - ClsagAddendum { key_image: dfg::EdwardsPoint(self.H) * keys.secret_share().deref() } + ClsagAddendum { + key_image_share: dfg::EdwardsPoint(self.key_image_generator) * keys.secret_share().deref(), + } } fn read_addendum(&self, reader: &mut R) -> io::Result { @@ -163,7 +202,7 @@ impl Algorithm for ClsagMultisig { Err(io::Error::other("non-canonical key image"))?; } - Ok(ClsagAddendum { key_image: xH }) + Ok(ClsagAddendum { key_image_share: xH }) } fn process_addendum( @@ -175,21 +214,27 @@ impl Algorithm for ClsagMultisig { if self.image.is_none() { self.transcript.domain_separate(b"CLSAG"); // Transcript the ring - self.input().transcript(&mut self.transcript); + self.context.transcript(&mut self.transcript); + // Fetch the mask from the Mutex + // We set it to a variable to ensure our view of it is consistent + // It was this or a mpsc channel... std doesn't have oneshot :/ + self.mask = Some(self.mask_recv.take().unwrap().recv()); // Transcript the mask - self.transcript.append_message(b"mask", self.mask().to_bytes()); + self.transcript.append_message(b"mask", self.mask.expect("mask wasn't set").to_bytes()); // Init the image to the offset - self.image = Some(dfg::EdwardsPoint(self.H) * view.offset()); + self.image = Some(dfg::EdwardsPoint(self.key_image_generator) * view.offset()); } // Transcript this participant's contribution self.transcript.append_message(b"participant", l.to_bytes()); - self.transcript.append_message(b"key_image_share", addendum.key_image.compress().to_bytes()); + self + .transcript + .append_message(b"key_image_share", addendum.key_image_share.compress().to_bytes()); // Accumulate the interpolated share let interpolated_key_image_share = - addendum.key_image * lagrange::(l, view.included()); + addendum.key_image_share * lagrange::(l, view.included()); *self.image.as_mut().unwrap() += interpolated_key_image_share; self @@ -211,28 +256,34 @@ impl Algorithm for ClsagMultisig { msg: &[u8], ) -> dfg::Scalar { // Use the transcript to get a seeded random number generator + // // The transcript contains private data, preventing passive adversaries from recreating this - // process even if they have access to commitments (specifically, the ring index being signed - // for, along with the mask which should not only require knowing the shared keys yet also the - // input commitment masks) + // process even if they have access to the commitments/key image share broadcast so far + // + // Specifically, the transcript contains the signer's index within the ring, along with the + // opening of the commitment being re-randomized (and what it's re-randomized to) let mut rng = ChaCha20Rng::from_seed(self.transcript.rng_seed(b"decoy_responses")); self.msg = Some(msg.try_into().expect("CLSAG message should be 32-bytes")); - #[allow(non_snake_case)] - let (clsag, pseudo_out, p, c) = Clsag::sign_core( + let sign_core = Clsag::sign_core( &mut rng, &self.image.expect("verifying a share despite never processing any addendums").0, - &self.input(), - self.mask(), + &self.context, + self.mask.expect("mask wasn't set"), self.msg.as_ref().unwrap(), nonce_sums[0][0].0, nonce_sums[0][1].0, ); - self.interim = Some(Interim { p, c, clsag, pseudo_out }); + self.interim = Some(Interim { + p: sign_core.key_challenge, + c: sign_core.challenged_mask, + clsag: sign_core.incomplete_clsag, + pseudo_out: sign_core.pseudo_out, + }); // r - p x, where p is the challenge for the keys - *nonces[0] - dfg::Scalar(p) * view.secret_share().deref() + *nonces[0] - dfg::Scalar(sign_core.key_challenge) * view.secret_share().deref() } #[must_use] @@ -244,12 +295,12 @@ impl Algorithm for ClsagMultisig { ) -> Option { let interim = self.interim.as_ref().unwrap(); let mut clsag = interim.clsag.clone(); - // We produced shares as `r - p x`, yet the signature is `r - p x - c x` + // We produced shares as `r - p x`, yet the signature is actually `r - p x - c x` // Substract `c x` (saved as `c`) now - clsag.s[usize::from(self.input().decoys.i)] = sum.0 - interim.c; + clsag.s[usize::from(self.context.decoys.signer_index())] = sum.0 - interim.c; if clsag .verify( - &self.input().decoys.ring, + self.context.decoys.ring(), &self.image.expect("verifying a signature despite never processing any addendums").0, &interim.pseudo_out, self.msg.as_ref().unwrap(), @@ -293,11 +344,11 @@ impl Algorithm for ClsagMultisig { let key_image_share = self.key_image_shares[&verification_share.to_bytes()]; - // Hash every variable relevant here, using the hahs output as the random weight + // Hash every variable relevant here, using the hash output as the random weight let mut weight_transcript = RecommendedTranscript::new(b"monero-serai v0.1 ClsagMultisig::verify_share"); weight_transcript.append_message(b"G", dfg::EdwardsPoint::generator().to_bytes()); - weight_transcript.append_message(b"H", self.H.to_bytes()); + weight_transcript.append_message(b"H", self.key_image_generator.to_bytes()); weight_transcript.append_message(b"xG", verification_share.to_bytes()); weight_transcript.append_message(b"xH", key_image_share.to_bytes()); weight_transcript.append_message(b"rG", nonces[0][0].to_bytes()); @@ -315,7 +366,7 @@ impl Algorithm for ClsagMultisig { ]; let mut part_two = vec![ - (weight * share, dfg::EdwardsPoint(self.H)), + (weight * share, dfg::EdwardsPoint(self.key_image_generator)), // -(R.1 - pK) == -R.1 + pK (-weight, nonces[0][1]), (weight * dfg::Scalar(interim.p), key_image_share), diff --git a/coins/monero/src/tests/clsag.rs b/networks/monero/ringct/clsag/src/tests.rs similarity index 62% rename from coins/monero/src/tests/clsag.rs rename to networks/monero/ringct/clsag/src/tests.rs index a17d7ba2..ba71d69c 100644 --- a/coins/monero/src/tests/clsag.rs +++ b/networks/monero/ringct/clsag/src/tests.rs @@ -1,6 +1,4 @@ use core::ops::Deref; -#[cfg(feature = "multisig")] -use std::sync::{Arc, RwLock}; use zeroize::Zeroizing; use rand_core::{RngCore, OsRng}; @@ -12,16 +10,11 @@ use transcript::{Transcript, RecommendedTranscript}; #[cfg(feature = "multisig")] use frost::curve::Ed25519; -use crate::{ - Commitment, random_scalar, - wallet::Decoys, - ringct::{ - generate_key_image, - clsag::{ClsagInput, Clsag}, - }, -}; +use monero_generators::hash_to_point; +use monero_primitives::{Commitment, Decoys}; +use crate::{ClsagContext, Clsag}; #[cfg(feature = "multisig")] -use crate::ringct::clsag::{ClsagDetails, ClsagMultisig}; +use crate::ClsagMultisig; #[cfg(feature = "multisig")] use frost::{ @@ -43,8 +36,8 @@ fn clsag() { let mut secrets = (Zeroizing::new(Scalar::ZERO), Scalar::ZERO); let mut ring = vec![]; for i in 0 .. RING_LEN { - let dest = Zeroizing::new(random_scalar(&mut OsRng)); - let mask = random_scalar(&mut OsRng); + let dest = Zeroizing::new(Scalar::random(&mut OsRng)); + let mask = Scalar::random(&mut OsRng); let amount; if i == real { secrets = (dest.clone(), mask); @@ -56,31 +49,29 @@ fn clsag() { .push([dest.deref() * ED25519_BASEPOINT_TABLE, Commitment::new(mask, amount).calculate()]); } - let image = generate_key_image(&secrets.0); let (mut clsag, pseudo_out) = Clsag::sign( &mut OsRng, vec![( - secrets.0, - image, - ClsagInput::new( + secrets.0.clone(), + ClsagContext::new( + Decoys::new((1 ..= RING_LEN).collect(), u8::try_from(real).unwrap(), ring.clone()) + .unwrap(), Commitment::new(secrets.1, AMOUNT), - Decoys { - i: u8::try_from(real).unwrap(), - offsets: (1 ..= RING_LEN).collect(), - ring: ring.clone(), - }, ) .unwrap(), )], - random_scalar(&mut OsRng), + Scalar::random(&mut OsRng), msg, ) + .unwrap() .swap_remove(0); + let image = + hash_to_point((ED25519_BASEPOINT_TABLE * secrets.0.deref()).compress().0) * secrets.0.deref(); clsag.verify(&ring, &image, &pseudo_out, &msg).unwrap(); // make sure verification fails if we throw a random `c1` at it. - clsag.c1 = random_scalar(&mut OsRng); + clsag.c1 = Scalar::random(&mut OsRng); assert!(clsag.verify(&ring, &image, &pseudo_out, &msg).is_err()); } } @@ -90,15 +81,15 @@ fn clsag() { fn clsag_multisig() { let keys = key_gen::<_, Ed25519>(&mut OsRng); - let randomness = random_scalar(&mut OsRng); + let randomness = Scalar::random(&mut OsRng); let mut ring = vec![]; for i in 0 .. RING_LEN { let dest; let mask; let amount; if i != u64::from(RING_INDEX) { - dest = &random_scalar(&mut OsRng) * ED25519_BASEPOINT_TABLE; - mask = random_scalar(&mut OsRng); + dest = &Scalar::random(&mut OsRng) * ED25519_BASEPOINT_TABLE; + mask = Scalar::random(&mut OsRng); amount = OsRng.next_u64(); } else { dest = keys[&Participant::new(1).unwrap()].group_key().0; @@ -108,19 +99,15 @@ fn clsag_multisig() { ring.push([dest, Commitment::new(mask, amount).calculate()]); } - let mask_sum = random_scalar(&mut OsRng); - let algorithm = ClsagMultisig::new( + let (algorithm, mask_send) = ClsagMultisig::new( RecommendedTranscript::new(b"Monero Serai CLSAG Test"), - keys[&Participant::new(1).unwrap()].group_key().0, - Arc::new(RwLock::new(Some(ClsagDetails::new( - ClsagInput::new( - Commitment::new(randomness, AMOUNT), - Decoys { i: RING_INDEX, offsets: (1 ..= RING_LEN).collect(), ring: ring.clone() }, - ) - .unwrap(), - mask_sum, - )))), + ClsagContext::new( + Decoys::new((1 ..= RING_LEN).collect(), RING_INDEX, ring.clone()).unwrap(), + Commitment::new(randomness, AMOUNT), + ) + .unwrap(), ); + mask_send.send(Scalar::random(&mut OsRng)); sign( &mut OsRng, diff --git a/networks/monero/ringct/mlsag/Cargo.toml b/networks/monero/ringct/mlsag/Cargo.toml new file mode 100644 index 00000000..718b2e7c --- /dev/null +++ b/networks/monero/ringct/mlsag/Cargo.toml @@ -0,0 +1,45 @@ +[package] +name = "monero-mlsag" +version = "0.1.0" +description = "The MLSAG linkable ring signature, as defined by the Monero protocol" +license = "MIT" +repository = "https://github.com/serai-dex/serai/tree/develop/networks/monero/ringct/mlsag" +authors = ["Luke Parker "] +edition = "2021" +rust-version = "1.79" + +[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 } + +thiserror = { version = "1", default-features = false, optional = true } + +zeroize = { version = "^1.5", default-features = false, features = ["zeroize_derive"] } + +# Cryptographic dependencies +curve25519-dalek = { version = "4", default-features = false, features = ["alloc", "zeroize"] } + +# Other Monero dependencies +monero-io = { path = "../../io", version = "0.1", default-features = false } +monero-generators = { path = "../../generators", version = "0.4", default-features = false } +monero-primitives = { path = "../../primitives", version = "0.1", default-features = false } + +[features] +std = [ + "std-shims/std", + + "thiserror", + + "zeroize/std", + + "monero-io/std", + "monero-generators/std", + "monero-primitives/std", +] +default = ["std"] diff --git a/networks/monero/ringct/mlsag/LICENSE b/networks/monero/ringct/mlsag/LICENSE new file mode 100644 index 00000000..91d893c1 --- /dev/null +++ b/networks/monero/ringct/mlsag/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022-2024 Luke Parker + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/networks/monero/ringct/mlsag/README.md b/networks/monero/ringct/mlsag/README.md new file mode 100644 index 00000000..40e979b6 --- /dev/null +++ b/networks/monero/ringct/mlsag/README.md @@ -0,0 +1,11 @@ +# Monero MLSAG + +The MLSAG linkable ring signature, as defined by the Monero protocol. + +This library is usable under no-std when the `std` feature (on by default) is +disabled. + +### Cargo Features + +- `std` (on by default): Enables `std` (and with it, more efficient internal + implementations). diff --git a/coins/monero/src/ringct/mlsag.rs b/networks/monero/ringct/mlsag/src/lib.rs similarity index 76% rename from coins/monero/src/ringct/mlsag.rs rename to networks/monero/ringct/mlsag/src/lib.rs index e5f00bf7..d9f15ead 100644 --- a/coins/monero/src/ringct/mlsag.rs +++ b/networks/monero/ringct/mlsag/src/lib.rs @@ -1,4 +1,11 @@ +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![doc = include_str!("../README.md")] +#![deny(missing_docs)] +#![cfg_attr(not(feature = "std"), no_std)] +#![allow(non_snake_case)] + use std_shims::{ + vec, vec::Vec, io::{self, Read, Write}, }; @@ -7,32 +14,40 @@ use zeroize::Zeroize; use curve25519_dalek::{traits::IsIdentity, Scalar, EdwardsPoint}; -use monero_generators::H; - -use crate::{hash_to_scalar, ringct::hash_to_point, serialize::*}; +use monero_io::*; +use monero_generators::{H, hash_to_point}; +use monero_primitives::keccak256_to_scalar; +/// Errors when working with MLSAGs. #[derive(Clone, Copy, PartialEq, Eq, Debug)] #[cfg_attr(feature = "std", derive(thiserror::Error))] pub enum MlsagError { + /// Invalid ring (such as too small or too large). #[cfg_attr(feature = "std", error("invalid ring"))] InvalidRing, + /// Invalid amount of key images. #[cfg_attr(feature = "std", error("invalid amount of key images"))] InvalidAmountOfKeyImages, + /// Invalid ss matrix. #[cfg_attr(feature = "std", error("invalid ss"))] InvalidSs, - #[cfg_attr(feature = "std", error("key image was identity"))] - IdentityKeyImage, + /// Invalid key image. + #[cfg_attr(feature = "std", error("invalid key image"))] + InvalidKeyImage, + /// Invalid ci vector. #[cfg_attr(feature = "std", error("invalid ci"))] InvalidCi, } +/// A vector of rings, forming a matrix, to verify the MLSAG with. #[derive(Clone, PartialEq, Eq, Debug, Zeroize)] pub struct RingMatrix { matrix: Vec>, } impl RingMatrix { - pub fn new(matrix: Vec>) -> Result { + /// Construct a ring matrix from an already formatted series of points. + fn new(matrix: Vec>) -> Result { // Monero requires that there is more than one ring member for MLSAG signatures: // https://github.com/monero-project/monero/blob/ac02af92867590ca80b2779a7bbeafa99ff94dcb/ // src/ringct/rctSigs.cpp#L462 @@ -60,16 +75,17 @@ impl RingMatrix { RingMatrix::new(matrix) } - pub fn iter(&self) -> impl Iterator { + /// Iterate over the members of the matrix. + fn iter(&self) -> impl Iterator { self.matrix.iter().map(AsRef::as_ref) } - /// Return the amount of members in the ring. + /// Get the amount of members in the ring. pub fn members(&self) -> usize { self.matrix.len() } - /// Returns the length of a ring member. + /// Get the length of a ring member. /// /// A ring member is a vector of points for which the signer knows all of the discrete logarithms /// of. @@ -79,13 +95,15 @@ impl RingMatrix { } } +/// The MLSAG linkable ring signature, as used in Monero. #[derive(Clone, PartialEq, Eq, Debug, Zeroize)] pub struct Mlsag { - pub ss: Vec>, - pub cc: Scalar, + ss: Vec>, + cc: Scalar, } impl Mlsag { + /// Write a MLSAG. pub fn write(&self, w: &mut W) -> io::Result<()> { for ss in &self.ss { write_raw_vec(write_scalar, ss, w)?; @@ -93,6 +111,7 @@ impl Mlsag { write_scalar(&self.cc, w) } + /// Read a MLSAG. pub fn read(mixins: usize, ss_2_elements: usize, r: &mut R) -> io::Result { Ok(Mlsag { ss: (0 .. mixins) @@ -102,6 +121,7 @@ impl Mlsag { }) } + /// Verify a MLSAG. pub fn verify( &self, msg: &[u8; 32], @@ -136,23 +156,24 @@ impl Mlsag { #[allow(non_snake_case)] let L = EdwardsPoint::vartime_double_scalar_mul_basepoint(&ci, ring_member_entry, s); - buf.extend_from_slice(ring_member_entry.compress().as_bytes()); + let compressed_ring_member_entry = ring_member_entry.compress(); + buf.extend_from_slice(compressed_ring_member_entry.as_bytes()); buf.extend_from_slice(L.compress().as_bytes()); // Not all dimensions need to be linkable, e.g. commitments, and only linkable layers need // to have key images. if let Some(ki) = ki { - if ki.is_identity() { - Err(MlsagError::IdentityKeyImage)?; + if ki.is_identity() || (!ki.is_torsion_free()) { + Err(MlsagError::InvalidKeyImage)?; } #[allow(non_snake_case)] - let R = (s * hash_to_point(ring_member_entry)) + (ci * ki); + let R = (s * hash_to_point(compressed_ring_member_entry.to_bytes())) + (ci * ki); buf.extend_from_slice(R.compress().as_bytes()); } } - ci = hash_to_scalar(&buf); + ci = keccak256_to_scalar(&buf); // keep the msg in the buffer. buf.drain(msg.len() ..); } @@ -164,8 +185,9 @@ impl Mlsag { } } -/// An aggregate ring matrix builder, usable to set up the ring matrix to prove/verify an aggregate -/// MLSAG signature. +/// Builder for a RingMatrix when using an aggregate signature. +/// +/// This handles the formatting as necessary. #[derive(Clone, PartialEq, Eq, Debug, Zeroize)] pub struct AggregateRingMatrixBuilder { key_ring: Vec>, @@ -176,7 +198,7 @@ pub struct AggregateRingMatrixBuilder { impl AggregateRingMatrixBuilder { /// Create a new AggregateRingMatrixBuilder. /// - /// Takes in the transaction's outputs; commitments and fee. + /// This takes in the transaction's outputs' commitments and fee used. pub fn new(commitments: &[EdwardsPoint], fee: u64) -> Self { AggregateRingMatrixBuilder { key_ring: vec![], @@ -206,7 +228,7 @@ impl AggregateRingMatrixBuilder { Ok(()) } - /// Build and return the [`RingMatrix`] + /// Build and return the [`RingMatrix`]. pub fn build(mut self) -> Result { for (i, amount_commitment) in self.amounts_ring.drain(..).enumerate() { self.key_ring[i].push(amount_commitment); diff --git a/networks/monero/rpc/Cargo.toml b/networks/monero/rpc/Cargo.toml new file mode 100644 index 00000000..e6e65284 --- /dev/null +++ b/networks/monero/rpc/Cargo.toml @@ -0,0 +1,48 @@ +[package] +name = "monero-rpc" +version = "0.1.0" +description = "Trait for an RPC connection to a Monero daemon, built around monero-serai" +license = "MIT" +repository = "https://github.com/serai-dex/serai/tree/develop/networks/monero/rpc" +authors = ["Luke Parker "] +edition = "2021" +rust-version = "1.79" + +[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"] } +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"] } + +curve25519-dalek = { version = "4", default-features = false, features = ["alloc", "zeroize"] } + +monero-serai = { path = "..", default-features = false } +monero-address = { path = "../wallet/address", default-features = false } + +[features] +std = [ + "std-shims/std", + + "thiserror", + + "zeroize/std", + "hex/std", + "serde/std", + "serde_json/std", + + "monero-serai/std", + "monero-address/std", +] +default = ["std"] diff --git a/networks/monero/rpc/LICENSE b/networks/monero/rpc/LICENSE new file mode 100644 index 00000000..91d893c1 --- /dev/null +++ b/networks/monero/rpc/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022-2024 Luke Parker + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/networks/monero/rpc/README.md b/networks/monero/rpc/README.md new file mode 100644 index 00000000..4badf1d8 --- /dev/null +++ b/networks/monero/rpc/README.md @@ -0,0 +1,11 @@ +# Monero RPC + +Trait for an RPC connection to a Monero daemon, built around monero-serai. + +This library is usable under no-std when the `std` feature (on by default) is +disabled. + +### Cargo Features + +- `std` (on by default): Enables `std` (and with it, more efficient internal + implementations). diff --git a/networks/monero/rpc/simple-request/Cargo.toml b/networks/monero/rpc/simple-request/Cargo.toml new file mode 100644 index 00000000..71f7cbfb --- /dev/null +++ b/networks/monero/rpc/simple-request/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "monero-simple-request-rpc" +version = "0.1.0" +description = "RPC connection to a Monero daemon via simple-request, built around monero-serai" +license = "MIT" +repository = "https://github.com/serai-dex/serai/tree/develop/networks/monero/rpc/simple-request" +authors = ["Luke Parker "] +edition = "2021" +rust-version = "1.79" + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + +[lints] +workspace = true + +[dependencies] +async-trait = { version = "0.1", default-features = false } + +hex = { version = "0.4", default-features = false, features = ["alloc"] } +digest_auth = { version = "0.3", default-features = false } +simple-request = { path = "../../../../common/request", version = "0.1", default-features = false, features = ["tls"] } +tokio = { version = "1", default-features = false } + +monero-rpc = { path = "..", default-features = false, features = ["std"] } + +[dev-dependencies] +monero-address = { path = "../../wallet/address", default-features = false, features = ["std"] } + +tokio = { version = "1", default-features = false, features = ["macros"] } diff --git a/networks/monero/rpc/simple-request/LICENSE b/networks/monero/rpc/simple-request/LICENSE new file mode 100644 index 00000000..91d893c1 --- /dev/null +++ b/networks/monero/rpc/simple-request/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022-2024 Luke Parker + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/networks/monero/rpc/simple-request/README.md b/networks/monero/rpc/simple-request/README.md new file mode 100644 index 00000000..947e777e --- /dev/null +++ b/networks/monero/rpc/simple-request/README.md @@ -0,0 +1,3 @@ +# Monero simple-request RPC + +RPC connection to a Monero daemon via simple-request, built around monero-serai. diff --git a/coins/monero/src/rpc/http.rs b/networks/monero/rpc/simple-request/src/lib.rs similarity index 95% rename from coins/monero/src/rpc/http.rs rename to networks/monero/rpc/simple-request/src/lib.rs index 4ed349a5..33651309 100644 --- a/coins/monero/src/rpc/http.rs +++ b/networks/monero/rpc/simple-request/src/lib.rs @@ -1,3 +1,7 @@ +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![doc = include_str!("../README.md")] +#![deny(missing_docs)] + use std::{sync::Arc, io::Read, time::Duration}; use async_trait::async_trait; @@ -10,7 +14,7 @@ use simple_request::{ Response, Client, }; -use crate::rpc::{RpcError, RpcConnection, Rpc}; +use monero_rpc::{RpcError, Rpc}; const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30); @@ -33,13 +37,13 @@ enum Authentication { /// /// Requires tokio. #[derive(Clone, Debug)] -pub struct HttpRpc { +pub struct SimpleRequestRpc { authentication: Authentication, url: String, request_timeout: Duration, } -impl HttpRpc { +impl SimpleRequestRpc { fn digest_auth_challenge( response: &Response, ) -> Result, RpcError> { @@ -60,7 +64,7 @@ impl HttpRpc { /// /// A daemon requiring authentication can be used via including the username and password in the /// URL. - pub async fn new(url: String) -> Result, RpcError> { + pub async fn new(url: String) -> Result { Self::with_custom_timeout(url, DEFAULT_TIMEOUT).await } @@ -71,7 +75,7 @@ impl HttpRpc { pub async fn with_custom_timeout( mut url: String, request_timeout: Duration, - ) -> Result, RpcError> { + ) -> Result { let authentication = if url.contains('@') { // Parse out the username and password let url_clone = url; @@ -119,11 +123,11 @@ impl HttpRpc { Authentication::Unauthenticated(Client::with_connection_pool()) }; - Ok(Rpc(HttpRpc { authentication, url, request_timeout })) + Ok(SimpleRequestRpc { authentication, url, request_timeout }) } } -impl HttpRpc { +impl SimpleRequestRpc { async fn inner_post(&self, route: &str, body: Vec) -> Result, RpcError> { let request_fn = |uri| { Request::post(uri) @@ -277,7 +281,7 @@ impl HttpRpc { } #[async_trait] -impl RpcConnection for HttpRpc { +impl Rpc for SimpleRequestRpc { async fn post(&self, route: &str, body: Vec) -> Result, RpcError> { tokio::time::timeout(self.request_timeout, self.inner_post(route, body)) .await diff --git a/networks/monero/rpc/simple-request/tests/tests.rs b/networks/monero/rpc/simple-request/tests/tests.rs new file mode 100644 index 00000000..7b28f9a7 --- /dev/null +++ b/networks/monero/rpc/simple-request/tests/tests.rs @@ -0,0 +1,144 @@ +use std::sync::OnceLock; +use tokio::sync::Mutex; + +use monero_address::{Network, MoneroAddress}; + +// monero-rpc doesn't include a transport +// We can't include the simple-request crate there as then we'd have a cyclical dependency +// Accordingly, we test monero-rpc here (implicitly testing the simple-request transport) +use monero_simple_request_rpc::*; + +static SEQUENTIAL: OnceLock> = OnceLock::new(); + +const ADDRESS: &str = + "4B33mFPMq6mKi7Eiyd5XuyKRVMGVZz1Rqb9ZTyGApXW5d1aT7UBDZ89ewmnWFkzJ5wPd2SFbn313vCT8a4E2Qf4KQH4pNey"; + +#[tokio::test] +async fn test_rpc() { + use monero_rpc::Rpc; + + let guard = SEQUENTIAL.get_or_init(|| Mutex::new(())).lock().await; + + let rpc = + SimpleRequestRpc::new("http://serai:seraidex@127.0.0.1:18081".to_string()).await.unwrap(); + + { + // Test get_height + let height = rpc.get_height().await.unwrap(); + // The height should be the amount of blocks on chain + // The number of a block should be its zero-indexed position + // Accordingly, there should be no block whose number is the height + assert!(rpc.get_block_by_number(height).await.is_err()); + let block_number = height - 1; + // There should be a block just prior + let block = rpc.get_block_by_number(block_number).await.unwrap(); + + // Also test the block RPC routes are consistent + assert_eq!(block.number().unwrap(), block_number); + assert_eq!(rpc.get_block(block.hash()).await.unwrap(), block); + assert_eq!(rpc.get_block_hash(block_number).await.unwrap(), block.hash()); + + // And finally the hardfork version route + assert_eq!(rpc.get_hardfork_version().await.unwrap(), block.header.hardfork_version); + } + + // Test generate_blocks + for amount_of_blocks in [1, 5] { + let (blocks, number) = rpc + .generate_blocks( + &MoneroAddress::from_str(Network::Mainnet, ADDRESS).unwrap(), + amount_of_blocks, + ) + .await + .unwrap(); + let height = rpc.get_height().await.unwrap(); + assert_eq!(number, height - 1); + + let mut actual_blocks = Vec::with_capacity(amount_of_blocks); + for i in (height - amount_of_blocks) .. height { + actual_blocks.push(rpc.get_block_by_number(i).await.unwrap().hash()); + } + assert_eq!(blocks, actual_blocks); + } + + drop(guard); +} + +#[tokio::test] +async fn test_decoy_rpc() { + use monero_rpc::{Rpc, DecoyRpc}; + + let guard = SEQUENTIAL.get_or_init(|| Mutex::new(())).lock().await; + + let rpc = + SimpleRequestRpc::new("http://serai:seraidex@127.0.0.1:18081".to_string()).await.unwrap(); + + // Ensure there's blocks on-chain + rpc + .generate_blocks(&MoneroAddress::from_str(Network::Mainnet, ADDRESS).unwrap(), 100) + .await + .unwrap(); + + // Test get_output_distribution + // Our documentation for our Rust fn defines it as taking two block numbers + { + let distribution_len = rpc.get_output_distribution_end_height().await.unwrap(); + assert_eq!(distribution_len, rpc.get_height().await.unwrap()); + + rpc.get_output_distribution(0 ..= distribution_len).await.unwrap_err(); + assert_eq!( + rpc.get_output_distribution(0 .. distribution_len).await.unwrap().len(), + distribution_len + ); + assert_eq!( + rpc.get_output_distribution(.. distribution_len).await.unwrap().len(), + distribution_len + ); + + assert_eq!( + rpc.get_output_distribution(.. (distribution_len - 1)).await.unwrap().len(), + distribution_len - 1 + ); + assert_eq!( + rpc.get_output_distribution(1 .. distribution_len).await.unwrap().len(), + distribution_len - 1 + ); + + assert_eq!(rpc.get_output_distribution(0 ..= 0).await.unwrap().len(), 1); + assert_eq!(rpc.get_output_distribution(0 ..= 1).await.unwrap().len(), 2); + assert_eq!(rpc.get_output_distribution(1 ..= 1).await.unwrap().len(), 1); + + rpc.get_output_distribution(0 .. 0).await.unwrap_err(); + #[allow(clippy::reversed_empty_ranges)] + rpc.get_output_distribution(1 .. 0).await.unwrap_err(); + } + + drop(guard); +} + +// This test passes yet requires a mainnet node, which we don't have reliable access to in CI. +/* +#[tokio::test] +async fn test_zero_out_tx_o_indexes() { + use monero_rpc::Rpc; + + let guard = SEQUENTIAL.get_or_init(|| Mutex::new(())).lock().await; + + let rpc = SimpleRequestRpc::new("https://node.sethforprivacy.com".to_string()).await.unwrap(); + + assert_eq!( + rpc + .get_o_indexes( + hex::decode("17ce4c8feeb82a6d6adaa8a89724b32bf4456f6909c7f84c8ce3ee9ebba19163") + .unwrap() + .try_into() + .unwrap() + ) + .await + .unwrap(), + Vec::::new() + ); + + drop(guard); +} +*/ diff --git a/networks/monero/rpc/src/lib.rs b/networks/monero/rpc/src/lib.rs new file mode 100644 index 00000000..3bfc76b5 --- /dev/null +++ b/networks/monero/rpc/src/lib.rs @@ -0,0 +1,1091 @@ +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![doc = include_str!("../README.md")] +#![deny(missing_docs)] +#![cfg_attr(not(feature = "std"), no_std)] + +use core::{ + fmt::Debug, + ops::{Bound, RangeBounds}, +}; +use std_shims::{ + alloc::{boxed::Box, format}, + vec, + vec::Vec, + io, + string::{String, ToString}, +}; + +use zeroize::Zeroize; + +use async_trait::async_trait; + +use curve25519_dalek::edwards::{CompressedEdwardsY, EdwardsPoint}; + +use serde::{Serialize, Deserialize, de::DeserializeOwned}; +use serde_json::{Value, json}; + +use monero_serai::{ + io::*, + transaction::{Input, Timelock, Pruned, Transaction}, + block::Block, + DEFAULT_LOCK_WINDOW, +}; +use monero_address::Address; + +// Number of blocks the fee estimate will be valid for +// https://github.com/monero-project/monero/blob/94e67bf96bbc010241f29ada6abc89f49a81759c +// /src/wallet/wallet2.cpp#L121 +const GRACE_BLOCKS_FOR_FEE_ESTIMATE: u64 = 10; + +// Monero errors if more than 100 is requested unless using a non-restricted RPC +// https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454 +// /src/rpc/core_rpc_server.cpp#L75 +const TXS_PER_REQUEST: usize = 100; + +/// An error from the RPC. +#[derive(Clone, PartialEq, Eq, Debug)] +#[cfg_attr(feature = "std", derive(thiserror::Error))] +pub enum RpcError { + /// An internal error. + #[cfg_attr(feature = "std", error("internal error ({0})"))] + InternalError(String), + /// A connection error with the node. + #[cfg_attr(feature = "std", error("connection error ({0})"))] + ConnectionError(String), + /// The node is invalid per the expected protocol. + #[cfg_attr(feature = "std", error("invalid node ({0})"))] + InvalidNode(String), + /// Requested transactions weren't found. + #[cfg_attr(feature = "std", error("transactions not found"))] + TransactionsNotFound(Vec<[u8; 32]>), + /// The transaction was pruned. + /// + /// Pruned transactions are not supported at this time. + #[cfg_attr(feature = "std", error("pruned transaction"))] + PrunedTransaction, + /// A transaction (sent or received) was invalid. + #[cfg_attr(feature = "std", error("invalid transaction ({0:?})"))] + InvalidTransaction([u8; 32]), + /// The returned fee was unusable. + #[cfg_attr(feature = "std", error("unexpected fee response"))] + InvalidFee, + /// The priority intended for use wasn't usable. + #[cfg_attr(feature = "std", error("invalid priority"))] + InvalidPriority, +} + +/// A struct containing a fee rate. +/// +/// The fee rate is defined as a per-weight cost, along with a mask for rounding purposes. +#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)] +pub struct FeeRate { + /// The fee per-weight of the transaction. + per_weight: u64, + /// The mask to round with. + mask: u64, +} + +impl FeeRate { + /// Construct a new fee rate. + pub fn new(per_weight: u64, mask: u64) -> Result { + if (per_weight == 0) || (mask == 0) { + Err(RpcError::InvalidFee)?; + } + Ok(FeeRate { per_weight, mask }) + } + + /// Write the FeeRate. + /// + /// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol + /// defined serialization. + pub fn write(&self, w: &mut impl io::Write) -> io::Result<()> { + w.write_all(&self.per_weight.to_le_bytes())?; + w.write_all(&self.mask.to_le_bytes()) + } + + /// Serialize the FeeRate to a `Vec`. + /// + /// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol + /// defined serialization. + pub fn serialize(&self) -> Vec { + let mut res = Vec::with_capacity(16); + self.write(&mut res).unwrap(); + res + } + + /// Read a FeeRate. + /// + /// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol + /// defined serialization. + pub fn read(r: &mut impl io::Read) -> io::Result { + let per_weight = read_u64(r)?; + let mask = read_u64(r)?; + FeeRate::new(per_weight, mask).map_err(io::Error::other) + } + + /// Calculate the fee to use from the weight. + /// + /// This function may panic upon overflow. + pub fn calculate_fee_from_weight(&self, weight: usize) -> u64 { + let fee = self.per_weight * u64::try_from(weight).unwrap(); + let fee = fee.div_ceil(self.mask) * self.mask; + debug_assert_eq!(weight, self.calculate_weight_from_fee(fee), "Miscalculated weight from fee"); + fee + } + + /// Calculate the weight from the fee. + pub fn calculate_weight_from_fee(&self, fee: u64) -> usize { + usize::try_from(fee / self.per_weight).unwrap() + } +} + +/// The priority for the fee. +/// +/// Higher-priority transactions will be included in blocks earlier. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +#[allow(non_camel_case_types)] +pub enum FeePriority { + /// The `Unimportant` priority, as defined by Monero. + Unimportant, + /// The `Normal` priority, as defined by Monero. + Normal, + /// The `Elevated` priority, as defined by Monero. + Elevated, + /// The `Priority` priority, as defined by Monero. + Priority, + /// A custom priority. + Custom { + /// The numeric representation of the priority, as used within the RPC. + priority: u32, + }, +} + +/// https://github.com/monero-project/monero/blob/ac02af92867590ca80b2779a7bbeafa99ff94dcb/ +/// src/simplewallet/simplewallet.cpp#L161 +impl FeePriority { + pub(crate) fn fee_priority(&self) -> u32 { + match self { + FeePriority::Unimportant => 1, + FeePriority::Normal => 2, + FeePriority::Elevated => 3, + FeePriority::Priority => 4, + FeePriority::Custom { priority, .. } => *priority, + } + } +} + +#[derive(Debug, Deserialize)] +struct EmptyResponse {} +#[derive(Debug, Deserialize)] +struct JsonRpcResponse { + result: T, +} + +#[derive(Debug, Deserialize)] +struct TransactionResponse { + tx_hash: String, + as_hex: String, + pruned_as_hex: String, +} +#[derive(Debug, Deserialize)] +struct TransactionsResponse { + #[serde(default)] + missed_tx: Vec, + txs: Vec, +} + +/// The response to an query for the information of a RingCT output. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct OutputInformation { + /// The block number of the block this output was added to the chain in. + /// + /// This is equivalent to he height of the blockchain at the time the block was added. + pub height: usize, + /// If the output is unlocked, per the node's local view. + pub unlocked: bool, + /// The output's key. + /// + /// This is a CompressedEdwardsY, not an EdwardsPoint, as it may be invalid. CompressedEdwardsY + /// only asserts validity on decompression and allows representing compressed types. + pub key: CompressedEdwardsY, + /// The output's commitment. + pub commitment: EdwardsPoint, + /// The transaction which created this output. + pub transaction: [u8; 32], +} + +fn rpc_hex(value: &str) -> Result, RpcError> { + hex::decode(value).map_err(|_| RpcError::InvalidNode("expected hex wasn't hex".to_string())) +} + +fn hash_hex(hash: &str) -> Result<[u8; 32], RpcError> { + rpc_hex(hash)?.try_into().map_err(|_| RpcError::InvalidNode("hash wasn't 32-bytes".to_string())) +} + +fn rpc_point(point: &str) -> Result { + decompress_point( + rpc_hex(point)? + .try_into() + .map_err(|_| RpcError::InvalidNode(format!("invalid point: {point}")))?, + ) + .ok_or_else(|| RpcError::InvalidNode(format!("invalid point: {point}"))) +} + +/// An RPC connection to a Monero daemon. +/// +/// This is abstract such that users can use an HTTP library (which being their choice), a +/// Tor/i2p-based transport, or even a memory buffer an external service somehow routes. +/// +/// While no implementors are directly provided, [monero-simple-request-rpc]( +/// https://github.com/serai-dex/serai/tree/develop/networks/monero/rpc/simple-request +/// ) is recommended. +#[async_trait] +pub trait Rpc: Sync + Clone + Debug { + /// Perform a POST request to the specified route with the specified body. + /// + /// The implementor is left to handle anything such as authentication. + async fn post(&self, route: &str, body: Vec) -> Result, RpcError>; + + /// Perform a RPC call to the specified route with the provided parameters. + /// + /// This is NOT a JSON-RPC call. They use a route of "json_rpc" and are available via + /// `json_rpc_call`. + async fn rpc_call( + &self, + route: &str, + params: Option, + ) -> Result { + let res = self + .post( + route, + if let Some(params) = params { + serde_json::to_string(¶ms).unwrap().into_bytes() + } else { + vec![] + }, + ) + .await?; + let res_str = std_shims::str::from_utf8(&res) + .map_err(|_| RpcError::InvalidNode("response wasn't utf-8".to_string()))?; + serde_json::from_str(res_str) + .map_err(|_| RpcError::InvalidNode(format!("response wasn't the expected json: {res_str}"))) + } + + /// Perform a JSON-RPC call with the specified method with the provided parameters. + async fn json_rpc_call( + &self, + method: &str, + params: Option, + ) -> Result { + let mut req = json!({ "method": method }); + if let Some(params) = params { + req.as_object_mut().unwrap().insert("params".into(), params); + } + Ok(self.rpc_call::<_, JsonRpcResponse>("json_rpc", Some(req)).await?.result) + } + + /// Perform a binary call to the specified route with the provided parameters. + async fn bin_call(&self, route: &str, params: Vec) -> Result, RpcError> { + self.post(route, params).await + } + + /// Get the active blockchain protocol version. + /// + /// This is specifically the major version within the most recent block header. + async fn get_hardfork_version(&self) -> Result { + #[derive(Debug, Deserialize)] + struct HeaderResponse { + major_version: u8, + } + + #[derive(Debug, Deserialize)] + struct LastHeaderResponse { + block_header: HeaderResponse, + } + + Ok( + self + .json_rpc_call::("get_last_block_header", None) + .await? + .block_header + .major_version, + ) + } + + /// Get the height of the Monero blockchain. + /// + /// The height is defined as the amount of blocks on the blockchain. For a blockchain with only + /// its genesis block, the height will be 1. + async fn get_height(&self) -> Result { + #[derive(Debug, Deserialize)] + struct HeightResponse { + height: usize, + } + let res = self.rpc_call::, HeightResponse>("get_height", None).await?.height; + if res == 0 { + Err(RpcError::InvalidNode("node responded with 0 for the height".to_string()))?; + } + Ok(res) + } + + /// Get the specified transactions. + /// + /// The received transactions will be hashed in order to verify the correct transactions were + /// returned. + async fn get_transactions(&self, hashes: &[[u8; 32]]) -> Result, RpcError> { + if hashes.is_empty() { + return Ok(vec![]); + } + + let mut hashes_hex = hashes.iter().map(hex::encode).collect::>(); + let mut all_txs = Vec::with_capacity(hashes.len()); + while !hashes_hex.is_empty() { + let this_count = TXS_PER_REQUEST.min(hashes_hex.len()); + + let txs: TransactionsResponse = self + .rpc_call( + "get_transactions", + Some(json!({ + "txs_hashes": hashes_hex.drain(.. this_count).collect::>(), + })), + ) + .await?; + + if !txs.missed_tx.is_empty() { + Err(RpcError::TransactionsNotFound( + txs.missed_tx.iter().map(|hash| hash_hex(hash)).collect::>()?, + ))?; + } + + all_txs.extend(txs.txs); + } + + all_txs + .iter() + .enumerate() + .map(|(i, res)| { + // https://github.com/monero-project/monero/issues/8311 + let buf = rpc_hex(if !res.as_hex.is_empty() { &res.as_hex } else { &res.pruned_as_hex })?; + let mut buf = buf.as_slice(); + let tx = Transaction::read(&mut buf).map_err(|_| match hash_hex(&res.tx_hash) { + Ok(hash) => RpcError::InvalidTransaction(hash), + Err(err) => err, + })?; + if !buf.is_empty() { + Err(RpcError::InvalidNode("transaction had extra bytes after it".to_string()))?; + } + + // We check this to ensure we didn't read a pruned transaction when we meant to read an + // actual transaction. That shouldn't be possible, as they have different serializations, + // yet it helps to ensure that if we applied the above exception (using the pruned data), + // it was for the right reason + if res.as_hex.is_empty() { + match tx.prefix().inputs.first() { + Some(Input::Gen { .. }) => (), + _ => Err(RpcError::PrunedTransaction)?, + } + } + + // This does run a few keccak256 hashes, which is pointless if the node is trusted + // In exchange, this provides resilience against invalid/malicious nodes + if tx.hash() != hashes[i] { + Err(RpcError::InvalidNode( + "replied with transaction wasn't the requested transaction".to_string(), + ))?; + } + + Ok(tx) + }) + .collect() + } + + /// Get the specified transactions in their pruned format. + async fn get_pruned_transactions( + &self, + hashes: &[[u8; 32]], + ) -> Result>, RpcError> { + if hashes.is_empty() { + return Ok(vec![]); + } + + let mut hashes_hex = hashes.iter().map(hex::encode).collect::>(); + let mut all_txs = Vec::with_capacity(hashes.len()); + while !hashes_hex.is_empty() { + let this_count = TXS_PER_REQUEST.min(hashes_hex.len()); + + let txs: TransactionsResponse = self + .rpc_call( + "get_transactions", + Some(json!({ + "txs_hashes": hashes_hex.drain(.. this_count).collect::>(), + "prune": true, + })), + ) + .await?; + + if !txs.missed_tx.is_empty() { + Err(RpcError::TransactionsNotFound( + txs.missed_tx.iter().map(|hash| hash_hex(hash)).collect::>()?, + ))?; + } + + all_txs.extend(txs.txs); + } + + all_txs + .iter() + .map(|res| { + let buf = rpc_hex(&res.pruned_as_hex)?; + let mut buf = buf.as_slice(); + let tx = + Transaction::::read(&mut buf).map_err(|_| match hash_hex(&res.tx_hash) { + Ok(hash) => RpcError::InvalidTransaction(hash), + Err(err) => err, + })?; + if !buf.is_empty() { + Err(RpcError::InvalidNode("pruned transaction had extra bytes after it".to_string()))?; + } + Ok(tx) + }) + .collect() + } + + /// Get the specified transaction. + /// + /// The received transaction will be hashed in order to verify the correct transaction was + /// returned. + async fn get_transaction(&self, tx: [u8; 32]) -> Result { + self.get_transactions(&[tx]).await.map(|mut txs| txs.swap_remove(0)) + } + + /// Get the specified transaction in its pruned format. + async fn get_pruned_transaction(&self, tx: [u8; 32]) -> Result, RpcError> { + self.get_pruned_transactions(&[tx]).await.map(|mut txs| txs.swap_remove(0)) + } + + /// Get the hash of a block from the node. + /// + /// `number` is the block's zero-indexed position on the blockchain (`0` for the genesis block, + /// `height - 1` for the latest block). + async fn get_block_hash(&self, number: usize) -> Result<[u8; 32], RpcError> { + #[derive(Debug, Deserialize)] + struct BlockHeaderResponse { + hash: String, + } + #[derive(Debug, Deserialize)] + struct BlockHeaderByHeightResponse { + block_header: BlockHeaderResponse, + } + + let header: BlockHeaderByHeightResponse = + self.json_rpc_call("get_block_header_by_height", Some(json!({ "height": number }))).await?; + hash_hex(&header.block_header.hash) + } + + /// Get a block from the node by its hash. + /// + /// The received block will be hashed in order to verify the correct block was returned. + async fn get_block(&self, hash: [u8; 32]) -> Result { + #[derive(Debug, Deserialize)] + struct BlockResponse { + blob: String, + } + + let res: BlockResponse = + self.json_rpc_call("get_block", Some(json!({ "hash": hex::encode(hash) }))).await?; + + let block = Block::read::<&[u8]>(&mut rpc_hex(&res.blob)?.as_ref()) + .map_err(|_| RpcError::InvalidNode("invalid block".to_string()))?; + if block.hash() != hash { + Err(RpcError::InvalidNode("different block than requested (hash)".to_string()))?; + } + Ok(block) + } + + /// Get a block from the node by its number. + /// + /// `number` is the block's zero-indexed position on the blockchain (`0` for the genesis block, + /// `height - 1` for the latest block). + async fn get_block_by_number(&self, number: usize) -> Result { + #[derive(Debug, Deserialize)] + struct BlockResponse { + blob: String, + } + + let res: BlockResponse = + self.json_rpc_call("get_block", Some(json!({ "height": number }))).await?; + + let block = Block::read::<&[u8]>(&mut rpc_hex(&res.blob)?.as_ref()) + .map_err(|_| RpcError::InvalidNode("invalid block".to_string()))?; + + // Make sure this is actually the block for this number + match block.miner_transaction.prefix().inputs.first() { + Some(Input::Gen(actual)) => { + if *actual == number { + Ok(block) + } else { + Err(RpcError::InvalidNode("different block than requested (number)".to_string())) + } + } + _ => Err(RpcError::InvalidNode( + "block's miner_transaction didn't have an input of kind Input::Gen".to_string(), + )), + } + } + + /// Get the currently estimated fee rate from the node. + /// + /// This may be manipulated to unsafe levels and MUST be sanity checked. + /// + /// This MUST NOT be expected to be deterministic in any way. + async fn get_fee_rate(&self, priority: FeePriority) -> Result { + #[derive(Debug, Deserialize)] + struct FeeResponse { + status: String, + fees: Option>, + fee: u64, + quantization_mask: u64, + } + + let res: FeeResponse = self + .json_rpc_call( + "get_fee_estimate", + Some(json!({ "grace_blocks": GRACE_BLOCKS_FOR_FEE_ESTIMATE })), + ) + .await?; + + if res.status != "OK" { + Err(RpcError::InvalidFee)?; + } + + if let Some(fees) = res.fees { + // https://github.com/monero-project/monero/blob/94e67bf96bbc010241f29ada6abc89f49a81759c/ + // src/wallet/wallet2.cpp#L7615-L7620 + let priority_idx = usize::try_from(if priority.fee_priority() >= 4 { + 3 + } else { + priority.fee_priority().saturating_sub(1) + }) + .map_err(|_| RpcError::InvalidPriority)?; + + if priority_idx >= fees.len() { + Err(RpcError::InvalidPriority) + } else { + FeeRate::new(fees[priority_idx], res.quantization_mask) + } + } else { + // https://github.com/monero-project/monero/blob/94e67bf96bbc010241f29ada6abc89f49a81759c/ + // src/wallet/wallet2.cpp#L7569-L7584 + // https://github.com/monero-project/monero/blob/94e67bf96bbc010241f29ada6abc89f49a81759c/ + // src/wallet/wallet2.cpp#L7660-L7661 + let priority_idx = + usize::try_from(if priority.fee_priority() == 0 { 1 } else { priority.fee_priority() - 1 }) + .map_err(|_| RpcError::InvalidPriority)?; + let multipliers = [1, 5, 25, 1000]; + if priority_idx >= multipliers.len() { + // though not an RPC error, it seems sensible to treat as such + Err(RpcError::InvalidPriority)?; + } + let fee_multiplier = multipliers[priority_idx]; + + FeeRate::new(res.fee * fee_multiplier, res.quantization_mask) + } + } + + /// Publish a transaction. + async fn publish_transaction(&self, tx: &Transaction) -> Result<(), RpcError> { + #[allow(dead_code)] + #[derive(Debug, Deserialize)] + struct SendRawResponse { + status: String, + double_spend: bool, + fee_too_low: bool, + invalid_input: bool, + invalid_output: bool, + low_mixin: bool, + not_relayed: bool, + overspend: bool, + too_big: bool, + too_few_outputs: bool, + reason: String, + } + + let res: SendRawResponse = self + .rpc_call( + "send_raw_transaction", + Some(json!({ "tx_as_hex": hex::encode(tx.serialize()), "do_sanity_checks": false })), + ) + .await?; + + if res.status != "OK" { + Err(RpcError::InvalidTransaction(tx.hash()))?; + } + + Ok(()) + } + + /// Generate blocks, with the specified address receiving the block reward. + /// + /// Returns the hashes of the generated blocks and the last block's number. + async fn generate_blocks( + &self, + address: &Address, + block_count: usize, + ) -> Result<(Vec<[u8; 32]>, usize), RpcError> { + #[derive(Debug, Deserialize)] + struct BlocksResponse { + blocks: Vec, + height: usize, + } + + let res = self + .json_rpc_call::( + "generateblocks", + Some(json!({ + "wallet_address": address.to_string(), + "amount_of_blocks": block_count + })), + ) + .await?; + + let mut blocks = Vec::with_capacity(res.blocks.len()); + for block in res.blocks { + blocks.push(hash_hex(&block)?); + } + Ok((blocks, res.height)) + } + + /// Get the output indexes of the specified transaction. + async fn get_o_indexes(&self, hash: [u8; 32]) -> Result, RpcError> { + // Given the immaturity of Rust epee libraries, this is a homegrown one which is only validated + // to work against this specific function + + // Header for EPEE, an 8-byte magic and a version + const EPEE_HEADER: &[u8] = b"\x01\x11\x01\x01\x01\x01\x02\x01\x01"; + + // Read an EPEE VarInt, distinct from the VarInts used throughout the rest of the protocol + fn read_epee_vi(reader: &mut R) -> io::Result { + let vi_start = read_byte(reader)?; + let len = match vi_start & 0b11 { + 0 => 1, + 1 => 2, + 2 => 4, + 3 => 8, + _ => unreachable!(), + }; + let mut vi = u64::from(vi_start >> 2); + for i in 1 .. len { + vi |= u64::from(read_byte(reader)?) << (((i - 1) * 8) + 6); + } + Ok(vi) + } + + let mut request = EPEE_HEADER.to_vec(); + // Number of fields (shifted over 2 bits as the 2 LSBs are reserved for metadata) + request.push(1 << 2); + // Length of field name + request.push(4); + // Field name + request.extend(b"txid"); + // Type of field + request.push(10); + // Length of string, since this byte array is technically a string + request.push(32 << 2); + // The "string" + request.extend(hash); + + let indexes_buf = self.bin_call("get_o_indexes.bin", request).await?; + let mut indexes: &[u8] = indexes_buf.as_ref(); + + (|| { + let mut res = None; + let mut has_status = false; + + if read_bytes::<_, { EPEE_HEADER.len() }>(&mut indexes)? != EPEE_HEADER { + Err(io::Error::other("invalid header"))?; + } + + let read_object = |reader: &mut &[u8]| -> io::Result> { + // Read the amount of fields + let fields = read_byte(reader)? >> 2; + + for _ in 0 .. fields { + // Read the length of the field's name + let name_len = read_byte(reader)?; + // Read the name of the field + let name = read_raw_vec(read_byte, name_len.into(), reader)?; + + let type_with_array_flag = read_byte(reader)?; + // The type of this field, without the potentially set array flag + let kind = type_with_array_flag & (!0x80); + let has_array_flag = type_with_array_flag != kind; + + // Read this many instances of the field + let iters = if has_array_flag { read_epee_vi(reader)? } else { 1 }; + + // Check the field type + { + #[allow(clippy::match_same_arms)] + let (expected_type, expected_array_flag) = match name.as_slice() { + b"o_indexes" => (5, true), + b"status" => (10, false), + b"untrusted" => (11, false), + b"credits" => (5, false), + b"top_hash" => (10, false), + // On-purposely prints name as a byte vector to prevent printing arbitrary strings + // This is a self-describing format so we don't have to error here, yet we don't + // claim this to be a complete deserialization function + // To ensure it works for this specific use case, it's best to ensure it's limited + // to this specific use case (ensuring we have less variables to deal with) + _ => Err(io::Error::other(format!("unrecognized field in get_o_indexes: {name:?}")))?, + }; + if (expected_type != kind) || (expected_array_flag != has_array_flag) { + let fmt_array_bool = |array_bool| if array_bool { "array" } else { "not array" }; + Err(io::Error::other(format!( + "field {name:?} was {kind} ({}), expected {expected_type} ({})", + fmt_array_bool(has_array_flag), + fmt_array_bool(expected_array_flag) + )))?; + } + } + + let read_field_as_bytes = match kind { + /* + // i64 + 1 => |reader: &mut &[u8]| read_raw_vec(read_byte, 8, reader), + // i32 + 2 => |reader: &mut &[u8]| read_raw_vec(read_byte, 4, reader), + // i16 + 3 => |reader: &mut &[u8]| read_raw_vec(read_byte, 2, reader), + // i8 + 4 => |reader: &mut &[u8]| read_raw_vec(read_byte, 1, reader), + */ + // u64 + 5 => |reader: &mut &[u8]| read_raw_vec(read_byte, 8, reader), + /* + // u32 + 6 => |reader: &mut &[u8]| read_raw_vec(read_byte, 4, reader), + // u16 + 7 => |reader: &mut &[u8]| read_raw_vec(read_byte, 2, reader), + // u8 + 8 => |reader: &mut &[u8]| read_raw_vec(read_byte, 1, reader), + // double + 9 => |reader: &mut &[u8]| read_raw_vec(read_byte, 8, reader), + */ + // string, or any collection of bytes + 10 => |reader: &mut &[u8]| { + let len = read_epee_vi(reader)?; + read_raw_vec( + read_byte, + len.try_into().map_err(|_| io::Error::other("u64 length exceeded usize"))?, + reader, + ) + }, + // bool + 11 => |reader: &mut &[u8]| read_raw_vec(read_byte, 1, reader), + /* + // object, errors here as it shouldn't be used on this call + 12 => { + |_: &mut &[u8]| Err(io::Error::other("node used object in reply to get_o_indexes")) + } + // array, so far unused + 13 => |_: &mut &[u8]| Err(io::Error::other("node used the unused array type")), + */ + _ => |_: &mut &[u8]| Err(io::Error::other("node used an invalid type")), + }; + + let mut bytes_res = vec![]; + for _ in 0 .. iters { + bytes_res.push(read_field_as_bytes(reader)?); + } + + let mut actual_res = Vec::with_capacity(bytes_res.len()); + match name.as_slice() { + b"o_indexes" => { + for o_index in bytes_res { + actual_res.push(read_u64(&mut o_index.as_slice())?); + } + res = Some(actual_res); + } + b"status" => { + if bytes_res + .first() + .ok_or_else(|| io::Error::other("status was a 0-length array"))? + .as_slice() != + b"OK" + { + Err(io::Error::other("response wasn't OK"))?; + } + has_status = true; + } + b"untrusted" | b"credits" | b"top_hash" => continue, + _ => Err(io::Error::other("unrecognized field in get_o_indexes"))?, + } + } + + if !has_status { + Err(io::Error::other("response didn't contain a status"))?; + } + + // If the Vec was empty, it would've been omitted, hence the unwrap_or + Ok(res.unwrap_or(vec![])) + }; + + read_object(&mut indexes) + })() + .map_err(|e| RpcError::InvalidNode(format!("invalid binary response: {e:?}"))) + } +} + +/// A trait for any object which can be used to select RingCT decoys. +/// +/// An implementation is provided for any satisfier of `Rpc`. It is not recommended to use an `Rpc` +/// object to satisfy this. This should be satisfied by a local store of the output distribution, +/// both for performance and to prevent potential attacks a remote node can perform. +#[async_trait] +pub trait DecoyRpc: Sync + Clone + Debug { + /// Get the height the output distribution ends at. + /// + /// This is equivalent to the hight of the blockchain it's for. This is intended to be cheaper + /// than fetching the entire output distribution. + async fn get_output_distribution_end_height(&self) -> Result; + + /// Get the RingCT (zero-amount) output distribution. + /// + /// `range` is in terms of block numbers. The result may be smaller than the requested range if + /// the range starts before RingCT outputs were created on-chain. + async fn get_output_distribution( + &self, + range: impl Send + RangeBounds, + ) -> Result, RpcError>; + + /// Get the specified outputs from the RingCT (zero-amount) pool. + async fn get_outs(&self, indexes: &[u64]) -> Result, RpcError>; + + /// Get the specified outputs from the RingCT (zero-amount) pool, but only return them if their + /// timelock has been satisfied. + /// + /// The timelock being satisfied is distinct from being free of the 10-block lock applied to all + /// Monero transactions. + /// + /// The node is trusted for if the output is unlocked unless `fingerprintable_deterministic` is + /// set to true. If `fingerprintable_deterministic` is set to true, the node's local view isn't + /// used, yet the transaction's timelock is checked to be unlocked at the specified `height`. + /// This offers a deterministic decoy selection, yet is fingerprintable as time-based timelocks + /// aren't evaluated (and considered locked, preventing their selection). + async fn get_unlocked_outputs( + &self, + indexes: &[u64], + height: usize, + fingerprintable_deterministic: bool, + ) -> Result>, RpcError>; +} + +#[async_trait] +impl DecoyRpc for R { + async fn get_output_distribution_end_height(&self) -> Result { + ::get_height(self).await + } + + async fn get_output_distribution( + &self, + range: impl Send + RangeBounds, + ) -> Result, RpcError> { + #[derive(Default, Debug, Deserialize)] + struct Distribution { + distribution: Vec, + // A blockchain with just its genesis block has a height of 1 + start_height: usize, + } + + #[derive(Debug, Deserialize)] + struct Distributions { + distributions: [Distribution; 1], + status: String, + } + + let from = match range.start_bound() { + Bound::Included(from) => *from, + Bound::Excluded(from) => from + .checked_add(1) + .ok_or_else(|| RpcError::InternalError("range's from wasn't representable".to_string()))?, + Bound::Unbounded => 0, + }; + let to = match range.end_bound() { + Bound::Included(to) => *to, + Bound::Excluded(to) => to + .checked_sub(1) + .ok_or_else(|| RpcError::InternalError("range's to wasn't representable".to_string()))?, + Bound::Unbounded => self.get_height().await? - 1, + }; + if from > to { + Err(RpcError::InternalError(format!( + "malformed range: inclusive start {from}, inclusive end {to}" + )))?; + } + + let zero_zero_case = (from == 0) && (to == 0); + let distributions: Distributions = self + .json_rpc_call( + "get_output_distribution", + Some(json!({ + "binary": false, + "amounts": [0], + "cumulative": true, + // These are actually block numbers, not heights + "from_height": from, + "to_height": if zero_zero_case { 1 } else { to }, + })), + ) + .await?; + + if distributions.status != "OK" { + Err(RpcError::ConnectionError( + "node couldn't service this request for the output distribution".to_string(), + ))?; + } + + let mut distributions = distributions.distributions; + let Distribution { start_height, mut distribution } = core::mem::take(&mut distributions[0]); + // start_height is also actually a block number, and it should be at least `from` + // It may be after depending on when these outputs first appeared on the blockchain + // Unfortunately, we can't validate without a binary search to find the RingCT activation block + // and an iterative search from there, so we solely sanity check it + if start_height < from { + Err(RpcError::InvalidNode(format!( + "requested distribution from {from} and got from {start_height}" + )))?; + } + // It shouldn't be after `to` though + if start_height > to { + Err(RpcError::InvalidNode(format!( + "requested distribution to {to} and got from {start_height}" + )))?; + } + + let expected_len = if zero_zero_case { 2 } else { (to - start_height) + 1 }; + // Yet this is actually a height + if expected_len != distribution.len() { + Err(RpcError::InvalidNode(format!( + "distribution length ({}) wasn't of the requested length ({})", + distribution.len(), + expected_len + )))?; + } + // Requesting to = 0 returns the distribution for the entire chain + // We work-around this by requesting 0, 1 (yielding two blocks), then popping the second block + if zero_zero_case { + distribution.pop(); + } + Ok(distribution) + } + + async fn get_outs(&self, indexes: &[u64]) -> Result, RpcError> { + #[derive(Debug, Deserialize)] + struct OutputResponse { + height: usize, + unlocked: bool, + key: String, + mask: String, + txid: String, + } + + #[derive(Debug, Deserialize)] + struct OutsResponse { + status: String, + outs: Vec, + } + + // https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454 + // /src/rpc/core_rpc_server.cpp#L67 + const MAX_OUTS: usize = 5000; + + let mut res = Vec::with_capacity(indexes.len()); + for indexes in indexes.chunks(MAX_OUTS) { + let rpc_res: OutsResponse = self + .rpc_call( + "get_outs", + Some(json!({ + "get_txid": true, + "outputs": indexes.iter().map(|o| json!({ + "amount": 0, + "index": o + })).collect::>() + })), + ) + .await?; + + if rpc_res.status != "OK" { + Err(RpcError::InvalidNode("bad response to get_outs".to_string()))?; + } + + res.extend( + rpc_res + .outs + .into_iter() + .map(|output| { + Ok(OutputInformation { + height: output.height, + unlocked: output.unlocked, + key: CompressedEdwardsY( + rpc_hex(&output.key)? + .try_into() + .map_err(|_| RpcError::InvalidNode("output key wasn't 32 bytes".to_string()))?, + ), + commitment: rpc_point(&output.mask)?, + transaction: hash_hex(&output.txid)?, + }) + }) + .collect::, RpcError>>()?, + ); + } + + Ok(res) + } + + async fn get_unlocked_outputs( + &self, + indexes: &[u64], + height: usize, + fingerprintable_deterministic: bool, + ) -> Result>, RpcError> { + let outs = self.get_outs(indexes).await?; + + // Only need to fetch txs to do deterministic check on timelock + let txs = if fingerprintable_deterministic { + self.get_transactions(&outs.iter().map(|out| out.transaction).collect::>()).await? + } else { + vec![] + }; + + // TODO: https://github.com/serai-dex/serai/issues/104 + outs + .iter() + .enumerate() + .map(|(i, out)| { + // Allow keys to be invalid, though if they are, return None to trigger selection of a new + // decoy + // Only valid keys can be used in CLSAG proofs, hence the need for re-selection, yet + // invalid keys may honestly exist on the blockchain + let Some(key) = out.key.decompress() else { + return Ok(None); + }; + Ok(Some([key, out.commitment]).filter(|_| { + if fingerprintable_deterministic { + // https://github.com/monero-project/monero/blob + // /cc73fe71162d564ffda8e549b79a350bca53c454/src/cryptonote_core/blockchain.cpp#L90 + const ACCEPTED_TIMELOCK_DELTA: usize = 1; + + // https://github.com/monero-project/monero/blob + // /cc73fe71162d564ffda8e549b79a350bca53c454/src/cryptonote_core/blockchain.cpp#L3836 + ((out.height + DEFAULT_LOCK_WINDOW) <= height) && + (Timelock::Block(height - 1 + ACCEPTED_TIMELOCK_DELTA) >= + txs[i].prefix().additional_timelock) + } else { + out.unlocked + } + })) + }) + .collect() + } +} diff --git a/networks/monero/src/block.rs b/networks/monero/src/block.rs new file mode 100644 index 00000000..62a77f8b --- /dev/null +++ b/networks/monero/src/block.rs @@ -0,0 +1,152 @@ +use std_shims::{ + vec, + vec::Vec, + io::{self, Read, Write}, +}; + +use crate::{ + io::*, + primitives::keccak256, + merkle::merkle_root, + transaction::{Input, Transaction}, +}; + +const CORRECT_BLOCK_HASH_202612: [u8; 32] = + hex_literal::hex!("426d16cff04c71f8b16340b722dc4010a2dd3831c22041431f772547ba6e331a"); +const EXISTING_BLOCK_HASH_202612: [u8; 32] = + hex_literal::hex!("bbd604d2ba11ba27935e006ed39c9bfdd99b76bf4a50654bc1e1e61217962698"); + +/// A Monero block's header. +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct BlockHeader { + /// The hard fork of the protocol this block follows. + /// + /// Per the C++ codebase, this is the `major_version`. + pub hardfork_version: u8, + /// A signal for a proposed hard fork. + /// + /// Per the C++ codebase, this is the `minor_version`. + pub hardfork_signal: u8, + /// Seconds since the epoch. + pub timestamp: u64, + /// The previous block's hash. + pub previous: [u8; 32], + /// The nonce used to mine the block. + /// + /// Miners should increment this while attempting to find a block with a hash satisfying the PoW + /// rules. + pub nonce: u32, +} + +impl BlockHeader { + /// Write the BlockHeader. + pub fn write(&self, w: &mut W) -> io::Result<()> { + write_varint(&self.hardfork_version, w)?; + write_varint(&self.hardfork_signal, w)?; + write_varint(&self.timestamp, w)?; + w.write_all(&self.previous)?; + w.write_all(&self.nonce.to_le_bytes()) + } + + /// Serialize the BlockHeader to a `Vec`. + pub fn serialize(&self) -> Vec { + let mut serialized = vec![]; + self.write(&mut serialized).unwrap(); + serialized + } + + /// Read a BlockHeader. + pub fn read(r: &mut R) -> io::Result { + Ok(BlockHeader { + hardfork_version: read_varint(r)?, + hardfork_signal: read_varint(r)?, + timestamp: read_varint(r)?, + previous: read_bytes(r)?, + nonce: read_bytes(r).map(u32::from_le_bytes)?, + }) + } +} + +/// A Monero block. +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct Block { + /// The block's header. + pub header: BlockHeader, + /// The miner's transaction. + pub miner_transaction: Transaction, + /// The transactions within this block. + pub transactions: Vec<[u8; 32]>, +} + +impl Block { + /// The zero-index position of this block within the blockchain. + /// + /// This information comes from the Block's miner transaction. If the miner transaction isn't + /// structed as expected, this will return None. + pub fn number(&self) -> Option { + match &self.miner_transaction { + Transaction::V1 { prefix, .. } | Transaction::V2 { prefix, .. } => { + match prefix.inputs.first() { + Some(Input::Gen(number)) => Some(*number), + _ => None, + } + } + } + } + + /// Write the Block. + pub fn write(&self, w: &mut W) -> io::Result<()> { + self.header.write(w)?; + self.miner_transaction.write(w)?; + write_varint(&self.transactions.len(), w)?; + for tx in &self.transactions { + w.write_all(tx)?; + } + Ok(()) + } + + /// Serialize the Block to a `Vec`. + pub fn serialize(&self) -> Vec { + let mut serialized = vec![]; + self.write(&mut serialized).unwrap(); + serialized + } + + /// Serialize the block as required for the proof of work hash. + /// + /// This is distinct from the serialization required for the block hash. To get the block hash, + /// use the [`Block::hash`] function. + pub fn serialize_pow_hash(&self) -> Vec { + let mut blob = self.header.serialize(); + blob.extend_from_slice(&merkle_root(self.miner_transaction.hash(), &self.transactions)); + write_varint(&(1 + u64::try_from(self.transactions.len()).unwrap()), &mut blob).unwrap(); + blob + } + + /// Get the hash of this block. + pub fn hash(&self) -> [u8; 32] { + let mut hashable = self.serialize_pow_hash(); + // Monero pre-appends a VarInt of the block-to-hash'ss length before getting the block hash, + // but doesn't do this when getting the proof of work hash :) + let mut hashing_blob = Vec::with_capacity(9 + hashable.len()); + write_varint(&u64::try_from(hashable.len()).unwrap(), &mut hashing_blob).unwrap(); + hashing_blob.append(&mut hashable); + + let hash = keccak256(hashing_blob); + if hash == CORRECT_BLOCK_HASH_202612 { + return EXISTING_BLOCK_HASH_202612; + }; + hash + } + + /// Read a Block. + pub fn read(r: &mut R) -> io::Result { + Ok(Block { + header: BlockHeader::read(r)?, + miner_transaction: Transaction::read(r)?, + transactions: (0_usize .. read_varint(r)?) + .map(|_| read_bytes(r)) + .collect::>()?, + }) + } +} diff --git a/networks/monero/src/lib.rs b/networks/monero/src/lib.rs new file mode 100644 index 00000000..2eaa63e1 --- /dev/null +++ b/networks/monero/src/lib.rs @@ -0,0 +1,39 @@ +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![doc = include_str!("../README.md")] +#![deny(missing_docs)] +#![cfg_attr(not(feature = "std"), no_std)] + +pub use monero_io as io; +pub use monero_generators as generators; +pub use monero_primitives as primitives; + +mod merkle; + +/// Ring Signature structs and functionality. +pub mod ring_signatures; + +/// RingCT structs and functionality. +pub mod ringct; + +/// Transaction structs and functionality. +pub mod transaction; +/// Block structs and functionality. +pub mod block; + +#[cfg(test)] +mod tests; + +/// The minimum amount of blocks an output is locked for. +/// +/// If Monero suffered a re-organization, any transactions which selected decoys belonging to +/// recent blocks would become invalidated. Accordingly, transactions must use decoys which are +/// presumed to not be invalidated in the future. If wallets only selected n-block-old outputs as +/// decoys, then any ring member within the past n blocks would have to be the real spend. +/// Preventing this at the consensus layer ensures privacy and integrity. +pub const DEFAULT_LOCK_WINDOW: usize = 10; + +/// The minimum amount of blocks a coinbase output is locked for. +pub const COINBASE_LOCK_WINDOW: usize = 60; + +/// Monero's block time target, in seconds. +pub const BLOCK_TIME: usize = 120; diff --git a/coins/monero/src/merkle.rs b/networks/monero/src/merkle.rs similarity index 86% rename from coins/monero/src/merkle.rs rename to networks/monero/src/merkle.rs index 8123b902..6c689618 100644 --- a/coins/monero/src/merkle.rs +++ b/networks/monero/src/merkle.rs @@ -1,11 +1,11 @@ use std_shims::vec::Vec; -use crate::hash; +use crate::primitives::keccak256; pub(crate) fn merkle_root(root: [u8; 32], leafs: &[[u8; 32]]) -> [u8; 32] { match leafs.len() { 0 => root, - 1 => hash(&[root, leafs[0]].concat()), + 1 => keccak256([root, leafs[0]].concat()), _ => { let mut hashes = Vec::with_capacity(1 + leafs.len()); hashes.push(root); @@ -29,7 +29,7 @@ pub(crate) fn merkle_root(root: [u8; 32], leafs: &[[u8; 32]]) -> [u8; 32] { let mut paired_hashes = Vec::with_capacity(overage); while let Some(left) = rightmost.next() { let right = rightmost.next().unwrap(); - paired_hashes.push(hash(&[left.as_ref(), &right].concat())); + paired_hashes.push(keccak256([left.as_ref(), &right].concat())); } drop(rightmost); @@ -42,7 +42,7 @@ pub(crate) fn merkle_root(root: [u8; 32], leafs: &[[u8; 32]]) -> [u8; 32] { while hashes.len() > 1 { let mut i = 0; while i < hashes.len() { - new_hashes.push(hash(&[hashes[i], hashes[i + 1]].concat())); + new_hashes.push(keccak256([hashes[i], hashes[i + 1]].concat())); i += 2; } diff --git a/networks/monero/src/ring_signatures.rs b/networks/monero/src/ring_signatures.rs new file mode 100644 index 00000000..c76d8d89 --- /dev/null +++ b/networks/monero/src/ring_signatures.rs @@ -0,0 +1,101 @@ +use std_shims::{ + io::{self, *}, + vec::Vec, +}; + +use zeroize::Zeroize; + +use curve25519_dalek::{EdwardsPoint, Scalar}; + +use crate::{io::*, generators::hash_to_point, primitives::keccak256_to_scalar}; + +#[derive(Clone, PartialEq, Eq, Debug, Zeroize)] +pub(crate) struct Signature { + #[cfg(test)] + pub(crate) c: Scalar, + #[cfg(test)] + pub(crate) s: Scalar, + #[cfg(not(test))] + c: Scalar, + #[cfg(not(test))] + s: Scalar, +} + +impl Signature { + fn write(&self, w: &mut W) -> io::Result<()> { + write_scalar(&self.c, w)?; + write_scalar(&self.s, w)?; + Ok(()) + } + + fn read(r: &mut R) -> io::Result { + Ok(Signature { c: read_scalar(r)?, s: read_scalar(r)? }) + } +} + +/// A ring signature. +/// +/// This was used by the original Cryptonote transaction protocol and was deprecated with RingCT. +#[derive(Clone, PartialEq, Eq, Debug, Zeroize)] +pub struct RingSignature { + #[cfg(test)] + pub(crate) sigs: Vec, + #[cfg(not(test))] + sigs: Vec, +} + +impl RingSignature { + /// Write the RingSignature. + pub fn write(&self, w: &mut W) -> io::Result<()> { + for sig in &self.sigs { + sig.write(w)?; + } + Ok(()) + } + + /// Read a RingSignature. + pub fn read(members: usize, r: &mut R) -> io::Result { + Ok(RingSignature { sigs: read_raw_vec(Signature::read, members, r)? }) + } + + /// Verify the ring signature. + pub fn verify(&self, msg: &[u8; 32], ring: &[EdwardsPoint], key_image: &EdwardsPoint) -> bool { + if ring.len() != self.sigs.len() { + return false; + } + + let mut buf = Vec::with_capacity(32 + (2 * 32 * ring.len())); + buf.extend_from_slice(msg); + + let mut sum = Scalar::ZERO; + for (ring_member, sig) in ring.iter().zip(&self.sigs) { + /* + The traditional Schnorr signature is: + r = sample() + c = H(r G || m) + s = r - c x + Verified as: + s G + c A == R + + Each ring member here performs a dual-Schnorr signature for: + s G + c A + s HtP(A) + c K + Where the transcript is pushed both these values, r G, r HtP(A) for the real spend. + This also serves as a DLEq proof between the key and the key image. + + Checking sum(c) == H(transcript) acts a disjunction, where any one of the `c`s can be + modified to cause the intended sum, if and only if a corresponding `s` value is known. + */ + + #[allow(non_snake_case)] + let Li = EdwardsPoint::vartime_double_scalar_mul_basepoint(&sig.c, ring_member, &sig.s); + buf.extend_from_slice(Li.compress().as_bytes()); + #[allow(non_snake_case)] + let Ri = (sig.s * hash_to_point(ring_member.compress().to_bytes())) + (sig.c * key_image); + buf.extend_from_slice(Ri.compress().as_bytes()); + + sum += sig.c; + } + sum == keccak256_to_scalar(buf) + } +} diff --git a/networks/monero/src/ringct.rs b/networks/monero/src/ringct.rs new file mode 100644 index 00000000..4cc42570 --- /dev/null +++ b/networks/monero/src/ringct.rs @@ -0,0 +1,470 @@ +use std_shims::{ + vec, + vec::Vec, + io::{self, Read, Write}, +}; + +use zeroize::Zeroize; + +use curve25519_dalek::edwards::EdwardsPoint; + +pub use monero_mlsag as mlsag; +pub use monero_clsag as clsag; +pub use monero_borromean as borromean; +pub use monero_bulletproofs as bulletproofs; + +use crate::{ + io::*, + ringct::{mlsag::Mlsag, clsag::Clsag, borromean::BorromeanRange, bulletproofs::Bulletproof}, +}; + +/// An encrypted amount. +#[derive(Clone, PartialEq, Eq, Debug)] +pub enum EncryptedAmount { + /// The original format for encrypted amounts. + Original { + /// A mask used with a mask derived from the shared secret to encrypt the amount. + mask: [u8; 32], + /// The amount, as a scalar, encrypted. + amount: [u8; 32], + }, + /// The "compact" format for encrypted amounts. + Compact { + /// The amount, as a u64, encrypted. + amount: [u8; 8], + }, +} + +impl EncryptedAmount { + /// Read an EncryptedAmount from a reader. + pub fn read(compact: bool, r: &mut R) -> io::Result { + Ok(if !compact { + EncryptedAmount::Original { mask: read_bytes(r)?, amount: read_bytes(r)? } + } else { + EncryptedAmount::Compact { amount: read_bytes(r)? } + }) + } + + /// Write the EncryptedAmount to a writer. + pub fn write(&self, w: &mut W) -> io::Result<()> { + match self { + EncryptedAmount::Original { mask, amount } => { + w.write_all(mask)?; + w.write_all(amount) + } + EncryptedAmount::Compact { amount } => w.write_all(amount), + } + } +} + +/// The type of the RingCT data. +#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)] +pub enum RctType { + /// One MLSAG for multiple inputs and Borromean range proofs. + /// + /// This aligns with RCTTypeFull. + AggregateMlsagBorromean, + // One MLSAG for each input and a Borromean range proof. + /// + /// This aligns with RCTTypeSimple. + MlsagBorromean, + // One MLSAG for each input and a Bulletproof. + /// + /// This aligns with RCTTypeBulletproof. + MlsagBulletproofs, + /// One MLSAG for each input and a Bulletproof, yet using EncryptedAmount::Compact. + /// + /// This aligns with RCTTypeBulletproof2. + MlsagBulletproofsCompactAmount, + /// One CLSAG for each input and a Bulletproof. + /// + /// This aligns with RCTTypeCLSAG. + ClsagBulletproof, + /// One CLSAG for each input and a Bulletproof+. + /// + /// This aligns with RCTTypeBulletproofPlus. + ClsagBulletproofPlus, +} + +impl From for u8 { + fn from(rct_type: RctType) -> u8 { + match rct_type { + RctType::AggregateMlsagBorromean => 1, + RctType::MlsagBorromean => 2, + RctType::MlsagBulletproofs => 3, + RctType::MlsagBulletproofsCompactAmount => 4, + RctType::ClsagBulletproof => 5, + RctType::ClsagBulletproofPlus => 6, + } + } +} + +impl TryFrom for RctType { + type Error = (); + fn try_from(byte: u8) -> Result { + Ok(match byte { + 1 => RctType::AggregateMlsagBorromean, + 2 => RctType::MlsagBorromean, + 3 => RctType::MlsagBulletproofs, + 4 => RctType::MlsagBulletproofsCompactAmount, + 5 => RctType::ClsagBulletproof, + 6 => RctType::ClsagBulletproofPlus, + _ => Err(())?, + }) + } +} + +impl RctType { + /// True if this RctType uses compact encrypted amounts, false otherwise. + pub fn compact_encrypted_amounts(&self) -> bool { + match self { + RctType::AggregateMlsagBorromean | RctType::MlsagBorromean | RctType::MlsagBulletproofs => { + false + } + RctType::MlsagBulletproofsCompactAmount | + RctType::ClsagBulletproof | + RctType::ClsagBulletproofPlus => true, + } + } + + /// True if this RctType uses a Bulletproof, false otherwise. + pub(crate) fn bulletproof(&self) -> bool { + match self { + RctType::MlsagBulletproofs | + RctType::MlsagBulletproofsCompactAmount | + RctType::ClsagBulletproof => true, + RctType::AggregateMlsagBorromean | + RctType::MlsagBorromean | + RctType::ClsagBulletproofPlus => false, + } + } + + /// True if this RctType uses a Bulletproof+, false otherwise. + pub(crate) fn bulletproof_plus(&self) -> bool { + match self { + RctType::ClsagBulletproofPlus => true, + RctType::AggregateMlsagBorromean | + RctType::MlsagBorromean | + RctType::MlsagBulletproofs | + RctType::MlsagBulletproofsCompactAmount | + RctType::ClsagBulletproof => false, + } + } +} + +/// The base of the RingCT data. +/// +/// This excludes all proofs (which once initially verified do not need to be kept around) and +/// solely keeps data which either impacts the effects of the transactions or is needed to scan it. +/// +/// The one exception for this is `pseudo_outs`, which was originally present here yet moved to +/// RctPrunable in a later hard fork (causing it to be present in both). +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct RctBase { + /// The fee used by this transaction. + pub fee: u64, + /// The re-randomized amount commitments used within inputs. + /// + /// This field was deprecated and is empty for modern RctTypes. + pub pseudo_outs: Vec, + /// The encrypted amounts for the recipients to decrypt. + pub encrypted_amounts: Vec, + /// The output commitments. + pub commitments: Vec, +} + +impl RctBase { + /// Write the RctBase. + pub fn write(&self, w: &mut W, rct_type: RctType) -> io::Result<()> { + w.write_all(&[u8::from(rct_type)])?; + + write_varint(&self.fee, w)?; + if rct_type == RctType::MlsagBorromean { + write_raw_vec(write_point, &self.pseudo_outs, w)?; + } + for encrypted_amount in &self.encrypted_amounts { + encrypted_amount.write(w)?; + } + write_raw_vec(write_point, &self.commitments, w) + } + + /// Read a RctBase. + pub fn read( + inputs: usize, + outputs: usize, + r: &mut R, + ) -> io::Result> { + let rct_type = read_byte(r)?; + if rct_type == 0 { + return Ok(None); + } + let rct_type = + RctType::try_from(rct_type).map_err(|()| io::Error::other("invalid RCT type"))?; + + match rct_type { + RctType::AggregateMlsagBorromean | RctType::MlsagBorromean => {} + RctType::MlsagBulletproofs | + RctType::MlsagBulletproofsCompactAmount | + RctType::ClsagBulletproof | + RctType::ClsagBulletproofPlus => { + if outputs == 0 { + // Because the Bulletproofs(+) layout must be canonical, there must be 1 Bulletproof if + // Bulletproofs are in use + // If there are Bulletproofs, there must be a matching amount of outputs, implicitly + // banning 0 outputs + // Since HF 12 (CLSAG being 13), a 2-output minimum has also been enforced + Err(io::Error::other("RCT with Bulletproofs(+) had 0 outputs"))?; + } + } + } + + Ok(Some(( + rct_type, + RctBase { + fee: read_varint(r)?, + // Only read pseudo_outs if they have yet to be moved to RctPrunable + // This would apply to AggregateMlsagBorromean and MlsagBorromean, except + // AggregateMlsagBorromean doesn't use pseudo_outs due to using the sum of the output + // commitments directly as the effective singular pseudo-out + pseudo_outs: if rct_type == RctType::MlsagBorromean { + read_raw_vec(read_point, inputs, r)? + } else { + vec![] + }, + encrypted_amounts: (0 .. outputs) + .map(|_| EncryptedAmount::read(rct_type.compact_encrypted_amounts(), r)) + .collect::>()?, + commitments: read_raw_vec(read_point, outputs, r)?, + }, + ))) + } +} + +/// The prunable part of the RingCT data. +#[derive(Clone, PartialEq, Eq, Debug)] +pub enum RctPrunable { + /// An aggregate MLSAG with Borromean range proofs. + AggregateMlsagBorromean { + /// The aggregate MLSAG ring signature. + mlsag: Mlsag, + /// The Borromean range proofs for each output. + borromean: Vec, + }, + /// MLSAGs with Borromean range proofs. + MlsagBorromean { + /// The MLSAG ring signatures for each input. + mlsags: Vec, + /// The Borromean range proofs for each output. + borromean: Vec, + }, + /// MLSAGs with Bulletproofs. + MlsagBulletproofs { + /// The MLSAG ring signatures for each input. + mlsags: Vec, + /// The re-blinded commitments for the outputs being spent. + pseudo_outs: Vec, + /// The aggregate Bulletproof, proving the outputs are within range. + bulletproof: Bulletproof, + }, + /// MLSAGs with Bulletproofs and compact encrypted amounts. + /// + /// This has an identical layout to MlsagBulletproofs and is interpreted the exact same way. It's + /// only differentiated to ensure discovery of the correct RctType. + MlsagBulletproofsCompactAmount { + /// The MLSAG ring signatures for each input. + mlsags: Vec, + /// The re-blinded commitments for the outputs being spent. + pseudo_outs: Vec, + /// The aggregate Bulletproof, proving the outputs are within range. + bulletproof: Bulletproof, + }, + /// CLSAGs with Bulletproofs(+). + Clsag { + /// The CLSAGs for each input. + clsags: Vec, + /// The re-blinded commitments for the outputs being spent. + pseudo_outs: Vec, + /// The aggregate Bulletproof(+), proving the outputs are within range. + bulletproof: Bulletproof, + }, +} + +impl RctPrunable { + /// Write the RctPrunable. + pub fn write(&self, w: &mut W, rct_type: RctType) -> io::Result<()> { + match self { + RctPrunable::AggregateMlsagBorromean { borromean, mlsag } => { + write_raw_vec(BorromeanRange::write, borromean, w)?; + mlsag.write(w) + } + RctPrunable::MlsagBorromean { borromean, mlsags } => { + write_raw_vec(BorromeanRange::write, borromean, w)?; + write_raw_vec(Mlsag::write, mlsags, w) + } + RctPrunable::MlsagBulletproofs { bulletproof, mlsags, pseudo_outs } | + RctPrunable::MlsagBulletproofsCompactAmount { bulletproof, mlsags, pseudo_outs } => { + if rct_type == RctType::MlsagBulletproofs { + w.write_all(&1u32.to_le_bytes())?; + } else { + w.write_all(&[1])?; + } + bulletproof.write(w)?; + + write_raw_vec(Mlsag::write, mlsags, w)?; + write_raw_vec(write_point, pseudo_outs, w) + } + RctPrunable::Clsag { bulletproof, clsags, pseudo_outs } => { + w.write_all(&[1])?; + bulletproof.write(w)?; + + write_raw_vec(Clsag::write, clsags, w)?; + write_raw_vec(write_point, pseudo_outs, w) + } + } + } + + /// Serialize the RctPrunable to a `Vec`. + pub fn serialize(&self, rct_type: RctType) -> Vec { + let mut serialized = vec![]; + self.write(&mut serialized, rct_type).unwrap(); + serialized + } + + /// Read a RctPrunable. + pub fn read( + rct_type: RctType, + ring_length: usize, + inputs: usize, + outputs: usize, + r: &mut R, + ) -> io::Result { + Ok(match rct_type { + RctType::AggregateMlsagBorromean => RctPrunable::AggregateMlsagBorromean { + borromean: read_raw_vec(BorromeanRange::read, outputs, r)?, + mlsag: Mlsag::read(ring_length, inputs + 1, r)?, + }, + RctType::MlsagBorromean => RctPrunable::MlsagBorromean { + borromean: read_raw_vec(BorromeanRange::read, outputs, r)?, + mlsags: (0 .. inputs).map(|_| Mlsag::read(ring_length, 2, r)).collect::>()?, + }, + RctType::MlsagBulletproofs | RctType::MlsagBulletproofsCompactAmount => { + let bulletproof = { + if (if rct_type == RctType::MlsagBulletproofs { + u64::from(read_u32(r)?) + } else { + read_varint(r)? + }) != 1 + { + Err(io::Error::other("n bulletproofs instead of one"))?; + } + Bulletproof::read(r)? + }; + let mlsags = + (0 .. inputs).map(|_| Mlsag::read(ring_length, 2, r)).collect::>()?; + let pseudo_outs = read_raw_vec(read_point, inputs, r)?; + if rct_type == RctType::MlsagBulletproofs { + RctPrunable::MlsagBulletproofs { bulletproof, mlsags, pseudo_outs } + } else { + debug_assert_eq!(rct_type, RctType::MlsagBulletproofsCompactAmount); + RctPrunable::MlsagBulletproofsCompactAmount { bulletproof, mlsags, pseudo_outs } + } + } + RctType::ClsagBulletproof | RctType::ClsagBulletproofPlus => RctPrunable::Clsag { + bulletproof: { + if read_varint::<_, u64>(r)? != 1 { + Err(io::Error::other("n bulletproofs instead of one"))?; + } + (if rct_type == RctType::ClsagBulletproof { + Bulletproof::read + } else { + Bulletproof::read_plus + })(r)? + }, + clsags: (0 .. inputs).map(|_| Clsag::read(ring_length, r)).collect::>()?, + pseudo_outs: read_raw_vec(read_point, inputs, r)?, + }, + }) + } + + /// Write the RctPrunable as necessary for signing the signature. + pub(crate) fn signature_write(&self, w: &mut W) -> io::Result<()> { + match self { + RctPrunable::AggregateMlsagBorromean { borromean, .. } | + RctPrunable::MlsagBorromean { borromean, .. } => { + borromean.iter().try_for_each(|rs| rs.write(w)) + } + RctPrunable::MlsagBulletproofs { bulletproof, .. } | + RctPrunable::MlsagBulletproofsCompactAmount { bulletproof, .. } | + RctPrunable::Clsag { bulletproof, .. } => bulletproof.signature_write(w), + } + } +} + +/// The RingCT proofs. +/// +/// This contains both the RctBase and RctPrunable structs. +/// +/// The C++ codebase refers to this as rct_signatures. +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct RctProofs { + /// The data necessary for handling this transaction. + pub base: RctBase, + /// The data necessary for verifying this transaction. + pub prunable: RctPrunable, +} + +impl RctProofs { + /// RctType for a given RctProofs struct. + pub fn rct_type(&self) -> RctType { + match &self.prunable { + RctPrunable::AggregateMlsagBorromean { .. } => RctType::AggregateMlsagBorromean, + RctPrunable::MlsagBorromean { .. } => RctType::MlsagBorromean, + RctPrunable::MlsagBulletproofs { .. } => RctType::MlsagBulletproofs, + RctPrunable::MlsagBulletproofsCompactAmount { .. } => RctType::MlsagBulletproofsCompactAmount, + RctPrunable::Clsag { bulletproof, .. } => { + if matches!(bulletproof, Bulletproof::Original { .. }) { + RctType::ClsagBulletproof + } else { + RctType::ClsagBulletproofPlus + } + } + } + } + + /// Write the RctProofs. + pub fn write(&self, w: &mut W) -> io::Result<()> { + let rct_type = self.rct_type(); + self.base.write(w, rct_type)?; + self.prunable.write(w, rct_type) + } + + /// Serialize the RctProofs to a `Vec`. + pub fn serialize(&self) -> Vec { + let mut serialized = vec![]; + self.write(&mut serialized).unwrap(); + serialized + } + + /// Read a RctProofs. + pub fn read( + ring_length: usize, + inputs: usize, + outputs: usize, + r: &mut R, + ) -> io::Result> { + let Some((rct_type, base)) = RctBase::read(inputs, outputs, r)? else { return Ok(None) }; + Ok(Some(RctProofs { + base, + prunable: RctPrunable::read(rct_type, ring_length, inputs, outputs, r)?, + })) + } +} + +/// A pruned set of RingCT proofs. +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct PrunedRctProofs { + /// The type of RctProofs this used to be. + pub rct_type: RctType, + /// The data necessary for handling this transaction. + pub base: RctBase, +} diff --git a/networks/monero/src/tests/mod.rs b/networks/monero/src/tests/mod.rs new file mode 100644 index 00000000..8d760e2a --- /dev/null +++ b/networks/monero/src/tests/mod.rs @@ -0,0 +1 @@ +mod transaction; diff --git a/networks/monero/src/tests/transaction.rs b/networks/monero/src/tests/transaction.rs new file mode 100644 index 00000000..165a36c3 --- /dev/null +++ b/networks/monero/src/tests/transaction.rs @@ -0,0 +1,287 @@ +use curve25519_dalek::{ + edwards::{CompressedEdwardsY, EdwardsPoint}, + scalar::Scalar, +}; + +use serde_json::Value; + +use crate::{ + ringct::RctPrunable, + transaction::{NotPruned, Transaction, Timelock, Input}, +}; + +const TRANSACTIONS: &str = include_str!("./vectors/transactions.json"); +const CLSAG_TX: &str = include_str!("./vectors/clsag_tx.json"); +const RING_DATA: &str = include_str!("./vectors/ring_data.json"); + +#[derive(serde::Deserialize)] +struct Vector { + id: String, + hex: String, + signature_hash: String, + tx: Value, +} + +fn tx_vectors() -> Vec { + serde_json::from_str(TRANSACTIONS).unwrap() +} + +fn point(hex: &Value) -> EdwardsPoint { + CompressedEdwardsY(hex::decode(hex.as_str().unwrap()).unwrap().try_into().unwrap()) + .decompress() + .unwrap() +} + +fn scalar(hex: &Value) -> Scalar { + Scalar::from_canonical_bytes(hex::decode(hex.as_str().unwrap()).unwrap().try_into().unwrap()) + .unwrap() +} + +fn point_vector(val: &Value) -> Vec { + let mut v = vec![]; + for hex in val.as_array().unwrap() { + v.push(point(hex)); + } + v +} + +fn scalar_vector(val: &Value) -> Vec { + let mut v = vec![]; + for hex in val.as_array().unwrap() { + v.push(scalar(hex)); + } + v +} + +#[test] +fn parse() { + for v in tx_vectors() { + let tx = + Transaction::::read(&mut hex::decode(v.hex.clone()).unwrap().as_slice()).unwrap(); + + // check version + assert_eq!(tx.version(), v.tx["version"]); + + // check unlock time + match tx.prefix().additional_timelock { + Timelock::None => assert_eq!(0, v.tx["unlock_time"]), + Timelock::Block(h) => assert_eq!(h, v.tx["unlock_time"]), + Timelock::Time(t) => assert_eq!(t, v.tx["unlock_time"]), + } + + // check inputs + let inputs = v.tx["vin"].as_array().unwrap(); + assert_eq!(tx.prefix().inputs.len(), inputs.len()); + for (i, input) in tx.prefix().inputs.iter().enumerate() { + match input { + Input::Gen(h) => assert_eq!(*h, inputs[i]["gen"]["height"]), + Input::ToKey { amount, key_offsets, key_image } => { + let key = &inputs[i]["key"]; + assert_eq!(amount.unwrap_or(0), key["amount"]); + assert_eq!(*key_image, point(&key["k_image"])); + assert_eq!(key_offsets, key["key_offsets"].as_array().unwrap()); + } + } + } + + // check outputs + let outputs = v.tx["vout"].as_array().unwrap(); + assert_eq!(tx.prefix().outputs.len(), outputs.len()); + for (i, output) in tx.prefix().outputs.iter().enumerate() { + assert_eq!(output.amount.unwrap_or(0), outputs[i]["amount"]); + if output.view_tag.is_some() { + assert_eq!(output.key, point(&outputs[i]["target"]["tagged_key"]["key"]).compress()); + let view_tag = + hex::decode(outputs[i]["target"]["tagged_key"]["view_tag"].as_str().unwrap()).unwrap(); + assert_eq!(view_tag.len(), 1); + assert_eq!(output.view_tag.unwrap(), view_tag[0]); + } else { + assert_eq!(output.key, point(&outputs[i]["target"]["key"]).compress()); + } + } + + // check extra + assert_eq!(tx.prefix().extra, v.tx["extra"].as_array().unwrap().as_slice()); + + match &tx { + Transaction::V1 { signatures, .. } => { + // check signatures for v1 txs + let sigs_array = v.tx["signatures"].as_array().unwrap(); + for (i, sig) in signatures.iter().enumerate() { + let tx_sig = hex::decode(sigs_array[i].as_str().unwrap()).unwrap(); + for (i, sig) in sig.sigs.iter().enumerate() { + let start = i * 64; + let c: [u8; 32] = tx_sig[start .. (start + 32)].try_into().unwrap(); + let s: [u8; 32] = tx_sig[(start + 32) .. (start + 64)].try_into().unwrap(); + assert_eq!(sig.c, Scalar::from_canonical_bytes(c).unwrap()); + assert_eq!(sig.s, Scalar::from_canonical_bytes(s).unwrap()); + } + } + } + Transaction::V2 { proofs: None, .. } => assert_eq!(v.tx["rct_signatures"]["type"], 0), + Transaction::V2 { proofs: Some(proofs), .. } => { + // check rct signatures + let rct = &v.tx["rct_signatures"]; + assert_eq!(u8::from(proofs.rct_type()), rct["type"]); + + assert_eq!(proofs.base.fee, rct["txnFee"]); + assert_eq!(proofs.base.commitments, point_vector(&rct["outPk"])); + let ecdh_info = rct["ecdhInfo"].as_array().unwrap(); + assert_eq!(proofs.base.encrypted_amounts.len(), ecdh_info.len()); + for (i, ecdh) in proofs.base.encrypted_amounts.iter().enumerate() { + let mut buf = vec![]; + ecdh.write(&mut buf).unwrap(); + assert_eq!(buf, hex::decode(ecdh_info[i]["amount"].as_str().unwrap()).unwrap()); + } + + // check ringct prunable + match &proofs.prunable { + RctPrunable::Clsag { bulletproof: _, clsags, pseudo_outs } => { + // check bulletproofs + /* TODO + for (i, bp) in bulletproofs.iter().enumerate() { + match bp { + Bulletproof::Original(o) => { + let bps = v.tx["rctsig_prunable"]["bp"].as_array().unwrap(); + assert_eq!(bulletproofs.len(), bps.len()); + assert_eq!(o.A, point(&bps[i]["A"])); + assert_eq!(o.S, point(&bps[i]["S"])); + assert_eq!(o.T1, point(&bps[i]["T1"])); + assert_eq!(o.T2, point(&bps[i]["T2"])); + assert_eq!(o.taux, scalar(&bps[i]["taux"])); + assert_eq!(o.mu, scalar(&bps[i]["mu"])); + assert_eq!(o.L, point_vector(&bps[i]["L"])); + assert_eq!(o.R, point_vector(&bps[i]["R"])); + assert_eq!(o.a, scalar(&bps[i]["a"])); + assert_eq!(o.b, scalar(&bps[i]["b"])); + assert_eq!(o.t, scalar(&bps[i]["t"])); + } + Bulletproof::Plus(p) => { + let bps = v.tx["rctsig_prunable"]["bpp"].as_array().unwrap(); + assert_eq!(bulletproofs.len(), bps.len()); + assert_eq!(p.A, point(&bps[i]["A"])); + assert_eq!(p.A1, point(&bps[i]["A1"])); + assert_eq!(p.B, point(&bps[i]["B"])); + assert_eq!(p.r1, scalar(&bps[i]["r1"])); + assert_eq!(p.s1, scalar(&bps[i]["s1"])); + assert_eq!(p.d1, scalar(&bps[i]["d1"])); + assert_eq!(p.L, point_vector(&bps[i]["L"])); + assert_eq!(p.R, point_vector(&bps[i]["R"])); + } + } + } + */ + + // check clsags + let cls = v.tx["rctsig_prunable"]["CLSAGs"].as_array().unwrap(); + for (i, cl) in clsags.iter().enumerate() { + assert_eq!(cl.D, point(&cls[i]["D"])); + assert_eq!(cl.c1, scalar(&cls[i]["c1"])); + assert_eq!(cl.s, scalar_vector(&cls[i]["s"])); + } + + // check pseudo outs + assert_eq!(pseudo_outs, &point_vector(&v.tx["rctsig_prunable"]["pseudoOuts"])); + } + // TODO: Add + _ => panic!("non-null/CLSAG test vector"), + } + } + } + + // check serialized hex + let mut buf = Vec::new(); + tx.write(&mut buf).unwrap(); + let serialized_tx = hex::encode(&buf); + assert_eq!(serialized_tx, v.hex); + } +} + +#[test] +fn signature_hash() { + for v in tx_vectors() { + let tx = Transaction::read(&mut hex::decode(v.hex.clone()).unwrap().as_slice()).unwrap(); + // check for signature hashes + if let Some(sig_hash) = tx.signature_hash() { + assert_eq!(sig_hash, hex::decode(v.signature_hash.clone()).unwrap().as_slice()); + } else { + // make sure it is a miner tx. + assert!(matches!(tx.prefix().inputs[0], Input::Gen(_))); + } + } +} + +#[test] +fn hash() { + for v in &tx_vectors() { + let tx = Transaction::read(&mut hex::decode(v.hex.clone()).unwrap().as_slice()).unwrap(); + assert_eq!(tx.hash(), hex::decode(v.id.clone()).unwrap().as_slice()); + } +} + +#[test] +fn clsag() { + /* + // following keys belong to the wallet that created the CLSAG_TX, and to the + // CLSAG_TX itself and here for debug purposes in case this test unexpectedly fails some day. + let view_key = "9df81dd2e369004d3737850e4f0abaf2111720f270b174acf8e08547e41afb0b"; + let spend_key = "25f7339ce03a0206129c0bdd78396f80bf28183ccd16084d4ab1cbaf74f0c204"; + let tx_key = "650c8038e5c6f1c533cacc1713ac27ef3ec70d7feedde0c5b37556d915b4460c"; + */ + + #[derive(serde::Deserialize)] + struct TxData { + hex: String, + tx: Value, + } + #[derive(serde::Deserialize)] + struct OutData { + key: Value, + mask: Value, + } + let tx_data = serde_json::from_str::(CLSAG_TX).unwrap(); + let out_data = serde_json::from_str::>>(RING_DATA).unwrap(); + let tx = + Transaction::::read(&mut hex::decode(tx_data.hex).unwrap().as_slice()).unwrap(); + + // gather rings + let mut rings = vec![]; + for data in out_data { + let mut ring = vec![]; + for out in &data { + ring.push([point(&out.key), point(&out.mask)]); + } + rings.push(ring) + } + + // gather key images + let mut key_images = vec![]; + let inputs = tx_data.tx["vin"].as_array().unwrap(); + for input in inputs { + key_images.push(point(&input["key"]["k_image"])); + } + + // gather pseudo_outs + let mut pseudo_outs = vec![]; + let pouts = tx_data.tx["rctsig_prunable"]["pseudoOuts"].as_array().unwrap(); + for po in pouts { + pseudo_outs.push(point(po)); + } + + // verify clsags + match tx { + Transaction::V2 { proofs: Some(ref proofs), .. } => match &proofs.prunable { + RctPrunable::Clsag { bulletproof: _, clsags, .. } => { + for (i, cls) in clsags.iter().enumerate() { + cls + .verify(&rings[i], &key_images[i], &pseudo_outs[i], &tx.signature_hash().unwrap()) + .unwrap(); + } + } + // TODO: Add + _ => panic!("non-CLSAG test vector"), + }, + // TODO: Add + _ => panic!("non-CLSAG test vector"), + } +} diff --git a/networks/monero/src/tests/vectors/clsag_tx.json b/networks/monero/src/tests/vectors/clsag_tx.json new file mode 100644 index 00000000..b41b5a15 --- /dev/null +++ b/networks/monero/src/tests/vectors/clsag_tx.json @@ -0,0 +1,78 @@ +{ + "hex": "020002020010020102010101010302010c0201060103d8c6f077bb201ffdc16407df206cb5962ec635a4a4c9cd7551b88698d1bef497020010000402040801010303030101020104018267c18a435f4a5dea50ad0f10755a4fd7783340beb3a3903a67fa14938edf420200039716cdbae38def9a74e7df5402c108270a1d5fc87c7e5ebaaaed68aae77701e3cf0003082e27ca8af2b9e3004156c152aa98503b548b1591fdcd839ab550612ae6c9dc7e2c01a57c93fb0ca77ab96b7dfd7380c4842d1e58c055430e0d425cd1c76c578cca390209019519f8c1ce5e20300680e5a0da09acd081c0dd2c7178a341382720ada87588a96ac5cff1623fd2e4aaf56ed395a325393fbd950428a3ff7e6dc6c559669c8d5e8fb80d5e979c8a81c89754201d4bd094c37c143759260e282555dfed3100013256ca0156c1c34dc569565039c27f784b45ec50ba816f69b54ae3df98d841070f51aec2a8afd4991d5bbf50b785d0bdc2a6491c5ab45795d7ce3b08d63282907c52f9951e711cb6a2cba1aba1f7849a669345711263cc736e2d4e1c7308c5e7cb97e948ed647f89fc9869fb9c9a5a742e5e7be419cce7a5e99a5b21cb491f00003ec1da7e8cec39b709d46fab65f59f5f6147c1e4429d18d8bf6e3e62639102a300ce6006a20403ef021a197b6c632ac280e674c7aad08290424271dec4de010710ee7895389150dd15017cfd5f47ea9dddd11e218251433906f62aff6b8cb2b5f8cca25add297da40d7cddbea718703ff9ad3795fcdc172a34c73179326c16f5274de69073281f3276d800fe7fbd01a07d14a42ce367c32727a9f0bc8c8d6ab4b3b17dd981bdc522595fc1bfe83ad3976876fb3bb2e4bd4392ac1a94ac22cbcc326ede82d1af2f1ec9d4ac596b22d035c7f1ac11d8ace7c5a70b30e39596ded794077ae55144e3f4b0c17cbc4f5a960129eb5321077bb7e2b9e4621e17fbcf2960abae1e1a9f89af21cc2fcce410a839186b8da92966415d6dd3ad772d652cbe075af46b97ae7062ccbaa328e371a351492f6860832c5bfdd7b77e8611b7441ecfa0967e66c13cb9ab348bf78a15bbd2d9bec6b8ec5cdd5f84a91580758247da84afed22ec2cf89d632e406fdc927e48ebfaacd0a0b715a968c9cfc74fff611f4cda4b6cb9eb1e044a71c58a832c5ae7551833c0ba2ab6f9d1e466e5757c230157cd3099686bf89e8f9eb822ea702e13e38f669603dd3c7c8be90daf192de689ab2078d16cf489f3782e70469fbe01f918297e0db6cef3bf48e0293b6856d348fda3a2d76bf899432acef74aa42961be28635d1899509b9d368bc42a18e08d2b94b055da149139c347f7c0b2a381dfaa12aaaabe076f38fe12372d1ba17cd0d808ed5b4b911f8cee2e45841a4c879f40968e455ba5a796b27c968be0f7e88daf0b766fcf2c5986fbe14b2e0433cecb04af100ec81d03e2875d25483d0a9dc9dc0a42150a64e894af1655e9ab99f629826f63c01e44b366c5fe2959c7396450360a3156ad081764b5904a7654fe82a2b1d52db46361c0b08dfeee383165641e6e0e5733e5fb99fc8c75ba5cf230518b1e384d4441251840e810aed950eb27899809711d42c54f8fc0647537e249e510738412c399b915ff923e9209cdd12820720b8b07086f3361d6b95934f994a8ac4fb6a9598f11d54bbbcfc33e71b9f73570012b3520914dfab3f3fe15abad981d8ed71dab71ac8f45f187f62ad440a83d000e08fc039ece25e7eadd0ce169ccda8182321cd73eba6f6d0e4f482a061eb4190fe4051e6988a47165cb2cf39973b1a555cc92d662f4e856a91c0cd51a486b960cfc850c4fc854f9a4aade4336942cb50cb50ae3bc31d3da50b719196d5fd40f02b1addad16de443e825bf7177beaac79adc6b198115f408a391a94a8517b7e50fd57663df52309c0a00b0b61373f895206771be8b185c54da6f805b561264aa019ef3bd1dcded26fc45a6a0e39cbb7bc6a7025ab858bc8e54a99da3aedce68f00bacc83a7eb3553ac626881188329b6ba86a53aaaaed9bd9efb0528f08c649c093f005dd0fa9620b0a40fc3f248c1d0edb8f70ff05c7254de0f8faab8315443021b3d279f5a4218c3126dee5d6eceae1c49eabdd04d8a0cdb6814c422b3ea69b3be3794f42081e65dc47b1d2fc2f5705cef816596416c373bd60abc4ff06b3f02ef34dc290f987607bdb16c1650307ea3bc0fc7a62ce86e7129293d7530c3cf09dc731e22c18daec3c639575421b079fa57be56693278125b2aa50c299ac4f8020714c6ac666b7fb7471c63adda93f1fa6733729f7b6e326ac04744f9c3223d0456ee515d0bfe27101f907cca958dddb90717bed5229c1a02928fab9e7be4e4012c96d3acda0ebca72e63f41efdad5c9baa19bffd1216e4c3e2e5564e823b57054a3a2cf2c3318f214d23f24304655e73d5001518633757f6cbe6711f2a5f2601df20a753caaa87a32fe627b6ce7573ce77957c7b6401959824fd49bc7063670fb18fcc1f2de113affd868eb76c7fbe12997024dc493b6a26563a80574a52760a7b384fd2f9d23d8dfe4d226b15086751d4f383d4bca7cf080fd471b8a218b709b539f4e5417677f43627ef06b70c24edacce80bdd10ca2ac9af8aa3f6453cc08da75ee99409447225843c143fca551167a4aa5fd2354a5420c35c0006731950d6c356218d8cf365e084d9bb52c793322aa2d8d05c4164d9ffe81ce09e4f17802efa7461d375a5cff4c17ab0cdc5767a8f7d34091921fd4620660470ea9305f00dd9e6ee5ca4054ac0b36d4e2b58006224559cc19a3a4e48f66aa596295541007f2524b2198f3c0c688fbbc38590f59674b25e528ac2115a0f7da805d9c5810065f95c7c7ece23d2de922e55a77f967baab6d9db543e49734a8c4bc23c5ae640edb904851b4856c5a1ce4729957f4d000e70cb88c56d80bf6e693a5c67d5661911374d7aa7f6e6f4a5b340a9954d9cf8bd5d2f4b4a37f946e15bca800978ae745eec2096b3def10f9703a6e2040df0d8a89bf1562bb29d3a13df2f9a77c3e064e", + "tx": { + "version": 2, + "unlock_time": 0, + "vin": [ { + "key": { + "amount": 0, + "key_offsets": [ 2, 1, 2, 1, 1, 1, 1, 3, 2, 1, 12, 2, 1, 6, 1, 3], + "k_image": "d8c6f077bb201ffdc16407df206cb5962ec635a4a4c9cd7551b88698d1bef497" + } + }, { + "key": { + "amount": 0, + "key_offsets": [ 0, 4, 2, 4, 8, 1, 1, 3, 3, 3, 1, 1, 2, 1, 4, 1 + ], + "k_image": "8267c18a435f4a5dea50ad0f10755a4fd7783340beb3a3903a67fa14938edf42" + } + } + ], + "vout": [ { + "amount": 0, + "target": { + "tagged_key": { + "key": "9716cdbae38def9a74e7df5402c108270a1d5fc87c7e5ebaaaed68aae77701e3", + "view_tag": "cf" + } + } + }, { + "amount": 0, + "target": { + "tagged_key": { + "key": "082e27ca8af2b9e3004156c152aa98503b548b1591fdcd839ab550612ae6c9dc", + "view_tag": "7e" + } + } + } + ], + "extra": [ 1, 165, 124, 147, 251, 12, 167, 122, 185, 107, 125, 253, 115, 128, 196, 132, 45, 30, 88, 192, 85, 67, 14, 13, 66, 92, 209, 199, 108, 87, 140, 202, 57, 2, 9, 1, 149, 25, 248, 193, 206, 94, 32, 48 + ], + "rct_signatures": { + "type": 6, + "txnFee": 2605200000, + "ecdhInfo": [ { + "amount": "acd081c0dd2c7178" + }, { + "amount": "a341382720ada875" + }], + "outPk": [ "88a96ac5cff1623fd2e4aaf56ed395a325393fbd950428a3ff7e6dc6c559669c", "8d5e8fb80d5e979c8a81c89754201d4bd094c37c143759260e282555dfed3100"] + }, + "rctsig_prunable": { + "nbp": 1, + "bpp": [ { + "A": "3256ca0156c1c34dc569565039c27f784b45ec50ba816f69b54ae3df98d84107", + "A1": "0f51aec2a8afd4991d5bbf50b785d0bdc2a6491c5ab45795d7ce3b08d6328290", + "B": "7c52f9951e711cb6a2cba1aba1f7849a669345711263cc736e2d4e1c7308c5e7", + "r1": "cb97e948ed647f89fc9869fb9c9a5a742e5e7be419cce7a5e99a5b21cb491f00", + "s1": "003ec1da7e8cec39b709d46fab65f59f5f6147c1e4429d18d8bf6e3e62639102", + "d1": "a300ce6006a20403ef021a197b6c632ac280e674c7aad08290424271dec4de01", + "L": [ "10ee7895389150dd15017cfd5f47ea9dddd11e218251433906f62aff6b8cb2b5", "f8cca25add297da40d7cddbea718703ff9ad3795fcdc172a34c73179326c16f5", "274de69073281f3276d800fe7fbd01a07d14a42ce367c32727a9f0bc8c8d6ab4", "b3b17dd981bdc522595fc1bfe83ad3976876fb3bb2e4bd4392ac1a94ac22cbcc", "326ede82d1af2f1ec9d4ac596b22d035c7f1ac11d8ace7c5a70b30e39596ded7", "94077ae55144e3f4b0c17cbc4f5a960129eb5321077bb7e2b9e4621e17fbcf29", "60abae1e1a9f89af21cc2fcce410a839186b8da92966415d6dd3ad772d652cbe" + ], + "R": [ "5af46b97ae7062ccbaa328e371a351492f6860832c5bfdd7b77e8611b7441ecf", "a0967e66c13cb9ab348bf78a15bbd2d9bec6b8ec5cdd5f84a91580758247da84", "afed22ec2cf89d632e406fdc927e48ebfaacd0a0b715a968c9cfc74fff611f4c", "da4b6cb9eb1e044a71c58a832c5ae7551833c0ba2ab6f9d1e466e5757c230157", "cd3099686bf89e8f9eb822ea702e13e38f669603dd3c7c8be90daf192de689ab", "2078d16cf489f3782e70469fbe01f918297e0db6cef3bf48e0293b6856d348fd", "a3a2d76bf899432acef74aa42961be28635d1899509b9d368bc42a18e08d2b94" + ] + } + ], + "CLSAGs": [ { + "s": [ "b055da149139c347f7c0b2a381dfaa12aaaabe076f38fe12372d1ba17cd0d808", "ed5b4b911f8cee2e45841a4c879f40968e455ba5a796b27c968be0f7e88daf0b", "766fcf2c5986fbe14b2e0433cecb04af100ec81d03e2875d25483d0a9dc9dc0a", "42150a64e894af1655e9ab99f629826f63c01e44b366c5fe2959c7396450360a", "3156ad081764b5904a7654fe82a2b1d52db46361c0b08dfeee383165641e6e0e", "5733e5fb99fc8c75ba5cf230518b1e384d4441251840e810aed950eb27899809", "711d42c54f8fc0647537e249e510738412c399b915ff923e9209cdd12820720b", "8b07086f3361d6b95934f994a8ac4fb6a9598f11d54bbbcfc33e71b9f7357001", "2b3520914dfab3f3fe15abad981d8ed71dab71ac8f45f187f62ad440a83d000e", "08fc039ece25e7eadd0ce169ccda8182321cd73eba6f6d0e4f482a061eb4190f", "e4051e6988a47165cb2cf39973b1a555cc92d662f4e856a91c0cd51a486b960c", "fc850c4fc854f9a4aade4336942cb50cb50ae3bc31d3da50b719196d5fd40f02", "b1addad16de443e825bf7177beaac79adc6b198115f408a391a94a8517b7e50f", "d57663df52309c0a00b0b61373f895206771be8b185c54da6f805b561264aa01", "9ef3bd1dcded26fc45a6a0e39cbb7bc6a7025ab858bc8e54a99da3aedce68f00", "bacc83a7eb3553ac626881188329b6ba86a53aaaaed9bd9efb0528f08c649c09"], + "c1": "3f005dd0fa9620b0a40fc3f248c1d0edb8f70ff05c7254de0f8faab831544302", + "D": "1b3d279f5a4218c3126dee5d6eceae1c49eabdd04d8a0cdb6814c422b3ea69b3" + }, { + "s": [ "be3794f42081e65dc47b1d2fc2f5705cef816596416c373bd60abc4ff06b3f02", "ef34dc290f987607bdb16c1650307ea3bc0fc7a62ce86e7129293d7530c3cf09", "dc731e22c18daec3c639575421b079fa57be56693278125b2aa50c299ac4f802", "0714c6ac666b7fb7471c63adda93f1fa6733729f7b6e326ac04744f9c3223d04", "56ee515d0bfe27101f907cca958dddb90717bed5229c1a02928fab9e7be4e401", "2c96d3acda0ebca72e63f41efdad5c9baa19bffd1216e4c3e2e5564e823b5705", "4a3a2cf2c3318f214d23f24304655e73d5001518633757f6cbe6711f2a5f2601", "df20a753caaa87a32fe627b6ce7573ce77957c7b6401959824fd49bc7063670f", "b18fcc1f2de113affd868eb76c7fbe12997024dc493b6a26563a80574a52760a", "7b384fd2f9d23d8dfe4d226b15086751d4f383d4bca7cf080fd471b8a218b709", "b539f4e5417677f43627ef06b70c24edacce80bdd10ca2ac9af8aa3f6453cc08", "da75ee99409447225843c143fca551167a4aa5fd2354a5420c35c0006731950d", "6c356218d8cf365e084d9bb52c793322aa2d8d05c4164d9ffe81ce09e4f17802", "efa7461d375a5cff4c17ab0cdc5767a8f7d34091921fd4620660470ea9305f00", "dd9e6ee5ca4054ac0b36d4e2b58006224559cc19a3a4e48f66aa596295541007", "f2524b2198f3c0c688fbbc38590f59674b25e528ac2115a0f7da805d9c581006"], + "c1": "5f95c7c7ece23d2de922e55a77f967baab6d9db543e49734a8c4bc23c5ae640e", + "D": "db904851b4856c5a1ce4729957f4d000e70cb88c56d80bf6e693a5c67d566191" + }], + "pseudoOuts": [ "1374d7aa7f6e6f4a5b340a9954d9cf8bd5d2f4b4a37f946e15bca800978ae745", "eec2096b3def10f9703a6e2040df0d8a89bf1562bb29d3a13df2f9a77c3e064e"] + } + } +} diff --git a/networks/monero/src/tests/vectors/ring_data.json b/networks/monero/src/tests/vectors/ring_data.json new file mode 100644 index 00000000..21601245 --- /dev/null +++ b/networks/monero/src/tests/vectors/ring_data.json @@ -0,0 +1,134 @@ +[ + [ + { + "key": "a1abc026eb4a18ca197ca7dbd32f7a4e66cda075a7c07ee6cbe68639a4b4ee46", + "mask": "48d7f0b8796720c7edef5e3797135b3e5ad2ae23db1d934bcf6d6bc396b8ed47" + }, + { + "key": "a374121e22ed620248c970e7f32ea7598b054f73c1edec33c4e1b18a73c35c14", + "mask": "15beeeedc9b33615097e0fac0acc6a0984e139fa2b4196896877a8cc3ebc3590" + }, + { + "key": "e2ac4d36f9567092563a09c7a19c5e21c39598f5d9d9dd8733b61cebb3ea8662", + "mask": "3d9105f85f9edd3f7f72b62385bb9a42d549331d3babea6cf73bbbcde8e4f53c" + }, + { + "key": "68c08bbbfdb3ad736dfed5854264a3b410de40d8f3d02b22f5cf75f69f6e2e1f", + "mask": "36c39958ddcad401d85d63883da510505650321ad7a26859e8b1b6c28204d274" + }, + { + "key": "7b8b580f7a2288040a0755810c5708c5a8277d139762545082785260275678e4", + "mask": "498105ec1dc7559becfb833140c5049382b846eff812616a2414494d7a46930d" + }, + { + "key": "348d9be3f2b42686c2a919ba1515c5a540c5ffb4c1762e4a371b42643ff69b3b", + "mask": "eeca9ed04ba72a89dbd85564cf3084daad577634db09d048895524f1ded26b19" + }, + { + "key": "91a59666453bcc55d2a02480dfe2029082e24548cdfd7d614be31657fdd75357", + "mask": "ae7f14cbb31d24b727d8680fbd03bcc177fc67b982edeca54e6b2b47d6b8d012" + }, + { + "key": "9868cb5201d4b00e5a3552a7f485662dfb3ca74b79f6bd069ee0a4650597abbc", + "mask": "570e3b126e429022177d22fd09d73c6950676c82a4872addb3afa950646c5f1d" + }, + { + "key": "56d05fced0eb9dda981a26fdd4170f46de2b0a35c70f02ceae23ad9f2ed8a5b0", + "mask": "a0e20ecd8526bd2a640c4df42c187fcf75d05660ba61262c93b19384b8fad49b" + }, + { + "key": "9e82f65349da1e0dacf5d96a9c0f80c0c5fd0fc2437cafbcc38b2f20e721abc5", + "mask": "e83344061c0632631eec627bb2103898cfc230b35e0177681e48f0ee4b6d37c8" + }, + { + "key": "2590a255607ab619fcd62142f4b002818f2d55dbb5b8665500854203b83e5c86", + "mask": "e9c103485b3f4dadab560e8efc67c594ba11f16513685f0faff78c6fdf4de061" + }, + { + "key": "c0e22332d897f0637440ad151089652e59dcbf27dc84b11c2efbe686a9e7afb5", + "mask": "363d5dcbc765854e830dc52762e24f71d7c85f6095227551f3ef6ada6aa25964" + }, + { + "key": "360e4efb484e8d419bdda5f581703de716671e3516d1c9deb97204f9b4c9c0d4", + "mask": "29ef141fa24ef86af35af48094928392543a9e7e7726ae92a9da322178e680ad" + }, + { + "key": "5bb515d131f03bbb3be4e710b83589f62f07f185b9ad344095df47092f41b8e0", + "mask": "94fd6083b669533eebfa49a1cb47b94555e8be7d5f84573354b0201229d07bed" + }, + { + "key": "5ce647c3017ec3c36a2385e2b11fb9a452a5766987d80531bec75952924ed896", + "mask": "8f61d7be3b4f2252810fbade3bbac970ccff55c453e34405836545f3e49be6f5" + }, + { + "key": "dbc787f7ca41996a981a0ebb498a8d565dfa62a3b3b169c4c3018fff2233a757", + "mask": "9bb749be705747d9c28168c0446d589b3ac18949fa0087e230805aaff5a9982f" + } + ], + [ + { + "key": "d10621b38fbc5237061b2d3503866f0be46aaa0694c9f9d747f7ed19acebe8ef", + "mask": "a1a7a42155f0abff0353a6008eda2a9b16d9ffcf7584a38933cce3e3976987cd" + }, + { + "key": "a9afb71ae2db057049131df856d246f7088a656cc85297ce7e1ef339bd6e0c96", + "mask": "96e9dc7a96a19c9ebaeb33ab94e7e9d86d88df1c1b11006b297b74f529f37f5a" + }, + { + "key": "68c08bbbfdb3ad736dfed5854264a3b410de40d8f3d02b22f5cf75f69f6e2e1f", + "mask": "36c39958ddcad401d85d63883da510505650321ad7a26859e8b1b6c28204d274" + }, + { + "key": "74193737897162c8b2c380ff34674e3bfbfb2ac7e1c7aacbb13f2a3a8fb2b043", + "mask": "8157e47f9998f4afdce72a328eb9e897a57a5819b838ed1b517ea2c938e0c94f" + }, + { + "key": "96e002055aafbfdd1136cc587543e5c0e51da0d9682879c107abab3cdcdb9479", + "mask": "f76929f6dba6d75bec713a02677aa7ad39dd4319077bfa7189fe65fe86b2ee9a" + }, + { + "key": "2a72f3b2cb3e10727fbfc09d2c726763000a92f77f2f000c63dee714a6c7424d", + "mask": "db459ca84da12ebab294b31961838c43cee1868f0690d143c93da1f2f825d07f" + }, + { + "key": "797f5f3a30ce8d4b19305ca9d8193033d649f0a74705203da9f3f106ad60dfb4", + "mask": "39339ac52a1194790b1bb5db0b119d403a1d5dcc4db4f8819fca4d425d5b2614" + }, + { + "key": "b0c42947607815eba320f97e7c9ecd092fe187fb67d7263540015e6308f6dc1a", + "mask": "6b92c8c269319192298307feb26a7b64fb78d877ac2e49a594650227f26e64bc" + }, + { + "key": "59015cfd533a742857454dce9d82846fce08ab7d96c5583640cf6e38ecf0445e", + "mask": "cf375f037e253ab6f52699fbba73f796ee2140e546710a1faa3c9f09b4f570ac" + }, + { + "key": "c0e22332d897f0637440ad151089652e59dcbf27dc84b11c2efbe686a9e7afb5", + "mask": "363d5dcbc765854e830dc52762e24f71d7c85f6095227551f3ef6ada6aa25964" + }, + { + "key": "360e4efb484e8d419bdda5f581703de716671e3516d1c9deb97204f9b4c9c0d4", + "mask": "29ef141fa24ef86af35af48094928392543a9e7e7726ae92a9da322178e680ad" + }, + { + "key": "92619df80e988c0b2dfb63dd6324ff2979ca319bf8200260b28944753dda4ac1", + "mask": "0a574b0aca86da38dd7aeb58d92550dc558c680deaa63c69e31e9a78e88a3559" + }, + { + "key": "0ac7e630a04be92b1f3c821c50ec80a2813f7bee4c1ab117967bc26263d4fd84", + "mask": "ed0bd4d707ab3deaf18437ae9d945da2d3f2c6e758068ce57972d676da2a24bf" + }, + { + "key": "b97300cdb6ef63a6990686521138b5c7c80cf6c9a8844518352f3ef1130d413d", + "mask": "690c312586bbdf123d9e34ad7955e1c2ae5259cd3effd0b08b19cb556d65ec25" + }, + { + "key": "1a62237b77e28713e5a47129f1ba18be27a5139d6f1e6d6d38c78705143b3ea5", + "mask": "39f6ba6d816695f20212042b1048301cd637161f685d7c2b61379b907b7b4c59" + }, + { + "key": "ffca492152d8206bb7f215d2408669856203edffd424f4fc6a0304def2195717", + "mask": "cd7684b7c32531b363784d86bee71731c113c545c67103ec1265c362de7e5555" + } + ] +] diff --git a/networks/monero/src/tests/vectors/transactions.json b/networks/monero/src/tests/vectors/transactions.json new file mode 100644 index 00000000..c7339224 --- /dev/null +++ b/networks/monero/src/tests/vectors/transactions.json @@ -0,0 +1,324 @@ +[ + { + "id": "373a2ace627debaf8bfd493155fd3c00c5c2fc164400ec22e79ee79a1ac487c4", + "hex": "02f78dae0101ffbb8dae0101e0b2d2b9c21103e6854544fbb66d55fc3546f4d3e69f8234257b69fa2237712af3b058a5f01ba14a340173f263b8a4bbc46dfb6f29e0584adbfffdf7a47c929d77c2d0c142afea2b05300211000000f7eeeb3f0e00000000000000000000", + "signature_hash": "", + "tx": { + "version": 2, + "unlock_time": 2852599, + "vin": [ + { + "gen": { + "height": 2852539 + } + } + ], + "vout": [ + { + "amount": 601953180000, + "target": { + "tagged_key": { + "key": "e6854544fbb66d55fc3546f4d3e69f8234257b69fa2237712af3b058a5f01ba1", + "view_tag": "4a" + } + } + } + ], + "extra": [ 1, 115, 242, 99, 184, 164, 187, 196, 109, 251, 111, 41, 224, 88, 74, 219, 255, 253, 247, 164, 124, 146, 157, 119, 194, 208, 193, 66, 175, 234, 43, 5, 48, 2, 17, 0, 0, 0, 247, 238, 235, 63, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0 ], + "rct_signatures": { + "type": 0 + } + } + }, + { + "id": "c39652b79beb888464525fee06c3d078463af5b76d493785f8903cae93405603", + "hex": "02000102000be9aac314d8e710844d8d258133d9f701b0649e568b0cb50b1dea8103138a37c5543f3c632ef80331940cabeba29b758045db328d8d8a99de380200025155f659da61b507b0b8591cbef0ba1534b9db29a69be4a933f7897e58870b250002002de4643160fb8351a841f8079aa5af2ac62c9631bb2d5a48fcdb564cb699d12c01eaaa5acba0bc44657da783903d3de7febf7124ae03cf578938244068b475d906020901da0190b93466979e0580efcf037eafd43a457b940e592f261bd165059a9d3b92aa3baaed18346157d1361e30c553df17940985b375fc89b9c09d613d5daf007ed1ce05128cfc4d37181a5f2d6f56690357032dbcc6e85424e5ad79338801df166e5b93e427f8b86079e0de0695d24a4ac5098d98876da5f892359c982015fc0461847fd2955f6099cfc37d348bdd003f064a45c6959478807f7cb6263fc539b2616e13117fc82303aba9afeed67104c344f971a182e741436bcc040468fa6307bb478ed8ef47397e603fe7d50d3c56f5c50c4410db470e21c04b5b883a94c78bc19587d8a611c701d51b956ae06e92987ecdcd5237a65725fdcf7b52a9085831c7214b1282fbd1faf81277a6519c941907f0ef838576e610272512d1860a07a19ac9aec33168b06c937d1ead2e63e58345252be0a5fe15d38a371a6347355d16104b19f1d4a4c18e07dba72bbf8c8150bade347831b446f2ad9f5af4aa91cd35ddf3c64b3f0690c77634efd4ef07b817c6bb90a29f917769cd2978d37a14ed61b437858c93100f55b82ac7f94c0f83ac68a54df1e4d2c54b9c96661f7a41ed9fff8b28fc31949a07a7b298b5819ac7fe7c9490b20a1107ba6f6b94e6d1a58fab09bed6d096edaaea887addea1b7735228bdff409674c0f8bfceb815cdbae6fc5de41379ff842eebe205611c58d80371feb97b1f2a23fb65a474665f991113407da1c0a6f6d1fa7b6ad659f45354178270f5541eb948cbc861c53705982fa67f88c70cb936e86bf45bb17d6214ab716dc0c5fa3b7f3a181e23fad244741c1b6c44c557cf0e4dfe7a1acf75d39bfb810a798b33bcbfc73d9a11b0c648c84ed6e28b27e4aa8681aa33c0779e01ac2d16f58031d92fae578d92b6ab5d7c98a47b64ead0ad4f036922b4604d1b23dd4f4b798bae917294fc458000e7ce1449060a98c62bb1d86a0841f26e70550778103fcc3d70b9adcdb32cc750016b88178513e94d61eb65c8381532feb3368f3db60638f3b88575dded95eb92910d16c51351e7379e5df4f1fd2ffe1b80b958ef6f189ff0483a802bd1c49ed56131e1cc02dbe0949251adbc980b5a7f45317bab8be87961b6b9496d24779023f77dd1b4552fc0b522e857f38e9c63349955e3be4b3a15e86aa96943900797b8139cbfdcd940903ce41249b9e7e4a933b88a204b1ded0e2ba0fa6a1cf3ba4c3ffcc0ca96b06b400051d66ede7013ccc453e4f042e17e1091a536456460fe3551f36c99c05d59105802ee93d645167ee586e71fcc23c33d2362754065955eca45f294bb28f38960826166dfdde409f76f75366234b0a08f6e49caa1c2169e8d9376673e9835dc709bb94e4d8a164ee6278998e891e05086ad8996e496e833d975cb3d4fceb3f7203a7b365891227448b2e968b5bb653b29741bad9ba107b417354e6e3e5acf5710a346e232e9f02606a31bd8b692a67578637a5ff82339dde31425ad59ada17360184f24e6b542810ec43ef11d6a8b2eba53b288ca445d1e1a0daf059320978160eb610631a9b382291418309fc3ceecf3a08900ff5d301044a3cf565567129b10dc2aa970f43493920c4ef3df1c408e12bbcfb01573b79e296e34912cde62716033f73cf74f42b0fde655f2848bec39fd63f3485499147fe170861e9e0eae0e90d400328b595456b6451dc07eac7c8f6849dc065bb7f5ac49cff1530658bcc4d09d03e2f9611a5561dc3a90b3b2f3d933dc110db73904aeee5abcd2362a0e1b045c4cf22334e32fd33193fbfa1c4cb9e3182fb2357cf7a800f552b87579cc41d99", + "signature_hash": "686cc5232f8d0d90c6a447b10b5296c98b0b4ad5e2f88f278a6bd8f3eeb13dbf", + "tx": { + "version": 2, + "unlock_time": 0, + "vin": [ + { + "key": { + "amount": 0, + "key_offsets": [43046249, 275416, 9860, 4749, 6529, 31705, 12848, 11038, 1547, 1461, 29], + "k_image": "ea8103138a37c5543f3c632ef80331940cabeba29b758045db328d8d8a99de38" + } + } + ], + "vout": [ + { + "amount": 0, + "target": { + "key": "5155f659da61b507b0b8591cbef0ba1534b9db29a69be4a933f7897e58870b25" + } + }, + { + "amount": 0, + "target": { + "key": "002de4643160fb8351a841f8079aa5af2ac62c9631bb2d5a48fcdb564cb699d1" + } + } + ], + "extra": [ 1, 234, 170, 90, 203, 160, 188, 68, 101, 125, 167, 131, 144, 61, 61, 231, 254, 191, 113, 36, 174, 3, 207, 87, 137, 56, 36, 64, 104, 180, 117, 217, 6, 2, 9, 1, 218, 1, 144, 185, 52, 102, 151, 158], + "rct_signatures": { + "type": 5, + "txnFee": 7600000, + "ecdhInfo": [ { + "amount": "7eafd43a457b940e" + }, { + "amount": "592f261bd165059a" + }], + "outPk": [ "9d3b92aa3baaed18346157d1361e30c553df17940985b375fc89b9c09d613d5d", "af007ed1ce05128cfc4d37181a5f2d6f56690357032dbcc6e85424e5ad793388"] + }, + "rctsig_prunable": { + "nbp": 1, + "bp": [ { + "A": "df166e5b93e427f8b86079e0de0695d24a4ac5098d98876da5f892359c982015", + "S": "fc0461847fd2955f6099cfc37d348bdd003f064a45c6959478807f7cb6263fc5", + "T1": "39b2616e13117fc82303aba9afeed67104c344f971a182e741436bcc040468fa", + "T2": "6307bb478ed8ef47397e603fe7d50d3c56f5c50c4410db470e21c04b5b883a94", + "taux": "c78bc19587d8a611c701d51b956ae06e92987ecdcd5237a65725fdcf7b52a908", + "mu": "5831c7214b1282fbd1faf81277a6519c941907f0ef838576e610272512d1860a", + "L": [ "a19ac9aec33168b06c937d1ead2e63e58345252be0a5fe15d38a371a6347355d", "16104b19f1d4a4c18e07dba72bbf8c8150bade347831b446f2ad9f5af4aa91cd", "35ddf3c64b3f0690c77634efd4ef07b817c6bb90a29f917769cd2978d37a14ed", "61b437858c93100f55b82ac7f94c0f83ac68a54df1e4d2c54b9c96661f7a41ed", "9fff8b28fc31949a07a7b298b5819ac7fe7c9490b20a1107ba6f6b94e6d1a58f", "ab09bed6d096edaaea887addea1b7735228bdff409674c0f8bfceb815cdbae6f", "c5de41379ff842eebe205611c58d80371feb97b1f2a23fb65a474665f9911134"], + "R": [ "da1c0a6f6d1fa7b6ad659f45354178270f5541eb948cbc861c53705982fa67f8", "8c70cb936e86bf45bb17d6214ab716dc0c5fa3b7f3a181e23fad244741c1b6c4", "4c557cf0e4dfe7a1acf75d39bfb810a798b33bcbfc73d9a11b0c648c84ed6e28", "b27e4aa8681aa33c0779e01ac2d16f58031d92fae578d92b6ab5d7c98a47b64e", "ad0ad4f036922b4604d1b23dd4f4b798bae917294fc458000e7ce1449060a98c", "62bb1d86a0841f26e70550778103fcc3d70b9adcdb32cc750016b88178513e94", "d61eb65c8381532feb3368f3db60638f3b88575dded95eb92910d16c51351e73"], + "a": "79e5df4f1fd2ffe1b80b958ef6f189ff0483a802bd1c49ed56131e1cc02dbe09", + "b": "49251adbc980b5a7f45317bab8be87961b6b9496d24779023f77dd1b4552fc0b", + "t": "522e857f38e9c63349955e3be4b3a15e86aa96943900797b8139cbfdcd940903" + }], + "CLSAGs": [ { + "s": [ "ce41249b9e7e4a933b88a204b1ded0e2ba0fa6a1cf3ba4c3ffcc0ca96b06b400", "051d66ede7013ccc453e4f042e17e1091a536456460fe3551f36c99c05d59105", "802ee93d645167ee586e71fcc23c33d2362754065955eca45f294bb28f389608", "26166dfdde409f76f75366234b0a08f6e49caa1c2169e8d9376673e9835dc709", "bb94e4d8a164ee6278998e891e05086ad8996e496e833d975cb3d4fceb3f7203", "a7b365891227448b2e968b5bb653b29741bad9ba107b417354e6e3e5acf5710a", "346e232e9f02606a31bd8b692a67578637a5ff82339dde31425ad59ada173601", "84f24e6b542810ec43ef11d6a8b2eba53b288ca445d1e1a0daf059320978160e", "b610631a9b382291418309fc3ceecf3a08900ff5d301044a3cf565567129b10d", "c2aa970f43493920c4ef3df1c408e12bbcfb01573b79e296e34912cde6271603", "3f73cf74f42b0fde655f2848bec39fd63f3485499147fe170861e9e0eae0e90d"], + "c1": "400328b595456b6451dc07eac7c8f6849dc065bb7f5ac49cff1530658bcc4d09", + "D": "d03e2f9611a5561dc3a90b3b2f3d933dc110db73904aeee5abcd2362a0e1b045" + }], + "pseudoOuts": [ "c4cf22334e32fd33193fbfa1c4cb9e3182fb2357cf7a800f552b87579cc41d99"] + } + } + }, + { + "id": "2f650db5bafd37ce8982f37ee443f2ecf0a8f08f639591583aecb6cd74d5a80c", + "hex": "020001020010d6f68721ea820c88d539a68f0b84af09d19401c08a02f0ee048250c219958401a49f02b33fa321a527dd227f94e759b07b2c025ce22a57db0cb062bfd1f50f6086b14ca3742730c7fa9e5d040003fcdf91296bb4560335835fda30804a7d8d200acfabe4e98a0c425d38556dac06870003d66821247fe13266bad423e445ddd6a1b51a86198e38049e2c8039ab6d5dc8b485000393ae131b8c649288a9fb61ebffa8ecb0fababa8f5159286f895f5bed10bad6388600038abfdfa2d445934fe750607f9654e02389c056644453c942d1841bbf418d94e22021014004716b1c1ffb8447e0c1d27f147a4691ed393fdf2eadb225ebfd54ffdf872f0680d5f814756596945ca3852476b456ac3c9942c978d0a3bcd9e6c236efadcdf54e6ed0db9c4bc6ac562b6859a40ad8f3bc85ca35c98badb4b4c5d43832f330d6fedb08e8f9e2acd339c648bf03957cb02aa69b8ab15326e3bbe1ce35df677306edabd89e5635f226a743068500e25028fbdbf1ea19d0921a27c8baec842b753080f407ee4b9a87f2c525e9bfb61fb4d14187c0577e799bf20e53a86359cb75f40ee4d291017c2b59e7278c94b6296dee9ac65ed5ccf61a77ba4f1b3edfb13c5d02748763b23a6bac2f6a891b474d55b625030b35f9b7b564e747afd4cb8e1ce830a9bc59fd6e146443965494f94a8433de054080fcb71f8d48803598cc91db3c7b3fd190ea8ff5f67980a63de4cb9cd06568a9b27aa994992bc33d70990225acb09faf68066aa27c1118c685cb8f3516c3b664450fabdced384de01650d6455287bc0f210aaa5c173c491844155736a64d7cbdda79f0c8a5ccc07d187ca112664a0e6eb500087178983179f1ba2ffb030d577638001b58f5e621b4723e5b0bd0853fb430113d03efd026660a18a23c7582e9788f770212b604759aa242b35b3ca4a835bb18881c8593ac4247ba51ea95946cf079721588bac494f563a687fe1010818caa94583969b0f4a4a40eeee395cbb7881a53d98cad51b1e5d12c7071a7424b4c534e32c53a31b11e6151edd0a13ef9695021bff9bd4c62df9a62d9e0fbd01e750d0b6abc56cc96d55ef06f6428b42fc63f6610633ecf023211e64a1ff89dcabfeeb4b938e64312dcc849929e8d4a290eff601e06dc65141665d7b312ac1f0f859a00fd6d6ccf7dc695e7ae3cf44bed1d9c8659ee3451dd3498f462912ba881a473c9bc0866e4fb33114b2ef7c25869f9cc3c40a06fc2407e2c678126ff1c38a35c5c949bc219fb33ba15730510c41554c727d5adfce33a518148234e0aa5411cb20c115e749792ad47ee19e9f1544dba61593d95cb98d4720a8ae6e60146416d673e5707c3de31d91043422ab848d4676a6845ced6e7075c5a09bc8b4e0ad706c8c07bda527a7325771438e04f37517f3ca5262ef2ddfc9e13db988a90c50be5422a83ad75b93f4faae980d6e6a3abfd0e96387121101afaf55f425dc876d9a8735c1e29d823e19fee5e502c18d16ec9225f232cfbc3dcd143aaa1904f42e880b612beeea3e5a745a7f32e6b2135a75f71117e2947c99647f14702417a9a76f6130b5d62fd149a606061709a86253c3c2a30c8ccc0e2b5ee636bda81973b011fa8b96e0f9149e7d02d903e982b025e0944029423ba9318637387d6f0a8a75f1fa957950ce6661368738251a418968ae390143e596a77bef7de4008ca66ed28b82e044d0ab293f792e8b1e9c1bc24b14ee53539f535b05f2f336c1b7698ca3cb1dc8a3a09568c6841724a19d412d4313760e3560616df7f5b2250b1a52bf32922b3964309b0bedb645579ee09d87959f4e997e4792ac9fa26858ef1aa1dbf7b10da08e7092cb200369d75f3d2b81ad2c237954cdfea1d173f84122ce4cf82a9ebaa04650a69f3675f2155bbb7ce508fdd6a328492b8788e37809f2accf082387b97a7660d427cac9eb93ceacda0cdb9db95a2d6c6fa9ca86276acce2cb8e432b14efb4d0e8a1f3cbc8534c5dfb9a42f7b0d5c212928115cb2c5b905c650b5325e2a849109c60329dcc20f1c1f10d9f6a87d17359938c520e00dd3f5e1857b5af502cc590cad89abca61f4a94513d8e42db9e7223b5d97afd80f490155bf49b79c7ea5c10d6cb74ba10211d6ec75458436a08794164d16bcb4d092274061449418d9fc3d0a9947a8854a399c7e77a49568676ff8df07c3aa21ca90a611dcdfe0c6bd44690a43a3263237f1def6658ba936e2f17c3853fdcd2c0e24cc0b26c59abb47031e00992ca59657da958b48d21d12ae0a93a68596b72c6cc826fd8e079de67b0539026a24c5dcea4875f16cd0722352424493647f7ad3b3148bcdf6c8504c25bbbb07a8b01a6352cb602a1964c02e7e10601644cee41c2bdbb39a9687fdd78dca919726312d076b9e7a4e5b0324e305b99bb1c3ea40bd2296de41f2fc43f668e1a9fb", + "signature_hash": "9c13c702e03b54a3000a008e4deb1763d7e232c3378bf928df1e2e976f5ba9c5", + "tx": { + "version": 2, + "unlock_time": 0, + "vin": [ { + "key": { + "amount": 0, + "key_offsets": [69335894, 196970, 944776, 182182, 153476, 19025, 34112, 79728, 10242, 3266, 16917, 36772, 8115, 4259, 5029, 4445], + "k_image": "7f94e759b07b2c025ce22a57db0cb062bfd1f50f6086b14ca3742730c7fa9e5d" + } + }], + "vout": [ + { + "amount": 0, + "target": { + "tagged_key": { + "key": "fcdf91296bb4560335835fda30804a7d8d200acfabe4e98a0c425d38556dac06", + "view_tag": "87" + } + } + }, + { + "amount": 0, + "target": { + "tagged_key": { + "key": "d66821247fe13266bad423e445ddd6a1b51a86198e38049e2c8039ab6d5dc8b4", + "view_tag": "85" + } + } + }, + { + "amount": 0, + "target": { + "tagged_key": { + "key": "93ae131b8c649288a9fb61ebffa8ecb0fababa8f5159286f895f5bed10bad638", + "view_tag": "86" + } + } + }, { + "amount": 0, + "target": { + "tagged_key": { + "key": "8abfdfa2d445934fe750607f9654e02389c056644453c942d1841bbf418d94e2", + "view_tag": "20" + } + } + } + ], + "extra": [ 1, 64, 4, 113, 107, 28, 31, 251, 132, 71, 224, 193, 210, 127, 20, 122, 70, 145, 237, 57, 63, 223, 46, 173, 178, 37, 235, 253, 84, 255, 223, 135, 47], + "rct_signatures": { + "type": 6, + "txnFee": 43920000, + "ecdhInfo": [ { + "amount": "756596945ca38524" + }, { + "amount": "76b456ac3c9942c9" + }, { + "amount": "78d0a3bcd9e6c236" + }, { + "amount": "efadcdf54e6ed0db" + }], + "outPk": [ "9c4bc6ac562b6859a40ad8f3bc85ca35c98badb4b4c5d43832f330d6fedb08e8", "f9e2acd339c648bf03957cb02aa69b8ab15326e3bbe1ce35df677306edabd89e", "5635f226a743068500e25028fbdbf1ea19d0921a27c8baec842b753080f407ee", "4b9a87f2c525e9bfb61fb4d14187c0577e799bf20e53a86359cb75f40ee4d291"] + }, + "rctsig_prunable": { + "nbp": 1, + "bpp": [ { + "A": "7c2b59e7278c94b6296dee9ac65ed5ccf61a77ba4f1b3edfb13c5d02748763b2", + "A1": "3a6bac2f6a891b474d55b625030b35f9b7b564e747afd4cb8e1ce830a9bc59fd", + "B": "6e146443965494f94a8433de054080fcb71f8d48803598cc91db3c7b3fd190ea", + "r1": "8ff5f67980a63de4cb9cd06568a9b27aa994992bc33d70990225acb09faf6806", + "s1": "6aa27c1118c685cb8f3516c3b664450fabdced384de01650d6455287bc0f210a", + "d1": "aa5c173c491844155736a64d7cbdda79f0c8a5ccc07d187ca112664a0e6eb500", + "L": [ "7178983179f1ba2ffb030d577638001b58f5e621b4723e5b0bd0853fb430113d", "03efd026660a18a23c7582e9788f770212b604759aa242b35b3ca4a835bb1888", "1c8593ac4247ba51ea95946cf079721588bac494f563a687fe1010818caa9458", "3969b0f4a4a40eeee395cbb7881a53d98cad51b1e5d12c7071a7424b4c534e32", "c53a31b11e6151edd0a13ef9695021bff9bd4c62df9a62d9e0fbd01e750d0b6a", "bc56cc96d55ef06f6428b42fc63f6610633ecf023211e64a1ff89dcabfeeb4b9", "38e64312dcc849929e8d4a290eff601e06dc65141665d7b312ac1f0f859a00fd", "6d6ccf7dc695e7ae3cf44bed1d9c8659ee3451dd3498f462912ba881a473c9bc" + ], + "R": [ "66e4fb33114b2ef7c25869f9cc3c40a06fc2407e2c678126ff1c38a35c5c949b", "c219fb33ba15730510c41554c727d5adfce33a518148234e0aa5411cb20c115e", "749792ad47ee19e9f1544dba61593d95cb98d4720a8ae6e60146416d673e5707", "c3de31d91043422ab848d4676a6845ced6e7075c5a09bc8b4e0ad706c8c07bda", "527a7325771438e04f37517f3ca5262ef2ddfc9e13db988a90c50be5422a83ad", "75b93f4faae980d6e6a3abfd0e96387121101afaf55f425dc876d9a8735c1e29", "d823e19fee5e502c18d16ec9225f232cfbc3dcd143aaa1904f42e880b612beee", "a3e5a745a7f32e6b2135a75f71117e2947c99647f14702417a9a76f6130b5d62" + ] + }], + "CLSAGs": [ { + "s": [ "fd149a606061709a86253c3c2a30c8ccc0e2b5ee636bda81973b011fa8b96e0f", "9149e7d02d903e982b025e0944029423ba9318637387d6f0a8a75f1fa957950c", "e6661368738251a418968ae390143e596a77bef7de4008ca66ed28b82e044d0a", "b293f792e8b1e9c1bc24b14ee53539f535b05f2f336c1b7698ca3cb1dc8a3a09", "568c6841724a19d412d4313760e3560616df7f5b2250b1a52bf32922b3964309", "b0bedb645579ee09d87959f4e997e4792ac9fa26858ef1aa1dbf7b10da08e709", "2cb200369d75f3d2b81ad2c237954cdfea1d173f84122ce4cf82a9ebaa04650a", "69f3675f2155bbb7ce508fdd6a328492b8788e37809f2accf082387b97a7660d", "427cac9eb93ceacda0cdb9db95a2d6c6fa9ca86276acce2cb8e432b14efb4d0e", "8a1f3cbc8534c5dfb9a42f7b0d5c212928115cb2c5b905c650b5325e2a849109", "c60329dcc20f1c1f10d9f6a87d17359938c520e00dd3f5e1857b5af502cc590c", "ad89abca61f4a94513d8e42db9e7223b5d97afd80f490155bf49b79c7ea5c10d", "6cb74ba10211d6ec75458436a08794164d16bcb4d092274061449418d9fc3d0a", "9947a8854a399c7e77a49568676ff8df07c3aa21ca90a611dcdfe0c6bd44690a", "43a3263237f1def6658ba936e2f17c3853fdcd2c0e24cc0b26c59abb47031e00", "992ca59657da958b48d21d12ae0a93a68596b72c6cc826fd8e079de67b053902"], + "c1": "6a24c5dcea4875f16cd0722352424493647f7ad3b3148bcdf6c8504c25bbbb07", + "D": "a8b01a6352cb602a1964c02e7e10601644cee41c2bdbb39a9687fdd78dca9197" + }], + "pseudoOuts": [ "26312d076b9e7a4e5b0324e305b99bb1c3ea40bd2296de41f2fc43f668e1a9fb"] + } + } + }, + { + "id": "f66f36be5a6b340bc8515d3606d4beceb20611dddb1802b387fbaba30c5c98d3", + "hex": "02000102000bf59ea50bf48bfb08e1d6a1039843f7ee0597d002ba3ca603de3be263ca194830cafb5a73ad93cd2fe5271505596a75d7cabb01ced2bb608028245ea73bb8020002fc3f396be673a4957fbc1976601941d225ffdbec54bc06461698d14fda7c8b1f00022757dd54027e93c917251de2cc6777f7a3fa484f5b244ab54bf8783e7da80c362c01959377b2cc5b76f40886262064cc71324414c2996720dcbea25eae8b8faf4f9802090126a37bb1d1414ab705c0cef71cf0704cd0b1fcde1d656737377f5106deb167d7cde7206c17c8f2f25a508be29e1ad78bb792c3fedcbdd9ce95815c59a5fd98ff5b251f105f3d51067fb90cb9f1e0b6138dfda82aad4906472cf7f8c1b501c786dc1c545d39b00502134e4a2935b9b81f420f4d926bed61b2dde30fde4a464d85cbfda1df5a07da2d4d135ff618e5cf4d6b22238b913af712dd59cb228fec35fc0639d3b54edf518e507034b5be35523ac3c98396bd9cc6e6a59e1c6eda6e93f9e84c4fdade80a6f449ac6ccf8d6583fcd495b78c53a43321210d73370f86999fcc79761f1810514b50b8a1fee1288289b64718d54bcef42abfe61fb8fb0f60373d190d80ad65cabd36d9600d6253f8d343961367526122ca0c5b0acdc60c071f5ab47025d4d89568a2f0b56fc73c6488cc4500e398d2b3059e0d35cede35c33b473e6e57121629db88f0f8c15e03036eae2887f1d9d76d90a0b5a5caa01272347c96e88e461acb04a6be5624c4a6742dd8a0d36ac75e7056ffb3a3a7ae68dda87894c7f503a794dc4458ff058e6f7cd903662e5961d5eeb052b8f075a5ad5cb84407f96ce47a5793ad0c8e4060ee4d90c9946c54b83e91737ccf71acc00045a919866586941d8deeb467a7d83335f84a10d66ba1f51afdf961649ac95ad97dd24d553c1c1004a73332d225d29d0c62bee22e2ea81ceffb02f27ff05a12144a076ed84bce66dd6e84d56fedf06c180a504706977084ff0a74174da623fcb03a88b0246b5a76de445334f447c4525e524e0d3e6b8751417249e0eb6f498ba12ccee562dfddb048ddbd5131263e210d54f04ec38a41c64f7a5812c2083b9fc46fecc2de7f0d1d0aeaaa2f2a9d956879f66e563b48ff9476d67ab98f6956019bf0a36428b0361b28383e7ef2b90bad6a66ae286af4753e54fe6be3131fdc9986ccdf04cbc167abdfd181b326a1d8b30990b9df0b1e702c7bab83c199aa1341c6b4ae1e227b831f0d068e619bb73ed5de32b0bfda62c5203be7f2a6a37c3feb4663da413387e0255ec0204e469fed5e0fde75662cf3a90ae0044778a5a75a319dec07d2d84c70c9b51c9a31f9bc7f25f1a89033dbd23072ac38eb59e08d6ad4f321011a08df0f0559d3b6dd8a22042b40be532d2fc811e42774cd129dc9c9e671600c3a7fe60336836a1e71f2bfeee69d2ae5e647b54dc1c54e9993a64c0f42daf70aaab2387b9a0fdcc82a7e2f3b52ed2a8135b55f166cb49fb6b1d34a64d30f370d8271408d0e4db75616b758014671d321c8c5086a8d7b1bedd44bcb75b382c60bffb7c7726232426ea19c9bd622ee096e772a4c5ab6305a6b2f27fb4a5f60e40737a4cc043ea061755ace64393a0af82ea8088307426acb33a34de95ea7252c01a5b07847f707777b8cd64cc73364a8e65181227ba1ba5aa63161408a7265980b6b6c18079d195a12ec7af4404ff61d3c756aa35b88e4fe4bd72c8b22298b1601b1d04f9861e7118f10808505812809d54d85aa79f4ceb905c8e87b1a5801c60bb12b0f3fdc0e5e0afb7839fd51742aa88ef4466bc1e1f9c1b6f978f736b1880c9a902621eaca740aa21cfaf36931e09b7cc3b28223ad6c2398bd828c7270460d78dd6dd5d9c181aed832e62d56b00e870961b0b6a3a77eb4604cef64f69898046ba7157673909c3b2cb3bedef665e83c364475d482dfa46e717c4c5fa29b7f09dda97110c8a66b48e8fab4cbff2578e4cb85e6b353bfe8f78b6f9182711de13c7093d2007f1dbdfe5f46332b797376af80efc67ab028c597d528461384683dae", + "signature_hash": "8cb405e1460df8134032db1430e1cfffb8f707c9de43ba1f68100f2af8a5e6b1", + "tx": { + "version": 2, + "unlock_time": 0, + "vin": [ { + "key": { + "amount": 0, + "key_offsets": [ 23678837, 18793972, 6843233, 8600, 96119, 43031, 7738, 422, 7646, 12770, 3274], + "k_image": "4830cafb5a73ad93cd2fe5271505596a75d7cabb01ced2bb608028245ea73bb8" + } + }], + "vout": [ + { + "amount": 0, + "target": { + "key": "fc3f396be673a4957fbc1976601941d225ffdbec54bc06461698d14fda7c8b1f" + } + }, + { + "amount": 0, + "target": { + "key": "2757dd54027e93c917251de2cc6777f7a3fa484f5b244ab54bf8783e7da80c36" + } + } + ], + "extra": [ 1, 149, 147, 119, 178, 204, 91, 118, 244, 8, 134, 38, 32, 100, 204, 113, 50, 68, 20, 194, 153, 103, 32, 220, 190, 162, 94, 174, 139, 143, 175, 79, 152, 2, 9, 1, 38, 163, 123, 177, 209, 65, 74, 183], + "rct_signatures": { + "type": 5, + "txnFee": 60680000, + "ecdhInfo": [ { + "amount": "f0704cd0b1fcde1d" + }, { + "amount": "656737377f5106de" + }], + "outPk": [ "b167d7cde7206c17c8f2f25a508be29e1ad78bb792c3fedcbdd9ce95815c59a5", "fd98ff5b251f105f3d51067fb90cb9f1e0b6138dfda82aad4906472cf7f8c1b5"] + }, + "rctsig_prunable": { + "nbp": 1, + "bp": [ { + "A": "c786dc1c545d39b00502134e4a2935b9b81f420f4d926bed61b2dde30fde4a46", + "S": "4d85cbfda1df5a07da2d4d135ff618e5cf4d6b22238b913af712dd59cb228fec", + "T1": "35fc0639d3b54edf518e507034b5be35523ac3c98396bd9cc6e6a59e1c6eda6e", + "T2": "93f9e84c4fdade80a6f449ac6ccf8d6583fcd495b78c53a43321210d73370f86", + "taux": "999fcc79761f1810514b50b8a1fee1288289b64718d54bcef42abfe61fb8fb0f", + "mu": "60373d190d80ad65cabd36d9600d6253f8d343961367526122ca0c5b0acdc60c", + "L": [ "1f5ab47025d4d89568a2f0b56fc73c6488cc4500e398d2b3059e0d35cede35c3", "3b473e6e57121629db88f0f8c15e03036eae2887f1d9d76d90a0b5a5caa01272", "347c96e88e461acb04a6be5624c4a6742dd8a0d36ac75e7056ffb3a3a7ae68dd", "a87894c7f503a794dc4458ff058e6f7cd903662e5961d5eeb052b8f075a5ad5c", "b84407f96ce47a5793ad0c8e4060ee4d90c9946c54b83e91737ccf71acc00045", "a919866586941d8deeb467a7d83335f84a10d66ba1f51afdf961649ac95ad97d", "d24d553c1c1004a73332d225d29d0c62bee22e2ea81ceffb02f27ff05a12144a" + ], + "R": [ "6ed84bce66dd6e84d56fedf06c180a504706977084ff0a74174da623fcb03a88", "b0246b5a76de445334f447c4525e524e0d3e6b8751417249e0eb6f498ba12cce", "e562dfddb048ddbd5131263e210d54f04ec38a41c64f7a5812c2083b9fc46fec", "c2de7f0d1d0aeaaa2f2a9d956879f66e563b48ff9476d67ab98f6956019bf0a3", "6428b0361b28383e7ef2b90bad6a66ae286af4753e54fe6be3131fdc9986ccdf", "04cbc167abdfd181b326a1d8b30990b9df0b1e702c7bab83c199aa1341c6b4ae", "1e227b831f0d068e619bb73ed5de32b0bfda62c5203be7f2a6a37c3feb4663da" + ], + "a": "413387e0255ec0204e469fed5e0fde75662cf3a90ae0044778a5a75a319dec07", + "b": "d2d84c70c9b51c9a31f9bc7f25f1a89033dbd23072ac38eb59e08d6ad4f32101", + "t": "1a08df0f0559d3b6dd8a22042b40be532d2fc811e42774cd129dc9c9e671600c" + }], + "CLSAGs": [ { + "s": [ "3a7fe60336836a1e71f2bfeee69d2ae5e647b54dc1c54e9993a64c0f42daf70a", "aab2387b9a0fdcc82a7e2f3b52ed2a8135b55f166cb49fb6b1d34a64d30f370d", "8271408d0e4db75616b758014671d321c8c5086a8d7b1bedd44bcb75b382c60b", "ffb7c7726232426ea19c9bd622ee096e772a4c5ab6305a6b2f27fb4a5f60e407", "37a4cc043ea061755ace64393a0af82ea8088307426acb33a34de95ea7252c01", "a5b07847f707777b8cd64cc73364a8e65181227ba1ba5aa63161408a7265980b", "6b6c18079d195a12ec7af4404ff61d3c756aa35b88e4fe4bd72c8b22298b1601", "b1d04f9861e7118f10808505812809d54d85aa79f4ceb905c8e87b1a5801c60b", "b12b0f3fdc0e5e0afb7839fd51742aa88ef4466bc1e1f9c1b6f978f736b1880c", "9a902621eaca740aa21cfaf36931e09b7cc3b28223ad6c2398bd828c7270460d", "78dd6dd5d9c181aed832e62d56b00e870961b0b6a3a77eb4604cef64f6989804"], + "c1": "6ba7157673909c3b2cb3bedef665e83c364475d482dfa46e717c4c5fa29b7f09", + "D": "dda97110c8a66b48e8fab4cbff2578e4cb85e6b353bfe8f78b6f9182711de13c" + }], + "pseudoOuts": [ "7093d2007f1dbdfe5f46332b797376af80efc67ab028c597d528461384683dae"] + } + } + }, + { + "id": "55ba10662968c57fc8fed2c82a99d6fd9516730c245f58e9e87bb9a35378014a", + "hex": "01000302b0f9cf0e0100e53d3d97d11974ccf49d23513b9465bc139bda14b8207288e41557707e59c2dc02c08092de06010133e69f524f1989738827c9fb9087d45d7b6865645453f620189939d926735b3902b0b2c4f62201001d680e360c156c7cc952b0c4fc39a39c95a766423109766ae3dcf0c5cf8abf9e06a0f736026bcac41f5468fd1bcf8994e1a4282aecb420005ec294324faec5c6f46806614780c8afa025027998ef0ac319d96b224fa3c33fe12677ef06f4aed80916f28e3f7a684a1c89d78094ebdc03028e0004b98f7c622f1d0364a2d0270c40e6606793bd948a41af9287016d264d7580ade20402f911ad66eabcd8112e90130d7594cb8fc413431da9b5b004c8651d100a2ac8fb8084af5f029caa24c41b1b4938e7e066a7e59592fdc3c832e2516048e57af5d8acca9bef0ac0b80202e99b6a2e000f03f73c1966e16875945ffad20b1895efa17202c6e240191b01e421011da6ff966df43bd44f513aaafa260c54e2ad31469664fa7b0a44fed4ead9a483056cd88c350b340289694ad525f1316367ed16673dc23911e624e6bac4a48b032a623d09ce5bd85a839534de4fbbfb72da2a6779a66f775c16379e8abd122401a5a19c47bb8ecd0c7aa9610513b611602d246f3b07fa19512dc8fde7b180c704694b2d75abbaa2907cda52f888f18ded34308dd1b40f1fb33a01340a1ddc5f07ce19ab4357c52764600861d2d331cfed5972fff42dac64583d617fa4ee27a509a8c29ff77fc71bc7cd106ce54cd3020d1c18c6c794aaa93cb32cd4ff55d82b04", + "signature_hash": "55ba10662968c57fc8fed2c82a99d6fd9516730c245f58e9e87bb9a35378014a", + "tx": { + "version": 1, + "unlock_time": 0, + "vin": [ + { + "key": { + "amount": 30670000, + "key_offsets": [0], + "k_image": "e53d3d97d11974ccf49d23513b9465bc139bda14b8207288e41557707e59c2dc" + } + }, + { + "key": { + "amount": 1808040000, + "key_offsets": [1], + "k_image": "33e69f524f1989738827c9fb9087d45d7b6865645453f620189939d926735b39" + } + }, + { + "key": { + "amount": 9375390000, + "key_offsets": [0], + "k_image": "1d680e360c156c7cc952b0c4fc39a39c95a766423109766ae3dcf0c5cf8abf9e" + } + } + ], + "vout": [ + { + "amount": 900000, + "target": { + "key": "6bcac41f5468fd1bcf8994e1a4282aecb420005ec294324faec5c6f468066147" + } + }, + { + "amount": 10000000000, + "target": { + "key": "7998ef0ac319d96b224fa3c33fe12677ef06f4aed80916f28e3f7a684a1c89d7" + } + }, + { + "amount": 1000000000, + "target": { + "key": "8e0004b98f7c622f1d0364a2d0270c40e6606793bd948a41af9287016d264d75" + } + }, + { + "amount": 10000000, + "target": { + "key": "f911ad66eabcd8112e90130d7594cb8fc413431da9b5b004c8651d100a2ac8fb" + } + }, + { + "amount": 200000000, + "target": { + "key": "9caa24c41b1b4938e7e066a7e59592fdc3c832e2516048e57af5d8acca9bef0a" + } + }, + { + "amount": 40000, + "target": { + "key": "e99b6a2e000f03f73c1966e16875945ffad20b1895efa17202c6e240191b01e4" + } + } + ], + "extra": [ 1, 29, 166, 255, 150, 109, 244, 59, 212, 79, 81, 58, 170, 250, 38, 12, 84, 226, 173, 49, 70, 150, 100, 250, 123, 10, 68, 254, 212, 234, 217, 164, 131], + "signatures": [ "056cd88c350b340289694ad525f1316367ed16673dc23911e624e6bac4a48b032a623d09ce5bd85a839534de4fbbfb72da2a6779a66f775c16379e8abd122401", "a5a19c47bb8ecd0c7aa9610513b611602d246f3b07fa19512dc8fde7b180c704694b2d75abbaa2907cda52f888f18ded34308dd1b40f1fb33a01340a1ddc5f07", "ce19ab4357c52764600861d2d331cfed5972fff42dac64583d617fa4ee27a509a8c29ff77fc71bc7cd106ce54cd3020d1c18c6c794aaa93cb32cd4ff55d82b04"] + } + } +] diff --git a/networks/monero/src/transaction.rs b/networks/monero/src/transaction.rs new file mode 100644 index 00000000..af936b48 --- /dev/null +++ b/networks/monero/src/transaction.rs @@ -0,0 +1,625 @@ +use core::cmp::Ordering; +use std_shims::{ + vec, + vec::Vec, + io::{self, Read, Write}, +}; + +use zeroize::Zeroize; + +use curve25519_dalek::edwards::{EdwardsPoint, CompressedEdwardsY}; + +use crate::{ + io::*, + primitives::keccak256, + ring_signatures::RingSignature, + ringct::{bulletproofs::Bulletproof, PrunedRctProofs}, +}; + +/// An input in the Monero protocol. +#[derive(Clone, PartialEq, Eq, Debug)] +pub enum Input { + /// An input for a miner transaction, which is generating new coins. + Gen(usize), + /// An input spending an output on-chain. + ToKey { + /// The pool this input spends an output of. + amount: Option, + /// The decoys used by this input's ring, specified as their offset distance from each other. + key_offsets: Vec, + /// The key image (linking tag, nullifer) for the spent output. + key_image: EdwardsPoint, + }, +} + +impl Input { + /// Write the Input. + pub fn write(&self, w: &mut W) -> io::Result<()> { + match self { + Input::Gen(height) => { + w.write_all(&[255])?; + write_varint(height, w) + } + + Input::ToKey { amount, key_offsets, key_image } => { + w.write_all(&[2])?; + write_varint(&amount.unwrap_or(0), w)?; + write_vec(write_varint, key_offsets, w)?; + write_point(key_image, w) + } + } + } + + /// Serialize the Input to a `Vec`. + pub fn serialize(&self) -> Vec { + let mut res = vec![]; + self.write(&mut res).unwrap(); + res + } + + /// Read an Input. + pub fn read(r: &mut R) -> io::Result { + Ok(match read_byte(r)? { + 255 => Input::Gen(read_varint(r)?), + 2 => { + let amount = read_varint(r)?; + // https://github.com/monero-project/monero/ + // blob/00fd416a99686f0956361d1cd0337fe56e58d4a7/ + // src/cryptonote_basic/cryptonote_format_utils.cpp#L860-L863 + // A non-RCT 0-amount input can't exist because only RCT TXs can have a 0-amount output + // That's why collapsing to None if the amount is 0 is safe, even without knowing if RCT + let amount = if amount == 0 { None } else { Some(amount) }; + Input::ToKey { + amount, + key_offsets: read_vec(read_varint, r)?, + key_image: read_torsion_free_point(r)?, + } + } + _ => Err(io::Error::other("Tried to deserialize unknown/unused input type"))?, + }) + } +} + +/// An output in the Monero protocol. +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct Output { + /// The pool this output should be sorted into. + pub amount: Option, + /// The key which can spend this output. + pub key: CompressedEdwardsY, + /// The view tag for this output, as used to accelerate scanning. + pub view_tag: Option, +} + +impl Output { + /// Write the Output. + pub fn write(&self, w: &mut W) -> io::Result<()> { + write_varint(&self.amount.unwrap_or(0), w)?; + w.write_all(&[2 + u8::from(self.view_tag.is_some())])?; + w.write_all(&self.key.to_bytes())?; + if let Some(view_tag) = self.view_tag { + w.write_all(&[view_tag])?; + } + Ok(()) + } + + /// Write the Output to a `Vec`. + pub fn serialize(&self) -> Vec { + let mut res = Vec::with_capacity(8 + 1 + 32); + self.write(&mut res).unwrap(); + res + } + + /// Read an Output. + pub fn read(rct: bool, r: &mut R) -> io::Result { + let amount = read_varint(r)?; + let amount = if rct { + if amount != 0 { + Err(io::Error::other("RCT TX output wasn't 0"))?; + } + None + } else { + Some(amount) + }; + + let view_tag = match read_byte(r)? { + 2 => false, + 3 => true, + _ => Err(io::Error::other("Tried to deserialize unknown/unused output type"))?, + }; + + Ok(Output { + amount, + key: CompressedEdwardsY(read_bytes(r)?), + view_tag: if view_tag { Some(read_byte(r)?) } else { None }, + }) + } +} + +/// An additional timelock for a Monero transaction. +/// +/// Monero outputs are locked by a default timelock. If a timelock is explicitly specified, the +/// longer of the two will be the timelock used. +#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)] +pub enum Timelock { + /// No additional timelock. + None, + /// Additionally locked until this block. + Block(usize), + /// Additionally locked until this many seconds since the epoch. + Time(u64), +} + +impl Timelock { + /// Write the Timelock. + pub fn write(&self, w: &mut W) -> io::Result<()> { + match self { + Timelock::None => write_varint(&0u8, w), + Timelock::Block(block) => write_varint(block, w), + Timelock::Time(time) => write_varint(time, w), + } + } + + /// Serialize the Timelock to a `Vec`. + pub fn serialize(&self) -> Vec { + let mut res = Vec::with_capacity(1); + self.write(&mut res).unwrap(); + res + } + + /// Read a Timelock. + pub fn read(r: &mut R) -> io::Result { + const TIMELOCK_BLOCK_THRESHOLD: usize = 500_000_000; + + let raw = read_varint::<_, u64>(r)?; + Ok(if raw == 0 { + Timelock::None + } else if raw < + u64::try_from(TIMELOCK_BLOCK_THRESHOLD) + .expect("TIMELOCK_BLOCK_THRESHOLD didn't fit in a u64") + { + Timelock::Block(usize::try_from(raw).expect( + "timelock overflowed usize despite being less than a const representable with a usize", + )) + } else { + Timelock::Time(raw) + }) + } +} + +impl PartialOrd for Timelock { + fn partial_cmp(&self, other: &Self) -> Option { + match (self, other) { + (Timelock::None, Timelock::None) => Some(Ordering::Equal), + (Timelock::None, _) => Some(Ordering::Less), + (_, Timelock::None) => Some(Ordering::Greater), + (Timelock::Block(a), Timelock::Block(b)) => a.partial_cmp(b), + (Timelock::Time(a), Timelock::Time(b)) => a.partial_cmp(b), + _ => None, + } + } +} + +/// The transaction prefix. +/// +/// This is common to all transaction versions and contains most parts of the transaction needed to +/// handle it. It excludes any proofs. +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct TransactionPrefix { + /// The timelock this transaction is additionally constrained by. + /// + /// All transactions on the blockchain are subject to a 10-block lock. This adds a further + /// constraint. + pub additional_timelock: Timelock, + /// The inputs for this transaction. + pub inputs: Vec, + /// The outputs for this transaction. + pub outputs: Vec, + /// The additional data included within the transaction. + /// + /// This is an arbitrary data field, yet is used by wallets for containing the data necessary to + /// scan the transaction. + pub extra: Vec, +} + +impl TransactionPrefix { + /// Write a TransactionPrefix. + /// + /// This is distinct from Monero in that it won't write any version. + fn write(&self, w: &mut W) -> io::Result<()> { + self.additional_timelock.write(w)?; + write_vec(Input::write, &self.inputs, w)?; + write_vec(Output::write, &self.outputs, w)?; + write_varint(&self.extra.len(), w)?; + w.write_all(&self.extra) + } + + /// Read a TransactionPrefix. + /// + /// This is distinct from Monero in that it won't read the version. The version must be passed + /// in. + pub fn read(r: &mut R, version: u64) -> io::Result { + let additional_timelock = Timelock::read(r)?; + + let inputs = read_vec(|r| Input::read(r), r)?; + if inputs.is_empty() { + Err(io::Error::other("transaction had no inputs"))?; + } + let is_miner_tx = matches!(inputs[0], Input::Gen { .. }); + + let mut prefix = TransactionPrefix { + additional_timelock, + inputs, + outputs: read_vec(|r| Output::read((!is_miner_tx) && (version == 2), r), r)?, + extra: vec![], + }; + prefix.extra = read_vec(read_byte, r)?; + Ok(prefix) + } + + fn hash(&self, version: u64) -> [u8; 32] { + let mut buf = vec![]; + write_varint(&version, &mut buf).unwrap(); + self.write(&mut buf).unwrap(); + keccak256(buf) + } +} + +mod sealed { + use core::fmt::Debug; + use crate::ringct::*; + use super::*; + + pub(crate) trait RingSignatures: Clone + PartialEq + Eq + Default + Debug { + fn signatures_to_write(&self) -> &[RingSignature]; + fn read_signatures(inputs: &[Input], r: &mut impl Read) -> io::Result; + } + + impl RingSignatures for Vec { + fn signatures_to_write(&self) -> &[RingSignature] { + self + } + fn read_signatures(inputs: &[Input], r: &mut impl Read) -> io::Result { + let mut signatures = Vec::with_capacity(inputs.len()); + for input in inputs { + match input { + Input::ToKey { key_offsets, .. } => { + signatures.push(RingSignature::read(key_offsets.len(), r)?) + } + _ => Err(io::Error::other("reading signatures for a transaction with non-ToKey inputs"))?, + } + } + Ok(signatures) + } + } + + impl RingSignatures for () { + fn signatures_to_write(&self) -> &[RingSignature] { + &[] + } + fn read_signatures(_: &[Input], _: &mut impl Read) -> io::Result { + Ok(()) + } + } + + pub(crate) trait RctProofsTrait: Clone + PartialEq + Eq + Debug { + fn write(&self, w: &mut impl Write) -> io::Result<()>; + fn read( + ring_length: usize, + inputs: usize, + outputs: usize, + r: &mut impl Read, + ) -> io::Result>; + fn rct_type(&self) -> RctType; + fn base(&self) -> &RctBase; + } + + impl RctProofsTrait for RctProofs { + fn write(&self, w: &mut impl Write) -> io::Result<()> { + self.write(w) + } + fn read( + ring_length: usize, + inputs: usize, + outputs: usize, + r: &mut impl Read, + ) -> io::Result> { + RctProofs::read(ring_length, inputs, outputs, r) + } + fn rct_type(&self) -> RctType { + self.rct_type() + } + fn base(&self) -> &RctBase { + &self.base + } + } + + impl RctProofsTrait for PrunedRctProofs { + fn write(&self, w: &mut impl Write) -> io::Result<()> { + self.base.write(w, self.rct_type) + } + fn read( + _ring_length: usize, + inputs: usize, + outputs: usize, + r: &mut impl Read, + ) -> io::Result> { + Ok(RctBase::read(inputs, outputs, r)?.map(|(rct_type, base)| Self { rct_type, base })) + } + fn rct_type(&self) -> RctType { + self.rct_type + } + fn base(&self) -> &RctBase { + &self.base + } + } + + pub(crate) trait PotentiallyPruned { + type RingSignatures: RingSignatures; + type RctProofs: RctProofsTrait; + } + /// A transaction which isn't pruned. + #[derive(Clone, PartialEq, Eq, Debug)] + pub struct NotPruned; + impl PotentiallyPruned for NotPruned { + type RingSignatures = Vec; + type RctProofs = RctProofs; + } + /// A transaction which is pruned. + #[derive(Clone, PartialEq, Eq, Debug)] + pub struct Pruned; + impl PotentiallyPruned for Pruned { + type RingSignatures = (); + type RctProofs = PrunedRctProofs; + } +} +pub use sealed::*; + +/// A Monero transaction. +#[allow(private_bounds, private_interfaces, clippy::large_enum_variant)] +#[derive(Clone, PartialEq, Eq, Debug)] +pub enum Transaction { + /// A version 1 transaction, used by the original Cryptonote codebase. + V1 { + /// The transaction's prefix. + prefix: TransactionPrefix, + /// The transaction's ring signatures. + signatures: P::RingSignatures, + }, + /// A version 2 transaction, used by the RingCT protocol. + V2 { + /// The transaction's prefix. + prefix: TransactionPrefix, + /// The transaction's proofs. + proofs: Option, + }, +} + +enum PrunableHash<'a> { + V1(&'a [RingSignature]), + V2([u8; 32]), +} + +#[allow(private_bounds)] +impl Transaction

{ + /// Get the version of this transaction. + pub fn version(&self) -> u8 { + match self { + Transaction::V1 { .. } => 1, + Transaction::V2 { .. } => 2, + } + } + + /// Get the TransactionPrefix of this transaction. + pub fn prefix(&self) -> &TransactionPrefix { + match self { + Transaction::V1 { prefix, .. } | Transaction::V2 { prefix, .. } => prefix, + } + } + + /// Get a mutable reference to the TransactionPrefix of this transaction. + pub fn prefix_mut(&mut self) -> &mut TransactionPrefix { + match self { + Transaction::V1 { prefix, .. } | Transaction::V2 { prefix, .. } => prefix, + } + } + + /// Write the Transaction. + /// + /// Some writable transactions may not be readable if they're malformed, per Monero's consensus + /// rules. + pub fn write(&self, w: &mut W) -> io::Result<()> { + write_varint(&self.version(), w)?; + match self { + Transaction::V1 { prefix, signatures } => { + prefix.write(w)?; + for ring_sig in signatures.signatures_to_write() { + ring_sig.write(w)?; + } + } + Transaction::V2 { prefix, proofs } => { + prefix.write(w)?; + match proofs { + None => w.write_all(&[0])?, + Some(proofs) => proofs.write(w)?, + } + } + } + Ok(()) + } + + /// Write the Transaction to a `Vec`. + pub fn serialize(&self) -> Vec { + let mut res = Vec::with_capacity(2048); + self.write(&mut res).unwrap(); + res + } + + /// Read a Transaction. + pub fn read(r: &mut R) -> io::Result { + let version = read_varint(r)?; + let prefix = TransactionPrefix::read(r, version)?; + + if version == 1 { + let signatures = if (prefix.inputs.len() == 1) && matches!(prefix.inputs[0], Input::Gen(_)) { + Default::default() + } else { + P::RingSignatures::read_signatures(&prefix.inputs, r)? + }; + + Ok(Transaction::V1 { prefix, signatures }) + } else if version == 2 { + let proofs = P::RctProofs::read( + prefix.inputs.first().map_or(0, |input| match input { + Input::Gen(_) => 0, + Input::ToKey { key_offsets, .. } => key_offsets.len(), + }), + prefix.inputs.len(), + prefix.outputs.len(), + r, + )?; + + Ok(Transaction::V2 { prefix, proofs }) + } else { + Err(io::Error::other("tried to deserialize unknown version")) + } + } + + // The hash of the transaction. + #[allow(clippy::needless_pass_by_value)] + fn hash_with_prunable_hash(&self, prunable: PrunableHash<'_>) -> [u8; 32] { + match self { + Transaction::V1 { prefix, .. } => { + let mut buf = Vec::with_capacity(512); + + // We don't use `self.write` as that may write the signatures (if this isn't pruned) + write_varint(&self.version(), &mut buf).unwrap(); + prefix.write(&mut buf).unwrap(); + + // We explicitly write the signatures ourselves here + let PrunableHash::V1(signatures) = prunable else { + panic!("hashing v1 TX with non-v1 prunable data") + }; + for signature in signatures { + signature.write(&mut buf).unwrap(); + } + + keccak256(buf) + } + Transaction::V2 { prefix, proofs } => { + let mut hashes = Vec::with_capacity(96); + + hashes.extend(prefix.hash(2)); + + if let Some(proofs) = proofs { + let mut buf = Vec::with_capacity(512); + proofs.base().write(&mut buf, proofs.rct_type()).unwrap(); + hashes.extend(keccak256(&buf)); + } else { + // Serialization of RctBase::Null + hashes.extend(keccak256([0])); + } + let PrunableHash::V2(prunable_hash) = prunable else { + panic!("hashing v2 TX with non-v2 prunable data") + }; + hashes.extend(prunable_hash); + + keccak256(hashes) + } + } + } +} + +impl Transaction { + /// The hash of the transaction. + pub fn hash(&self) -> [u8; 32] { + match self { + Transaction::V1 { signatures, .. } => { + self.hash_with_prunable_hash(PrunableHash::V1(signatures)) + } + Transaction::V2 { proofs, .. } => { + self.hash_with_prunable_hash(PrunableHash::V2(if let Some(proofs) = proofs { + let mut buf = Vec::with_capacity(1024); + proofs.prunable.write(&mut buf, proofs.rct_type()).unwrap(); + keccak256(buf) + } else { + [0; 32] + })) + } + } + } + + /// Calculate the hash of this transaction as needed for signing it. + /// + /// This returns None if the transaction is without signatures. + pub fn signature_hash(&self) -> Option<[u8; 32]> { + Some(match self { + Transaction::V1 { prefix, signatures } => { + if (prefix.inputs.len() == 1) && matches!(prefix.inputs[0], Input::Gen(_)) { + None?; + } + self.hash_with_prunable_hash(PrunableHash::V1(signatures)) + } + Transaction::V2 { proofs, .. } => self.hash_with_prunable_hash({ + let Some(proofs) = proofs else { None? }; + let mut buf = Vec::with_capacity(1024); + proofs.prunable.signature_write(&mut buf).unwrap(); + PrunableHash::V2(keccak256(buf)) + }), + }) + } + + fn is_rct_bulletproof(&self) -> bool { + match self { + Transaction::V1 { .. } => false, + Transaction::V2 { proofs, .. } => { + let Some(proofs) = proofs else { return false }; + proofs.rct_type().bulletproof() + } + } + } + + fn is_rct_bulletproof_plus(&self) -> bool { + match self { + Transaction::V1 { .. } => false, + Transaction::V2 { proofs, .. } => { + let Some(proofs) = proofs else { return false }; + proofs.rct_type().bulletproof_plus() + } + } + } + + /// Calculate the transaction's weight. + pub fn weight(&self) -> usize { + let blob_size = self.serialize().len(); + + let bp = self.is_rct_bulletproof(); + let bp_plus = self.is_rct_bulletproof_plus(); + if !(bp || bp_plus) { + blob_size + } else { + blob_size + + Bulletproof::calculate_bp_clawback( + bp_plus, + match self { + Transaction::V1 { .. } => panic!("v1 transaction was BP(+)"), + Transaction::V2 { prefix, .. } => prefix.outputs.len(), + }, + ) + .0 + } + } +} + +impl From> for Transaction { + fn from(tx: Transaction) -> Transaction { + match tx { + Transaction::V1 { prefix, .. } => Transaction::V1 { prefix, signatures: () }, + Transaction::V2 { prefix, proofs } => Transaction::V2 { + prefix, + proofs: proofs + .map(|proofs| PrunedRctProofs { rct_type: proofs.rct_type(), base: proofs.base }), + }, + } + } +} diff --git a/networks/monero/tests/tests.rs b/networks/monero/tests/tests.rs new file mode 100644 index 00000000..7b6656f2 --- /dev/null +++ b/networks/monero/tests/tests.rs @@ -0,0 +1,3 @@ +// TODO +#[test] +fn test() {} diff --git a/networks/monero/verify-chain/Cargo.toml b/networks/monero/verify-chain/Cargo.toml new file mode 100644 index 00000000..82b2182e --- /dev/null +++ b/networks/monero/verify-chain/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "monero-serai-verify-chain" +version = "0.1.0" +description = "A binary to deserialize and verify the Monero blockchain" +license = "MIT" +repository = "https://github.com/serai-dex/serai/tree/develop/networks/monero/verify-chain" +authors = ["Luke Parker "] +edition = "2021" +rust-version = "1.79" +publish = false + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + +[lints] +workspace = true + +[dependencies] +rand_core = { version = "0.6", default-features = false, features = ["std"] } + +curve25519-dalek = { version = "4", default-features = false, features = ["alloc", "zeroize"] } + +hex = { version = "0.4", default-features = false, features = ["std"] } +serde = { version = "1", default-features = false, features = ["derive", "alloc", "std"] } +serde_json = { version = "1", default-features = false, features = ["alloc", "std"] } + +monero-serai = { path = "..", default-features = false, features = ["std", "compile-time-generators"] } +monero-rpc = { path = "../rpc", default-features = false, features = ["std"] } +monero-simple-request-rpc = { path = "../rpc/simple-request", default-features = false } + +tokio = { version = "1", default-features = false, features = ["rt-multi-thread", "macros"] } diff --git a/networks/monero/verify-chain/LICENSE b/networks/monero/verify-chain/LICENSE new file mode 100644 index 00000000..91d893c1 --- /dev/null +++ b/networks/monero/verify-chain/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022-2024 Luke Parker + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/networks/monero/verify-chain/README.md b/networks/monero/verify-chain/README.md new file mode 100644 index 00000000..4348b2c1 --- /dev/null +++ b/networks/monero/verify-chain/README.md @@ -0,0 +1,7 @@ +# monero-serai Verify Chain + +A binary to deserialize and verify the Monero blockchain. + +This is not complete. This is not intended to be complete. This is intended to +test monero-serai against actual blockchain data. Do not use this as an +inflation checker. diff --git a/networks/monero/verify-chain/src/main.rs b/networks/monero/verify-chain/src/main.rs new file mode 100644 index 00000000..2cc56c55 --- /dev/null +++ b/networks/monero/verify-chain/src/main.rs @@ -0,0 +1,284 @@ +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![doc = include_str!("../README.md")] +#![deny(missing_docs)] + +use curve25519_dalek::{scalar::Scalar, edwards::EdwardsPoint}; + +use serde::Deserialize; +use serde_json::json; + +use monero_serai::{ + io::decompress_point, + primitives::Commitment, + ringct::{RctPrunable, bulletproofs::BatchVerifier}, + transaction::{Input, Transaction}, + block::Block, +}; + +use monero_rpc::{RpcError, Rpc}; +use monero_simple_request_rpc::SimpleRequestRpc; + +use tokio::task::JoinHandle; + +async fn check_block(rpc: impl Rpc, block_i: usize) { + let hash = loop { + match rpc.get_block_hash(block_i).await { + Ok(hash) => break hash, + Err(RpcError::ConnectionError(e)) => { + println!("get_block_hash ConnectionError: {e}"); + continue; + } + Err(e) => panic!("couldn't get block {block_i}'s hash: {e:?}"), + } + }; + + // TODO: Grab the JSON to also check it was deserialized correctly + #[derive(Deserialize, Debug)] + struct BlockResponse { + blob: String, + } + let res: BlockResponse = loop { + match rpc.json_rpc_call("get_block", Some(json!({ "hash": hex::encode(hash) }))).await { + Ok(res) => break res, + Err(RpcError::ConnectionError(e)) => { + println!("get_block ConnectionError: {e}"); + continue; + } + Err(e) => panic!("couldn't get block {block_i} via block.hash(): {e:?}"), + } + }; + + let blob = hex::decode(res.blob).expect("node returned non-hex block"); + let block = Block::read(&mut blob.as_slice()) + .unwrap_or_else(|e| panic!("couldn't deserialize block {block_i}: {e}")); + assert_eq!(block.hash(), hash, "hash differs"); + assert_eq!(block.serialize(), blob, "serialization differs"); + + let txs_len = 1 + block.transactions.len(); + + if !block.transactions.is_empty() { + // Test getting pruned transactions + loop { + match rpc.get_pruned_transactions(&block.transactions).await { + Ok(_) => break, + Err(RpcError::ConnectionError(e)) => { + println!("get_pruned_transactions ConnectionError: {e}"); + continue; + } + Err(e) => panic!("couldn't call get_pruned_transactions: {e:?}"), + } + } + + let txs = loop { + match rpc.get_transactions(&block.transactions).await { + Ok(txs) => break txs, + Err(RpcError::ConnectionError(e)) => { + println!("get_transactions ConnectionError: {e}"); + continue; + } + Err(e) => panic!("couldn't call get_transactions: {e:?}"), + } + }; + + let mut batch = BatchVerifier::new(); + for tx in txs { + match tx { + Transaction::V1 { prefix: _, signatures } => { + assert!(!signatures.is_empty()); + continue; + } + Transaction::V2 { prefix: _, proofs: None } => { + panic!("proofs were empty in non-miner v2 transaction"); + } + Transaction::V2 { ref prefix, proofs: Some(ref proofs) } => { + let sig_hash = tx.signature_hash().expect("no signature hash for TX with proofs"); + // Verify all proofs we support proving for + // This is due to having debug_asserts calling verify within their proving, and CLSAG + // multisig explicitly calling verify as part of its signing process + // Accordingly, making sure our signature_hash algorithm is correct is great, and further + // making sure the verification functions are valid is appreciated + match &proofs.prunable { + RctPrunable::AggregateMlsagBorromean { .. } | RctPrunable::MlsagBorromean { .. } => {} + RctPrunable::MlsagBulletproofs { bulletproof, .. } | + RctPrunable::MlsagBulletproofsCompactAmount { bulletproof, .. } => { + assert!(bulletproof.batch_verify( + &mut rand_core::OsRng, + &mut batch, + &proofs.base.commitments + )); + } + RctPrunable::Clsag { bulletproof, clsags, pseudo_outs } => { + assert!(bulletproof.batch_verify( + &mut rand_core::OsRng, + &mut batch, + &proofs.base.commitments + )); + + for (i, clsag) in clsags.iter().enumerate() { + let (amount, key_offsets, image) = match &prefix.inputs[i] { + Input::Gen(_) => panic!("Input::Gen"), + Input::ToKey { amount, key_offsets, key_image } => { + (amount, key_offsets, key_image) + } + }; + + let mut running_sum = 0; + let mut actual_indexes = vec![]; + for offset in key_offsets { + running_sum += offset; + actual_indexes.push(running_sum); + } + + async fn get_outs( + rpc: &impl Rpc, + amount: u64, + indexes: &[u64], + ) -> Vec<[EdwardsPoint; 2]> { + #[derive(Deserialize, Debug)] + struct Out { + key: String, + mask: String, + } + + #[derive(Deserialize, Debug)] + struct Outs { + outs: Vec, + } + + let outs: Outs = loop { + match rpc + .rpc_call( + "get_outs", + Some(json!({ + "get_txid": true, + "outputs": indexes.iter().map(|o| json!({ + "amount": amount, + "index": o + })).collect::>() + })), + ) + .await + { + Ok(outs) => break outs, + Err(RpcError::ConnectionError(e)) => { + println!("get_outs ConnectionError: {e}"); + continue; + } + Err(e) => panic!("couldn't connect to RPC to get outs: {e:?}"), + } + }; + + let rpc_point = |point: &str| { + decompress_point( + hex::decode(point) + .expect("invalid hex for ring member") + .try_into() + .expect("invalid point len for ring member"), + ) + .expect("invalid point for ring member") + }; + + outs + .outs + .iter() + .map(|out| { + let mask = rpc_point(&out.mask); + if amount != 0 { + assert_eq!(mask, Commitment::new(Scalar::from(1u8), amount).calculate()); + } + [rpc_point(&out.key), mask] + }) + .collect() + } + + clsag + .verify( + &get_outs(&rpc, amount.unwrap_or(0), &actual_indexes).await, + image, + &pseudo_outs[i], + &sig_hash, + ) + .unwrap(); + } + } + } + } + } + } + assert!(batch.verify()); + } + + println!("Deserialized, hashed, and reserialized {block_i} with {txs_len} TXs"); +} + +#[tokio::main] +async fn main() { + let args = std::env::args().collect::>(); + + // Read start block as the first arg + let mut block_i = + args.get(1).expect("no start block specified").parse::().expect("invalid start block"); + + // How many blocks to work on at once + let async_parallelism: usize = + args.get(2).unwrap_or(&"8".to_string()).parse::().expect("invalid parallelism argument"); + + // Read further args as RPC URLs + let default_nodes = vec![ + "http://xmr-node-uk.cakewallet.com:18081".to_string(), + "http://xmr-node-eu.cakewallet.com:18081".to_string(), + ]; + let mut specified_nodes = vec![]; + { + let mut i = 0; + loop { + let Some(node) = args.get(3 + i) else { break }; + specified_nodes.push(node.clone()); + i += 1; + } + } + let nodes = if specified_nodes.is_empty() { default_nodes } else { specified_nodes }; + + let rpc = |url: String| async move { + SimpleRequestRpc::new(url.clone()) + .await + .unwrap_or_else(|_| panic!("couldn't create SimpleRequestRpc connected to {url}")) + }; + let main_rpc = rpc(nodes[0].clone()).await; + let mut rpcs = vec![]; + for i in 0 .. async_parallelism { + rpcs.push(rpc(nodes[i % nodes.len()].clone()).await); + } + + let mut rpc_i = 0; + let mut handles: Vec> = vec![]; + let mut height = 0; + loop { + let new_height = main_rpc.get_height().await.expect("couldn't call get_height"); + if new_height == height { + break; + } + height = new_height; + + while block_i < height { + if handles.len() >= async_parallelism { + // Guarantee one handle is complete + handles.swap_remove(0).await.unwrap(); + + // Remove all of the finished handles + let mut i = 0; + while i < handles.len() { + if handles[i].is_finished() { + handles.swap_remove(i).await.unwrap(); + continue; + } + i += 1; + } + } + + handles.push(tokio::spawn(check_block(rpcs[rpc_i].clone(), block_i))); + rpc_i = (rpc_i + 1) % rpcs.len(); + block_i += 1; + } + } +} diff --git a/networks/monero/wallet/Cargo.toml b/networks/monero/wallet/Cargo.toml new file mode 100644 index 00000000..0a2f9c55 --- /dev/null +++ b/networks/monero/wallet/Cargo.toml @@ -0,0 +1,75 @@ +[package] +name = "monero-wallet" +version = "0.1.0" +description = "Wallet functionality for the Monero protocol, built around monero-serai" +license = "MIT" +repository = "https://github.com/serai-dex/serai/tree/develop/networks/monero/wallet" +authors = ["Luke Parker "] +edition = "2021" +rust-version = "1.79" + +[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 } + +thiserror = { version = "1", default-features = false, optional = true } + +zeroize = { version = "^1.5", default-features = false, features = ["zeroize_derive"] } + +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 } + +curve25519-dalek = { version = "4", default-features = false, features = ["alloc", "zeroize", "group"] } + +# Multisig dependencies +transcript = { package = "flexible-transcript", path = "../../../crypto/transcript", version = "0.3", default-features = false, features = ["recommended"], optional = true } +group = { version = "0.13", default-features = false, optional = true } +dalek-ff-group = { path = "../../../crypto/dalek-ff-group", version = "0.4", default-features = false, optional = true } +frost = { package = "modular-frost", path = "../../../crypto/frost", default-features = false, features = ["ed25519"], optional = true } + +hex = { version = "0.4", default-features = false, features = ["alloc"] } + +monero-serai = { path = "..", default-features = false } +monero-rpc = { path = "../rpc", default-features = false } +monero-address = { path = "./address", default-features = false } + +[dev-dependencies] +serde = { version = "1", default-features = false, features = ["derive", "alloc", "std"] } +serde_json = { version = "1", default-features = false, features = ["alloc", "std"] } + +frost = { package = "modular-frost", path = "../../../crypto/frost", default-features = false, features = ["ed25519", "tests"] } + +tokio = { version = "1", features = ["sync", "macros"] } + +monero-simple-request-rpc = { path = "../rpc/simple-request", default-features = false } + +[features] +std = [ + "std-shims/std", + + "thiserror", + + "zeroize/std", + + "rand_core/std", + "rand/std", + "rand_chacha/std", + "rand_distr/std", + + "monero-serai/std", + "monero-rpc/std", + "monero-address/std", +] +compile-time-generators = ["curve25519-dalek/precomputed-tables", "monero-serai/compile-time-generators"] +multisig = ["transcript", "group", "dalek-ff-group", "frost", "monero-serai/multisig", "std"] +default = ["std", "compile-time-generators"] diff --git a/networks/monero/wallet/LICENSE b/networks/monero/wallet/LICENSE new file mode 100644 index 00000000..91d893c1 --- /dev/null +++ b/networks/monero/wallet/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022-2024 Luke Parker + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/networks/monero/wallet/README.md b/networks/monero/wallet/README.md new file mode 100644 index 00000000..d88a56d9 --- /dev/null +++ b/networks/monero/wallet/README.md @@ -0,0 +1,58 @@ +# Monero Wallet + +Wallet functionality for the Monero protocol, built around monero-serai. This +library prides itself on resolving common pit falls developers may face. + +monero-wallet also offers a FROST-inspired multisignature protocol orders of +magnitude more performant than Monero's own. + +This library is usable under no-std when the `std` feature (on by default) is +disabled. + +### Features + +- Scanning Monero transactions +- Sending Monero transactions +- Sending Monero transactions with a FROST-inspired threshold multisignature + protocol, orders of magnitude more performant than Monero's own + +### 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 wallet functionality 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. + +Finally, this library only supports producing transactions with CLSAG +signatures. That means this library cannot spend non-RingCT outputs. + +### Cargo Features + +- `std` (on by default): Enables `std` (and with it, more efficient internal + implementations). +- `compile-time-generators` (on by default): Derives the generators at + compile-time so they don't need to be derived at runtime. This is recommended + if program size doesn't need to be kept minimal. +- `multisig`: Adds support for creation of transactions using a threshold + multisignature wallet. diff --git a/networks/monero/wallet/address/Cargo.toml b/networks/monero/wallet/address/Cargo.toml new file mode 100644 index 00000000..f6f90b16 --- /dev/null +++ b/networks/monero/wallet/address/Cargo.toml @@ -0,0 +1,49 @@ +[package] +name = "monero-address" +version = "0.1.0" +description = "Rust implementation of Monero addresses" +license = "MIT" +repository = "https://github.com/serai-dex/serai/tree/develop/networks/monero/wallet/address" +authors = ["Luke Parker "] +edition = "2021" +rust-version = "1.79" + +[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 } + +thiserror = { version = "1", default-features = false, optional = true } + +zeroize = { version = "^1.5", default-features = false, features = ["zeroize_derive"] } + +curve25519-dalek = { version = "4", default-features = false, features = ["alloc", "zeroize"] } + +monero-io = { path = "../../io", default-features = false } +monero-primitives = { path = "../../primitives", default-features = false } + +[dev-dependencies] +rand_core = { version = "0.6", default-features = false, features = ["std"] } + +hex-literal = { version = "0.4", default-features = false } +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"] } + +[features] +std = [ + "std-shims/std", + + "thiserror", + + "zeroize/std", + + "monero-io/std", +] +default = ["std"] diff --git a/networks/monero/wallet/address/LICENSE b/networks/monero/wallet/address/LICENSE new file mode 100644 index 00000000..91d893c1 --- /dev/null +++ b/networks/monero/wallet/address/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022-2024 Luke Parker + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/networks/monero/wallet/address/README.md b/networks/monero/wallet/address/README.md new file mode 100644 index 00000000..8fe3b77d --- /dev/null +++ b/networks/monero/wallet/address/README.md @@ -0,0 +1,11 @@ +# Monero Address + +Rust implementation of Monero addresses. + +This library is usable under no-std when the `std` feature (on by default) is +disabled. + +### Cargo Features + +- `std` (on by default): Enables `std` (and with it, more efficient internal + implementations). diff --git a/networks/monero/wallet/address/src/base58check.rs b/networks/monero/wallet/address/src/base58check.rs new file mode 100644 index 00000000..003f21f1 --- /dev/null +++ b/networks/monero/wallet/address/src/base58check.rs @@ -0,0 +1,106 @@ +use std_shims::{vec::Vec, string::String}; + +use monero_primitives::keccak256; + +const ALPHABET_LEN: u64 = 58; +const ALPHABET: &[u8] = b"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; + +pub(crate) const BLOCK_LEN: usize = 8; +const ENCODED_BLOCK_LEN: usize = 11; + +const CHECKSUM_LEN: usize = 4; + +// The maximum possible length of an encoding of this many bytes +// +// This is used for determining padding/how many bytes an encoding actually uses +pub(crate) fn encoded_len_for_bytes(bytes: usize) -> usize { + let bits = u64::try_from(bytes).expect("length exceeded 2**64") * 8; + let mut max = if bits == 64 { u64::MAX } else { (1 << bits) - 1 }; + + let mut i = 0; + while max != 0 { + max /= ALPHABET_LEN; + i += 1; + } + i +} + +// Encode an arbitrary-length stream of data +pub(crate) fn encode(bytes: &[u8]) -> String { + let mut res = String::with_capacity(bytes.len().div_ceil(BLOCK_LEN) * ENCODED_BLOCK_LEN); + + for chunk in bytes.chunks(BLOCK_LEN) { + // Convert to a u64 + let mut fixed_len_chunk = [0; BLOCK_LEN]; + fixed_len_chunk[(BLOCK_LEN - chunk.len()) ..].copy_from_slice(chunk); + let mut val = u64::from_be_bytes(fixed_len_chunk); + + // Convert to the base58 encoding + let mut chunk_str = [char::from(ALPHABET[0]); ENCODED_BLOCK_LEN]; + let mut i = 0; + while val > 0 { + chunk_str[i] = ALPHABET[usize::try_from(val % ALPHABET_LEN) + .expect("ALPHABET_LEN exceeds usize despite being a usize")] + .into(); + i += 1; + val /= ALPHABET_LEN; + } + + // Only take used bytes, and since we put the LSBs in the first byte, reverse the byte order + for c in chunk_str.into_iter().take(encoded_len_for_bytes(chunk.len())).rev() { + res.push(c); + } + } + + res +} + +// Decode an arbitrary-length stream of data +pub(crate) fn decode(data: &str) -> Option> { + let mut res = Vec::with_capacity((data.len() / ENCODED_BLOCK_LEN) * BLOCK_LEN); + + for chunk in data.as_bytes().chunks(ENCODED_BLOCK_LEN) { + // Convert the chunk back to a u64 + let mut sum = 0u64; + for this_char in chunk { + sum = sum.checked_mul(ALPHABET_LEN)?; + sum += u64::try_from(ALPHABET.iter().position(|a| a == this_char)?) + .expect("alphabet len exceeded 2**64"); + } + + // From the size of the encoding, determine the size of the bytes + let mut used_bytes = None; + for i in 1 ..= BLOCK_LEN { + if encoded_len_for_bytes(i) == chunk.len() { + used_bytes = Some(i); + break; + } + } + // Only push on the used bytes + res.extend(&sum.to_be_bytes()[(BLOCK_LEN - used_bytes.unwrap()) ..]); + } + + Some(res) +} + +// Encode an arbitrary-length stream of data, with a checksum +pub(crate) fn encode_check(mut data: Vec) -> String { + let checksum = keccak256(&data); + data.extend(&checksum[.. CHECKSUM_LEN]); + encode(&data) +} + +// Decode an arbitrary-length stream of data, with a checksum +pub(crate) fn decode_check(data: &str) -> Option> { + if data.len() < CHECKSUM_LEN { + None?; + } + + let mut res = decode(data)?; + let checksum_pos = res.len() - CHECKSUM_LEN; + if keccak256(&res[.. checksum_pos])[.. CHECKSUM_LEN] != res[checksum_pos ..] { + None?; + } + res.truncate(checksum_pos); + Some(res) +} diff --git a/networks/monero/wallet/address/src/lib.rs b/networks/monero/wallet/address/src/lib.rs new file mode 100644 index 00000000..b3c43d8d --- /dev/null +++ b/networks/monero/wallet/address/src/lib.rs @@ -0,0 +1,507 @@ +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![doc = include_str!("../README.md")] +#![deny(missing_docs)] +#![cfg_attr(not(feature = "std"), no_std)] + +use core::fmt::{self, Write}; +use std_shims::{ + vec, + string::{String, ToString}, +}; + +use zeroize::Zeroize; + +use curve25519_dalek::EdwardsPoint; + +use monero_io::*; + +mod base58check; +use base58check::{encode_check, decode_check}; + +#[cfg(test)] +mod tests; + +/// The address type. +/// +/// The officially specified addresses are supported, along with +/// [Featured Addresses](https://gist.github.com/kayabaNerve/01c50bbc35441e0bbdcee63a9d823789). +#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)] +pub enum AddressType { + /// A legacy address type. + Legacy, + /// A legacy address with a payment ID embedded. + LegacyIntegrated([u8; 8]), + /// A subaddress. + /// + /// This is what SHOULD be used if specific functionality isn't needed. + Subaddress, + /// A featured address. + /// + /// Featured Addresses are an unofficial address specification which is meant to be extensible + /// and support a variety of functionality. This functionality includes being a subaddresses AND + /// having a payment ID, along with being immune to the burning bug. + /// + /// At this time, support for featured addresses is limited to this crate. There should be no + /// expectation of interoperability. + Featured { + /// If this address is a subaddress. + subaddress: bool, + /// The payment ID associated with this address. + payment_id: Option<[u8; 8]>, + /// If this address is guaranteed. + /// + /// A guaranteed address is one where any outputs scanned to it are guaranteed to be spendable + /// under the hardness of various cryptographic problems (which are assumed hard). This is via + /// a modified shared-key derivation which eliminates the burning bug. + guaranteed: bool, + }, +} + +impl AddressType { + /// If this address is a subaddress. + pub fn is_subaddress(&self) -> bool { + matches!(self, AddressType::Subaddress) || + matches!(self, AddressType::Featured { subaddress: true, .. }) + } + + /// The payment ID within this address. + pub fn payment_id(&self) -> Option<[u8; 8]> { + if let AddressType::LegacyIntegrated(id) = self { + Some(*id) + } else if let AddressType::Featured { payment_id, .. } = self { + *payment_id + } else { + None + } + } + + /// If this address is guaranteed. + /// + /// A guaranteed address is one where any outputs scanned to it are guaranteed to be spendable + /// under the hardness of various cryptographic problems (which are assumed hard). This is via + /// a modified shared-key derivation which eliminates the burning bug. + pub fn is_guaranteed(&self) -> bool { + matches!(self, AddressType::Featured { guaranteed: true, .. }) + } +} + +/// A subaddress index. +/// +/// Subaddresses are derived from a root using a `(account, address)` tuple as an index. +#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)] +pub struct SubaddressIndex { + account: u32, + address: u32, +} + +impl SubaddressIndex { + /// Create a new SubaddressIndex. + pub const fn new(account: u32, address: u32) -> Option { + if (account == 0) && (address == 0) { + return None; + } + Some(SubaddressIndex { account, address }) + } + + /// Get the account this subaddress index is under. + pub const fn account(&self) -> u32 { + self.account + } + + /// Get the address this subaddress index is for, within its account. + pub const fn address(&self) -> u32 { + self.address + } +} + +/// Bytes used as prefixes when encoding addresses. +/// +/// These distinguish the address's type. +#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)] +pub struct AddressBytes { + legacy: u8, + legacy_integrated: u8, + subaddress: u8, + featured: u8, +} + +impl AddressBytes { + /// Create a new set of address bytes, one for each address type. + pub const fn new( + legacy: u8, + legacy_integrated: u8, + subaddress: u8, + featured: u8, + ) -> Option { + if (legacy == legacy_integrated) || (legacy == subaddress) || (legacy == featured) { + return None; + } + if (legacy_integrated == subaddress) || (legacy_integrated == featured) { + return None; + } + if subaddress == featured { + return None; + } + Some(AddressBytes { legacy, legacy_integrated, subaddress, featured }) + } + + const fn to_const_generic(self) -> u32 { + ((self.legacy as u32) << 24) + + ((self.legacy_integrated as u32) << 16) + + ((self.subaddress as u32) << 8) + + (self.featured as u32) + } + + #[allow(clippy::cast_possible_truncation)] + const fn from_const_generic(const_generic: u32) -> Self { + let legacy = (const_generic >> 24) as u8; + let legacy_integrated = ((const_generic >> 16) & (u8::MAX as u32)) as u8; + let subaddress = ((const_generic >> 8) & (u8::MAX as u32)) as u8; + let featured = (const_generic & (u8::MAX as u32)) as u8; + + AddressBytes { legacy, legacy_integrated, subaddress, featured } + } +} + +// https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454 +// /src/cryptonote_config.h#L216-L225 +// https://gist.github.com/kayabaNerve/01c50bbc35441e0bbdcee63a9d823789 for featured +const MONERO_MAINNET_BYTES: AddressBytes = match AddressBytes::new(18, 19, 42, 70) { + Some(bytes) => bytes, + None => panic!("mainnet byte constants conflicted"), +}; +// https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454 +// /src/cryptonote_config.h#L277-L281 +const MONERO_STAGENET_BYTES: AddressBytes = match AddressBytes::new(24, 25, 36, 86) { + Some(bytes) => bytes, + None => panic!("stagenet byte constants conflicted"), +}; +// https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454 +// /src/cryptonote_config.h#L262-L266 +const MONERO_TESTNET_BYTES: AddressBytes = match AddressBytes::new(53, 54, 63, 111) { + Some(bytes) => bytes, + None => panic!("testnet byte constants conflicted"), +}; + +/// The network this address is for. +#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)] +pub enum Network { + /// A mainnet address. + Mainnet, + /// A stagenet address. + /// + /// Stagenet maintains parity with mainnet and is useful for testing integrations accordingly. + Stagenet, + /// A testnet address. + /// + /// Testnet is used to test new consensus rules and functionality. + Testnet, +} + +/// Errors when decoding an address. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +#[cfg_attr(feature = "std", derive(thiserror::Error))] +pub enum AddressError { + /// The address had an invalid (network, type) byte. + #[cfg_attr(feature = "std", error("invalid byte for the address's network/type ({0})"))] + InvalidTypeByte(u8), + /// The address wasn't a valid Base58Check (as defined by Monero) string. + #[cfg_attr(feature = "std", error("invalid address encoding"))] + InvalidEncoding, + /// The data encoded wasn't the proper length. + #[cfg_attr(feature = "std", error("invalid length"))] + InvalidLength, + /// The address had an invalid key. + #[cfg_attr(feature = "std", error("invalid key"))] + InvalidKey, + /// The address was featured with unrecognized features. + #[cfg_attr(feature = "std", error("unknown features"))] + UnknownFeatures(u64), + /// The network was for a different network than expected. + #[cfg_attr( + feature = "std", + error("different network ({actual:?}) than expected ({expected:?})") + )] + DifferentNetwork { + /// The Network expected. + expected: Network, + /// The Network embedded within the Address. + actual: Network, + }, + /// The view key was of small order despite being in a guaranteed address. + #[cfg_attr(feature = "std", error("small-order view key in guaranteed address"))] + SmallOrderView, +} + +/// Bytes used as prefixes when encoding addresses, variable to the network instance. +/// +/// These distinguish the address's network and type. +#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)] +pub struct NetworkedAddressBytes { + mainnet: AddressBytes, + stagenet: AddressBytes, + testnet: AddressBytes, +} + +impl NetworkedAddressBytes { + /// Create a new set of address bytes, one for each network. + pub const fn new( + mainnet: AddressBytes, + stagenet: AddressBytes, + testnet: AddressBytes, + ) -> Option { + let res = NetworkedAddressBytes { mainnet, stagenet, testnet }; + let all_bytes = res.to_const_generic(); + + let mut i = 0; + while i < 12 { + let this_byte = (all_bytes >> (32 + (i * 8))) & (u8::MAX as u128); + + let mut j = 0; + while j < 12 { + if i == j { + j += 1; + continue; + } + let other_byte = (all_bytes >> (32 + (j * 8))) & (u8::MAX as u128); + if this_byte == other_byte { + return None; + } + + j += 1; + } + + i += 1; + } + + Some(res) + } + + /// Convert this set of address bytes to its representation as a u128. + /// + /// We cannot use this struct directly as a const generic unfortunately. + pub const fn to_const_generic(self) -> u128 { + ((self.mainnet.to_const_generic() as u128) << 96) + + ((self.stagenet.to_const_generic() as u128) << 64) + + ((self.testnet.to_const_generic() as u128) << 32) + } + + #[allow(clippy::cast_possible_truncation)] + const fn from_const_generic(const_generic: u128) -> Self { + let mainnet = AddressBytes::from_const_generic((const_generic >> 96) as u32); + let stagenet = + AddressBytes::from_const_generic(((const_generic >> 64) & (u32::MAX as u128)) as u32); + let testnet = + AddressBytes::from_const_generic(((const_generic >> 32) & (u32::MAX as u128)) as u32); + + NetworkedAddressBytes { mainnet, stagenet, testnet } + } + + fn network(&self, network: Network) -> &AddressBytes { + match network { + Network::Mainnet => &self.mainnet, + Network::Stagenet => &self.stagenet, + Network::Testnet => &self.testnet, + } + } + + fn byte(&self, network: Network, kind: AddressType) -> u8 { + let address_bytes = self.network(network); + + match kind { + AddressType::Legacy => address_bytes.legacy, + AddressType::LegacyIntegrated(_) => address_bytes.legacy_integrated, + AddressType::Subaddress => address_bytes.subaddress, + AddressType::Featured { .. } => address_bytes.featured, + } + } + + // This will return an incomplete AddressType for LegacyIntegrated/Featured. + fn metadata_from_byte(&self, byte: u8) -> Result<(Network, AddressType), AddressError> { + let mut meta = None; + for network in [Network::Mainnet, Network::Testnet, Network::Stagenet] { + let address_bytes = self.network(network); + if let Some(kind) = match byte { + _ if byte == address_bytes.legacy => Some(AddressType::Legacy), + _ if byte == address_bytes.legacy_integrated => Some(AddressType::LegacyIntegrated([0; 8])), + _ if byte == address_bytes.subaddress => Some(AddressType::Subaddress), + _ if byte == address_bytes.featured => { + Some(AddressType::Featured { subaddress: false, payment_id: None, guaranteed: false }) + } + _ => None, + } { + meta = Some((network, kind)); + break; + } + } + + meta.ok_or(AddressError::InvalidTypeByte(byte)) + } +} + +/// The bytes used for distinguishing Monero addresses. +pub const MONERO_BYTES: NetworkedAddressBytes = match NetworkedAddressBytes::new( + MONERO_MAINNET_BYTES, + MONERO_STAGENET_BYTES, + MONERO_TESTNET_BYTES, +) { + Some(bytes) => bytes, + None => panic!("Monero network byte constants conflicted"), +}; + +/// A Monero address. +#[derive(Clone, Copy, PartialEq, Eq, Zeroize)] +pub struct Address { + network: Network, + kind: AddressType, + spend: EdwardsPoint, + view: EdwardsPoint, +} + +impl fmt::Debug for Address { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + let hex = |bytes: &[u8]| -> String { + let mut res = String::with_capacity(2 + (2 * bytes.len())); + res.push_str("0x"); + for b in bytes { + write!(&mut res, "{b:02x}").unwrap(); + } + res + }; + + fmt + .debug_struct("Address") + .field("network", &self.network) + .field("kind", &self.kind) + .field("spend", &hex(&self.spend.compress().to_bytes())) + .field("view", &hex(&self.view.compress().to_bytes())) + // This is not a real field yet is the most valuable thing to know when debugging + .field("(address)", &self.to_string()) + .finish() + } +} + +impl fmt::Display for Address { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let address_bytes: NetworkedAddressBytes = + NetworkedAddressBytes::from_const_generic(ADDRESS_BYTES); + + let mut data = vec![address_bytes.byte(self.network, self.kind)]; + data.extend(self.spend.compress().to_bytes()); + data.extend(self.view.compress().to_bytes()); + if let AddressType::Featured { subaddress, payment_id, guaranteed } = self.kind { + let features_uint = + (u8::from(guaranteed) << 2) + (u8::from(payment_id.is_some()) << 1) + u8::from(subaddress); + write_varint(&features_uint, &mut data).unwrap(); + } + if let Some(id) = self.kind.payment_id() { + data.extend(id); + } + write!(f, "{}", encode_check(data)) + } +} + +impl Address { + /// Create a new address. + pub fn new(network: Network, kind: AddressType, spend: EdwardsPoint, view: EdwardsPoint) -> Self { + Address { network, kind, spend, view } + } + + /// Parse an address from a String, accepting any network it is. + pub fn from_str_with_unchecked_network(s: &str) -> Result { + let raw = decode_check(s).ok_or(AddressError::InvalidEncoding)?; + let mut raw = raw.as_slice(); + + let address_bytes: NetworkedAddressBytes = + NetworkedAddressBytes::from_const_generic(ADDRESS_BYTES); + let (network, mut kind) = address_bytes + .metadata_from_byte(read_byte(&mut raw).map_err(|_| AddressError::InvalidLength)?)?; + let spend = read_point(&mut raw).map_err(|_| AddressError::InvalidKey)?; + let view = read_point(&mut raw).map_err(|_| AddressError::InvalidKey)?; + + if matches!(kind, AddressType::Featured { .. }) { + let features = read_varint::<_, u64>(&mut raw).map_err(|_| AddressError::InvalidLength)?; + if (features >> 3) != 0 { + Err(AddressError::UnknownFeatures(features))?; + } + + let subaddress = (features & 1) == 1; + let integrated = ((features >> 1) & 1) == 1; + let guaranteed = ((features >> 2) & 1) == 1; + + kind = + AddressType::Featured { subaddress, payment_id: integrated.then_some([0; 8]), guaranteed }; + } + + // Read the payment ID, if there should be one + match kind { + AddressType::LegacyIntegrated(ref mut id) | + AddressType::Featured { payment_id: Some(ref mut id), .. } => { + *id = read_bytes(&mut raw).map_err(|_| AddressError::InvalidLength)?; + } + _ => {} + }; + + if !raw.is_empty() { + Err(AddressError::InvalidLength)?; + } + + Ok(Address { network, kind, spend, view }) + } + + /// Create a new address from a `&str`. + /// + /// This takes in an argument for the expected network, erroring if a distinct network was used. + /// It also errors if the address is invalid (as expected). + pub fn from_str(network: Network, s: &str) -> Result { + Self::from_str_with_unchecked_network(s).and_then(|addr| { + if addr.network == network { + Ok(addr) + } else { + Err(AddressError::DifferentNetwork { actual: addr.network, expected: network })? + } + }) + } + + /// The network this address is intended for use on. + pub fn network(&self) -> Network { + self.network + } + + /// The type of address this is. + pub fn kind(&self) -> &AddressType { + &self.kind + } + + /// If this is a subaddress. + pub fn is_subaddress(&self) -> bool { + self.kind.is_subaddress() + } + + /// The payment ID for this address. + pub fn payment_id(&self) -> Option<[u8; 8]> { + self.kind.payment_id() + } + + /// If this address is guaranteed. + /// + /// A guaranteed address is one where any outputs scanned to it are guaranteed to be spendable + /// under the hardness of various cryptographic problems (which are assumed hard). This is via + /// a modified shared-key derivation which eliminates the burning bug. + pub fn is_guaranteed(&self) -> bool { + self.kind.is_guaranteed() + } + + /// The public spend key for this address. + pub fn spend(&self) -> EdwardsPoint { + self.spend + } + + /// The public view key for this address. + pub fn view(&self) -> EdwardsPoint { + self.view + } +} + +/// Instantiation of the Address type with Monero's network bytes. +pub type MoneroAddress = Address<{ MONERO_BYTES.to_const_generic() }>; diff --git a/coins/monero/src/tests/address.rs b/networks/monero/wallet/address/src/tests.rs similarity index 65% rename from coins/monero/src/tests/address.rs rename to networks/monero/wallet/address/src/tests.rs index e26901e4..2804832a 100644 --- a/coins/monero/src/tests/address.rs +++ b/networks/monero/wallet/address/src/tests.rs @@ -2,14 +2,11 @@ use hex_literal::hex; use rand_core::{RngCore, OsRng}; -use curve25519_dalek::constants::ED25519_BASEPOINT_TABLE; +use curve25519_dalek::{constants::ED25519_BASEPOINT_TABLE, scalar::Scalar}; -use monero_generators::decompress_point; +use monero_io::decompress_point; -use crate::{ - random_scalar, - wallet::address::{Network, AddressType, AddressMeta, MoneroAddress}, -}; +use crate::{Network, AddressType, MoneroAddress}; const SPEND: [u8; 32] = hex!("f8631661f6ab4e6fda310c797330d86e23a682f20d5bc8cc27b18051191f16d7"); const VIEW: [u8; 32] = hex!("4a1535063ad1fee2dabbf909d4fd9a873e29541b401f0944754e17c9a41820ce"); @@ -30,14 +27,49 @@ const SUBADDRESS: &str = const FEATURED_JSON: &str = include_str!("vectors/featured_addresses.json"); +#[test] +fn test_encoded_len_for_bytes() { + // For an encoding of length `l`, we prune to the amount of bytes which encodes with length `l` + // This assumes length `l` -> amount of bytes has a singular answer, which is tested here + use crate::base58check::*; + let mut set = std::collections::HashSet::new(); + for i in 0 .. BLOCK_LEN { + set.insert(encoded_len_for_bytes(i)); + } + assert_eq!(set.len(), BLOCK_LEN); +} + +#[test] +fn base58check() { + use crate::base58check::*; + + assert_eq!(encode(&[]), String::new()); + assert!(decode("").unwrap().is_empty()); + + let full_block = &[1, 2, 3, 4, 5, 6, 7, 8]; + assert_eq!(&decode(&encode(full_block)).unwrap(), full_block); + + let partial_block = &[1, 2, 3]; + assert_eq!(&decode(&encode(partial_block)).unwrap(), partial_block); + + let max_encoded_block = &[u8::MAX; 8]; + assert_eq!(&decode(&encode(max_encoded_block)).unwrap(), max_encoded_block); + + let max_decoded_block = "zzzzzzzzzzz"; + assert!(decode(max_decoded_block).is_none()); + + let full_and_partial_block = &[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]; + assert_eq!(&decode(&encode(full_and_partial_block)).unwrap(), full_and_partial_block); +} + #[test] fn standard_address() { let addr = MoneroAddress::from_str(Network::Mainnet, STANDARD).unwrap(); - assert_eq!(addr.meta.network, Network::Mainnet); - assert_eq!(addr.meta.kind, AddressType::Standard); - assert!(!addr.meta.kind.is_subaddress()); - assert_eq!(addr.meta.kind.payment_id(), None); - assert!(!addr.meta.kind.is_guaranteed()); + assert_eq!(addr.network(), Network::Mainnet); + assert_eq!(addr.kind(), &AddressType::Legacy); + assert!(!addr.is_subaddress()); + assert_eq!(addr.payment_id(), None); + assert!(!addr.is_guaranteed()); assert_eq!(addr.spend.compress().to_bytes(), SPEND); assert_eq!(addr.view.compress().to_bytes(), VIEW); assert_eq!(addr.to_string(), STANDARD); @@ -46,11 +78,11 @@ fn standard_address() { #[test] fn integrated_address() { let addr = MoneroAddress::from_str(Network::Mainnet, INTEGRATED).unwrap(); - assert_eq!(addr.meta.network, Network::Mainnet); - assert_eq!(addr.meta.kind, AddressType::Integrated(PAYMENT_ID)); - assert!(!addr.meta.kind.is_subaddress()); - assert_eq!(addr.meta.kind.payment_id(), Some(PAYMENT_ID)); - assert!(!addr.meta.kind.is_guaranteed()); + assert_eq!(addr.network(), Network::Mainnet); + assert_eq!(addr.kind(), &AddressType::LegacyIntegrated(PAYMENT_ID)); + assert!(!addr.is_subaddress()); + assert_eq!(addr.payment_id(), Some(PAYMENT_ID)); + assert!(!addr.is_guaranteed()); assert_eq!(addr.spend.compress().to_bytes(), SPEND); assert_eq!(addr.view.compress().to_bytes(), VIEW); assert_eq!(addr.to_string(), INTEGRATED); @@ -59,11 +91,11 @@ fn integrated_address() { #[test] fn subaddress() { let addr = MoneroAddress::from_str(Network::Mainnet, SUBADDRESS).unwrap(); - assert_eq!(addr.meta.network, Network::Mainnet); - assert_eq!(addr.meta.kind, AddressType::Subaddress); - assert!(addr.meta.kind.is_subaddress()); - assert_eq!(addr.meta.kind.payment_id(), None); - assert!(!addr.meta.kind.is_guaranteed()); + assert_eq!(addr.network(), Network::Mainnet); + assert_eq!(addr.kind(), &AddressType::Subaddress); + assert!(addr.is_subaddress()); + assert_eq!(addr.payment_id(), None); + assert!(!addr.is_guaranteed()); assert_eq!(addr.spend.compress().to_bytes(), SUB_SPEND); assert_eq!(addr.view.compress().to_bytes(), SUB_VIEW); assert_eq!(addr.to_string(), SUBADDRESS); @@ -75,8 +107,8 @@ fn featured() { [(Network::Mainnet, 'C'), (Network::Testnet, 'K'), (Network::Stagenet, 'F')] { for _ in 0 .. 100 { - let spend = &random_scalar(&mut OsRng) * ED25519_BASEPOINT_TABLE; - let view = &random_scalar(&mut OsRng) * ED25519_BASEPOINT_TABLE; + let spend = &Scalar::random(&mut OsRng) * ED25519_BASEPOINT_TABLE; + let view = &Scalar::random(&mut OsRng) * ED25519_BASEPOINT_TABLE; for features in 0 .. (1 << 3) { const SUBADDRESS_FEATURE_BIT: u8 = 1; @@ -93,8 +125,7 @@ fn featured() { let guaranteed = (features & GUARANTEED_FEATURE_BIT) == GUARANTEED_FEATURE_BIT; let kind = AddressType::Featured { subaddress, payment_id, guaranteed }; - let meta = AddressMeta::new(network, kind); - let addr = MoneroAddress::new(meta, spend, view); + let addr = MoneroAddress::new(network, kind, spend, view); assert_eq!(addr.to_string().chars().next().unwrap(), first); assert_eq!(MoneroAddress::from_str(network, &addr.to_string()).unwrap(), addr); @@ -158,14 +189,12 @@ fn featured_vectors() { assert_eq!( MoneroAddress::new( - AddressMeta::new( - network, - AddressType::Featured { - subaddress: vector.subaddress, - payment_id: vector.payment_id, - guaranteed: vector.guaranteed - } - ), + network, + AddressType::Featured { + subaddress: vector.subaddress, + payment_id: vector.payment_id, + guaranteed: vector.guaranteed + }, spend, view ) diff --git a/coins/monero/src/tests/vectors/featured_addresses.json b/networks/monero/wallet/address/src/vectors/featured_addresses.json similarity index 100% rename from coins/monero/src/tests/vectors/featured_addresses.json rename to networks/monero/wallet/address/src/vectors/featured_addresses.json diff --git a/networks/monero/wallet/polyseed/Cargo.toml b/networks/monero/wallet/polyseed/Cargo.toml new file mode 100644 index 00000000..4d17e58b --- /dev/null +++ b/networks/monero/wallet/polyseed/Cargo.toml @@ -0,0 +1,46 @@ +[package] +name = "polyseed" +version = "0.1.0" +description = "Rust implementation of Polyseed" +license = "MIT" +repository = "https://github.com/serai-dex/serai/tree/develop/networks/monero/wallet/polyseed" +authors = ["Luke Parker "] +edition = "2021" +rust-version = "1.79" + +[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 } + +thiserror = { version = "1", default-features = false, optional = true } + +subtle = { version = "^2.4", default-features = false } +zeroize = { version = "^1.5", default-features = false, features = ["zeroize_derive"] } +rand_core = { version = "0.6", default-features = false } + +sha3 = { version = "0.10", default-features = false } +pbkdf2 = { version = "0.12", features = ["simple"], default-features = false } + +[dev-dependencies] +hex = { version = "0.4", default-features = false, features = ["std"] } + +[features] +std = [ + "std-shims/std", + + "thiserror", + + "subtle/std", + "zeroize/std", + "rand_core/std", + + "sha3/std", + "pbkdf2/std", +] +default = ["std"] diff --git a/networks/monero/wallet/polyseed/LICENSE b/networks/monero/wallet/polyseed/LICENSE new file mode 100644 index 00000000..91d893c1 --- /dev/null +++ b/networks/monero/wallet/polyseed/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022-2024 Luke Parker + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/networks/monero/wallet/polyseed/README.md b/networks/monero/wallet/polyseed/README.md new file mode 100644 index 00000000..4869bba0 --- /dev/null +++ b/networks/monero/wallet/polyseed/README.md @@ -0,0 +1,11 @@ +# Polyseed + +Rust implementation of [Polyseed](https://github.com/tevador/polyseed). + +This library is usable under no-std when the `std` feature (on by default) is +disabled. + +### Cargo Features + +- `std` (on by default): Enables `std` (and with it, more efficient internal + implementations). diff --git a/coins/monero/src/wallet/seed/polyseed.rs b/networks/monero/wallet/polyseed/src/lib.rs similarity index 79% rename from coins/monero/src/wallet/seed/polyseed.rs rename to networks/monero/wallet/polyseed/src/lib.rs index cecdef9e..7cd19ac7 100644 --- a/coins/monero/src/wallet/seed/polyseed.rs +++ b/networks/monero/wallet/polyseed/src/lib.rs @@ -1,5 +1,10 @@ +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![doc = include_str!("../README.md")] +#![deny(missing_docs)] +#![cfg_attr(not(feature = "std"), no_std)] + use core::fmt; -use std_shims::{sync::OnceLock, vec::Vec, string::String, collections::HashMap}; +use std_shims::{sync::OnceLock, string::String, collections::HashMap}; #[cfg(feature = "std")] use std::time::{SystemTime, UNIX_EPOCH}; @@ -10,7 +15,8 @@ use rand_core::{RngCore, CryptoRng}; use sha3::Sha3_256; use pbkdf2::pbkdf2_hmac; -use super::SeedError; +#[cfg(test)] +mod tests; // Features const FEATURE_BITS: u8 = 5; @@ -34,7 +40,7 @@ fn polyseed_features_supported(features: u8) -> bool { const DATE_BITS: u8 = 10; const DATE_MASK: u16 = (1u16 << DATE_BITS) - 1; const POLYSEED_EPOCH: u64 = 1635768000; // 1st November 2021 12:00 UTC -pub(crate) const TIME_STEP: u64 = 2629746; // 30.436875 days = 1/12 of the Gregorian year +const TIME_STEP: u64 = 2629746; // 30.436875 days = 1/12 of the Gregorian year // After ~85 years, this will roll over. fn birthday_encode(time: u64) -> u16 { @@ -60,8 +66,8 @@ const LAST_BYTE_SECRET_BITS_MASK: u8 = ((1 << (BITS_PER_BYTE - CLEAR_BITS)) - 1) const SECRET_BITS_PER_WORD: usize = 10; -// Amount of words in a seed -pub(crate) const POLYSEED_LENGTH: usize = 16; +// The amount of words in a seed. +const POLYSEED_LENGTH: usize = 16; // Amount of characters each word must have if trimmed pub(crate) const PREFIX_LEN: usize = 4; @@ -95,33 +101,62 @@ const POLYSEED_SALT: &[u8] = b"POLYSEED key"; const POLYSEED_KEYGEN_ITERATIONS: u32 = 10000; // Polyseed technically supports multiple coins, and the value for Monero is 0 -// See: https://github.com/tevador/polyseed/blob/master/include/polyseed.h#L58 +// See: https://github.com/tevador/polyseed/blob/dfb05d8edb682b0e8f743b1b70c9131712ff4157 +// /include/polyseed.h#L57 const COIN: u16 = 0; +/// An error when working with a Polyseed. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +#[cfg_attr(feature = "std", derive(thiserror::Error))] +pub enum PolyseedError { + /// The seed was invalid. + #[cfg_attr(feature = "std", error("invalid seed"))] + InvalidSeed, + /// The entropy was invalid. + #[cfg_attr(feature = "std", error("invalid entropy"))] + InvalidEntropy, + /// The checksum did not match the data. + #[cfg_attr(feature = "std", error("invalid checksum"))] + InvalidChecksum, + /// Unsupported feature bits were set. + #[cfg_attr(feature = "std", error("unsupported features"))] + UnsupportedFeatures, +} + /// Language options for Polyseed. #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Zeroize)] pub enum Language { + /// English language option. English, + /// Spanish language option. Spanish, + /// French language option. French, + /// Italian language option. Italian, + /// Japanese language option. Japanese, + /// Korean language option. Korean, + /// Czech language option. Czech, + /// Portuguese language option. Portuguese, + /// Simplified Chinese language option. ChineseSimplified, + /// Traditional Chinese language option. ChineseTraditional, } struct WordList { - words: Vec, + words: &'static [&'static str], has_prefix: bool, has_accent: bool, } impl WordList { - fn new(words: &str, has_prefix: bool, has_accent: bool) -> WordList { - let res = WordList { words: serde_json::from_str(words).unwrap(), has_prefix, has_accent }; + fn new(words: &'static [&'static str], has_prefix: bool, has_accent: bool) -> WordList { + let res = WordList { words, has_prefix, has_accent }; // This is needed for a later unwrap to not fails assert!(words.len() < usize::from(u16::MAX)); res @@ -133,26 +168,27 @@ static LANGUAGES_CELL: OnceLock> = OnceLock::new(); fn LANGUAGES() -> &'static HashMap { LANGUAGES_CELL.get_or_init(|| { HashMap::from([ - (Language::Czech, WordList::new(include_str!("./polyseed/cs.json"), true, false)), - (Language::French, WordList::new(include_str!("./polyseed/fr.json"), true, true)), - (Language::Korean, WordList::new(include_str!("./polyseed/ko.json"), false, false)), - (Language::English, WordList::new(include_str!("./polyseed/en.json"), true, false)), - (Language::Italian, WordList::new(include_str!("./polyseed/it.json"), true, false)), - (Language::Spanish, WordList::new(include_str!("./polyseed/es.json"), true, true)), - (Language::Japanese, WordList::new(include_str!("./polyseed/ja.json"), false, false)), - (Language::Portuguese, WordList::new(include_str!("./polyseed/pt.json"), true, false)), + (Language::Czech, WordList::new(include!("./words/cs.rs"), true, false)), + (Language::French, WordList::new(include!("./words/fr.rs"), true, true)), + (Language::Korean, WordList::new(include!("./words/ko.rs"), false, false)), + (Language::English, WordList::new(include!("./words/en.rs"), true, false)), + (Language::Italian, WordList::new(include!("./words/it.rs"), true, false)), + (Language::Spanish, WordList::new(include!("./words/es.rs"), true, true)), + (Language::Japanese, WordList::new(include!("./words/ja.rs"), false, false)), + (Language::Portuguese, WordList::new(include!("./words/pt.rs"), true, false)), ( Language::ChineseSimplified, - WordList::new(include_str!("./polyseed/zh_simplified.json"), false, false), + WordList::new(include!("./words/zh_simplified.rs"), false, false), ), ( Language::ChineseTraditional, - WordList::new(include_str!("./polyseed/zh_traditional.json"), false, false), + WordList::new(include!("./words/zh_traditional.rs"), false, false), ), ]) }) } +/// A Polyseed. #[derive(Clone, PartialEq, Eq, Zeroize, ZeroizeOnDrop)] pub struct Polyseed { language: Language, @@ -222,13 +258,13 @@ impl Polyseed { masked_features: u8, encoded_birthday: u16, entropy: Zeroizing<[u8; 32]>, - ) -> Result { + ) -> Result { if !polyseed_features_supported(masked_features) { - Err(SeedError::UnsupportedFeatures)?; + Err(PolyseedError::UnsupportedFeatures)?; } if !valid_entropy(&entropy) { - Err(SeedError::InvalidEntropy)?; + Err(PolyseedError::InvalidEntropy)?; } let mut res = Polyseed { @@ -244,23 +280,24 @@ impl Polyseed { /// Create a new `Polyseed` with specific internals. /// - /// `birthday` is defined in seconds since the Unix epoch. + /// `birthday` is defined in seconds since the epoch. pub fn from( language: Language, features: u8, birthday: u64, entropy: Zeroizing<[u8; 32]>, - ) -> Result { + ) -> Result { Self::from_internal(language, user_features(features), birthday_encode(birthday), entropy) } /// Create a new `Polyseed`. /// - /// This uses the system's time for the birthday, if available. + /// This uses the system's time for the birthday, if available, else 0. pub fn new(rng: &mut R, language: Language) -> Polyseed { // Get the birthday #[cfg(feature = "std")] - let birthday = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs(); + let birthday = + SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or(core::time::Duration::ZERO).as_secs(); #[cfg(not(feature = "std"))] let birthday = 0; @@ -275,7 +312,7 @@ impl Polyseed { /// Create a new `Polyseed` from a String. #[allow(clippy::needless_pass_by_value)] - pub fn from_string(lang: Language, seed: Zeroizing) -> Result { + pub fn from_string(lang: Language, seed: Zeroizing) -> Result { // Decode the seed into its polynomial coefficients let mut poly = [0; POLYSEED_LENGTH]; @@ -325,7 +362,7 @@ impl Polyseed { } else { check_if_matches(lang_word_list.has_prefix, lang_word_list.words.iter(), word) }) else { - Err(SeedError::InvalidSeed)? + Err(PolyseedError::InvalidSeed)? }; // WordList asserts the word list length is less than u16::MAX @@ -337,7 +374,7 @@ impl Polyseed { // Validate the checksum if poly_eval(&poly) != 0 { - Err(SeedError::InvalidChecksum)?; + Err(PolyseedError::InvalidChecksum)?; } // Convert the polynomial into entropy @@ -416,6 +453,7 @@ impl Polyseed { key } + /// The String representation of this seed. pub fn to_string(&self) -> Zeroizing { // Encode the polynomial with the existing checksum let mut poly = self.to_poly(); @@ -428,7 +466,7 @@ impl Polyseed { let mut seed = Zeroizing::new(String::new()); let words = &LANGUAGES()[&self.language].words; for i in 0 .. poly.len() { - seed.push_str(&words[usize::from(poly[i])]); + seed.push_str(words[usize::from(poly[i])]); if i < poly.len() - 1 { seed.push(' '); } diff --git a/networks/monero/wallet/polyseed/src/tests.rs b/networks/monero/wallet/polyseed/src/tests.rs new file mode 100644 index 00000000..4913c217 --- /dev/null +++ b/networks/monero/wallet/polyseed/src/tests.rs @@ -0,0 +1,218 @@ +use zeroize::Zeroizing; +use rand_core::OsRng; + +use crate::*; + +#[test] +fn test_polyseed() { + struct Vector { + language: Language, + seed: String, + entropy: String, + birthday: u64, + has_prefix: bool, + has_accent: bool, + } + + let vectors = [ + Vector { + language: Language::English, + seed: "raven tail swear infant grief assist regular lamp \ + duck valid someone little harsh puppy airport language" + .into(), + entropy: "dd76e7359a0ded37cd0ff0f3c829a5ae01673300000000000000000000000000".into(), + birthday: 1638446400, + has_prefix: true, + has_accent: false, + }, + Vector { + language: Language::Spanish, + seed: "eje fin parte célebre tabú pestaña lienzo puma \ + prisión hora regalo lengua existir lápiz lote sonoro" + .into(), + entropy: "5a2b02df7db21fcbe6ec6df137d54c7b20fd2b00000000000000000000000000".into(), + birthday: 3118651200, + has_prefix: true, + has_accent: true, + }, + Vector { + language: Language::French, + seed: "valable arracher décaler jeudi amusant dresser mener épaissir risible \ + prouesse réserve ampleur ajuster muter caméra enchère" + .into(), + entropy: "11cfd870324b26657342c37360c424a14a050b00000000000000000000000000".into(), + birthday: 1679314966, + has_prefix: true, + has_accent: true, + }, + Vector { + language: Language::Italian, + seed: "caduco midollo copione meninge isotopo illogico riflesso tartaruga fermento \ + olandese normale tristezza episodio voragine forbito achille" + .into(), + entropy: "7ecc57c9b4652d4e31428f62bec91cfd55500600000000000000000000000000".into(), + birthday: 1679316358, + has_prefix: true, + has_accent: false, + }, + Vector { + language: Language::Portuguese, + seed: "caverna custear azedo adeus senador apertada sedoso omitir \ + sujeito aurora videira molho cartaz gesso dentista tapar" + .into(), + entropy: "45473063711376cae38f1b3eba18c874124e1d00000000000000000000000000".into(), + birthday: 1679316657, + has_prefix: true, + has_accent: false, + }, + Vector { + language: Language::Czech, + seed: "usmrtit nora dotaz komunita zavalit funkce mzda sotva akce \ + vesta kabel herna stodola uvolnit ustrnout email" + .into(), + entropy: "7ac8a4efd62d9c3c4c02e350d32326df37821c00000000000000000000000000".into(), + birthday: 1679316898, + has_prefix: true, + has_accent: false, + }, + Vector { + language: Language::Korean, + seed: "전망 선풍기 국제 무궁화 설사 기름 이론적 해안 절망 예선 \ + 지우개 보관 절망 말기 시각 귀신" + .into(), + entropy: "684663fda420298f42ed94b2c512ed38ddf12b00000000000000000000000000".into(), + birthday: 1679317073, + has_prefix: false, + has_accent: false, + }, + Vector { + language: Language::Japanese, + seed: "うちあわせ ちつじょ つごう しはい けんこう とおる てみやげ はんとし たんとう \ + といれ おさない おさえる むかう ぬぐう なふだ せまる" + .into(), + entropy: "94e6665518a6286c6e3ba508a2279eb62b771f00000000000000000000000000".into(), + birthday: 1679318722, + has_prefix: false, + has_accent: false, + }, + Vector { + language: Language::ChineseTraditional, + seed: "亂 挖 斤 柄 代 圈 枝 轄 魯 論 函 開 勘 番 榮 壁".into(), + entropy: "b1594f585987ab0fd5a31da1f0d377dae5283f00000000000000000000000000".into(), + birthday: 1679426433, + has_prefix: false, + has_accent: false, + }, + Vector { + language: Language::ChineseSimplified, + seed: "啊 百 族 府 票 划 伪 仓 叶 虾 借 溜 晨 左 等 鬼".into(), + entropy: "21cdd366f337b89b8d1bc1df9fe73047c22b0300000000000000000000000000".into(), + birthday: 1679426817, + has_prefix: false, + has_accent: false, + }, + // The following seed requires the language specification in order to calculate + // a single valid checksum + Vector { + language: Language::Spanish, + seed: "impo sort usua cabi venu nobl oliv clim \ + cont barr marc auto prod vaca torn fati" + .into(), + entropy: "dbfce25fe09b68a340e01c62417eeef43ad51800000000000000000000000000".into(), + birthday: 1701511650, + has_prefix: true, + has_accent: true, + }, + ]; + + for vector in vectors { + let add_whitespace = |mut seed: String| { + seed.push(' '); + seed + }; + + let seed_without_accents = |seed: &str| { + seed + .split_whitespace() + .map(|w| w.chars().filter(char::is_ascii).collect::()) + .collect::>() + .join(" ") + }; + + let trim_seed = |seed: &str| { + let seed_to_trim = + if vector.has_accent { seed_without_accents(seed) } else { seed.to_string() }; + seed_to_trim + .split_whitespace() + .map(|w| { + let mut ascii = 0; + let mut to_take = w.len(); + for (i, char) in w.chars().enumerate() { + if char.is_ascii() { + ascii += 1; + } + if ascii == PREFIX_LEN { + // +1 to include this character, which put us at the prefix length + to_take = i + 1; + break; + } + } + w.chars().take(to_take).collect::() + }) + .collect::>() + .join(" ") + }; + + // String -> Seed + println!("{}. language: {:?}, seed: {}", line!(), vector.language, vector.seed.clone()); + let seed = Polyseed::from_string(vector.language, Zeroizing::new(vector.seed.clone())).unwrap(); + let trim = trim_seed(&vector.seed); + let add_whitespace = add_whitespace(vector.seed.clone()); + let seed_without_accents = seed_without_accents(&vector.seed); + + // Make sure a version with added whitespace still works + let whitespaced_seed = + Polyseed::from_string(vector.language, Zeroizing::new(add_whitespace)).unwrap(); + assert_eq!(seed, whitespaced_seed); + // Check trimmed versions works + if vector.has_prefix { + let trimmed_seed = Polyseed::from_string(vector.language, Zeroizing::new(trim)).unwrap(); + assert_eq!(seed, trimmed_seed); + } + // Check versions without accents work + if vector.has_accent { + let seed_without_accents = + Polyseed::from_string(vector.language, Zeroizing::new(seed_without_accents)).unwrap(); + assert_eq!(seed, seed_without_accents); + } + + let entropy = Zeroizing::new(hex::decode(vector.entropy).unwrap().try_into().unwrap()); + assert_eq!(*seed.entropy(), entropy); + assert!(seed.birthday().abs_diff(vector.birthday) < TIME_STEP); + + // Entropy -> Seed + let from_entropy = Polyseed::from(vector.language, 0, seed.birthday(), entropy).unwrap(); + assert_eq!(seed.to_string(), from_entropy.to_string()); + + // Check against ourselves + { + let seed = Polyseed::new(&mut OsRng, vector.language); + println!("{}. seed: {}", line!(), *seed.to_string()); + assert_eq!(seed, Polyseed::from_string(vector.language, seed.to_string()).unwrap()); + assert_eq!( + seed, + Polyseed::from(vector.language, 0, seed.birthday(), seed.entropy().clone(),).unwrap() + ); + } + } +} + +#[test] +fn test_invalid_polyseed() { + // This seed includes unsupported features bits and should error on decode + let seed = "include domain claim resemble urban hire lunch bird \ + crucial fire best wife ring warm ignore model" + .into(); + let res = Polyseed::from_string(Language::English, Zeroizing::new(seed)); + assert_eq!(res, Err(PolyseedError::UnsupportedFeatures)); +} diff --git a/coins/monero/src/wallet/seed/polyseed/cs.json b/networks/monero/wallet/polyseed/src/words/cs.rs similarity index 99% rename from coins/monero/src/wallet/seed/polyseed/cs.json rename to networks/monero/wallet/polyseed/src/words/cs.rs index a1c466cd..aab4b8d4 100644 --- a/coins/monero/src/wallet/seed/polyseed/cs.json +++ b/networks/monero/wallet/polyseed/src/words/cs.rs @@ -1,4 +1,4 @@ -[ +&[ "abdikace", "abeceda", "adresa", @@ -2047,4 +2047,4 @@ "zvrat", "zvukovod", "zvyk" -] \ No newline at end of file +] diff --git a/coins/monero/src/wallet/seed/polyseed/en.json b/networks/monero/wallet/polyseed/src/words/en.rs similarity index 99% rename from coins/monero/src/wallet/seed/polyseed/en.json rename to networks/monero/wallet/polyseed/src/words/en.rs index 7c2d07df..aa90e28d 100644 --- a/coins/monero/src/wallet/seed/polyseed/en.json +++ b/networks/monero/wallet/polyseed/src/words/en.rs @@ -1,4 +1,4 @@ -[ +&[ "abandon", "ability", "able", diff --git a/coins/monero/src/wallet/seed/polyseed/es.json b/networks/monero/wallet/polyseed/src/words/es.rs similarity index 99% rename from coins/monero/src/wallet/seed/polyseed/es.json rename to networks/monero/wallet/polyseed/src/words/es.rs index 32287459..21ab1299 100644 --- a/coins/monero/src/wallet/seed/polyseed/es.json +++ b/networks/monero/wallet/polyseed/src/words/es.rs @@ -1,4 +1,4 @@ -[ +&[ "ábaco", "abdomen", "abeja", diff --git a/coins/monero/src/wallet/seed/polyseed/fr.json b/networks/monero/wallet/polyseed/src/words/fr.rs similarity index 99% rename from coins/monero/src/wallet/seed/polyseed/fr.json rename to networks/monero/wallet/polyseed/src/words/fr.rs index 9b11a245..5b3303e9 100644 --- a/coins/monero/src/wallet/seed/polyseed/fr.json +++ b/networks/monero/wallet/polyseed/src/words/fr.rs @@ -1,4 +1,4 @@ -[ +&[ "abaisser", "abandon", "abdiquer", diff --git a/coins/monero/src/wallet/seed/polyseed/it.json b/networks/monero/wallet/polyseed/src/words/it.rs similarity index 99% rename from coins/monero/src/wallet/seed/polyseed/it.json rename to networks/monero/wallet/polyseed/src/words/it.rs index d452d1e2..6d0e4382 100644 --- a/coins/monero/src/wallet/seed/polyseed/it.json +++ b/networks/monero/wallet/polyseed/src/words/it.rs @@ -1,4 +1,4 @@ -[ +&[ "abaco", "abbaglio", "abbinato", diff --git a/coins/monero/src/wallet/seed/polyseed/ja.json b/networks/monero/wallet/polyseed/src/words/ja.rs similarity index 99% rename from coins/monero/src/wallet/seed/polyseed/ja.json rename to networks/monero/wallet/polyseed/src/words/ja.rs index e7055081..3c94572a 100644 --- a/coins/monero/src/wallet/seed/polyseed/ja.json +++ b/networks/monero/wallet/polyseed/src/words/ja.rs @@ -1,4 +1,4 @@ -[ +&[ "あいこくしん", "あいさつ", "あいだ", diff --git a/coins/monero/src/wallet/seed/polyseed/ko.json b/networks/monero/wallet/polyseed/src/words/ko.rs similarity index 99% rename from coins/monero/src/wallet/seed/polyseed/ko.json rename to networks/monero/wallet/polyseed/src/words/ko.rs index 4d8dd9f3..a41902f5 100644 --- a/coins/monero/src/wallet/seed/polyseed/ko.json +++ b/networks/monero/wallet/polyseed/src/words/ko.rs @@ -1,4 +1,4 @@ -[ +&[ "가격", "가끔", "가난", diff --git a/coins/monero/src/wallet/seed/polyseed/pt.json b/networks/monero/wallet/polyseed/src/words/pt.rs similarity index 99% rename from coins/monero/src/wallet/seed/polyseed/pt.json rename to networks/monero/wallet/polyseed/src/words/pt.rs index 4f35462c..38b9ff4c 100644 --- a/coins/monero/src/wallet/seed/polyseed/pt.json +++ b/networks/monero/wallet/polyseed/src/words/pt.rs @@ -1,4 +1,4 @@ -[ +&[ "abacate", "abaixo", "abalar", diff --git a/coins/monero/src/wallet/seed/polyseed/zh_simplified.json b/networks/monero/wallet/polyseed/src/words/zh_simplified.rs similarity index 99% rename from coins/monero/src/wallet/seed/polyseed/zh_simplified.json rename to networks/monero/wallet/polyseed/src/words/zh_simplified.rs index 0c0da9e2..50d2b586 100644 --- a/coins/monero/src/wallet/seed/polyseed/zh_simplified.json +++ b/networks/monero/wallet/polyseed/src/words/zh_simplified.rs @@ -1,4 +1,4 @@ -[ +&[ "的", "一", "是", diff --git a/coins/monero/src/wallet/seed/polyseed/zh_traditional.json b/networks/monero/wallet/polyseed/src/words/zh_traditional.rs similarity index 99% rename from coins/monero/src/wallet/seed/polyseed/zh_traditional.json rename to networks/monero/wallet/polyseed/src/words/zh_traditional.rs index cc73470c..3847f6e6 100644 --- a/coins/monero/src/wallet/seed/polyseed/zh_traditional.json +++ b/networks/monero/wallet/polyseed/src/words/zh_traditional.rs @@ -1,4 +1,4 @@ -[ +&[ "的", "一", "是", diff --git a/networks/monero/wallet/seed/Cargo.toml b/networks/monero/wallet/seed/Cargo.toml new file mode 100644 index 00000000..d66f4334 --- /dev/null +++ b/networks/monero/wallet/seed/Cargo.toml @@ -0,0 +1,41 @@ +[package] +name = "monero-seed" +version = "0.1.0" +description = "Rust implementation of Monero's seed algorithm" +license = "MIT" +repository = "https://github.com/serai-dex/serai/tree/develop/networks/monero/wallet/seed" +authors = ["Luke Parker "] +edition = "2021" +rust-version = "1.79" + +[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 } + +thiserror = { version = "1", default-features = false, optional = true } + +zeroize = { version = "^1.5", default-features = false, features = ["zeroize_derive"] } +rand_core = { version = "0.6", default-features = false } + +curve25519-dalek = { version = "4", default-features = false, features = ["alloc", "zeroize"] } + +[dev-dependencies] +hex = { version = "0.4", default-features = false, features = ["std"] } +monero-primitives = { path = "../../primitives", default-features = false, features = ["std"] } + +[features] +std = [ + "std-shims/std", + + "thiserror", + + "zeroize/std", + "rand_core/std", +] +default = ["std"] diff --git a/networks/monero/wallet/seed/LICENSE b/networks/monero/wallet/seed/LICENSE new file mode 100644 index 00000000..91d893c1 --- /dev/null +++ b/networks/monero/wallet/seed/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022-2024 Luke Parker + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/networks/monero/wallet/seed/README.md b/networks/monero/wallet/seed/README.md new file mode 100644 index 00000000..dded4133 --- /dev/null +++ b/networks/monero/wallet/seed/README.md @@ -0,0 +1,11 @@ +# Monero Seeds + +Rust implementation of Monero's seed algorithm. + +This library is usable under no-std when the `std` feature (on by default) is +disabled. + +### Cargo Features + +- `std` (on by default): Enables `std` (and with it, more efficient internal + implementations). diff --git a/coins/monero/src/wallet/seed/classic.rs b/networks/monero/wallet/seed/src/lib.rs similarity index 64% rename from coins/monero/src/wallet/seed/classic.rs rename to networks/monero/wallet/seed/src/lib.rs index 0605e4bc..5c8cbe34 100644 --- a/coins/monero/src/wallet/seed/classic.rs +++ b/networks/monero/wallet/seed/src/lib.rs @@ -1,6 +1,12 @@ -use core::ops::Deref; +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![doc = include_str!("../README.md")] +#![deny(missing_docs)] +#![cfg_attr(not(feature = "std"), no_std)] + +use core::{ops::Deref, fmt}; use std_shims::{ sync::OnceLock, + vec, vec::Vec, string::{String, ToString}, collections::HashMap, @@ -11,26 +17,60 @@ use rand_core::{RngCore, CryptoRng}; use curve25519_dalek::scalar::Scalar; -use crate::{random_scalar, wallet::seed::SeedError}; +#[cfg(test)] +mod tests; -pub(crate) const CLASSIC_SEED_LENGTH: usize = 24; -pub(crate) const CLASSIC_SEED_LENGTH_WITH_CHECKSUM: usize = 25; +// The amount of words in a seed without a checksum. +const SEED_LENGTH: usize = 24; +// The amount of words in a seed with a checksum. +const SEED_LENGTH_WITH_CHECKSUM: usize = 25; +/// An error when working with a seed. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +#[cfg_attr(feature = "std", derive(thiserror::Error))] +pub enum SeedError { + #[cfg_attr(feature = "std", error("invalid seed"))] + /// The seed was invalid. + InvalidSeed, + /// The checksum did not match the data. + #[cfg_attr(feature = "std", error("invalid checksum"))] + InvalidChecksum, + /// The deprecated English language option was used with a checksum. + /// + /// The deprecated English language option did not include a checksum. + #[cfg_attr(feature = "std", error("deprecated English language option included a checksum"))] + DeprecatedEnglishWithChecksum, +} + +/// Language options. #[derive(Clone, Copy, PartialEq, Eq, Debug, Hash, Zeroize)] pub enum Language { + /// Chinese language option. Chinese, + /// English language option. English, + /// Dutch language option. Dutch, + /// French language option. French, + /// Spanish language option. Spanish, + /// German language option. German, + /// Italian language option. Italian, + /// Portuguese language option. Portuguese, + /// Japanese language option. Japanese, + /// Russian language option. Russian, + /// Esperanto language option. Esperanto, + /// Lojban language option. Lojban, - EnglishOld, + /// The original, and deprecated, English language. + DeprecatedEnglish, } fn trim(word: &str, len: usize) -> Zeroizing { @@ -38,14 +78,14 @@ fn trim(word: &str, len: usize) -> Zeroizing { } struct WordList { - word_list: Vec<&'static str>, + word_list: &'static [&'static str], word_map: HashMap<&'static str, usize>, trimmed_word_map: HashMap, unique_prefix_length: usize, } impl WordList { - fn new(word_list: Vec<&'static str>, prefix_length: usize) -> WordList { + fn new(word_list: &'static [&'static str], prefix_length: usize) -> WordList { let mut lang = WordList { word_list, word_map: HashMap::new(), @@ -67,32 +107,23 @@ static LANGUAGES_CELL: OnceLock> = OnceLock::new(); fn LANGUAGES() -> &'static HashMap { LANGUAGES_CELL.get_or_init(|| { HashMap::from([ - (Language::Chinese, WordList::new(include!("./classic/zh.rs"), 1)), - (Language::English, WordList::new(include!("./classic/en.rs"), 3)), - (Language::Dutch, WordList::new(include!("./classic/nl.rs"), 4)), - (Language::French, WordList::new(include!("./classic/fr.rs"), 4)), - (Language::Spanish, WordList::new(include!("./classic/es.rs"), 4)), - (Language::German, WordList::new(include!("./classic/de.rs"), 4)), - (Language::Italian, WordList::new(include!("./classic/it.rs"), 4)), - (Language::Portuguese, WordList::new(include!("./classic/pt.rs"), 4)), - (Language::Japanese, WordList::new(include!("./classic/ja.rs"), 3)), - (Language::Russian, WordList::new(include!("./classic/ru.rs"), 4)), - (Language::Esperanto, WordList::new(include!("./classic/eo.rs"), 4)), - (Language::Lojban, WordList::new(include!("./classic/jbo.rs"), 4)), - (Language::EnglishOld, WordList::new(include!("./classic/ang.rs"), 4)), + (Language::Chinese, WordList::new(include!("./words/zh.rs"), 1)), + (Language::English, WordList::new(include!("./words/en.rs"), 3)), + (Language::Dutch, WordList::new(include!("./words/nl.rs"), 4)), + (Language::French, WordList::new(include!("./words/fr.rs"), 4)), + (Language::Spanish, WordList::new(include!("./words/es.rs"), 4)), + (Language::German, WordList::new(include!("./words/de.rs"), 4)), + (Language::Italian, WordList::new(include!("./words/it.rs"), 4)), + (Language::Portuguese, WordList::new(include!("./words/pt.rs"), 4)), + (Language::Japanese, WordList::new(include!("./words/ja.rs"), 3)), + (Language::Russian, WordList::new(include!("./words/ru.rs"), 4)), + (Language::Esperanto, WordList::new(include!("./words/eo.rs"), 4)), + (Language::Lojban, WordList::new(include!("./words/jbo.rs"), 4)), + (Language::DeprecatedEnglish, WordList::new(include!("./words/ang.rs"), 4)), ]) }) } -#[cfg(test)] -pub(crate) fn trim_by_lang(word: &str, lang: Language) -> String { - if lang != Language::EnglishOld { - word.chars().take(LANGUAGES()[&lang].unique_prefix_length).collect() - } else { - word.to_string() - } -} - fn checksum_index(words: &[Zeroizing], lang: &WordList) -> usize { let mut trimmed_words = Zeroizing::new(String::new()); for w in words { @@ -135,7 +166,7 @@ fn checksum_index(words: &[Zeroizing], lang: &WordList) -> usize { // Convert a private key to a seed #[allow(clippy::needless_pass_by_value)] -fn key_to_seed(lang: Language, key: Zeroizing) -> ClassicSeed { +fn key_to_seed(lang: Language, key: Zeroizing) -> Seed { let bytes = Zeroizing::new(key.to_bytes()); // get the language words @@ -172,7 +203,7 @@ fn key_to_seed(lang: Language, key: Zeroizing) -> ClassicSeed { indices.zeroize(); // create a checksum word for all languages except old english - if lang != Language::EnglishOld { + if lang != Language::DeprecatedEnglish { let checksum = seed[checksum_index(&seed, &LANGUAGES()[&lang])].clone(); seed.push(checksum); } @@ -184,26 +215,26 @@ fn key_to_seed(lang: Language, key: Zeroizing) -> ClassicSeed { } *res += word; } - ClassicSeed(lang, res) + Seed(lang, res) } // Convert a seed to bytes -pub(crate) fn seed_to_bytes(lang: Language, words: &str) -> Result, SeedError> { +fn seed_to_bytes(lang: Language, words: &str) -> Result, SeedError> { // get seed words let words = words.split_whitespace().map(|w| Zeroizing::new(w.to_string())).collect::>(); - if (words.len() != CLASSIC_SEED_LENGTH) && (words.len() != CLASSIC_SEED_LENGTH_WITH_CHECKSUM) { + if (words.len() != SEED_LENGTH) && (words.len() != SEED_LENGTH_WITH_CHECKSUM) { panic!("invalid seed passed to seed_to_bytes"); } - let has_checksum = words.len() == CLASSIC_SEED_LENGTH_WITH_CHECKSUM; - if has_checksum && lang == Language::EnglishOld { - Err(SeedError::EnglishOldWithChecksum)?; + let has_checksum = words.len() == SEED_LENGTH_WITH_CHECKSUM; + if has_checksum && lang == Language::DeprecatedEnglish { + Err(SeedError::DeprecatedEnglishWithChecksum)?; } // Validate words are in the language word list let lang_word_list: &WordList = &LANGUAGES()[&lang]; let matched_indices = (|| { - let has_checksum = words.len() == CLASSIC_SEED_LENGTH_WITH_CHECKSUM; + let has_checksum = words.len() == SEED_LENGTH_WITH_CHECKSUM; let mut matched_indices = Zeroizing::new(vec![]); // Iterate through all the words and see if they're all present @@ -272,15 +303,27 @@ pub(crate) fn seed_to_bytes(lang: Language, words: &str) -> Result); -impl ClassicSeed { - pub(crate) fn new(rng: &mut R, lang: Language) -> ClassicSeed { - key_to_seed(lang, Zeroizing::new(random_scalar(rng))) +pub struct Seed(Language, Zeroizing); + +impl fmt::Debug for Seed { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_struct("Seed").finish_non_exhaustive() + } +} + +impl Seed { + /// Create a new seed. + pub fn new(rng: &mut R, lang: Language) -> Seed { + let mut scalar_bytes = Zeroizing::new([0; 64]); + rng.fill_bytes(scalar_bytes.as_mut()); + key_to_seed(lang, Zeroizing::new(Scalar::from_bytes_mod_order_wide(scalar_bytes.deref()))) } + /// Parse a seed from a string. #[allow(clippy::needless_pass_by_value)] - pub fn from_string(lang: Language, words: Zeroizing) -> Result { + pub fn from_string(lang: Language, words: Zeroizing) -> Result { let entropy = seed_to_bytes(lang, &words)?; // Make sure this is a valid scalar @@ -295,17 +338,20 @@ impl ClassicSeed { Ok(Self::from_entropy(lang, entropy).unwrap()) } + /// Create a seed from entropy. #[allow(clippy::needless_pass_by_value)] - pub fn from_entropy(lang: Language, entropy: Zeroizing<[u8; 32]>) -> Option { + pub fn from_entropy(lang: Language, entropy: Zeroizing<[u8; 32]>) -> Option { Option::from(Scalar::from_canonical_bytes(*entropy)) .map(|scalar| key_to_seed(lang, Zeroizing::new(scalar))) } - pub(crate) fn to_string(&self) -> Zeroizing { + /// Convert a seed to a string. + pub fn to_string(&self) -> Zeroizing { self.1.clone() } - pub(crate) fn entropy(&self) -> Zeroizing<[u8; 32]> { + /// Return the entropy underlying this seed. + pub fn entropy(&self) -> Zeroizing<[u8; 32]> { seed_to_bytes(self.0, &self.1).unwrap() } } diff --git a/networks/monero/wallet/seed/src/tests.rs b/networks/monero/wallet/seed/src/tests.rs new file mode 100644 index 00000000..c477a00d --- /dev/null +++ b/networks/monero/wallet/seed/src/tests.rs @@ -0,0 +1,234 @@ +use zeroize::Zeroizing; +use rand_core::OsRng; + +use curve25519_dalek::scalar::Scalar; + +use monero_primitives::keccak256; + +use crate::*; + +#[test] +fn test_original_seed() { + struct Vector { + language: Language, + seed: String, + spend: String, + view: String, + } + + let vectors = [ + Vector { + language: Language::Chinese, + seed: "摇 曲 艺 武 滴 然 效 似 赏 式 祥 歌 买 疑 小 碧 堆 博 键 房 鲜 悲 付 喷 武".into(), + spend: "a5e4fff1706ef9212993a69f246f5c95ad6d84371692d63e9bb0ea112a58340d".into(), + view: "1176c43ce541477ea2f3ef0b49b25112b084e26b8a843e1304ac4677b74cdf02".into(), + }, + Vector { + language: Language::English, + seed: "washing thirsty occur lectures tuesday fainted toxic adapt \ + abnormal memoir nylon mostly building shrugged online ember northern \ + ruby woes dauntless boil family illness inroads northern" + .into(), + spend: "c0af65c0dd837e666b9d0dfed62745f4df35aed7ea619b2798a709f0fe545403".into(), + view: "513ba91c538a5a9069e0094de90e927c0cd147fa10428ce3ac1afd49f63e3b01".into(), + }, + Vector { + language: Language::Dutch, + seed: "setwinst riphagen vimmetje extase blief tuitelig fuiven meifeest \ + ponywagen zesmaal ripdeal matverf codetaal leut ivoor rotten \ + wisgerhof winzucht typograaf atrium rein zilt traktaat verzaagd setwinst" + .into(), + spend: "e2d2873085c447c2bc7664222ac8f7d240df3aeac137f5ff2022eaa629e5b10a".into(), + view: "eac30b69477e3f68093d131c7fd961564458401b07f8c87ff8f6030c1a0c7301".into(), + }, + Vector { + language: Language::French, + seed: "poids vaseux tarte bazar poivre effet entier nuance \ + sensuel ennui pacte osselet poudre battre alibi mouton \ + stade paquet pliage gibier type question position projet pliage" + .into(), + spend: "2dd39ff1a4628a94b5c2ec3e42fb3dfe15c2b2f010154dc3b3de6791e805b904".into(), + view: "6725b32230400a1032f31d622b44c3a227f88258939b14a7c72e00939e7bdf0e".into(), + }, + Vector { + language: Language::Spanish, + seed: "minero ocupar mirar evadir octubre cal logro miope \ + opaco disco ancla litio clase cuello nasal clase \ + fiar avance deseo mente grumo negro cordón croqueta clase" + .into(), + spend: "ae2c9bebdddac067d73ec0180147fc92bdf9ac7337f1bcafbbe57dd13558eb02".into(), + view: "18deafb34d55b7a43cae2c1c1c206a3c80c12cc9d1f84640b484b95b7fec3e05".into(), + }, + Vector { + language: Language::German, + seed: "Kaliber Gabelung Tapir Liveband Favorit Specht Enklave Nabel \ + Jupiter Foliant Chronik nisten löten Vase Aussage Rekord \ + Yeti Gesetz Eleganz Alraune Künstler Almweide Jahr Kastanie Almweide" + .into(), + spend: "79801b7a1b9796856e2397d862a113862e1fdc289a205e79d8d70995b276db06".into(), + view: "99f0ec556643bd9c038a4ed86edcb9c6c16032c4622ed2e000299d527a792701".into(), + }, + Vector { + language: Language::Italian, + seed: "cavo pancetta auto fulmine alleanza filmato diavolo prato \ + forzare meritare litigare lezione segreto evasione votare buio \ + licenza cliente dorso natale crescere vento tutelare vetta evasione" + .into(), + spend: "5e7fd774eb00fa5877e2a8b4dc9c7ffe111008a3891220b56a6e49ac816d650a".into(), + view: "698a1dce6018aef5516e82ca0cb3e3ec7778d17dfb41a137567bfa2e55e63a03".into(), + }, + Vector { + language: Language::Portuguese, + seed: "agito eventualidade onus itrio holograma sodomizar objetos dobro \ + iugoslavo bcrepuscular odalisca abjeto iuane darwinista eczema acetona \ + cibernetico hoquei gleba driver buffer azoto megera nogueira agito" + .into(), + spend: "13b3115f37e35c6aa1db97428b897e584698670c1b27854568d678e729200c0f".into(), + view: "ad1b4fd35270f5f36c4da7166672b347e75c3f4d41346ec2a06d1d0193632801".into(), + }, + Vector { + language: Language::Japanese, + seed: "ぜんぶ どうぐ おたがい せんきょ おうじ そんちょう じゅしん いろえんぴつ \ + かほう つかれる えらぶ にちじょう くのう にちようび ぬまえび さんきゃく \ + おおや ちぬき うすめる いがく せつでん さうな すいえい せつだん おおや" + .into(), + spend: "c56e895cdb13007eda8399222974cdbab493640663804b93cbef3d8c3df80b0b".into(), + view: "6c3634a313ec2ee979d565c33888fd7c3502d696ce0134a8bc1a2698c7f2c508".into(), + }, + Vector { + language: Language::Russian, + seed: "шатер икра нация ехать получать инерция доза реальный \ + рыжий таможня лопата душа веселый клетка атлас лекция \ + обгонять паек наивный лыжный дурак стать ежик задача паек" + .into(), + spend: "7cb5492df5eb2db4c84af20766391cd3e3662ab1a241c70fc881f3d02c381f05".into(), + view: "fcd53e41ec0df995ab43927f7c44bc3359c93523d5009fb3f5ba87431d545a03".into(), + }, + Vector { + language: Language::Esperanto, + seed: "ukazo klini peco etikedo fabriko imitado onklino urino \ + pudro incidento kumuluso ikono smirgi hirundo uretro krii \ + sparkado super speciala pupo alpinisto cvana vokegi zombio fabriko" + .into(), + spend: "82ebf0336d3b152701964ed41df6b6e9a035e57fc98b84039ed0bd4611c58904".into(), + view: "cd4d120e1ea34360af528f6a3e6156063312d9cefc9aa6b5218d366c0ed6a201".into(), + }, + Vector { + language: Language::Lojban, + seed: "jetnu vensa julne xrotu xamsi julne cutci dakli \ + mlatu xedja muvgau palpi xindo sfubu ciste cinri \ + blabi darno dembi janli blabi fenki bukpu burcu blabi" + .into(), + spend: "e4f8c6819ab6cf792cebb858caabac9307fd646901d72123e0367ebc0a79c200".into(), + view: "c806ce62bafaa7b2d597f1a1e2dbe4a2f96bfd804bf6f8420fc7f4a6bd700c00".into(), + }, + Vector { + language: Language::DeprecatedEnglish, + seed: "glorious especially puff son moment add youth nowhere \ + throw glide grip wrong rhythm consume very swear \ + bitter heavy eventually begin reason flirt type unable" + .into(), + spend: "647f4765b66b636ff07170ab6280a9a6804dfbaf19db2ad37d23be024a18730b".into(), + view: "045da65316a906a8c30046053119c18020b07a7a3a6ef5c01ab2a8755416bd02".into(), + }, + // The following seeds require the language specification in order to calculate + // a single valid checksum + Vector { + language: Language::Spanish, + seed: "pluma laico atraer pintor peor cerca balde buscar \ + lancha batir nulo reloj resto gemelo nevera poder columna gol \ + oveja latir amplio bolero feliz fuerza nevera" + .into(), + spend: "30303983fc8d215dd020cc6b8223793318d55c466a86e4390954f373fdc7200a".into(), + view: "97c649143f3c147ba59aa5506cc09c7992c5c219bb26964442142bf97980800e".into(), + }, + Vector { + language: Language::Spanish, + seed: "pluma pluma pluma pluma pluma pluma pluma pluma \ + pluma pluma pluma pluma pluma pluma pluma pluma \ + pluma pluma pluma pluma pluma pluma pluma pluma pluma" + .into(), + spend: "b4050000b4050000b4050000b4050000b4050000b4050000b4050000b4050000".into(), + view: "d73534f7912b395eb70ef911791a2814eb6df7ce56528eaaa83ff2b72d9f5e0f".into(), + }, + Vector { + language: Language::English, + seed: "plus plus plus plus plus plus plus plus \ + plus plus plus plus plus plus plus plus \ + plus plus plus plus plus plus plus plus plus" + .into(), + spend: "3b0400003b0400003b0400003b0400003b0400003b0400003b0400003b040000".into(), + view: "43a8a7715eed11eff145a2024ddcc39740255156da7bbd736ee66a0838053a02".into(), + }, + Vector { + language: Language::Spanish, + seed: "audio audio audio audio audio audio audio audio \ + audio audio audio audio audio audio audio audio \ + audio audio audio audio audio audio audio audio audio" + .into(), + spend: "ba000000ba000000ba000000ba000000ba000000ba000000ba000000ba000000".into(), + view: "1437256da2c85d029b293d8c6b1d625d9374969301869b12f37186e3f906c708".into(), + }, + Vector { + language: Language::English, + seed: "audio audio audio audio audio audio audio audio \ + audio audio audio audio audio audio audio audio \ + audio audio audio audio audio audio audio audio audio" + .into(), + spend: "7900000079000000790000007900000079000000790000007900000079000000".into(), + view: "20bec797ab96780ae6a045dd816676ca7ed1d7c6773f7022d03ad234b581d600".into(), + }, + ]; + + for vector in vectors { + fn trim_by_lang(word: &str, lang: Language) -> String { + if lang != Language::DeprecatedEnglish { + word.chars().take(LANGUAGES()[&lang].unique_prefix_length).collect() + } else { + word.to_string() + } + } + + let trim_seed = |seed: &str| { + seed + .split_whitespace() + .map(|word| trim_by_lang(word, vector.language)) + .collect::>() + .join(" ") + }; + + // Test against Monero + { + println!("{}. language: {:?}, seed: {}", line!(), vector.language, vector.seed.clone()); + let seed = Seed::from_string(vector.language, Zeroizing::new(vector.seed.clone())).unwrap(); + let trim = trim_seed(&vector.seed); + assert_eq!(seed, Seed::from_string(vector.language, Zeroizing::new(trim)).unwrap()); + + let spend: [u8; 32] = hex::decode(vector.spend).unwrap().try_into().unwrap(); + // For originalal seeds, Monero directly uses the entropy as a spend key + assert_eq!( + Option::::from(Scalar::from_canonical_bytes(*seed.entropy())), + Option::::from(Scalar::from_canonical_bytes(spend)), + ); + + let view: [u8; 32] = hex::decode(vector.view).unwrap().try_into().unwrap(); + // Monero then derives the view key as H(spend) + assert_eq!( + Scalar::from_bytes_mod_order(keccak256(spend)), + Scalar::from_canonical_bytes(view).unwrap() + ); + + assert_eq!(Seed::from_entropy(vector.language, Zeroizing::new(spend)).unwrap(), seed); + } + + // Test against ourselves + { + let seed = Seed::new(&mut OsRng, vector.language); + println!("{}. seed: {}", line!(), *seed.to_string()); + let trim = trim_seed(&seed.to_string()); + assert_eq!(seed, Seed::from_string(vector.language, Zeroizing::new(trim)).unwrap()); + assert_eq!(seed, Seed::from_entropy(vector.language, seed.entropy()).unwrap()); + assert_eq!(seed, Seed::from_string(vector.language, seed.to_string()).unwrap()); + } + } +} diff --git a/coins/monero/src/wallet/seed/classic/ang.rs b/networks/monero/wallet/seed/src/words/ang.rs similarity index 99% rename from coins/monero/src/wallet/seed/classic/ang.rs rename to networks/monero/wallet/seed/src/words/ang.rs index d2e47840..2800b1a9 100644 --- a/coins/monero/src/wallet/seed/classic/ang.rs +++ b/networks/monero/wallet/seed/src/words/ang.rs @@ -1,4 +1,4 @@ -vec![ +&[ "like", "just", "love", diff --git a/coins/monero/src/wallet/seed/classic/de.rs b/networks/monero/wallet/seed/src/words/de.rs similarity index 99% rename from coins/monero/src/wallet/seed/classic/de.rs rename to networks/monero/wallet/seed/src/words/de.rs index c6618356..85dee081 100644 --- a/coins/monero/src/wallet/seed/classic/de.rs +++ b/networks/monero/wallet/seed/src/words/de.rs @@ -1,4 +1,4 @@ -vec![ +&[ "Abakus", "Abart", "abbilden", diff --git a/coins/monero/src/wallet/seed/classic/en.rs b/networks/monero/wallet/seed/src/words/en.rs similarity index 99% rename from coins/monero/src/wallet/seed/classic/en.rs rename to networks/monero/wallet/seed/src/words/en.rs index ae788691..c6f9a454 100644 --- a/coins/monero/src/wallet/seed/classic/en.rs +++ b/networks/monero/wallet/seed/src/words/en.rs @@ -1,4 +1,4 @@ -vec![ +&[ "abbey", "abducts", "ability", diff --git a/coins/monero/src/wallet/seed/classic/eo.rs b/networks/monero/wallet/seed/src/words/eo.rs similarity index 99% rename from coins/monero/src/wallet/seed/classic/eo.rs rename to networks/monero/wallet/seed/src/words/eo.rs index eb518af0..d9d6ff40 100644 --- a/coins/monero/src/wallet/seed/classic/eo.rs +++ b/networks/monero/wallet/seed/src/words/eo.rs @@ -1,4 +1,4 @@ -vec![ +&[ "abako", "abdiki", "abelo", diff --git a/coins/monero/src/wallet/seed/classic/es.rs b/networks/monero/wallet/seed/src/words/es.rs similarity index 99% rename from coins/monero/src/wallet/seed/classic/es.rs rename to networks/monero/wallet/seed/src/words/es.rs index d6f26855..09fb346d 100644 --- a/coins/monero/src/wallet/seed/classic/es.rs +++ b/networks/monero/wallet/seed/src/words/es.rs @@ -1,4 +1,4 @@ -vec![ +&[ "ábaco", "abdomen", "abeja", diff --git a/coins/monero/src/wallet/seed/classic/fr.rs b/networks/monero/wallet/seed/src/words/fr.rs similarity index 99% rename from coins/monero/src/wallet/seed/classic/fr.rs rename to networks/monero/wallet/seed/src/words/fr.rs index e2ca8ad4..338eeb38 100644 --- a/coins/monero/src/wallet/seed/classic/fr.rs +++ b/networks/monero/wallet/seed/src/words/fr.rs @@ -1,4 +1,4 @@ -vec![ +&[ "abandon", "abattre", "aboi", diff --git a/coins/monero/src/wallet/seed/classic/it.rs b/networks/monero/wallet/seed/src/words/it.rs similarity index 99% rename from coins/monero/src/wallet/seed/classic/it.rs rename to networks/monero/wallet/seed/src/words/it.rs index d303452c..343984e6 100644 --- a/coins/monero/src/wallet/seed/classic/it.rs +++ b/networks/monero/wallet/seed/src/words/it.rs @@ -1,4 +1,4 @@ -vec![ +&[ "abbinare", "abbonato", "abisso", diff --git a/coins/monero/src/wallet/seed/classic/ja.rs b/networks/monero/wallet/seed/src/words/ja.rs similarity index 99% rename from coins/monero/src/wallet/seed/classic/ja.rs rename to networks/monero/wallet/seed/src/words/ja.rs index bc2aafde..da2d9fb6 100644 --- a/coins/monero/src/wallet/seed/classic/ja.rs +++ b/networks/monero/wallet/seed/src/words/ja.rs @@ -1,4 +1,4 @@ -vec![ +&[ "あいこくしん", "あいさつ", "あいだ", diff --git a/coins/monero/src/wallet/seed/classic/jbo.rs b/networks/monero/wallet/seed/src/words/jbo.rs similarity index 99% rename from coins/monero/src/wallet/seed/classic/jbo.rs rename to networks/monero/wallet/seed/src/words/jbo.rs index bcfcc6bc..a58f8d11 100644 --- a/coins/monero/src/wallet/seed/classic/jbo.rs +++ b/networks/monero/wallet/seed/src/words/jbo.rs @@ -1,4 +1,4 @@ -vec![ +&[ "backi", "bacru", "badna", diff --git a/coins/monero/src/wallet/seed/classic/nl.rs b/networks/monero/wallet/seed/src/words/nl.rs similarity index 99% rename from coins/monero/src/wallet/seed/classic/nl.rs rename to networks/monero/wallet/seed/src/words/nl.rs index e2f1912f..0c191e7f 100644 --- a/coins/monero/src/wallet/seed/classic/nl.rs +++ b/networks/monero/wallet/seed/src/words/nl.rs @@ -1,4 +1,4 @@ -vec![ +&[ "aalglad", "aalscholver", "aambeeld", diff --git a/coins/monero/src/wallet/seed/classic/pt.rs b/networks/monero/wallet/seed/src/words/pt.rs similarity index 99% rename from coins/monero/src/wallet/seed/classic/pt.rs rename to networks/monero/wallet/seed/src/words/pt.rs index 6f37336b..cede0ac5 100644 --- a/coins/monero/src/wallet/seed/classic/pt.rs +++ b/networks/monero/wallet/seed/src/words/pt.rs @@ -1,4 +1,4 @@ -vec![ +&[ "abaular", "abdominal", "abeto", diff --git a/coins/monero/src/wallet/seed/classic/ru.rs b/networks/monero/wallet/seed/src/words/ru.rs similarity index 99% rename from coins/monero/src/wallet/seed/classic/ru.rs rename to networks/monero/wallet/seed/src/words/ru.rs index 3b36ef61..609fa4cb 100644 --- a/coins/monero/src/wallet/seed/classic/ru.rs +++ b/networks/monero/wallet/seed/src/words/ru.rs @@ -1,4 +1,4 @@ -vec![ +&[ "абажур", "абзац", "абонент", diff --git a/coins/monero/src/wallet/seed/classic/zh.rs b/networks/monero/wallet/seed/src/words/zh.rs similarity index 99% rename from coins/monero/src/wallet/seed/classic/zh.rs rename to networks/monero/wallet/seed/src/words/zh.rs index 2ea7916e..42f05b4a 100644 --- a/coins/monero/src/wallet/seed/classic/zh.rs +++ b/networks/monero/wallet/seed/src/words/zh.rs @@ -1,4 +1,4 @@ -vec![ +&[ "的", "一", "是", diff --git a/networks/monero/wallet/src/decoys.rs b/networks/monero/wallet/src/decoys.rs new file mode 100644 index 00000000..4a4faae5 --- /dev/null +++ b/networks/monero/wallet/src/decoys.rs @@ -0,0 +1,311 @@ +use std_shims::{io, vec::Vec, string::ToString, collections::HashSet}; + +use zeroize::{Zeroize, ZeroizeOnDrop}; + +use rand_core::{RngCore, CryptoRng}; +use rand_distr::{Distribution, Gamma}; +#[cfg(not(feature = "std"))] +use rand_distr::num_traits::Float; + +use curve25519_dalek::{Scalar, EdwardsPoint}; + +use crate::{ + DEFAULT_LOCK_WINDOW, COINBASE_LOCK_WINDOW, BLOCK_TIME, + primitives::{Commitment, Decoys}, + rpc::{RpcError, DecoyRpc}, + output::OutputData, + WalletOutput, +}; + +const RECENT_WINDOW: usize = 15; +const BLOCKS_PER_YEAR: usize = 365 * 24 * 60 * 60 / BLOCK_TIME; +#[allow(clippy::cast_precision_loss)] +const TIP_APPLICATION: f64 = (DEFAULT_LOCK_WINDOW * BLOCK_TIME) as f64; + +async fn select_n( + rng: &mut (impl RngCore + CryptoRng), + rpc: &impl DecoyRpc, + height: usize, + real_output: u64, + ring_len: usize, + fingerprintable_deterministic: bool, +) -> Result, RpcError> { + if height < DEFAULT_LOCK_WINDOW { + Err(RpcError::InternalError("not enough blocks to select decoys".to_string()))?; + } + if height > rpc.get_output_distribution_end_height().await? { + Err(RpcError::InternalError( + "decoys being requested from blocks this node doesn't have".to_string(), + ))?; + } + + // Get the distribution + let distribution = rpc.get_output_distribution(.. height).await?; + if distribution.len() < DEFAULT_LOCK_WINDOW { + Err(RpcError::InternalError("not enough blocks to select decoys".to_string()))?; + } + let highest_output_exclusive_bound = distribution[distribution.len() - DEFAULT_LOCK_WINDOW]; + // This assumes that each miner TX had one output (as sane) and checks we have sufficient + // outputs even when excluding them (due to their own timelock requirements) + // Considering this a temporal error for very new chains, it's sufficiently sane to have + if highest_output_exclusive_bound.saturating_sub(u64::try_from(COINBASE_LOCK_WINDOW).unwrap()) < + u64::try_from(ring_len).unwrap() + { + Err(RpcError::InternalError("not enough decoy candidates".to_string()))?; + } + + // Determine the outputs per second + #[allow(clippy::cast_precision_loss)] + let per_second = { + let blocks = distribution.len().min(BLOCKS_PER_YEAR); + let initial = distribution[distribution.len().saturating_sub(blocks + 1)]; + let outputs = distribution[distribution.len() - 1].saturating_sub(initial); + (outputs as f64) / ((blocks * BLOCK_TIME) as f64) + }; + + // Don't select the real output + let mut do_not_select = HashSet::new(); + do_not_select.insert(real_output); + + let decoy_count = ring_len - 1; + let mut res = Vec::with_capacity(decoy_count); + + let mut iters = 0; + // Iterates until we have enough decoys + // If an iteration only returns a partial set of decoys, the remainder will be obvious as decoys + // to the RPC + // The length of that remainder is expected to be minimal + while res.len() != decoy_count { + iters += 1; + #[cfg(not(test))] + const MAX_ITERS: usize = 10; + // When testing on fresh chains, increased iterations can be useful and we don't necessitate + // reasonable performance + #[cfg(test)] + const MAX_ITERS: usize = 100; + // Ensure this isn't infinitely looping + // We check both that we aren't at the maximum amount of iterations and that the not-yet + // selected candidates exceed the amount of candidates necessary to trigger the next iteration + if (iters == MAX_ITERS) || + ((highest_output_exclusive_bound - u64::try_from(do_not_select.len()).unwrap()) < + u64::try_from(ring_len).unwrap()) + { + Err(RpcError::InternalError("hit decoy selection round limit".to_string()))?; + } + + let remaining = decoy_count - res.len(); + let mut candidates = Vec::with_capacity(remaining); + while candidates.len() != remaining { + // Use a gamma distribution, as Monero does + // https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c45 + // /src/wallet/wallet2.cpp#L142-L143 + let mut age = Gamma::::new(19.28, 1.0 / 1.61).unwrap().sample(rng).exp(); + #[allow(clippy::cast_precision_loss)] + if age > TIP_APPLICATION { + age -= TIP_APPLICATION; + } else { + // f64 does not have try_from available, which is why these are written with `as` + age = (rng.next_u64() % u64::try_from(RECENT_WINDOW * BLOCK_TIME).unwrap()) as f64; + } + + #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)] + let o = (age * per_second) as u64; + if o < highest_output_exclusive_bound { + // Find which block this points to + let i = distribution.partition_point(|s| *s < (highest_output_exclusive_bound - 1 - o)); + let prev = i.saturating_sub(1); + let n = distribution[i] - distribution[prev]; + if n != 0 { + // Select an output from within this block + let o = distribution[prev] + (rng.next_u64() % n); + if !do_not_select.contains(&o) { + candidates.push(o); + // This output will either be used or is unusable + // In either case, we should not try it again + do_not_select.insert(o); + } + } + } + } + + // If this is the first time we're requesting these outputs, include the real one as well + // Prevents the node we're connected to from having a list of known decoys and then seeing a + // TX which uses all of them, with one additional output (the true spend) + let real_index = if iters == 0 { + candidates.push(real_output); + // Sort candidates so the real spends aren't the ones at the end + candidates.sort(); + Some(candidates.binary_search(&real_output).unwrap()) + } else { + None + }; + + for (i, output) in rpc + .get_unlocked_outputs(&candidates, height, fingerprintable_deterministic) + .await? + .iter_mut() + .enumerate() + { + // We could check the returned info is equivalent to our expectations, yet that'd allow the + // node to malleate the returned info to see if they can cause this error (allowing them to + // figure out the output being spent) + // + // Some degree of this attack (forcing resampling/trying to observe errors) is likely + // always possible + if real_index == Some(i) { + continue; + } + + // If this is an unlocked output, push it to the result + if let Some(output) = output.take() { + res.push((candidates[i], output)); + } + } + } + + Ok(res) +} + +async fn select_decoys( + rng: &mut R, + rpc: &impl DecoyRpc, + ring_len: usize, + height: usize, + input: &WalletOutput, + fingerprintable_deterministic: bool, +) -> Result { + // Select all decoys for this transaction, assuming we generate a sane transaction + // We should almost never naturally generate an insane transaction, hence why this doesn't + // bother with an overage + let decoys = select_n( + rng, + rpc, + height, + input.relative_id.index_on_blockchain, + ring_len, + fingerprintable_deterministic, + ) + .await?; + + // Form the complete ring + let mut ring = decoys; + ring.push((input.relative_id.index_on_blockchain, [input.key(), input.commitment().calculate()])); + ring.sort_by(|a, b| a.0.cmp(&b.0)); + + /* + Monero does have sanity checks which it applies to the selected ring. + + They're statistically unlikely to be hit and only occur when the transaction is published over + the RPC (so they are not a relay rule). The RPC allows disabling them, which monero-rpc does to + ensure they don't pose a problem. + + They aren't worth the complexity to implement here, especially since they're non-deterministic. + */ + + // We need to convert our positional indexes to offset indexes + let mut offsets = Vec::with_capacity(ring.len()); + { + offsets.push(ring[0].0); + for m in 1 .. ring.len() { + offsets.push(ring[m].0 - ring[m - 1].0); + } + } + + Ok( + Decoys::new( + offsets, + // Binary searches for the real spend since we don't know where it sorted to + u8::try_from(ring.partition_point(|x| x.0 < input.relative_id.index_on_blockchain)).unwrap(), + ring.into_iter().map(|output| output.1).collect(), + ) + .unwrap(), + ) +} + +/// An output with decoys selected. +#[derive(Clone, PartialEq, Eq, Debug, Zeroize, ZeroizeOnDrop)] +pub struct OutputWithDecoys { + output: OutputData, + decoys: Decoys, +} + +impl OutputWithDecoys { + /// Select decoys for this output. + pub async fn new( + rng: &mut (impl Send + Sync + RngCore + CryptoRng), + rpc: &impl DecoyRpc, + ring_len: usize, + height: usize, + output: WalletOutput, + ) -> Result { + let decoys = select_decoys(rng, rpc, ring_len, height, &output, false).await?; + Ok(OutputWithDecoys { output: output.data.clone(), decoys }) + } + + /// Select a set of decoys for this output with a deterministic process. + /// + /// This function will always output the same set of decoys when called with the same arguments. + /// This makes it very useful in multisignature contexts, where instead of having one participant + /// select the decoys, everyone can locally select the decoys while coming to the same result. + /// + /// The set of decoys selected may be fingerprintable as having been produced by this + /// methodology. + pub async fn fingerprintable_deterministic_new( + rng: &mut (impl Send + Sync + RngCore + CryptoRng), + rpc: &impl DecoyRpc, + ring_len: usize, + height: usize, + output: WalletOutput, + ) -> Result { + let decoys = select_decoys(rng, rpc, ring_len, height, &output, true).await?; + Ok(OutputWithDecoys { output: output.data.clone(), decoys }) + } + + /// The key this output may be spent by. + pub fn key(&self) -> EdwardsPoint { + self.output.key() + } + + /// The scalar to add to the private spend key for it to be the discrete logarithm of this + /// output's key. + pub fn key_offset(&self) -> Scalar { + self.output.key_offset + } + + /// The commitment this output created. + pub fn commitment(&self) -> &Commitment { + &self.output.commitment + } + + /// The decoys this output selected. + pub fn decoys(&self) -> &Decoys { + &self.decoys + } + + /// Write the OutputWithDecoys. + /// + /// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol + /// defined serialization. + pub fn write(&self, w: &mut W) -> io::Result<()> { + self.output.write(w)?; + self.decoys.write(w) + } + + /// Serialize the OutputWithDecoys to a `Vec`. + /// + /// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol + /// defined serialization. + pub fn serialize(&self) -> Vec { + let mut serialized = Vec::with_capacity(128); + self.write(&mut serialized).unwrap(); + serialized + } + + /// Read an OutputWithDecoys. + /// + /// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol + /// defined serialization. + pub fn read(r: &mut R) -> io::Result { + Ok(Self { output: OutputData::read(r)?, decoys: Decoys::read(r)? }) + } +} diff --git a/coins/monero/src/wallet/extra.rs b/networks/monero/wallet/src/extra.rs similarity index 60% rename from coins/monero/src/wallet/extra.rs rename to networks/monero/wallet/src/extra.rs index deed8036..521d69ae 100644 --- a/coins/monero/src/wallet/extra.rs +++ b/networks/monero/wallet/src/extra.rs @@ -1,5 +1,6 @@ use core::ops::BitXor; use std_shims::{ + vec, vec::Vec, io::{self, Read, BufRead, Write}, }; @@ -8,25 +9,28 @@ use zeroize::Zeroize; use curve25519_dalek::edwards::EdwardsPoint; -use crate::serialize::{ - varint_len, read_byte, read_bytes, read_varint, read_point, read_vec, write_byte, write_varint, - write_point, write_vec, -}; +use monero_serai::io::*; -pub const MAX_TX_EXTRA_PADDING_COUNT: usize = 255; -pub const MAX_TX_EXTRA_NONCE_SIZE: usize = 255; +pub(crate) const MAX_TX_EXTRA_PADDING_COUNT: usize = 255; +const MAX_TX_EXTRA_NONCE_SIZE: usize = 255; -pub const PAYMENT_ID_MARKER: u8 = 0; -pub const ENCRYPTED_PAYMENT_ID_MARKER: u8 = 1; +const PAYMENT_ID_MARKER: u8 = 0; +const ENCRYPTED_PAYMENT_ID_MARKER: u8 = 1; // Used as it's the highest value not interpretable as a continued VarInt -pub const ARBITRARY_DATA_MARKER: u8 = 127; +pub(crate) const ARBITRARY_DATA_MARKER: u8 = 127; +/// The max amount of data which will fit within a blob of arbitrary data. // 1 byte is used for the marker pub const MAX_ARBITRARY_DATA_SIZE: usize = MAX_TX_EXTRA_NONCE_SIZE - 1; +/// A Payment ID. +/// +/// This is a legacy method of identifying why Monero was sent to the receiver. #[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)] pub enum PaymentId { + /// A deprecated form of payment ID which is no longer supported. Unencrypted([u8; 32]), + /// An encrypted payment ID. Encrypted([u8; 8]), } @@ -45,6 +49,7 @@ impl BitXor<[u8; 8]> for PaymentId { } impl PaymentId { + /// Write the PaymentId. pub fn write(&self, w: &mut W) -> io::Result<()> { match self { PaymentId::Unencrypted(id) => { @@ -59,6 +64,14 @@ impl PaymentId { Ok(()) } + /// Serialize the PaymentId to a `Vec`. + pub fn serialize(&self) -> Vec { + let mut res = Vec::with_capacity(1 + 8); + self.write(&mut res).unwrap(); + res + } + + /// Read a PaymentId. pub fn read(r: &mut R) -> io::Result { Ok(match read_byte(r)? { 0 => PaymentId::Unencrypted(read_bytes(r)?), @@ -68,18 +81,39 @@ impl PaymentId { } } -// Doesn't bother with padding nor MinerGate +/// A field within the TX extra. #[derive(Clone, PartialEq, Eq, Debug, Zeroize)] pub enum ExtraField { + /// Padding. + /// + /// This is a block of zeroes within the TX extra. Padding(usize), + /// The transaction key. + /// + /// This is a commitment to the randomness used for deriving outputs. PublicKey(EdwardsPoint), + /// The nonce field. + /// + /// This is used for data, such as payment IDs. Nonce(Vec), + /// The field for merge-mining. + /// + /// This is used within miner transactions who are merge-mining Monero to specify the foreign + /// block they mined. MergeMining(usize, [u8; 32]), + /// The additional transaction keys. + /// + /// These are the per-output commitments to the randomness used for deriving outputs. PublicKeys(Vec), + /// The 'mysterious' Minergate tag. + /// + /// This was used by a closed source entity without documentation. Support for parsing it was + /// added to reduce extra which couldn't be decoded. MysteriousMinergate(Vec), } impl ExtraField { + /// Write the ExtraField. pub fn write(&self, w: &mut W) -> io::Result<()> { match self { ExtraField::Padding(size) => { @@ -113,6 +147,14 @@ impl ExtraField { Ok(()) } + /// Serialize the ExtraField to a `Vec`. + pub fn serialize(&self) -> Vec { + let mut res = Vec::with_capacity(1 + 8); + self.write(&mut res).unwrap(); + res + } + + /// Read an ExtraField. pub fn read(r: &mut R) -> io::Result { Ok(match read_byte(r)? { 0 => ExtraField::Padding({ @@ -154,9 +196,18 @@ impl ExtraField { } } +/// The result of decoding a transaction's extra field. #[derive(Clone, PartialEq, Eq, Debug, Zeroize)] pub struct Extra(pub(crate) Vec); impl Extra { + /// The keys within this extra. + /// + /// This returns all keys specified with `PublicKey` and the first set of keys specified with + /// `PublicKeys`, so long as they're well-formed. + // https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c45 + // /src/wallet/wallet2.cpp#L2290-L2300 + // https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454 + // /src/wallet/wallet2.cpp#L2337-L2340 pub fn keys(&self) -> Option<(Vec, Option>)> { let mut keys = vec![]; let mut additional = None; @@ -177,6 +228,10 @@ impl Extra { } } + /// The payment ID embedded within this extra. + // Monero finds the first nonce field and reads the payment ID from it: + // https://github.com/monero-project/monero/blob/ac02af92867590ca80b2779a7bbeafa99ff94dcb/ + // src/wallet/wallet2.cpp#L2709-L2752 pub fn payment_id(&self) -> Option { for field in &self.0 { if let ExtraField::Nonce(data) = field { @@ -186,6 +241,9 @@ impl Extra { None } + /// The arbitrary data within this extra. + /// + /// This uses a marker custom to monero-wallet. pub fn data(&self) -> Vec> { let mut res = vec![]; for field in &self.0 { @@ -200,34 +258,24 @@ impl Extra { pub(crate) fn new(key: EdwardsPoint, additional: Vec) -> Extra { let mut res = Extra(Vec::with_capacity(3)); - res.push(ExtraField::PublicKey(key)); + // https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454 + // /src/cryptonote_basic/cryptonote_format_utils.cpp#L627-L633 + // We only support pushing nonces which come after these in the sort order + res.0.push(ExtraField::PublicKey(key)); if !additional.is_empty() { - res.push(ExtraField::PublicKeys(additional)); + res.0.push(ExtraField::PublicKeys(additional)); } res } - pub(crate) fn push(&mut self, field: ExtraField) { - self.0.push(field); - } - - #[rustfmt::skip] - pub(crate) fn fee_weight( - outputs: usize, - additional: bool, - payment_id: bool, - data: &[Vec] - ) -> usize { - // PublicKey, key - (1 + 32) + - // PublicKeys, length, additional keys - (if additional { 1 + 1 + (outputs * 32) } else { 0 }) + - // PaymentId (Nonce), length, encrypted, ID - (if payment_id { 1 + 1 + 1 + 8 } else { 0 }) + - // Nonce, length, ARBITRARY_DATA_MARKER, data - data.iter().map(|v| 1 + varint_len(1 + v.len()) + 1 + v.len()).sum::() + pub(crate) fn push_nonce(&mut self, nonce: Vec) { + self.0.push(ExtraField::Nonce(nonce)); } + /// Write the Extra. + /// + /// This is not of deterministic length nor length-prefixed. It should only be written to a + /// buffer which will be delimited. pub fn write(&self, w: &mut W) -> io::Result<()> { for field in &self.0 { field.write(w)?; @@ -235,20 +283,26 @@ impl Extra { Ok(()) } + /// Serialize the Extra to a `Vec`. pub fn serialize(&self) -> Vec { let mut buf = vec![]; self.write(&mut buf).unwrap(); buf } + /// Read an `Extra`. + /// + /// This is not of deterministic length nor length-prefixed. It should only be read from a buffer + /// already delimited. + #[allow(clippy::unnecessary_wraps)] pub fn read(r: &mut R) -> io::Result { let mut res = Extra(vec![]); - let mut field; - while { - field = ExtraField::read(r); - field.is_ok() - } { - res.0.push(field.unwrap()); + // Extra reads until EOF + // We take a BufRead so we can detect when the buffer is empty + // `fill_buf` returns the current buffer, filled if empty, only empty if the reader is + // exhausted + while !r.fill_buf()?.is_empty() { + res.0.push(ExtraField::read(r)?); } Ok(res) } diff --git a/networks/monero/wallet/src/lib.rs b/networks/monero/wallet/src/lib.rs new file mode 100644 index 00000000..a54d51c9 --- /dev/null +++ b/networks/monero/wallet/src/lib.rs @@ -0,0 +1,160 @@ +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![doc = include_str!("../README.md")] +#![deny(missing_docs)] +#![cfg_attr(not(feature = "std"), no_std)] + +use std_shims::vec::Vec; + +use zeroize::{Zeroize, Zeroizing}; + +use curve25519_dalek::{Scalar, EdwardsPoint}; + +use monero_serai::{ + io::write_varint, + primitives::{Commitment, keccak256, keccak256_to_scalar}, + ringct::EncryptedAmount, + transaction::Input, +}; + +pub use monero_serai::*; + +pub use monero_rpc as rpc; + +pub use monero_address as address; + +mod view_pair; +pub use view_pair::{ViewPair, GuaranteedViewPair}; + +/// Structures and functionality for working with transactions' extra fields. +pub mod extra; +pub(crate) use extra::{PaymentId, Extra}; + +pub(crate) mod output; +pub use output::WalletOutput; + +mod scan; +pub use scan::{Scanner, GuaranteedScanner}; + +mod decoys; +pub use decoys::OutputWithDecoys; + +/// Structs and functionality for sending transactions. +pub mod send; + +#[cfg(test)] +mod tests; + +#[derive(Clone, PartialEq, Eq, Zeroize)] +struct SharedKeyDerivations { + // Hs("view_tag" || 8Ra || o) + view_tag: u8, + // Hs(uniqueness || 8Ra || o) where uniqueness may be empty + shared_key: Scalar, +} + +impl SharedKeyDerivations { + // https://gist.github.com/kayabaNerve/8066c13f1fe1573286ba7a2fd79f6100 + fn uniqueness(inputs: &[Input]) -> [u8; 32] { + let mut u = b"uniqueness".to_vec(); + for input in inputs { + match input { + // If Gen, this should be the only input, making this loop somewhat pointless + // This works and even if there were somehow multiple inputs, it'd be a false negative + Input::Gen(height) => { + write_varint(height, &mut u).unwrap(); + } + Input::ToKey { key_image, .. } => u.extend(key_image.compress().to_bytes()), + } + } + keccak256(u) + } + + #[allow(clippy::needless_pass_by_value)] + fn output_derivations( + uniqueness: Option<[u8; 32]>, + ecdh: Zeroizing, + o: usize, + ) -> Zeroizing { + // 8Ra + let mut output_derivation = Zeroizing::new( + Zeroizing::new(Zeroizing::new(ecdh.mul_by_cofactor()).compress().to_bytes()).to_vec(), + ); + + // || o + { + let output_derivation: &mut Vec = output_derivation.as_mut(); + write_varint(&o, output_derivation).unwrap(); + } + + let view_tag = keccak256([b"view_tag".as_ref(), &output_derivation].concat())[0]; + + // uniqueness || + let output_derivation = if let Some(uniqueness) = uniqueness { + Zeroizing::new([uniqueness.as_ref(), &output_derivation].concat()) + } else { + output_derivation + }; + + Zeroizing::new(SharedKeyDerivations { + view_tag, + shared_key: keccak256_to_scalar(&output_derivation), + }) + } + + // H(8Ra || 0x8d) + #[allow(clippy::needless_pass_by_value)] + fn payment_id_xor(ecdh: Zeroizing) -> [u8; 8] { + // 8Ra + let output_derivation = Zeroizing::new( + Zeroizing::new(Zeroizing::new(ecdh.mul_by_cofactor()).compress().to_bytes()).to_vec(), + ); + + let mut payment_id_xor = [0; 8]; + payment_id_xor + .copy_from_slice(&keccak256([output_derivation.as_ref(), [0x8d].as_ref()].concat())[.. 8]); + payment_id_xor + } + + fn commitment_mask(&self) -> Scalar { + let mut mask = b"commitment_mask".to_vec(); + mask.extend(self.shared_key.as_bytes()); + let res = keccak256_to_scalar(&mask); + mask.zeroize(); + res + } + + fn compact_amount_encryption(&self, amount: u64) -> [u8; 8] { + let mut amount_mask = Zeroizing::new(b"amount".to_vec()); + amount_mask.extend(self.shared_key.to_bytes()); + let mut amount_mask = keccak256(&amount_mask); + + let mut amount_mask_8 = [0; 8]; + amount_mask_8.copy_from_slice(&amount_mask[.. 8]); + amount_mask.zeroize(); + + (amount ^ u64::from_le_bytes(amount_mask_8)).to_le_bytes() + } + + fn decrypt(&self, enc_amount: &EncryptedAmount) -> Commitment { + match enc_amount { + // TODO: Add a test vector for this + EncryptedAmount::Original { mask, amount } => { + let mask_shared_sec = keccak256(self.shared_key.as_bytes()); + let mask = + Scalar::from_bytes_mod_order(*mask) - Scalar::from_bytes_mod_order(mask_shared_sec); + + let amount_shared_sec = keccak256(mask_shared_sec); + let amount_scalar = + Scalar::from_bytes_mod_order(*amount) - Scalar::from_bytes_mod_order(amount_shared_sec); + // d2b from rctTypes.cpp + let amount = u64::from_le_bytes(amount_scalar.to_bytes()[0 .. 8].try_into().unwrap()); + + Commitment::new(mask, amount) + } + EncryptedAmount::Compact { amount } => Commitment::new( + self.commitment_mask(), + u64::from_le_bytes(self.compact_amount_encryption(u64::from_le_bytes(*amount))), + ), + } + } +} diff --git a/networks/monero/wallet/src/output.rs b/networks/monero/wallet/src/output.rs new file mode 100644 index 00000000..1c405ca5 --- /dev/null +++ b/networks/monero/wallet/src/output.rs @@ -0,0 +1,352 @@ +use std_shims::{ + vec, + vec::Vec, + io::{self, Read, Write}, +}; + +use zeroize::{Zeroize, ZeroizeOnDrop}; + +use curve25519_dalek::{Scalar, edwards::EdwardsPoint}; + +use crate::{ + io::*, primitives::Commitment, transaction::Timelock, address::SubaddressIndex, extra::PaymentId, +}; + +/// An absolute output ID, defined as its transaction hash and output index. +/// +/// This is not the output's key as multiple outputs may share an output key. +#[derive(Clone, PartialEq, Eq, Zeroize, ZeroizeOnDrop)] +pub(crate) struct AbsoluteId { + pub(crate) transaction: [u8; 32], + pub(crate) index_in_transaction: u32, +} + +impl core::fmt::Debug for AbsoluteId { + fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> { + fmt + .debug_struct("AbsoluteId") + .field("transaction", &hex::encode(self.transaction)) + .field("index_in_transaction", &self.index_in_transaction) + .finish() + } +} + +impl AbsoluteId { + /// Write the AbsoluteId. + /// + /// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol + /// defined serialization. + fn write(&self, w: &mut W) -> io::Result<()> { + w.write_all(&self.transaction)?; + w.write_all(&self.index_in_transaction.to_le_bytes()) + } + + /// Read an AbsoluteId. + /// + /// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol + /// defined serialization. + fn read(r: &mut R) -> io::Result { + Ok(AbsoluteId { transaction: read_bytes(r)?, index_in_transaction: read_u32(r)? }) + } +} + +/// An output's relative ID. +/// +/// This is defined as the output's index on the blockchain. +#[derive(Clone, PartialEq, Eq, Zeroize, ZeroizeOnDrop)] +pub(crate) struct RelativeId { + pub(crate) index_on_blockchain: u64, +} + +impl core::fmt::Debug for RelativeId { + fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> { + fmt.debug_struct("RelativeId").field("index_on_blockchain", &self.index_on_blockchain).finish() + } +} + +impl RelativeId { + /// Write the RelativeId. + /// + /// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol + /// defined serialization. + fn write(&self, w: &mut W) -> io::Result<()> { + w.write_all(&self.index_on_blockchain.to_le_bytes()) + } + + /// Read an RelativeId. + /// + /// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol + /// defined serialization. + fn read(r: &mut R) -> io::Result { + Ok(RelativeId { index_on_blockchain: read_u64(r)? }) + } +} + +/// The data within an output, as necessary to spend the output. +#[derive(Clone, PartialEq, Eq, Zeroize, ZeroizeOnDrop)] +pub(crate) struct OutputData { + pub(crate) key: EdwardsPoint, + pub(crate) key_offset: Scalar, + pub(crate) commitment: Commitment, +} + +impl core::fmt::Debug for OutputData { + fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> { + fmt + .debug_struct("OutputData") + .field("key", &hex::encode(self.key.compress().0)) + .field("key_offset", &hex::encode(self.key_offset.to_bytes())) + .field("commitment", &self.commitment) + .finish() + } +} + +impl OutputData { + /// The key this output may be spent by. + pub(crate) fn key(&self) -> EdwardsPoint { + self.key + } + + /// The scalar to add to the private spend key for it to be the discrete logarithm of this + /// output's key. + pub(crate) fn key_offset(&self) -> Scalar { + self.key_offset + } + + /// The commitment this output created. + pub(crate) fn commitment(&self) -> &Commitment { + &self.commitment + } + + /// Write the OutputData. + /// + /// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol + /// defined serialization. + pub(crate) fn write(&self, w: &mut W) -> io::Result<()> { + w.write_all(&self.key.compress().to_bytes())?; + w.write_all(&self.key_offset.to_bytes())?; + self.commitment.write(w) + } + + /* + /// Serialize the OutputData to a `Vec`. + pub fn serialize(&self) -> Vec { + let mut res = Vec::with_capacity(32 + 32 + 40); + self.write(&mut res).unwrap(); + res + } + */ + + /// Read an OutputData. + /// + /// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol + /// defined serialization. + pub(crate) fn read(r: &mut R) -> io::Result { + Ok(OutputData { + key: read_point(r)?, + key_offset: read_scalar(r)?, + commitment: Commitment::read(r)?, + }) + } +} + +/// The metadata for an output. +#[derive(Clone, PartialEq, Eq, Zeroize, ZeroizeOnDrop)] +pub(crate) struct Metadata { + pub(crate) additional_timelock: Timelock, + pub(crate) subaddress: Option, + pub(crate) payment_id: Option, + pub(crate) arbitrary_data: Vec>, +} + +impl core::fmt::Debug for Metadata { + fn fmt(&self, fmt: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> { + fmt + .debug_struct("Metadata") + .field("additional_timelock", &self.additional_timelock) + .field("subaddress", &self.subaddress) + .field("payment_id", &self.payment_id) + .field("arbitrary_data", &self.arbitrary_data.iter().map(hex::encode).collect::>()) + .finish() + } +} + +impl Metadata { + /// Write the Metadata. + /// + /// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol + /// defined serialization. + fn write(&self, w: &mut W) -> io::Result<()> { + self.additional_timelock.write(w)?; + + if let Some(subaddress) = self.subaddress { + w.write_all(&[1])?; + w.write_all(&subaddress.account().to_le_bytes())?; + w.write_all(&subaddress.address().to_le_bytes())?; + } else { + w.write_all(&[0])?; + } + + if let Some(payment_id) = self.payment_id { + w.write_all(&[1])?; + payment_id.write(w)?; + } else { + w.write_all(&[0])?; + } + + w.write_all(&u32::try_from(self.arbitrary_data.len()).unwrap().to_le_bytes())?; + for part in &self.arbitrary_data { + w.write_all(&[u8::try_from(part.len()).unwrap()])?; + w.write_all(part)?; + } + Ok(()) + } + + /// Read a Metadata. + /// + /// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol + /// defined serialization. + fn read(r: &mut R) -> io::Result { + let additional_timelock = Timelock::read(r)?; + + let subaddress = match read_byte(r)? { + 0 => None, + 1 => Some( + SubaddressIndex::new(read_u32(r)?, read_u32(r)?) + .ok_or_else(|| io::Error::other("invalid subaddress in metadata"))?, + ), + _ => Err(io::Error::other("invalid subaddress is_some boolean in metadata"))?, + }; + + Ok(Metadata { + additional_timelock, + subaddress, + payment_id: if read_byte(r)? == 1 { PaymentId::read(r).ok() } else { None }, + arbitrary_data: { + let mut data = vec![]; + for _ in 0 .. read_u32(r)? { + let len = read_byte(r)?; + data.push(read_raw_vec(read_byte, usize::from(len), r)?); + } + data + }, + }) + } +} + +/// A scanned output and all associated data. +/// +/// This struct contains all data necessary to spend this output, or handle it as a payment. +/// +/// This struct is bound to a specific instance of the blockchain. If the blockchain reorganizes +/// the block this struct is bound to, it MUST be discarded. If any outputs are mutual to both +/// blockchains, scanning the new blockchain will yield those outputs again. +#[derive(Clone, PartialEq, Eq, Debug, Zeroize, ZeroizeOnDrop)] +pub struct WalletOutput { + /// The absolute ID for this transaction. + pub(crate) absolute_id: AbsoluteId, + /// The ID for this transaction, relative to the blockchain. + pub(crate) relative_id: RelativeId, + /// The output's data. + pub(crate) data: OutputData, + /// Associated metadata relevant for handling it as a payment. + pub(crate) metadata: Metadata, +} + +impl WalletOutput { + /// The hash of the transaction which created this output. + pub fn transaction(&self) -> [u8; 32] { + self.absolute_id.transaction + } + + /// The index of the output within the transaction. + pub fn index_in_transaction(&self) -> u32 { + self.absolute_id.index_in_transaction + } + + /// The index of the output on the blockchain. + pub fn index_on_blockchain(&self) -> u64 { + self.relative_id.index_on_blockchain + } + + /// The key this output may be spent by. + pub fn key(&self) -> EdwardsPoint { + self.data.key() + } + + /// The scalar to add to the private spend key for it to be the discrete logarithm of this + /// output's key. + pub fn key_offset(&self) -> Scalar { + self.data.key_offset() + } + + /// The commitment this output created. + pub fn commitment(&self) -> &Commitment { + self.data.commitment() + } + + /// The additional timelock this output is subject to. + /// + /// All outputs are subject to the '10-block lock', a 10-block window after their inclusion + /// on-chain during which they cannot be spent. Outputs may be additionally timelocked. This + /// function only returns the additional timelock. + pub fn additional_timelock(&self) -> Timelock { + self.metadata.additional_timelock + } + + /// The index of the subaddress this output was identified as sent to. + pub fn subaddress(&self) -> Option { + self.metadata.subaddress + } + + /// The payment ID included with this output. + /// + /// This field may be `Some` even if wallet2 would not return a payment ID. This will happen if + /// the scanned output belongs to the subaddress which spent Monero within the transaction which + /// created the output. If multiple subaddresses spent Monero within this transactions, the key + /// image with the highest index is determined to be the subaddress considered as the one + /// spending. + // TODO: Clarify and cite for point A ("highest index spent key image"??) + pub fn payment_id(&self) -> Option { + self.metadata.payment_id + } + + /// The arbitrary data from the `extra` field of the transaction which created this output. + pub fn arbitrary_data(&self) -> &[Vec] { + &self.metadata.arbitrary_data + } + + /// Write the WalletOutput. + /// + /// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol + /// defined serialization. + pub fn write(&self, w: &mut W) -> io::Result<()> { + self.absolute_id.write(w)?; + self.relative_id.write(w)?; + self.data.write(w)?; + self.metadata.write(w) + } + + /// Serialize the WalletOutput to a `Vec`. + /// + /// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol + /// defined serialization. + pub fn serialize(&self) -> Vec { + let mut serialized = Vec::with_capacity(128); + self.write(&mut serialized).unwrap(); + serialized + } + + /// Read a WalletOutput. + /// + /// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol + /// defined serialization. + pub fn read(r: &mut R) -> io::Result { + Ok(WalletOutput { + absolute_id: AbsoluteId::read(r)?, + relative_id: RelativeId::read(r)?, + data: OutputData::read(r)?, + metadata: Metadata::read(r)?, + }) + } +} diff --git a/networks/monero/wallet/src/scan.rs b/networks/monero/wallet/src/scan.rs new file mode 100644 index 00000000..07dc81c3 --- /dev/null +++ b/networks/monero/wallet/src/scan.rs @@ -0,0 +1,419 @@ +use core::ops::Deref; +use std_shims::{alloc::format, vec, vec::Vec, string::ToString, collections::HashMap}; + +use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing}; + +use curve25519_dalek::{constants::ED25519_BASEPOINT_TABLE, edwards::CompressedEdwardsY}; + +use monero_rpc::{RpcError, Rpc}; +use monero_serai::{ + io::*, + primitives::Commitment, + transaction::{Timelock, Pruned, Transaction}, + block::Block, +}; +use crate::{ + address::SubaddressIndex, ViewPair, GuaranteedViewPair, output::*, PaymentId, Extra, + SharedKeyDerivations, +}; + +/// A collection of potentially additionally timelocked outputs. +#[derive(Zeroize, ZeroizeOnDrop)] +pub struct Timelocked(Vec); + +impl Timelocked { + /// Return the outputs which aren't subject to an additional timelock. + #[must_use] + pub fn not_additionally_locked(self) -> Vec { + let mut res = vec![]; + for output in &self.0 { + if output.additional_timelock() == Timelock::None { + res.push(output.clone()); + } + } + res + } + + /// Return the outputs whose additional timelock unlocks by the specified block/time. + /// + /// Additional timelocks are almost never used outside of miner transactions, and are + /// increasingly planned for removal. Ignoring non-miner additionally-timelocked outputs is + /// recommended. + /// + /// `block` is the block number of the block the additional timelock must be satsified by. + /// + /// `time` is represented in seconds since the epoch. Please note Monero uses an on-chain + /// deterministic clock for time which is subject to variance from the real world time. This time + /// argument will be evaluated against Monero's clock, not the local system's clock. + #[must_use] + pub fn additional_timelock_satisfied_by(self, block: usize, time: u64) -> Vec { + let mut res = vec![]; + for output in &self.0 { + if (output.additional_timelock() <= Timelock::Block(block)) || + (output.additional_timelock() <= Timelock::Time(time)) + { + res.push(output.clone()); + } + } + res + } + + /// Ignore the timelocks and return all outputs within this container. + #[must_use] + pub fn ignore_additional_timelock(mut self) -> Vec { + let mut res = vec![]; + core::mem::swap(&mut self.0, &mut res); + res + } +} + +#[derive(Clone)] +struct InternalScanner { + pair: ViewPair, + guaranteed: bool, + subaddresses: HashMap>, +} + +impl Zeroize for InternalScanner { + fn zeroize(&mut self) { + self.pair.zeroize(); + self.guaranteed.zeroize(); + + // This may not be effective, unfortunately + for (mut key, mut value) in self.subaddresses.drain() { + key.zeroize(); + value.zeroize(); + } + } +} +impl Drop for InternalScanner { + fn drop(&mut self) { + self.zeroize(); + } +} +impl ZeroizeOnDrop for InternalScanner {} + +impl InternalScanner { + fn new(pair: ViewPair, guaranteed: bool) -> Self { + let mut subaddresses = HashMap::new(); + subaddresses.insert(pair.spend().compress(), None); + Self { pair, guaranteed, subaddresses } + } + + fn register_subaddress(&mut self, subaddress: SubaddressIndex) { + let (spend, _) = self.pair.subaddress_keys(subaddress); + self.subaddresses.insert(spend.compress(), Some(subaddress)); + } + + fn scan_transaction( + &self, + tx_start_index_on_blockchain: u64, + tx_hash: [u8; 32], + tx: &Transaction, + ) -> Result { + // Only scan TXs creating RingCT outputs + // For the full details on why this check is equivalent, please see the documentation in `scan` + if tx.version() != 2 { + return Ok(Timelocked(vec![])); + } + + // Read the extra field + let Ok(extra) = Extra::read::<&[u8]>(&mut tx.prefix().extra.as_ref()) else { + return Ok(Timelocked(vec![])); + }; + + let Some((tx_keys, additional)) = extra.keys() else { + return Ok(Timelocked(vec![])); + }; + let payment_id = extra.payment_id(); + + let mut res = vec![]; + for (o, output) in tx.prefix().outputs.iter().enumerate() { + let Some(output_key) = decompress_point(output.key.to_bytes()) else { continue }; + + // Monero checks with each TX key and with the additional key for this output + + // This will be None if there's no additional keys, Some(None) if there's additional keys + // yet not one for this output (which is non-standard), and Some(Some(_)) if there's an + // additional key for this output + // https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454 + // /src/cryptonote_basic/cryptonote_format_utils.cpp#L1060-L1070 + let additional = additional.as_ref().map(|additional| additional.get(o)); + + #[allow(clippy::manual_let_else)] + for key in tx_keys.iter().map(|key| Some(Some(key))).chain(core::iter::once(additional)) { + // Get the key, or continue if there isn't one + let key = match key { + Some(Some(key)) => key, + Some(None) | None => continue, + }; + // Calculate the ECDH + let ecdh = Zeroizing::new(self.pair.view.deref() * key); + let output_derivations = SharedKeyDerivations::output_derivations( + if self.guaranteed { + Some(SharedKeyDerivations::uniqueness(&tx.prefix().inputs)) + } else { + None + }, + ecdh.clone(), + o, + ); + + // Check the view tag matches, if there is a view tag + if let Some(actual_view_tag) = output.view_tag { + if actual_view_tag != output_derivations.view_tag { + continue; + } + } + + // P - shared == spend + let Some(subaddress) = ({ + // The output key may be of torsion [0, 8) + // Our subtracting of a prime-order element means any torsion will be preserved + // If someone wanted to malleate output keys with distinct torsions, only one will be + // scanned accordingly (the one which has matching torsion of the spend key) + let subaddress_spend_key = + output_key - (&output_derivations.shared_key * ED25519_BASEPOINT_TABLE); + self.subaddresses.get(&subaddress_spend_key.compress()) + }) else { + continue; + }; + let subaddress = *subaddress; + + // The key offset is this shared key + let mut key_offset = output_derivations.shared_key; + if let Some(subaddress) = subaddress { + // And if this was to a subaddress, it's additionally the offset from subaddress spend + // key to the normal spend key + key_offset += self.pair.subaddress_derivation(subaddress); + } + // Since we've found an output to us, get its amount + let mut commitment = Commitment::zero(); + + // Miner transaction + if let Some(amount) = output.amount { + commitment.amount = amount; + // Regular transaction + } else { + let Transaction::V2 { proofs: Some(ref proofs), .. } = &tx else { + // Invalid transaction, as of consensus rules at the time of writing this code + Err(RpcError::InvalidNode("non-miner v2 transaction without RCT proofs".to_string()))? + }; + + commitment = match proofs.base.encrypted_amounts.get(o) { + Some(amount) => output_derivations.decrypt(amount), + // Invalid transaction, as of consensus rules at the time of writing this code + None => Err(RpcError::InvalidNode( + "RCT proofs without an encrypted amount per output".to_string(), + ))?, + }; + + // Rebuild the commitment to verify it + if Some(&commitment.calculate()) != proofs.base.commitments.get(o) { + continue; + } + } + + // Decrypt the payment ID + let payment_id = payment_id.map(|id| id ^ SharedKeyDerivations::payment_id_xor(ecdh)); + + res.push(WalletOutput { + absolute_id: AbsoluteId { + transaction: tx_hash, + index_in_transaction: o.try_into().unwrap(), + }, + relative_id: RelativeId { + index_on_blockchain: tx_start_index_on_blockchain + u64::try_from(o).unwrap(), + }, + data: OutputData { key: output_key, key_offset, commitment }, + metadata: Metadata { + additional_timelock: tx.prefix().additional_timelock, + subaddress, + payment_id, + arbitrary_data: extra.data(), + }, + }); + + // Break to prevent public keys from being included multiple times, triggering multiple + // inclusions of the same output + break; + } + } + + Ok(Timelocked(res)) + } + + async fn scan(&mut self, rpc: &impl Rpc, block: &Block) -> Result { + if block.header.hardfork_version > 16 { + Err(RpcError::InternalError(format!( + "scanning a hardfork {} block, when we only support up to 16", + block.header.hardfork_version + )))?; + } + + // We obtain all TXs in full + let mut txs_with_hashes = vec![( + block.miner_transaction.hash(), + Transaction::::from(block.miner_transaction.clone()), + )]; + let txs = rpc.get_pruned_transactions(&block.transactions).await?; + for (hash, tx) in block.transactions.iter().zip(txs) { + txs_with_hashes.push((*hash, tx)); + } + + /* + Requesting the output index for each output we sucessfully scan would cause a loss of privacy + We could instead request the output indexes for all outputs we scan, yet this would notably + increase the amount of RPC calls we make. + + We solve this by requesting the output index for the first RingCT output in the block, which + should be within the miner transaction. Then, as we scan transactions, we update the output + index ourselves. + + Please note we only will scan RingCT outputs so we only need to track the RingCT output + index. This decision was made due to spending CN outputs potentially having burdensome + requirements (the need to make a v1 TX due to insufficient decoys). + + We bound ourselves to only scanning RingCT outputs by only scanning v2 transactions. This is + safe and correct since: + + 1) v1 transactions cannot create RingCT outputs. + + https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454 + /src/cryptonote_basic/cryptonote_format_utils.cpp#L866-L869 + + 2) v2 miner transactions implicitly create RingCT outputs. + + https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454 + /src/blockchain_db/blockchain_db.cpp#L232-L241 + + 3) v2 transactions must create RingCT outputs. + + https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c45 + /src/cryptonote_core/blockchain.cpp#L3055-L3065 + + That does bound on the hard fork version being >= 3, yet all v2 TXs have a hard fork + version > 3. + + https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454 + /src/cryptonote_core/blockchain.cpp#L3417 + */ + + // Get the starting index + let mut tx_start_index_on_blockchain = { + let mut tx_start_index_on_blockchain = None; + for (hash, tx) in &txs_with_hashes { + // If this isn't a RingCT output, or there are no outputs, move to the next TX + if (!matches!(tx, Transaction::V2 { .. })) || tx.prefix().outputs.is_empty() { + continue; + } + + let index = *rpc.get_o_indexes(*hash).await?.first().ok_or_else(|| { + RpcError::InvalidNode( + "requested output indexes for a TX with outputs and got none".to_string(), + ) + })?; + tx_start_index_on_blockchain = Some(index); + break; + } + let Some(tx_start_index_on_blockchain) = tx_start_index_on_blockchain else { + // Block had no RingCT outputs + return Ok(Timelocked(vec![])); + }; + tx_start_index_on_blockchain + }; + + let mut res = Timelocked(vec![]); + for (hash, tx) in txs_with_hashes { + // Push all outputs into our result + { + let mut this_txs_outputs = vec![]; + core::mem::swap( + &mut self.scan_transaction(tx_start_index_on_blockchain, hash, &tx)?.0, + &mut this_txs_outputs, + ); + res.0.extend(this_txs_outputs); + } + + // Update the RingCT starting index for the next TX + if matches!(tx, Transaction::V2 { .. }) { + tx_start_index_on_blockchain += u64::try_from(tx.prefix().outputs.len()).unwrap() + } + } + + // If the block's version is >= 12, drop all unencrypted payment IDs + // https://github.com/monero-project/monero/blob/ac02af92867590ca80b2779a7bbeafa99ff94dcb/ + // src/wallet/wallet2.cpp#L2739-L2744 + if block.header.hardfork_version >= 12 { + for output in &mut res.0 { + if matches!(output.metadata.payment_id, Some(PaymentId::Unencrypted(_))) { + output.metadata.payment_id = None; + } + } + } + + Ok(res) + } +} + +/// A transaction scanner to find outputs received. +/// +/// When an output is successfully scanned, the output key MUST be checked against the local +/// database for lack of prior observation. If it was prior observed, that output is an instance +/// of the +/// [burning bug](https://web.getmonero.org/2018/09/25/a-post-mortum-of-the-burning-bug.html) and +/// MAY be unspendable. Only the prior received output(s) or the newly received output will be +/// spendable (as spending one will burn all of them). +/// +/// Once checked, the output key MUST be saved to the local database so future checks can be +/// performed. +#[derive(Clone, Zeroize, ZeroizeOnDrop)] +pub struct Scanner(InternalScanner); + +impl Scanner { + /// Create a Scanner from a ViewPair. + pub fn new(pair: ViewPair) -> Self { + Self(InternalScanner::new(pair, false)) + } + + /// Register a subaddress to scan for. + /// + /// Subaddresses must be explicitly registered ahead of time in order to be successfully scanned. + pub fn register_subaddress(&mut self, subaddress: SubaddressIndex) { + self.0.register_subaddress(subaddress) + } + + /// Scan a block. + pub async fn scan(&mut self, rpc: &impl Rpc, block: &Block) -> Result { + self.0.scan(rpc, block).await + } +} + +/// A transaction scanner to find outputs received which are guaranteed to be spendable. +/// +/// 'Guaranteed' outputs, or transactions outputs to the burning bug, are not officially specified +/// by the Monero project. They should only be used if necessary. No support outside of +/// monero-wallet is promised. +/// +/// "guaranteed to be spendable" assumes satisfaction of any timelocks in effect. +#[derive(Clone, Zeroize, ZeroizeOnDrop)] +pub struct GuaranteedScanner(InternalScanner); + +impl GuaranteedScanner { + /// Create a GuaranteedScanner from a GuaranteedViewPair. + pub fn new(pair: GuaranteedViewPair) -> Self { + Self(InternalScanner::new(pair.0, true)) + } + + /// Register a subaddress to scan for. + /// + /// Subaddresses must be explicitly registered ahead of time in order to be successfully scanned. + pub fn register_subaddress(&mut self, subaddress: SubaddressIndex) { + self.0.register_subaddress(subaddress) + } + + /// Scan a block. + pub async fn scan(&mut self, rpc: &impl Rpc, block: &Block) -> Result { + self.0.scan(rpc, block).await + } +} diff --git a/networks/monero/wallet/src/send/eventuality.rs b/networks/monero/wallet/src/send/eventuality.rs new file mode 100644 index 00000000..cd2543a4 --- /dev/null +++ b/networks/monero/wallet/src/send/eventuality.rs @@ -0,0 +1,137 @@ +use std_shims::{vec::Vec, io}; + +use zeroize::Zeroize; + +use crate::{ + ringct::PrunedRctProofs, + transaction::{Input, Timelock, Pruned, Transaction}, + send::SignableTransaction, +}; + +/// The eventual output of a SignableTransaction. +/// +/// If a SignableTransaction is signed and published on-chain, it will create a Transaction +/// identifiable to whoever else has the same SignableTransaction (with the same outgoing view +/// key). This structure enables checking if a Transaction is in fact such an output, as it can. +/// +/// Since Monero is a privacy coin without outgoing view keys, this only performs a fuzzy match. +/// The fuzzy match executes over the outputs and associated data necessary to work with the +/// outputs (the transaction randomness, ciphertexts). This transaction does not check if the +/// inputs intended to be spent where actually the inputs spent (as infeasible). +/// +/// The transaction randomness does bind to the inputs intended to be spent, so an on-chain +/// transaction will not match for multiple `Eventuality`s unless the `SignableTransaction`s they +/// were built from were in conflict (and their intended transactions cannot simultaneously exist +/// on-chain). +#[derive(Clone, PartialEq, Eq, Debug, Zeroize)] +pub struct Eventuality(SignableTransaction); + +impl From for Eventuality { + fn from(tx: SignableTransaction) -> Eventuality { + Eventuality(tx) + } +} + +impl Eventuality { + /// Return the `extra` field any transaction following this intent would use. + /// + /// This enables building a HashMap of Extra -> Eventuality for efficiently fetching the + /// `Eventuality` an on-chain transaction may complete. + /// + /// This extra is cryptographically bound to the inputs intended to be spent. If the + /// `SignableTransaction`s the `Eventuality`s are built from are not in conflict (their intended + /// transactions can simultaneously exist on-chain), then each extra will only have a single + /// Eventuality associated (barring a cryptographic problem considered hard failing). + pub fn extra(&self) -> Vec { + self.0.extra() + } + + /// Return if this TX matches the SignableTransaction this was created from. + /// + /// Matching the SignableTransaction means this transaction created the expected outputs, they're + /// scannable, they're not locked, and this transaction claims to use the intended inputs (though + /// this is not guaranteed). This 'claim' is evaluated by this transaction using the transaction + /// keys derived from the intended inputs. This ensures two SignableTransactions with the same + /// intended payments don't match for each other's `Eventuality`s (as they'll have distinct + /// inputs intended). + #[must_use] + pub fn matches(&self, tx: &Transaction) -> bool { + // Verify extra + if self.0.extra() != tx.prefix().extra { + return false; + } + + // Also ensure no timelock was set + if tx.prefix().additional_timelock != Timelock::None { + return false; + } + + // Check the amount of inputs aligns + if tx.prefix().inputs.len() != self.0.inputs.len() { + return false; + } + // Collect the key images used by this transaction + let Ok(key_images) = tx + .prefix() + .inputs + .iter() + .map(|input| match input { + Input::Gen(_) => Err(()), + Input::ToKey { key_image, .. } => Ok(*key_image), + }) + .collect::, _>>() + else { + return false; + }; + + // Check the outputs + if self.0.outputs(&key_images) != tx.prefix().outputs { + return false; + } + + // Check the encrypted amounts and commitments + let commitments_and_encrypted_amounts = self.0.commitments_and_encrypted_amounts(&key_images); + let Transaction::V2 { proofs: Some(PrunedRctProofs { ref base, .. }), .. } = tx else { + return false; + }; + if base.commitments != + commitments_and_encrypted_amounts + .iter() + .map(|(commitment, _)| commitment.calculate()) + .collect::>() + { + return false; + } + if base.encrypted_amounts != + commitments_and_encrypted_amounts.into_iter().map(|(_, amount)| amount).collect::>() + { + return false; + } + + true + } + + /// Write the Eventuality. + /// + /// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol + /// defined serialization. + pub fn write(&self, w: &mut W) -> io::Result<()> { + self.0.write(w) + } + + /// Serialize the Eventuality to a `Vec`. + /// + /// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol + /// defined serialization. + pub fn serialize(&self) -> Vec { + self.0.serialize() + } + + /// Read a Eventuality. + /// + /// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol + /// defined serialization. + pub fn read(r: &mut R) -> io::Result { + Ok(Eventuality(SignableTransaction::read(r)?)) + } +} diff --git a/networks/monero/wallet/src/send/mod.rs b/networks/monero/wallet/src/send/mod.rs new file mode 100644 index 00000000..87d98d69 --- /dev/null +++ b/networks/monero/wallet/src/send/mod.rs @@ -0,0 +1,581 @@ +use core::{ops::Deref, fmt}; +use std_shims::{ + io, vec, + vec::Vec, + string::{String, ToString}, +}; + +use zeroize::{Zeroize, Zeroizing}; + +use rand_core::{RngCore, CryptoRng}; +use rand::seq::SliceRandom; + +use curve25519_dalek::{constants::ED25519_BASEPOINT_TABLE, Scalar, EdwardsPoint}; +#[cfg(feature = "multisig")] +use frost::FrostError; + +use crate::{ + io::*, + generators::{MAX_COMMITMENTS, hash_to_point}, + ringct::{ + clsag::{ClsagError, ClsagContext, Clsag}, + RctType, RctPrunable, RctProofs, + }, + transaction::Transaction, + address::{Network, SubaddressIndex, MoneroAddress}, + extra::MAX_ARBITRARY_DATA_SIZE, + rpc::FeeRate, + ViewPair, GuaranteedViewPair, OutputWithDecoys, +}; + +mod tx_keys; +mod tx; +mod eventuality; +pub use eventuality::Eventuality; + +#[cfg(feature = "multisig")] +mod multisig; +#[cfg(feature = "multisig")] +pub use multisig::{TransactionMachine, TransactionSignMachine, TransactionSignatureMachine}; + +pub(crate) fn key_image_sort(x: &EdwardsPoint, y: &EdwardsPoint) -> core::cmp::Ordering { + x.compress().to_bytes().cmp(&y.compress().to_bytes()).reverse() +} + +#[derive(Clone, PartialEq, Eq, Zeroize)] +enum ChangeEnum { + AddressOnly(MoneroAddress), + Standard { view_pair: ViewPair, subaddress: Option }, + Guaranteed { view_pair: GuaranteedViewPair, subaddress: Option }, +} + +impl fmt::Debug for ChangeEnum { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + ChangeEnum::AddressOnly(addr) => { + f.debug_struct("ChangeEnum::AddressOnly").field("addr", &addr).finish() + } + ChangeEnum::Standard { subaddress, .. } => f + .debug_struct("ChangeEnum::Standard") + .field("subaddress", &subaddress) + .finish_non_exhaustive(), + ChangeEnum::Guaranteed { subaddress, .. } => f + .debug_struct("ChangeEnum::Guaranteed") + .field("subaddress", &subaddress) + .finish_non_exhaustive(), + } + } +} + +/// Specification for a change output. +#[derive(Clone, PartialEq, Eq, Debug, Zeroize)] +pub struct Change(Option); + +impl Change { + /// Create a change output specification. + /// + /// This take the view key as Monero assumes it has the view key for change outputs. It optimizes + /// its wallet protocol accordingly. + pub fn new(view_pair: ViewPair, subaddress: Option) -> Change { + Change(Some(ChangeEnum::Standard { view_pair, subaddress })) + } + + /// Create a change output specification for a guaranteed view pair. + /// + /// This take the view key as Monero assumes it has the view key for change outputs. It optimizes + /// its wallet protocol accordingly. + pub fn guaranteed(view_pair: GuaranteedViewPair, subaddress: Option) -> Change { + Change(Some(ChangeEnum::Guaranteed { view_pair, subaddress })) + } + + /// Create a fingerprintable change output specification. + /// + /// You MUST assume this will harm your privacy. Only use this if you know what you're doing. + /// + /// If the change address is Some, this will be unable to optimize the transaction as the + /// Monero wallet protocol expects it can (due to presumably having the view key for the change + /// output). If a transaction should be optimized, and isn'tm it will be fingerprintable. + /// + /// If the change address is None, there are two fingerprints: + /// + /// 1) The change in the TX is shunted to the fee (making it fingerprintable). + /// + /// 2) If there are two outputs in the TX, Monero would create a payment ID for the non-change + /// output so an observer can't tell apart TXs with a payment ID from TXs without a payment + /// ID. monero-wallet will simply not create a payment ID in this case, revealing it's a + /// monero-wallet TX without change. + pub fn fingerprintable(address: Option) -> Change { + if let Some(address) = address { + Change(Some(ChangeEnum::AddressOnly(address))) + } else { + Change(None) + } + } +} + +#[derive(Clone, PartialEq, Eq, Debug, Zeroize)] +enum InternalPayment { + Payment(MoneroAddress, u64), + Change(ChangeEnum), +} + +impl InternalPayment { + fn address(&self) -> MoneroAddress { + match self { + InternalPayment::Payment(addr, _) => *addr, + InternalPayment::Change(change) => match change { + ChangeEnum::AddressOnly(addr) => *addr, + // Network::Mainnet as the network won't effect the derivations + ChangeEnum::Standard { view_pair, subaddress } => match subaddress { + Some(subaddress) => view_pair.subaddress(Network::Mainnet, *subaddress), + None => view_pair.legacy_address(Network::Mainnet), + }, + ChangeEnum::Guaranteed { view_pair, subaddress } => { + view_pair.address(Network::Mainnet, *subaddress, None) + } + }, + } + } +} + +/// An error while sending Monero. +#[derive(Clone, PartialEq, Eq, Debug)] +#[cfg_attr(feature = "std", derive(thiserror::Error))] +pub enum SendError { + /// The RingCT type to produce proofs for this transaction with weren't supported. + #[cfg_attr(feature = "std", error("this library doesn't yet support that RctType"))] + UnsupportedRctType, + /// The transaction had no inputs specified. + #[cfg_attr(feature = "std", error("no inputs"))] + NoInputs, + /// The decoy quantity was invalid for the specified RingCT type. + #[cfg_attr(feature = "std", error("invalid number of decoys"))] + InvalidDecoyQuantity, + /// The transaction had no outputs specified. + #[cfg_attr(feature = "std", error("no outputs"))] + NoOutputs, + /// The transaction had too many outputs specified. + #[cfg_attr(feature = "std", error("too many outputs"))] + TooManyOutputs, + /// The transaction did not have a change output, and did not have two outputs. + /// + /// Monero requires all transactions have at least two outputs, assuming one payment and one + /// change (or at least one dummy and one change). Accordingly, specifying no change and only + /// one payment prevents creating a valid transaction + #[cfg_attr(feature = "std", error("only one output and no change address"))] + NoChange, + /// Multiple addresses had payment IDs specified. + /// + /// Only one payment ID is allowed per transaction. + #[cfg_attr(feature = "std", error("multiple addresses with payment IDs"))] + MultiplePaymentIds, + /// Too much arbitrary data was specified. + #[cfg_attr(feature = "std", error("too much data"))] + TooMuchArbitraryData, + /// The created transaction was too large. + #[cfg_attr(feature = "std", error("too large of a transaction"))] + TooLargeTransaction, + /// This transaction could not pay for itself. + #[cfg_attr( + feature = "std", + error( + "not enough funds (inputs {inputs}, outputs {outputs}, necessary_fee {necessary_fee:?})" + ) + )] + NotEnoughFunds { + /// The amount of funds the inputs contributed. + inputs: u64, + /// The amount of funds the outputs required. + outputs: u64, + /// The fee necessary to be paid on top. + /// + /// If this is None, it is because the fee was not calculated as the outputs alone caused this + /// error. + necessary_fee: Option, + }, + /// This transaction is being signed with the wrong private key. + #[cfg_attr(feature = "std", error("wrong spend private key"))] + WrongPrivateKey, + /// This transaction was read from a bytestream which was malicious. + #[cfg_attr( + feature = "std", + error("this SignableTransaction was created by deserializing a malicious serialization") + )] + MaliciousSerialization, + /// There was an error when working with the CLSAGs. + #[cfg_attr(feature = "std", error("clsag error ({0})"))] + ClsagError(ClsagError), + /// There was an error when working with FROST. + #[cfg(feature = "multisig")] + #[cfg_attr(feature = "std", error("frost error {0}"))] + FrostError(FrostError), +} + +/// A signable transaction. +#[derive(Clone, PartialEq, Eq, Debug, Zeroize)] +pub struct SignableTransaction { + rct_type: RctType, + outgoing_view_key: Zeroizing<[u8; 32]>, + inputs: Vec, + payments: Vec, + data: Vec>, + fee_rate: FeeRate, +} + +struct SignableTransactionWithKeyImages { + intent: SignableTransaction, + key_images: Vec, +} + +impl SignableTransaction { + fn validate(&self) -> Result<(), SendError> { + match self.rct_type { + RctType::ClsagBulletproof | RctType::ClsagBulletproofPlus => {} + _ => Err(SendError::UnsupportedRctType)?, + } + + if self.inputs.is_empty() { + Err(SendError::NoInputs)?; + } + for input in &self.inputs { + if input.decoys().len() != + match self.rct_type { + RctType::ClsagBulletproof => 11, + RctType::ClsagBulletproofPlus => 16, + _ => panic!("unsupported RctType"), + } + { + Err(SendError::InvalidDecoyQuantity)?; + } + } + + // Check we have at least one non-change output + if !self.payments.iter().any(|payment| matches!(payment, InternalPayment::Payment(_, _))) { + Err(SendError::NoOutputs)?; + } + // If we don't have at least two outputs, as required by Monero, error + if self.payments.len() < 2 { + Err(SendError::NoChange)?; + } + // Check we don't have multiple Change outputs due to decoding a malicious serialization + { + let mut change_count = 0; + for payment in &self.payments { + change_count += usize::from(u8::from(matches!(payment, InternalPayment::Change(_)))); + } + if change_count > 1 { + Err(SendError::MaliciousSerialization)?; + } + } + + // Make sure there's at most one payment ID + { + let mut payment_ids = 0; + for payment in &self.payments { + payment_ids += usize::from(u8::from(payment.address().payment_id().is_some())); + } + if payment_ids > 1 { + Err(SendError::MultiplePaymentIds)?; + } + } + + if self.payments.len() > MAX_COMMITMENTS { + Err(SendError::TooManyOutputs)?; + } + + // Check the length of each arbitrary data + for part in &self.data { + if part.len() > MAX_ARBITRARY_DATA_SIZE { + Err(SendError::TooMuchArbitraryData)?; + } + } + + // Check the length of TX extra + // https://github.com/monero-project/monero/pull/8733 + const MAX_EXTRA_SIZE: usize = 1060; + if self.extra().len() > MAX_EXTRA_SIZE { + Err(SendError::TooMuchArbitraryData)?; + } + + // Make sure we have enough funds + let in_amount = self.inputs.iter().map(|input| input.commitment().amount).sum::(); + let payments_amount = self + .payments + .iter() + .filter_map(|payment| match payment { + InternalPayment::Payment(_, amount) => Some(amount), + InternalPayment::Change(_) => None, + }) + .sum::(); + let (weight, necessary_fee) = self.weight_and_necessary_fee(); + if in_amount < (payments_amount + necessary_fee) { + Err(SendError::NotEnoughFunds { + inputs: in_amount, + outputs: payments_amount, + necessary_fee: Some(necessary_fee), + })?; + } + + // The limit is half the no-penalty block size + // https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454 + // /src/wallet/wallet2.cpp#L110766-L11085 + // https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454 + // /src/cryptonote_config.h#L61 + // https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454 + // /src/cryptonote_config.h#L64 + const MAX_TX_SIZE: usize = (300_000 / 2) - 600; + if weight >= MAX_TX_SIZE { + Err(SendError::TooLargeTransaction)?; + } + + Ok(()) + } + + /// Create a new SignableTransaction. + /// + /// `outgoing_view_key` is used to seed the RNGs for this transaction. Anyone with knowledge of + /// the outgoing view key will be able to identify a transaction produced with this methodology, + /// and the data within it. Accordingly, it must be treated as a private key. + /// + /// `data` represents arbitrary data which will be embedded into the transaction's `extra` field. + /// The embedding occurs using an `ExtraField::Nonce` with a custom marker byte (as to not + /// conflict with a payment ID). + pub fn new( + rct_type: RctType, + outgoing_view_key: Zeroizing<[u8; 32]>, + inputs: Vec, + payments: Vec<(MoneroAddress, u64)>, + change: Change, + data: Vec>, + fee_rate: FeeRate, + ) -> Result { + // Re-format the payments and change into a consolidated payments list + let mut payments = payments + .into_iter() + .map(|(addr, amount)| InternalPayment::Payment(addr, amount)) + .collect::>(); + + if let Some(change) = change.0 { + payments.push(InternalPayment::Change(change)); + } + + let mut res = + SignableTransaction { rct_type, outgoing_view_key, inputs, payments, data, fee_rate }; + res.validate()?; + + // Shuffle the payments + { + let mut rng = res.seeded_rng(b"shuffle_payments"); + res.payments.shuffle(&mut rng); + } + + Ok(res) + } + + /// The fee rate this transaction uses. + pub fn fee_rate(&self) -> FeeRate { + self.fee_rate + } + + /// The fee this transaction requires. + /// + /// This is distinct from the fee this transaction will use. If no change output is specified, + /// all unspent coins will be shunted to the fee. + pub fn necessary_fee(&self) -> u64 { + self.weight_and_necessary_fee().1 + } + + /// Write a SignableTransaction. + /// + /// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol + /// defined serialization. + pub fn write(&self, w: &mut W) -> io::Result<()> { + fn write_payment(payment: &InternalPayment, w: &mut W) -> io::Result<()> { + match payment { + InternalPayment::Payment(addr, amount) => { + w.write_all(&[0])?; + write_vec(write_byte, addr.to_string().as_bytes(), w)?; + w.write_all(&amount.to_le_bytes()) + } + InternalPayment::Change(change) => match change { + ChangeEnum::AddressOnly(addr) => { + w.write_all(&[1])?; + write_vec(write_byte, addr.to_string().as_bytes(), w) + } + ChangeEnum::Standard { view_pair, subaddress } => { + w.write_all(&[2])?; + write_point(&view_pair.spend(), w)?; + write_scalar(&view_pair.view, w)?; + if let Some(subaddress) = subaddress { + w.write_all(&subaddress.account().to_le_bytes())?; + w.write_all(&subaddress.address().to_le_bytes()) + } else { + w.write_all(&0u32.to_le_bytes())?; + w.write_all(&0u32.to_le_bytes()) + } + } + ChangeEnum::Guaranteed { view_pair, subaddress } => { + w.write_all(&[3])?; + write_point(&view_pair.spend(), w)?; + write_scalar(&view_pair.0.view, w)?; + if let Some(subaddress) = subaddress { + w.write_all(&subaddress.account().to_le_bytes())?; + w.write_all(&subaddress.address().to_le_bytes()) + } else { + w.write_all(&0u32.to_le_bytes())?; + w.write_all(&0u32.to_le_bytes()) + } + } + }, + } + } + + write_byte(&u8::from(self.rct_type), w)?; + w.write_all(self.outgoing_view_key.as_slice())?; + write_vec(OutputWithDecoys::write, &self.inputs, w)?; + write_vec(write_payment, &self.payments, w)?; + write_vec(|data, w| write_vec(write_byte, data, w), &self.data, w)?; + self.fee_rate.write(w) + } + + /// Serialize the SignableTransaction to a `Vec`. + /// + /// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol + /// defined serialization. + pub fn serialize(&self) -> Vec { + let mut buf = Vec::with_capacity(256); + self.write(&mut buf).unwrap(); + buf + } + + /// Read a `SignableTransaction`. + /// + /// This is not a Monero protocol defined struct, and this is accordingly not a Monero protocol + /// defined serialization. + pub fn read(r: &mut R) -> io::Result { + fn read_address(r: &mut R) -> io::Result { + String::from_utf8(read_vec(read_byte, r)?) + .ok() + .and_then(|str| MoneroAddress::from_str_with_unchecked_network(&str).ok()) + .ok_or_else(|| io::Error::other("invalid address")) + } + + fn read_payment(r: &mut R) -> io::Result { + Ok(match read_byte(r)? { + 0 => InternalPayment::Payment(read_address(r)?, read_u64(r)?), + 1 => InternalPayment::Change(ChangeEnum::AddressOnly(read_address(r)?)), + 2 => InternalPayment::Change(ChangeEnum::Standard { + view_pair: ViewPair::new(read_point(r)?, Zeroizing::new(read_scalar(r)?)) + .map_err(io::Error::other)?, + subaddress: SubaddressIndex::new(read_u32(r)?, read_u32(r)?), + }), + 3 => InternalPayment::Change(ChangeEnum::Guaranteed { + view_pair: GuaranteedViewPair::new(read_point(r)?, Zeroizing::new(read_scalar(r)?)) + .map_err(io::Error::other)?, + subaddress: SubaddressIndex::new(read_u32(r)?, read_u32(r)?), + }), + _ => Err(io::Error::other("invalid payment"))?, + }) + } + + let res = SignableTransaction { + rct_type: RctType::try_from(read_byte(r)?) + .map_err(|()| io::Error::other("unsupported/invalid RctType"))?, + outgoing_view_key: Zeroizing::new(read_bytes(r)?), + inputs: read_vec(OutputWithDecoys::read, r)?, + payments: read_vec(read_payment, r)?, + data: read_vec(|r| read_vec(read_byte, r), r)?, + fee_rate: FeeRate::read(r)?, + }; + match res.validate() { + Ok(()) => {} + Err(e) => Err(io::Error::other(e))?, + } + Ok(res) + } + + fn with_key_images(mut self, key_images: Vec) -> SignableTransactionWithKeyImages { + debug_assert_eq!(self.inputs.len(), key_images.len()); + + // Sort the inputs by their key images + let mut sorted_inputs = self.inputs.into_iter().zip(key_images).collect::>(); + sorted_inputs + .sort_by(|(_, key_image_a), (_, key_image_b)| key_image_sort(key_image_a, key_image_b)); + + self.inputs = Vec::with_capacity(sorted_inputs.len()); + let mut key_images = Vec::with_capacity(sorted_inputs.len()); + for (input, key_image) in sorted_inputs { + self.inputs.push(input); + key_images.push(key_image); + } + + SignableTransactionWithKeyImages { intent: self, key_images } + } + + /// Sign this transaction. + pub fn sign( + self, + rng: &mut (impl RngCore + CryptoRng), + sender_spend_key: &Zeroizing, + ) -> Result { + // Calculate the key images + let mut key_images = vec![]; + for input in &self.inputs { + let input_key = Zeroizing::new(sender_spend_key.deref() + input.key_offset()); + if (input_key.deref() * ED25519_BASEPOINT_TABLE) != input.key() { + Err(SendError::WrongPrivateKey)?; + } + let key_image = input_key.deref() * hash_to_point(input.key().compress().to_bytes()); + key_images.push(key_image); + } + + // Convert to a SignableTransactionWithKeyImages + let tx = self.with_key_images(key_images); + + // Prepare the CLSAG signatures + let mut clsag_signs = Vec::with_capacity(tx.intent.inputs.len()); + for input in &tx.intent.inputs { + // Re-derive the input key as this will be in a different order + let input_key = Zeroizing::new(sender_spend_key.deref() + input.key_offset()); + clsag_signs.push(( + input_key, + ClsagContext::new(input.decoys().clone(), input.commitment().clone()) + .map_err(SendError::ClsagError)?, + )); + } + + // Get the output commitments' mask sum + let mask_sum = tx.intent.sum_output_masks(&tx.key_images); + + // Get the actual TX, just needing the CLSAGs + let mut tx = tx.transaction_without_signatures(); + + // Sign the CLSAGs + let clsags_and_pseudo_outs = + Clsag::sign(rng, clsag_signs, mask_sum, tx.signature_hash().unwrap()) + .map_err(SendError::ClsagError)?; + + // Fill in the CLSAGs/pseudo-outs + let inputs_len = tx.prefix().inputs.len(); + let Transaction::V2 { + proofs: + Some(RctProofs { + prunable: RctPrunable::Clsag { ref mut clsags, ref mut pseudo_outs, .. }, + .. + }), + .. + } = tx + else { + panic!("not signing clsag?") + }; + *clsags = Vec::with_capacity(inputs_len); + *pseudo_outs = Vec::with_capacity(inputs_len); + for (clsag, pseudo_out) in clsags_and_pseudo_outs { + clsags.push(clsag); + pseudo_outs.push(pseudo_out); + } + + // Return the signed TX + Ok(tx) + } +} diff --git a/networks/monero/wallet/src/send/multisig.rs b/networks/monero/wallet/src/send/multisig.rs new file mode 100644 index 00000000..b3d58ba5 --- /dev/null +++ b/networks/monero/wallet/src/send/multisig.rs @@ -0,0 +1,304 @@ +use std_shims::{ + vec::Vec, + io::{self, Read}, + collections::HashMap, +}; + +use rand_core::{RngCore, CryptoRng}; + +use group::ff::Field; +use curve25519_dalek::{traits::Identity, Scalar, EdwardsPoint}; +use dalek_ff_group as dfg; + +use transcript::{Transcript, RecommendedTranscript}; +use frost::{ + curve::Ed25519, + Participant, FrostError, ThresholdKeys, + dkg::lagrange, + sign::{ + Preprocess, CachedPreprocess, SignatureShare, PreprocessMachine, SignMachine, SignatureMachine, + AlgorithmMachine, AlgorithmSignMachine, AlgorithmSignatureMachine, + }, +}; + +use monero_serai::{ + ringct::{ + clsag::{ClsagContext, ClsagMultisigMaskSender, ClsagAddendum, ClsagMultisig}, + RctPrunable, RctProofs, + }, + transaction::Transaction, +}; +use crate::send::{SendError, SignableTransaction, key_image_sort}; + +/// Initial FROST machine to produce a signed transaction. +pub struct TransactionMachine { + signable: SignableTransaction, + + i: Participant, + + // The key image generator, and the scalar offset from the spend key + key_image_generators_and_offsets: Vec<(EdwardsPoint, Scalar)>, + clsags: Vec<(ClsagMultisigMaskSender, AlgorithmMachine)>, +} + +/// Second FROST machine to produce a signed transaction. +pub struct TransactionSignMachine { + signable: SignableTransaction, + + i: Participant, + + key_image_generators_and_offsets: Vec<(EdwardsPoint, Scalar)>, + clsags: Vec<(ClsagMultisigMaskSender, AlgorithmSignMachine)>, + + our_preprocess: Vec>, +} + +/// Final FROST machine to produce a signed transaction. +pub struct TransactionSignatureMachine { + tx: Transaction, + clsags: Vec>, +} + +impl SignableTransaction { + /// Create a FROST signing machine out of this signable transaction. + pub fn multisig(self, keys: &ThresholdKeys) -> Result { + let mut clsags = vec![]; + + let mut key_image_generators_and_offsets = vec![]; + for input in &self.inputs { + // Check this is the right set of keys + let offset = keys.offset(dfg::Scalar(input.key_offset())); + if offset.group_key().0 != input.key() { + Err(SendError::WrongPrivateKey)?; + } + + let context = ClsagContext::new(input.decoys().clone(), input.commitment().clone()) + .map_err(SendError::ClsagError)?; + let (clsag, clsag_mask_send) = ClsagMultisig::new( + RecommendedTranscript::new(b"Monero Multisignature Transaction"), + context, + ); + key_image_generators_and_offsets.push(( + clsag.key_image_generator(), + keys.current_offset().unwrap_or(dfg::Scalar::ZERO).0 + input.key_offset(), + )); + clsags.push((clsag_mask_send, AlgorithmMachine::new(clsag, offset))); + } + + Ok(TransactionMachine { + signable: self, + i: keys.params().i(), + key_image_generators_and_offsets, + clsags, + }) + } +} + +impl PreprocessMachine for TransactionMachine { + type Preprocess = Vec>; + type Signature = Transaction; + type SignMachine = TransactionSignMachine; + + fn preprocess( + mut self, + rng: &mut R, + ) -> (TransactionSignMachine, Self::Preprocess) { + // Iterate over each CLSAG calling preprocess + let mut preprocesses = Vec::with_capacity(self.clsags.len()); + let clsags = self + .clsags + .drain(..) + .map(|(clsag_mask_send, clsag)| { + let (clsag, preprocess) = clsag.preprocess(rng); + preprocesses.push(preprocess); + (clsag_mask_send, clsag) + }) + .collect(); + let our_preprocess = preprocesses.clone(); + + ( + TransactionSignMachine { + signable: self.signable, + + i: self.i, + + key_image_generators_and_offsets: self.key_image_generators_and_offsets, + clsags, + + our_preprocess, + }, + preprocesses, + ) + } +} + +impl SignMachine for TransactionSignMachine { + type Params = (); + type Keys = ThresholdKeys; + type Preprocess = Vec>; + type SignatureShare = Vec>; + type SignatureMachine = TransactionSignatureMachine; + + fn cache(self) -> CachedPreprocess { + unimplemented!( + "Monero transactions don't support caching their preprocesses due to {}", + "being already bound to a specific transaction" + ); + } + + fn from_cache( + (): (), + _: ThresholdKeys, + _: CachedPreprocess, + ) -> (Self, Self::Preprocess) { + unimplemented!( + "Monero transactions don't support caching their preprocesses due to {}", + "being already bound to a specific transaction" + ); + } + + fn read_preprocess(&self, reader: &mut R) -> io::Result { + self.clsags.iter().map(|clsag| clsag.1.read_preprocess(reader)).collect() + } + + fn sign( + self, + mut commitments: HashMap, + msg: &[u8], + ) -> Result<(TransactionSignatureMachine, Self::SignatureShare), FrostError> { + if !msg.is_empty() { + panic!("message was passed to the TransactionMachine when it generates its own"); + } + + // We do not need to be included here, yet this set of signers has yet to be validated + // We explicitly remove ourselves to ensure we aren't included twice, if we were redundantly + // included + commitments.remove(&self.i); + + // Find out who's included + let mut included = commitments.keys().copied().collect::>(); + // This push won't duplicate due to the above removal + included.push(self.i); + // unstable sort may reorder elements of equal order + // Given our lack of duplicates, we should have no elements of equal order + included.sort_unstable(); + + // Start calculating the key images, as needed on the TX level + let mut key_images = vec![EdwardsPoint::identity(); self.clsags.len()]; + for (image, (generator, offset)) in + key_images.iter_mut().zip(&self.key_image_generators_and_offsets) + { + *image = generator * offset; + } + + // Convert the serialized nonces commitments to a parallelized Vec + let mut commitments = (0 .. self.clsags.len()) + .map(|c| { + included + .iter() + .map(|l| { + let preprocess = if *l == self.i { + self.our_preprocess[c].clone() + } else { + commitments.get_mut(l).ok_or(FrostError::MissingParticipant(*l))?[c].clone() + }; + + // While here, calculate the key image as needed to call sign + // The CLSAG algorithm will independently calculate the key image/verify these shares + key_images[c] += + preprocess.addendum.key_image_share().0 * lagrange::(*l, &included).0; + + Ok((*l, preprocess)) + }) + .collect::, _>>() + }) + .collect::, _>>()?; + + // The above inserted our own preprocess into these maps (which is unnecessary) + // Remove it now + for map in &mut commitments { + map.remove(&self.i); + } + + // The actual TX will have sorted its inputs by key image + // We apply the same sort now to our CLSAG machines + let mut clsags = Vec::with_capacity(self.clsags.len()); + for ((key_image, clsag), commitments) in key_images.iter().zip(self.clsags).zip(commitments) { + clsags.push((key_image, clsag, commitments)); + } + clsags.sort_by(|x, y| key_image_sort(x.0, y.0)); + let clsags = + clsags.into_iter().map(|(_, clsag, commitments)| (clsag, commitments)).collect::>(); + + // Specify the TX's key images + let tx = self.signable.with_key_images(key_images); + + // We now need to decide the masks for each CLSAG + let clsag_len = clsags.len(); + let output_masks = tx.intent.sum_output_masks(&tx.key_images); + let mut rng = tx.intent.seeded_rng(b"multisig_pseudo_out_masks"); + let mut sum_pseudo_outs = Scalar::ZERO; + let mut to_sign = Vec::with_capacity(clsag_len); + for (i, ((clsag_mask_send, clsag), commitments)) in clsags.into_iter().enumerate() { + let mut mask = Scalar::random(&mut rng); + if i == (clsag_len - 1) { + mask = output_masks - sum_pseudo_outs; + } else { + sum_pseudo_outs += mask; + } + clsag_mask_send.send(mask); + to_sign.push((clsag, commitments)); + } + + let tx = tx.transaction_without_signatures(); + let msg = tx.signature_hash().unwrap(); + + // Iterate over each CLSAG calling sign + let mut shares = Vec::with_capacity(to_sign.len()); + let clsags = to_sign + .drain(..) + .map(|(clsag, commitments)| { + let (clsag, share) = clsag.sign(commitments, &msg)?; + shares.push(share); + Ok(clsag) + }) + .collect::>()?; + + Ok((TransactionSignatureMachine { tx, clsags }, shares)) + } +} + +impl SignatureMachine for TransactionSignatureMachine { + type SignatureShare = Vec>; + + fn read_share(&self, reader: &mut R) -> io::Result { + self.clsags.iter().map(|clsag| clsag.read_share(reader)).collect() + } + + fn complete( + mut self, + shares: HashMap, + ) -> Result { + let mut tx = self.tx; + match tx { + Transaction::V2 { + proofs: + Some(RctProofs { + prunable: RctPrunable::Clsag { ref mut clsags, ref mut pseudo_outs, .. }, + .. + }), + .. + } => { + for (c, clsag) in self.clsags.drain(..).enumerate() { + let (clsag, pseudo_out) = clsag.complete( + shares.iter().map(|(l, shares)| (*l, shares[c].clone())).collect::>(), + )?; + clsags.push(clsag); + pseudo_outs.push(pseudo_out); + } + } + _ => unreachable!("attempted to sign a multisig TX which wasn't CLSAG"), + } + Ok(tx) + } +} diff --git a/networks/monero/wallet/src/send/tx.rs b/networks/monero/wallet/src/send/tx.rs new file mode 100644 index 00000000..65962211 --- /dev/null +++ b/networks/monero/wallet/src/send/tx.rs @@ -0,0 +1,323 @@ +use std_shims::{vec, vec::Vec}; + +use curve25519_dalek::{ + constants::{ED25519_BASEPOINT_POINT, ED25519_BASEPOINT_TABLE}, + Scalar, EdwardsPoint, +}; + +use crate::{ + io::{varint_len, write_varint}, + primitives::Commitment, + ringct::{ + clsag::Clsag, bulletproofs::Bulletproof, EncryptedAmount, RctType, RctBase, RctPrunable, + RctProofs, + }, + transaction::{Input, Output, Timelock, TransactionPrefix, Transaction}, + extra::{ARBITRARY_DATA_MARKER, PaymentId, Extra}, + send::{InternalPayment, SignableTransaction, SignableTransactionWithKeyImages}, +}; + +impl SignableTransaction { + // Output the inputs for this transaction. + pub(crate) fn inputs(&self, key_images: &[EdwardsPoint]) -> Vec { + debug_assert_eq!(self.inputs.len(), key_images.len()); + + let mut res = Vec::with_capacity(self.inputs.len()); + for (input, key_image) in self.inputs.iter().zip(key_images) { + res.push(Input::ToKey { + amount: None, + key_offsets: input.decoys().offsets().to_vec(), + key_image: *key_image, + }); + } + res + } + + // Output the outputs for this transaction. + pub(crate) fn outputs(&self, key_images: &[EdwardsPoint]) -> Vec { + let shared_key_derivations = self.shared_key_derivations(key_images); + debug_assert_eq!(self.payments.len(), shared_key_derivations.len()); + + let mut res = Vec::with_capacity(self.payments.len()); + for (payment, shared_key_derivations) in self.payments.iter().zip(&shared_key_derivations) { + let key = + (&shared_key_derivations.shared_key * ED25519_BASEPOINT_TABLE) + payment.address().spend(); + res.push(Output { + key: key.compress(), + amount: None, + view_tag: (match self.rct_type { + RctType::ClsagBulletproof => false, + RctType::ClsagBulletproofPlus => true, + _ => panic!("unsupported RctType"), + }) + .then_some(shared_key_derivations.view_tag), + }); + } + res + } + + // Calculate the TX extra for this transaction. + pub(crate) fn extra(&self) -> Vec { + let (tx_key, additional_keys) = self.transaction_keys_pub(); + debug_assert!(additional_keys.is_empty() || (additional_keys.len() == self.payments.len())); + let payment_id_xors = self.payment_id_xors(); + debug_assert_eq!(self.payments.len(), payment_id_xors.len()); + + let amount_of_keys = 1 + additional_keys.len(); + let mut extra = Extra::new(tx_key, additional_keys); + + if let Some((id, id_xor)) = + self.payments.iter().zip(&payment_id_xors).find_map(|(payment, payment_id_xor)| { + payment.address().payment_id().map(|id| (id, payment_id_xor)) + }) + { + let id = (u64::from_le_bytes(id) ^ u64::from_le_bytes(*id_xor)).to_le_bytes(); + let mut id_vec = Vec::with_capacity(1 + 8); + PaymentId::Encrypted(id).write(&mut id_vec).unwrap(); + extra.push_nonce(id_vec); + } else { + // If there's no payment ID, we push a dummy (as wallet2 does) if there's only one payment + if (self.payments.len() == 2) && + self.payments.iter().any(|payment| matches!(payment, InternalPayment::Change(_))) + { + let (_, payment_id_xor) = self + .payments + .iter() + .zip(&payment_id_xors) + .find(|(payment, _)| matches!(payment, InternalPayment::Payment(_, _))) + .expect("multiple change outputs?"); + let mut id_vec = Vec::with_capacity(1 + 8); + // The dummy payment ID is [0; 8], which when xor'd with the mask, is just the mask + PaymentId::Encrypted(*payment_id_xor).write(&mut id_vec).unwrap(); + extra.push_nonce(id_vec); + } + } + + // Include data if present + for part in &self.data { + let mut arb = vec![ARBITRARY_DATA_MARKER]; + arb.extend(part); + extra.push_nonce(arb); + } + + let mut serialized = Vec::with_capacity(32 * amount_of_keys); + extra.write(&mut serialized).unwrap(); + serialized + } + + pub(crate) fn weight_and_necessary_fee(&self) -> (usize, u64) { + /* + This transaction is variable length to: + - The decoy offsets (fixed) + - The TX extra (variable to key images, requiring an interactive protocol) + + Thankfully, the TX extra *length* is fixed. Accordingly, we can calculate the inevitable TX's + weight at this time with a shimmed transaction. + */ + let base_weight = { + let mut key_images = Vec::with_capacity(self.inputs.len()); + let mut clsags = Vec::with_capacity(self.inputs.len()); + let mut pseudo_outs = Vec::with_capacity(self.inputs.len()); + for _ in &self.inputs { + key_images.push(ED25519_BASEPOINT_POINT); + clsags.push(Clsag { + D: ED25519_BASEPOINT_POINT, + s: vec![ + Scalar::ZERO; + match self.rct_type { + RctType::ClsagBulletproof => 11, + RctType::ClsagBulletproofPlus => 16, + _ => unreachable!("unsupported RCT type"), + } + ], + c1: Scalar::ZERO, + }); + pseudo_outs.push(ED25519_BASEPOINT_POINT); + } + let mut encrypted_amounts = Vec::with_capacity(self.payments.len()); + let mut bp_commitments = Vec::with_capacity(self.payments.len()); + let mut commitments = Vec::with_capacity(self.payments.len()); + for _ in &self.payments { + encrypted_amounts.push(EncryptedAmount::Compact { amount: [0; 8] }); + bp_commitments.push(Commitment::zero()); + commitments.push(ED25519_BASEPOINT_POINT); + } + + let padded_log2 = { + let mut log2_find = 0; + while (1 << log2_find) < self.payments.len() { + log2_find += 1; + } + log2_find + }; + // This is log2 the padded amount of IPA rows + // We have 64 rows per commitment, so we need 64 * c IPA rows + // We rewrite this as 2**6 * c + // By finding the padded log2 of c, we get 2**6 * 2**p + // This declares the log2 to be 6 + p + let lr_len = 6 + padded_log2; + + let bulletproof = match self.rct_type { + RctType::ClsagBulletproof => { + let mut bp = Vec::with_capacity(((9 + (2 * lr_len)) * 32) + 2); + let push_point = |bp: &mut Vec| { + bp.push(1); + bp.extend([0; 31]); + }; + let push_scalar = |bp: &mut Vec| bp.extend([0; 32]); + for _ in 0 .. 4 { + push_point(&mut bp); + } + for _ in 0 .. 2 { + push_scalar(&mut bp); + } + for _ in 0 .. 2 { + write_varint(&lr_len, &mut bp).unwrap(); + for _ in 0 .. lr_len { + push_point(&mut bp); + } + } + for _ in 0 .. 3 { + push_scalar(&mut bp); + } + Bulletproof::read(&mut bp.as_slice()).expect("made an invalid dummy BP") + } + RctType::ClsagBulletproofPlus => { + let mut bp = Vec::with_capacity(((6 + (2 * lr_len)) * 32) + 2); + let push_point = |bp: &mut Vec| { + bp.push(1); + bp.extend([0; 31]); + }; + let push_scalar = |bp: &mut Vec| bp.extend([0; 32]); + for _ in 0 .. 3 { + push_point(&mut bp); + } + for _ in 0 .. 3 { + push_scalar(&mut bp); + } + for _ in 0 .. 2 { + write_varint(&lr_len, &mut bp).unwrap(); + for _ in 0 .. lr_len { + push_point(&mut bp); + } + } + Bulletproof::read_plus(&mut bp.as_slice()).expect("made an invalid dummy BP+") + } + _ => panic!("unsupported RctType"), + }; + + // `- 1` to remove the one byte for the 0 fee + Transaction::V2 { + prefix: TransactionPrefix { + additional_timelock: Timelock::None, + inputs: self.inputs(&key_images), + outputs: self.outputs(&key_images), + extra: self.extra(), + }, + proofs: Some(RctProofs { + base: RctBase { fee: 0, encrypted_amounts, pseudo_outs: vec![], commitments }, + prunable: RctPrunable::Clsag { bulletproof, clsags, pseudo_outs }, + }), + } + .weight() - + 1 + }; + + // We now have the base weight, without the fee encoded + // The fee itself will impact the weight as its encoding is [1, 9] bytes long + let mut possible_weights = Vec::with_capacity(9); + for i in 1 ..= 9 { + possible_weights.push(base_weight + i); + } + debug_assert_eq!(possible_weights.len(), 9); + + // We now calculate the fee which would be used for each weight + let mut possible_fees = Vec::with_capacity(9); + for weight in possible_weights { + possible_fees.push(self.fee_rate.calculate_fee_from_weight(weight)); + } + + // We now look for the fee whose length matches the length used to derive it + let mut weight_and_fee = None; + for (fee_len, possible_fee) in possible_fees.into_iter().enumerate() { + let fee_len = 1 + fee_len; + debug_assert!(1 <= fee_len); + debug_assert!(fee_len <= 9); + + // We use the first fee whose encoded length is not larger than the length used within this + // weight + // This should be because the lengths are equal, yet means if somehow none are equal, this + // will still terminate successfully + if varint_len(possible_fee) <= fee_len { + weight_and_fee = Some((base_weight + fee_len, possible_fee)); + break; + } + } + weight_and_fee.unwrap() + } +} + +impl SignableTransactionWithKeyImages { + pub(crate) fn transaction_without_signatures(&self) -> Transaction { + let commitments_and_encrypted_amounts = + self.intent.commitments_and_encrypted_amounts(&self.key_images); + let mut commitments = Vec::with_capacity(self.intent.payments.len()); + let mut bp_commitments = Vec::with_capacity(self.intent.payments.len()); + let mut encrypted_amounts = Vec::with_capacity(self.intent.payments.len()); + for (commitment, encrypted_amount) in commitments_and_encrypted_amounts { + commitments.push(commitment.calculate()); + bp_commitments.push(commitment); + encrypted_amounts.push(encrypted_amount); + } + let bulletproof = { + let mut bp_rng = self.intent.seeded_rng(b"bulletproof"); + (match self.intent.rct_type { + RctType::ClsagBulletproof => Bulletproof::prove(&mut bp_rng, bp_commitments), + RctType::ClsagBulletproofPlus => Bulletproof::prove_plus(&mut bp_rng, bp_commitments), + _ => panic!("unsupported RctType"), + }) + .expect("couldn't prove BP(+)s for this many payments despite checking in constructor?") + }; + + Transaction::V2 { + prefix: TransactionPrefix { + additional_timelock: Timelock::None, + inputs: self.intent.inputs(&self.key_images), + outputs: self.intent.outputs(&self.key_images), + extra: self.intent.extra(), + }, + proofs: Some(RctProofs { + base: RctBase { + fee: if self + .intent + .payments + .iter() + .any(|payment| matches!(payment, InternalPayment::Change(_))) + { + // The necessary fee is the fee + self.intent.weight_and_necessary_fee().1 + } else { + // If we don't have a change output, the difference is the fee + let inputs = + self.intent.inputs.iter().map(|input| input.commitment().amount).sum::(); + let payments = self + .intent + .payments + .iter() + .filter_map(|payment| match payment { + InternalPayment::Payment(_, amount) => Some(amount), + InternalPayment::Change(_) => None, + }) + .sum::(); + // Safe since the constructor checks inputs >= (payments + fee) + inputs - payments + }, + encrypted_amounts, + pseudo_outs: vec![], + commitments, + }, + prunable: RctPrunable::Clsag { bulletproof, clsags: vec![], pseudo_outs: vec![] }, + }), + } + } +} diff --git a/networks/monero/wallet/src/send/tx_keys.rs b/networks/monero/wallet/src/send/tx_keys.rs new file mode 100644 index 00000000..7edc9ffd --- /dev/null +++ b/networks/monero/wallet/src/send/tx_keys.rs @@ -0,0 +1,246 @@ +use core::ops::Deref; +use std_shims::{vec, vec::Vec}; + +use zeroize::Zeroizing; + +use rand_core::SeedableRng; +use rand_chacha::ChaCha20Rng; + +use curve25519_dalek::{constants::ED25519_BASEPOINT_TABLE, Scalar, EdwardsPoint}; + +use crate::{ + primitives::{keccak256, Commitment}, + ringct::EncryptedAmount, + SharedKeyDerivations, OutputWithDecoys, + send::{ChangeEnum, InternalPayment, SignableTransaction, key_image_sort}, +}; + +impl SignableTransaction { + pub(crate) fn seeded_rng(&self, dst: &'static [u8]) -> ChaCha20Rng { + // Apply the DST + let mut transcript = Zeroizing::new(vec![u8::try_from(dst.len()).unwrap()]); + transcript.extend(dst); + + // Bind to the outgoing view key to prevent foreign entities from rebuilding the transcript + transcript.extend(self.outgoing_view_key.as_slice()); + + // Ensure uniqueness across transactions by binding to a use-once object + // The keys for the inputs is binding to their key images, making them use-once + let mut input_keys = self.inputs.iter().map(OutputWithDecoys::key).collect::>(); + // We sort the inputs mid-way through TX construction, so apply our own sort to ensure a + // consistent order + // We use the key image sort as it's applicable and well-defined, not because these are key + // images + input_keys.sort_by(key_image_sort); + for key in input_keys { + transcript.extend(key.compress().to_bytes()); + } + + ChaCha20Rng::from_seed(keccak256(&transcript)) + } + + fn has_payments_to_subaddresses(&self) -> bool { + self.payments.iter().any(|payment| match payment { + InternalPayment::Payment(addr, _) => addr.is_subaddress(), + InternalPayment::Change(change) => match change { + ChangeEnum::AddressOnly(addr) => addr.is_subaddress(), + // These aren't considered payments to subaddresses as we don't need to send to them as + // subaddresses + // We can calculate the shared key using the view key, as if we were receiving, instead + ChangeEnum::Standard { .. } | ChangeEnum::Guaranteed { .. } => false, + }, + }) + } + + fn should_use_additional_keys(&self) -> bool { + let has_payments_to_subaddresses = self.has_payments_to_subaddresses(); + if !has_payments_to_subaddresses { + return false; + } + + let has_change_view = self.payments.iter().any(|payment| match payment { + InternalPayment::Payment(_, _) => false, + InternalPayment::Change(change) => match change { + ChangeEnum::AddressOnly(_) => false, + ChangeEnum::Standard { .. } | ChangeEnum::Guaranteed { .. } => true, + }, + }); + + /* + If sending to a subaddress, the shared key is not `rG` yet `rB`. Because of this, a + per-subaddress shared key is necessary, causing the usage of additional keys. + + The one exception is if we're sending to a subaddress in a 2-output transaction. The second + output, the change output, will attempt scanning the singular key `rB` with `v rB`. While we + cannot calculate `r vB` with just `r` (as that'd require `vB` when we presumably only have + `vG` when sending), since we do in fact have `v` (due to it being our own view key for our + change output), we can still calculate the shared secret. + */ + has_payments_to_subaddresses && !((self.payments.len() == 2) && has_change_view) + } + + // Calculate the transaction keys used as randomness. + fn transaction_keys(&self) -> (Zeroizing, Vec>) { + let mut rng = self.seeded_rng(b"transaction_keys"); + + let tx_key = Zeroizing::new(Scalar::random(&mut rng)); + + let mut additional_keys = vec![]; + if self.should_use_additional_keys() { + for _ in 0 .. self.payments.len() { + additional_keys.push(Zeroizing::new(Scalar::random(&mut rng))); + } + } + (tx_key, additional_keys) + } + + fn ecdhs(&self) -> Vec> { + let (tx_key, additional_keys) = self.transaction_keys(); + debug_assert!(additional_keys.is_empty() || (additional_keys.len() == self.payments.len())); + let (tx_key_pub, additional_keys_pub) = self.transaction_keys_pub(); + debug_assert_eq!(additional_keys_pub.len(), additional_keys.len()); + + let mut res = Vec::with_capacity(self.payments.len()); + for (i, payment) in self.payments.iter().enumerate() { + let addr = payment.address(); + let key_to_use = + if addr.is_subaddress() { additional_keys.get(i).unwrap_or(&tx_key) } else { &tx_key }; + + let ecdh = match payment { + // If we don't have the view key, use the key dedicated for this address (r A) + InternalPayment::Payment(_, _) | + InternalPayment::Change(ChangeEnum::AddressOnly { .. }) => { + Zeroizing::new(key_to_use.deref() * addr.view()) + } + // If we do have the view key, use the commitment to the key (a R) + InternalPayment::Change(ChangeEnum::Standard { view_pair, .. }) => { + Zeroizing::new(view_pair.view.deref() * tx_key_pub) + } + InternalPayment::Change(ChangeEnum::Guaranteed { view_pair, .. }) => { + Zeroizing::new(view_pair.0.view.deref() * tx_key_pub) + } + }; + + res.push(ecdh); + } + res + } + + // Calculate the shared keys and the necessary derivations. + pub(crate) fn shared_key_derivations( + &self, + key_images: &[EdwardsPoint], + ) -> Vec> { + let ecdhs = self.ecdhs(); + + let uniqueness = SharedKeyDerivations::uniqueness(&self.inputs(key_images)); + + let mut res = Vec::with_capacity(self.payments.len()); + for (i, (payment, ecdh)) in self.payments.iter().zip(ecdhs).enumerate() { + let addr = payment.address(); + res.push(SharedKeyDerivations::output_derivations( + addr.is_guaranteed().then_some(uniqueness), + ecdh, + i, + )); + } + res + } + + // Calculate the payment ID XOR masks. + pub(crate) fn payment_id_xors(&self) -> Vec<[u8; 8]> { + let mut res = Vec::with_capacity(self.payments.len()); + for ecdh in self.ecdhs() { + res.push(SharedKeyDerivations::payment_id_xor(ecdh)); + } + res + } + + // Calculate the transaction_keys' commitments. + // + // These depend on the payments. Commitments for payments to subaddresses use the spend key for + // the generator. + pub(crate) fn transaction_keys_pub(&self) -> (EdwardsPoint, Vec) { + let (tx_key, additional_keys) = self.transaction_keys(); + debug_assert!(additional_keys.is_empty() || (additional_keys.len() == self.payments.len())); + + // The single transaction key uses the subaddress's spend key as its generator + let has_payments_to_subaddresses = self.has_payments_to_subaddresses(); + let should_use_additional_keys = self.should_use_additional_keys(); + if has_payments_to_subaddresses && (!should_use_additional_keys) { + debug_assert_eq!(additional_keys.len(), 0); + + let InternalPayment::Payment(addr, _) = self + .payments + .iter() + .find(|payment| matches!(payment, InternalPayment::Payment(_, _))) + .expect("payment to subaddress yet no payment") + else { + panic!("filtered payment wasn't a payment") + }; + + return (tx_key.deref() * addr.spend(), vec![]); + } + + if should_use_additional_keys { + let mut additional_keys_pub = vec![]; + for (additional_key, payment) in additional_keys.into_iter().zip(&self.payments) { + let addr = payment.address(); + // https://github.com/monero-project/monero/blob/cc73fe71162d564ffda8e549b79a350bca53c454 + // /src/device/device_default.cpp#L308-L312 + if addr.is_subaddress() { + additional_keys_pub.push(additional_key.deref() * addr.spend()); + } else { + additional_keys_pub.push(additional_key.deref() * ED25519_BASEPOINT_TABLE) + } + } + return (tx_key.deref() * ED25519_BASEPOINT_TABLE, additional_keys_pub); + } + + debug_assert!(!has_payments_to_subaddresses); + debug_assert!(!should_use_additional_keys); + (tx_key.deref() * ED25519_BASEPOINT_TABLE, vec![]) + } + + pub(crate) fn commitments_and_encrypted_amounts( + &self, + key_images: &[EdwardsPoint], + ) -> Vec<(Commitment, EncryptedAmount)> { + let shared_key_derivations = self.shared_key_derivations(key_images); + + let mut res = Vec::with_capacity(self.payments.len()); + for (payment, shared_key_derivations) in self.payments.iter().zip(shared_key_derivations) { + let amount = match payment { + InternalPayment::Payment(_, amount) => *amount, + InternalPayment::Change(_) => { + let inputs = self.inputs.iter().map(|input| input.commitment().amount).sum::(); + let payments = self + .payments + .iter() + .filter_map(|payment| match payment { + InternalPayment::Payment(_, amount) => Some(amount), + InternalPayment::Change(_) => None, + }) + .sum::(); + let necessary_fee = self.weight_and_necessary_fee().1; + // Safe since the constructor checked this TX has enough funds for itself + inputs - (payments + necessary_fee) + } + }; + let commitment = Commitment::new(shared_key_derivations.commitment_mask(), amount); + let encrypted_amount = EncryptedAmount::Compact { + amount: shared_key_derivations.compact_amount_encryption(amount), + }; + res.push((commitment, encrypted_amount)); + } + res + } + + pub(crate) fn sum_output_masks(&self, key_images: &[EdwardsPoint]) -> Scalar { + self + .commitments_and_encrypted_amounts(key_images) + .into_iter() + .map(|(commitment, _)| commitment.mask) + .sum() + } +} diff --git a/coins/monero/src/tests/extra.rs b/networks/monero/wallet/src/tests/extra.rs similarity index 56% rename from coins/monero/src/tests/extra.rs rename to networks/monero/wallet/src/tests/extra.rs index b727fe9d..2f331e61 100644 --- a/coins/monero/src/tests/extra.rs +++ b/networks/monero/wallet/src/tests/extra.rs @@ -1,13 +1,57 @@ -use crate::{ - wallet::{ExtraField, Extra, extra::MAX_TX_EXTRA_PADDING_COUNT}, - serialize::write_varint, -}; - use curve25519_dalek::edwards::{EdwardsPoint, CompressedEdwardsY}; -// Borrowed tests from +use crate::{ + io::write_varint, + extra::{MAX_TX_EXTRA_PADDING_COUNT, ExtraField, Extra}, +}; + +// Tests derived from // https://github.com/monero-project/monero/blob/ac02af92867590ca80b2779a7bbeafa99ff94dcb/ // tests/unit_tests/test_tx_utils.cpp +// which is licensed +#[rustfmt::skip] +/* +Copyright (c) 2014-2022, The Monero Project + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors +may be used to endorse or promote products derived from this software without +specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Parts of the project are originally copyright (c) 2012-2013 The Cryptonote +developers + +Parts of the project are originally copyright (c) 2014 The Boolberry +developers, distributed under the MIT licence: + + 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. +*/ const PUB_KEY_BYTES: [u8; 33] = [ 1, 30, 208, 98, 162, 133, 64, 85, 83, 112, 91, 188, 89, 211, 24, 131, 39, 154, 22, 228, 80, 63, @@ -61,15 +105,13 @@ fn padding_only_max_size() { #[test] fn padding_only_exceed_max_size() { let buf: Vec = vec![0; MAX_TX_EXTRA_PADDING_COUNT + 1]; - let extra = Extra::read::<&[u8]>(&mut buf.as_ref()).unwrap(); - assert!(extra.0.is_empty()); + Extra::read::<&[u8]>(&mut buf.as_ref()).unwrap_err(); } #[test] fn invalid_padding_only() { let buf: Vec = vec![0, 42]; - let extra = Extra::read::<&[u8]>(&mut buf.as_ref()).unwrap(); - assert!(extra.0.is_empty()); + Extra::read::<&[u8]>(&mut buf.as_ref()).unwrap_err(); } #[test] @@ -93,8 +135,7 @@ fn extra_nonce_only_wrong_size() { let mut buf: Vec = vec![0; 20]; buf[0] = 2; buf[1] = 255; - let extra = Extra::read::<&[u8]>(&mut buf.as_ref()).unwrap(); - assert!(extra.0.is_empty()); + Extra::read::<&[u8]>(&mut buf.as_ref()).unwrap_err(); } #[test] @@ -114,8 +155,7 @@ fn pub_key_and_padding() { fn pub_key_and_invalid_padding() { let mut buf: Vec = PUB_KEY_BYTES.to_vec(); buf.extend([0, 1]); - let extra = Extra::read::<&[u8]>(&mut buf.as_ref()).unwrap(); - assert_eq!(extra.0, vec![ExtraField::PublicKey(pub_key())]); + Extra::read::<&[u8]>(&mut buf.as_ref()).unwrap_err(); } #[test] @@ -141,8 +181,7 @@ fn extra_mysterious_minergate_only_wrong_size() { let mut buf: Vec = vec![0; 20]; buf[0] = 222; buf[1] = 255; - let extra = Extra::read::<&[u8]>(&mut buf.as_ref()).unwrap(); - assert!(extra.0.is_empty()); + Extra::read::<&[u8]>(&mut buf.as_ref()).unwrap_err(); } #[test] diff --git a/networks/monero/wallet/src/tests/mod.rs b/networks/monero/wallet/src/tests/mod.rs new file mode 100644 index 00000000..d8fa7cb0 --- /dev/null +++ b/networks/monero/wallet/src/tests/mod.rs @@ -0,0 +1 @@ +mod extra; diff --git a/networks/monero/wallet/src/view_pair.rs b/networks/monero/wallet/src/view_pair.rs new file mode 100644 index 00000000..3b09f088 --- /dev/null +++ b/networks/monero/wallet/src/view_pair.rs @@ -0,0 +1,144 @@ +use core::ops::Deref; + +use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing}; + +use curve25519_dalek::{constants::ED25519_BASEPOINT_TABLE, Scalar, EdwardsPoint}; + +use crate::{ + primitives::keccak256_to_scalar, + address::{Network, AddressType, SubaddressIndex, MoneroAddress}, +}; + +/// An error while working with a ViewPair. +#[derive(Clone, PartialEq, Eq, Debug)] +#[cfg_attr(feature = "std", derive(thiserror::Error))] +pub enum ViewPairError { + /// The spend key was torsioned. + /// + /// Torsioned spend keys are of questionable spendability. This library avoids that question by + /// rejecting such ViewPairs. + // CLSAG seems to support it if the challenge does a torsion clear, FCMP++ should ship with a + // torsion clear, yet it's not worth it to modify CLSAG sign to generate challenges until the + // torsion clears and ensure spendability (nor can we reasonably guarantee that in the future) + #[cfg_attr(feature = "std", error("torsioned spend key"))] + TorsionedSpendKey, +} + +/// The pair of keys necessary to scan transactions. +/// +/// This is composed of the public spend key and the private view key. +#[derive(Clone, PartialEq, Eq, Zeroize, ZeroizeOnDrop)] +pub struct ViewPair { + spend: EdwardsPoint, + pub(crate) view: Zeroizing, +} + +impl ViewPair { + /// Create a new ViewPair. + pub fn new(spend: EdwardsPoint, view: Zeroizing) -> Result { + if !spend.is_torsion_free() { + Err(ViewPairError::TorsionedSpendKey)?; + } + Ok(ViewPair { spend, view }) + } + + /// The public spend key for this ViewPair. + pub fn spend(&self) -> EdwardsPoint { + self.spend + } + + /// The public view key for this ViewPair. + pub fn view(&self) -> EdwardsPoint { + self.view.deref() * ED25519_BASEPOINT_TABLE + } + + pub(crate) fn subaddress_derivation(&self, index: SubaddressIndex) -> Scalar { + keccak256_to_scalar(Zeroizing::new( + [ + b"SubAddr\0".as_ref(), + Zeroizing::new(self.view.to_bytes()).as_ref(), + &index.account().to_le_bytes(), + &index.address().to_le_bytes(), + ] + .concat(), + )) + } + + pub(crate) fn subaddress_keys(&self, index: SubaddressIndex) -> (EdwardsPoint, EdwardsPoint) { + let scalar = self.subaddress_derivation(index); + let spend = self.spend + (&scalar * ED25519_BASEPOINT_TABLE); + let view = self.view.deref() * spend; + (spend, view) + } + + /// Derive a legacy address from this ViewPair. + /// + /// Subaddresses SHOULD be used instead. + pub fn legacy_address(&self, network: Network) -> MoneroAddress { + MoneroAddress::new(network, AddressType::Legacy, self.spend, self.view()) + } + + /// Derive a legacy integrated address from this ViewPair. + /// + /// Subaddresses SHOULD be used instead. + pub fn legacy_integrated_address(&self, network: Network, payment_id: [u8; 8]) -> MoneroAddress { + MoneroAddress::new(network, AddressType::LegacyIntegrated(payment_id), self.spend, self.view()) + } + + /// Derive a subaddress from this ViewPair. + pub fn subaddress(&self, network: Network, subaddress: SubaddressIndex) -> MoneroAddress { + let (spend, view) = self.subaddress_keys(subaddress); + MoneroAddress::new(network, AddressType::Subaddress, spend, view) + } +} + +/// The pair of keys necessary to scan outputs immune to the burning bug. +/// +/// This is composed of the public spend key and a non-zero private view key. +/// +/// 'Guaranteed' outputs, or transactions outputs to the burning bug, are not officially specified +/// by the Monero project. They should only be used if necessary. No support outside of +/// monero-wallet is promised. +#[derive(Clone, PartialEq, Eq, Zeroize)] +pub struct GuaranteedViewPair(pub(crate) ViewPair); + +impl GuaranteedViewPair { + /// Create a new GuaranteedViewPair. + pub fn new(spend: EdwardsPoint, view: Zeroizing) -> Result { + ViewPair::new(spend, view).map(GuaranteedViewPair) + } + + /// The public spend key for this GuaranteedViewPair. + pub fn spend(&self) -> EdwardsPoint { + self.0.spend() + } + + /// The public view key for this GuaranteedViewPair. + pub fn view(&self) -> EdwardsPoint { + self.0.view() + } + + /// Returns an address with the provided specification. + /// + /// The returned address will be a featured address with the guaranteed flag set. These should + /// not be presumed to be interoperable with any other software. + pub fn address( + &self, + network: Network, + subaddress: Option, + payment_id: Option<[u8; 8]>, + ) -> MoneroAddress { + let (spend, view) = if let Some(index) = subaddress { + self.0.subaddress_keys(index) + } else { + (self.spend(), self.view()) + }; + + MoneroAddress::new( + network, + AddressType::Featured { subaddress: subaddress.is_some(), payment_id, guaranteed: true }, + spend, + view, + ) + } +} diff --git a/coins/monero/tests/add_data.rs b/networks/monero/wallet/tests/add_data.rs similarity index 63% rename from coins/monero/tests/add_data.rs rename to networks/monero/wallet/tests/add_data.rs index ab45177b..6aa57dbc 100644 --- a/coins/monero/tests/add_data.rs +++ b/networks/monero/wallet/tests/add_data.rs @@ -1,7 +1,5 @@ -use monero_serai::{ - transaction::Transaction, - wallet::{TransactionError, extra::MAX_ARBITRARY_DATA_SIZE}, -}; +use monero_serai::transaction::Transaction; +use monero_wallet::{rpc::Rpc, extra::MAX_ARBITRARY_DATA_SIZE, send::SendError}; mod runner; @@ -17,8 +15,10 @@ test!( builder.add_payment(addr, 5); (builder.build().unwrap(), (arbitrary_data,)) }, - |_, tx: Transaction, mut scanner: Scanner, data: (Vec,)| async move { - let output = scanner.scan_transaction(&tx).not_locked().swap_remove(0); + |rpc, block, tx: Transaction, mut scanner: Scanner, data: (Vec,)| async move { + let output = + scanner.scan(&rpc, &block).await.unwrap().not_additionally_locked().swap_remove(0); + assert_eq!(output.transaction(), tx.hash()); assert_eq!(output.commitment().amount, 5); assert_eq!(output.arbitrary_data()[0], data.0); }, @@ -42,8 +42,10 @@ test!( builder.add_payment(addr, 5); (builder.build().unwrap(), data) }, - |_, tx: Transaction, mut scanner: Scanner, data: Vec>| async move { - let output = scanner.scan_transaction(&tx).not_locked().swap_remove(0); + |rpc, block, tx: Transaction, mut scanner: Scanner, data: Vec>| async move { + let output = + scanner.scan(&rpc, &block).await.unwrap().not_additionally_locked().swap_remove(0); + assert_eq!(output.transaction(), tx.hash()); assert_eq!(output.commitment().amount, 5); assert_eq!(output.arbitrary_data(), data); }, @@ -58,7 +60,7 @@ test!( let mut data = vec![b'a'; MAX_ARBITRARY_DATA_SIZE + 1]; // Make sure we get an error if we try to add it to the TX - assert_eq!(builder.add_data(data.clone()), Err(TransactionError::TooMuchData)); + assert_eq!(builder.add_data(data.clone()), Err(SendError::TooMuchArbitraryData)); // Reduce data size and retry. The data will now be 255 bytes long (including the added // marker), exactly @@ -68,8 +70,10 @@ test!( builder.add_payment(addr, 5); (builder.build().unwrap(), data) }, - |_, tx: Transaction, mut scanner: Scanner, data: Vec| async move { - let output = scanner.scan_transaction(&tx).not_locked().swap_remove(0); + |rpc, block, tx: Transaction, mut scanner: Scanner, data: Vec| async move { + let output = + scanner.scan(&rpc, &block).await.unwrap().not_additionally_locked().swap_remove(0); + assert_eq!(output.transaction(), tx.hash()); assert_eq!(output.commitment().amount, 5); assert_eq!(output.arbitrary_data(), vec![data]); }, diff --git a/coins/monero/tests/decoys.rs b/networks/monero/wallet/tests/decoys.rs similarity index 51% rename from coins/monero/tests/decoys.rs rename to networks/monero/wallet/tests/decoys.rs index e85eab9d..6aaaeb07 100644 --- a/coins/monero/tests/decoys.rs +++ b/networks/monero/wallet/tests/decoys.rs @@ -1,8 +1,9 @@ -use monero_serai::{ +use monero_simple_request_rpc::SimpleRequestRpc; +use monero_wallet::{ + DEFAULT_LOCK_WINDOW, transaction::Transaction, - wallet::SpendableOutput, - rpc::{Rpc, OutputResponse}, - Protocol, DEFAULT_LOCK_WINDOW, + rpc::{Rpc, DecoyRpc}, + WalletOutput, }; mod runner; @@ -15,66 +16,67 @@ test!( builder.add_payment(addr, 2000000000000); (builder.build().unwrap(), ()) }, - |rpc: Rpc<_>, tx: Transaction, mut scanner: Scanner, ()| async move { - let output = scanner.scan_transaction(&tx).not_locked().swap_remove(0); + |rpc, block, tx: Transaction, mut scanner: Scanner, ()| async move { + let output = + scanner.scan(&rpc, &block).await.unwrap().not_additionally_locked().swap_remove(0); + assert_eq!(output.transaction(), tx.hash()); assert_eq!(output.commitment().amount, 2000000000000); - SpendableOutput::from(&rpc, output).await.unwrap() + output }, ), ( // Then make a second tx1 - |protocol: Protocol, rpc: Rpc<_>, mut builder: Builder, addr, state: _| async move { - let output_tx0: SpendableOutput = state; - let decoys = Decoys::fingerprintable_canonical_select( + |rct_type: RctType, rpc: SimpleRequestRpc, mut builder: Builder, addr, state: _| async move { + let output_tx0: WalletOutput = state; + + let input = OutputWithDecoys::fingerprintable_deterministic_new( &mut OsRng, &rpc, - protocol.ring_len(), + ring_len(rct_type), rpc.get_height().await.unwrap(), - &[output_tx0.clone()], + output_tx0.clone(), ) .await .unwrap(); - - let inputs = [output_tx0.clone()].into_iter().zip(decoys).collect::>(); - builder.add_inputs(&inputs); + builder.add_input(input); builder.add_payment(addr, 1000000000000); - (builder.build().unwrap(), (protocol, output_tx0)) + (builder.build().unwrap(), (rct_type, output_tx0)) }, // Then make sure DSA selects freshly unlocked output from tx1 as a decoy - |rpc: Rpc<_>, tx: Transaction, mut scanner: Scanner, state: (_, _)| async move { + |rpc, _, tx: Transaction, _: Scanner, state: (_, _)| async move { use rand_core::OsRng; + let rpc: SimpleRequestRpc = rpc; + let height = rpc.get_height().await.unwrap(); - let output_tx1 = - SpendableOutput::from(&rpc, scanner.scan_transaction(&tx).not_locked().swap_remove(0)) - .await - .unwrap(); + let most_recent_o_index = rpc.get_o_indexes(tx.hash()).await.unwrap().pop().unwrap(); // Make sure output from tx1 is in the block in which it unlocks - let out_tx1: OutputResponse = - rpc.get_outs(&[output_tx1.global_index]).await.unwrap().swap_remove(0); + let out_tx1 = rpc.get_outs(&[most_recent_o_index]).await.unwrap().swap_remove(0); assert_eq!(out_tx1.height, height - DEFAULT_LOCK_WINDOW); assert!(out_tx1.unlocked); // Select decoys using spendable output from tx0 as the real, and make sure DSA selects // the freshly unlocked output from tx1 as a decoy - let (protocol, output_tx0): (Protocol, SpendableOutput) = state; + let (rct_type, output_tx0): (RctType, WalletOutput) = state; let mut selected_fresh_decoy = false; let mut attempts = 1000; while !selected_fresh_decoy && attempts > 0 { - let decoys = Decoys::fingerprintable_canonical_select( + let decoys = OutputWithDecoys::fingerprintable_deterministic_new( &mut OsRng, // TODO: use a seeded RNG to consistently select the latest output &rpc, - protocol.ring_len(), + ring_len(rct_type), height, - &[output_tx0.clone()], + output_tx0.clone(), ) .await - .unwrap(); + .unwrap() + .decoys() + .clone(); - selected_fresh_decoy = decoys[0].indexes().contains(&output_tx1.global_index); + selected_fresh_decoy = decoys.positions().contains(&most_recent_o_index); attempts -= 1; } @@ -92,66 +94,67 @@ test!( builder.add_payment(addr, 2000000000000); (builder.build().unwrap(), ()) }, - |rpc: Rpc<_>, tx: Transaction, mut scanner: Scanner, ()| async move { - let output = scanner.scan_transaction(&tx).not_locked().swap_remove(0); + |rpc: SimpleRequestRpc, block, tx: Transaction, mut scanner: Scanner, ()| async move { + let output = + scanner.scan(&rpc, &block).await.unwrap().not_additionally_locked().swap_remove(0); + assert_eq!(output.transaction(), tx.hash()); assert_eq!(output.commitment().amount, 2000000000000); - SpendableOutput::from(&rpc, output).await.unwrap() + output }, ), ( // Then make a second tx1 - |protocol: Protocol, rpc: Rpc<_>, mut builder: Builder, addr, state: _| async move { - let output_tx0: SpendableOutput = state; - let decoys = Decoys::select( + |rct_type: RctType, rpc, mut builder: Builder, addr, output_tx0: WalletOutput| async move { + let rpc: SimpleRequestRpc = rpc; + + let input = OutputWithDecoys::new( &mut OsRng, &rpc, - protocol.ring_len(), + ring_len(rct_type), rpc.get_height().await.unwrap(), - &[output_tx0.clone()], + output_tx0.clone(), ) .await .unwrap(); - - let inputs = [output_tx0.clone()].into_iter().zip(decoys).collect::>(); - builder.add_inputs(&inputs); + builder.add_input(input); builder.add_payment(addr, 1000000000000); - (builder.build().unwrap(), (protocol, output_tx0)) + (builder.build().unwrap(), (rct_type, output_tx0)) }, // Then make sure DSA selects freshly unlocked output from tx1 as a decoy - |rpc: Rpc<_>, tx: Transaction, mut scanner: Scanner, state: (_, _)| async move { + |rpc, _, tx: Transaction, _: Scanner, state: (_, _)| async move { use rand_core::OsRng; + let rpc: SimpleRequestRpc = rpc; + let height = rpc.get_height().await.unwrap(); - let output_tx1 = - SpendableOutput::from(&rpc, scanner.scan_transaction(&tx).not_locked().swap_remove(0)) - .await - .unwrap(); + let most_recent_o_index = rpc.get_o_indexes(tx.hash()).await.unwrap().pop().unwrap(); // Make sure output from tx1 is in the block in which it unlocks - let out_tx1: OutputResponse = - rpc.get_outs(&[output_tx1.global_index]).await.unwrap().swap_remove(0); + let out_tx1 = rpc.get_outs(&[most_recent_o_index]).await.unwrap().swap_remove(0); assert_eq!(out_tx1.height, height - DEFAULT_LOCK_WINDOW); assert!(out_tx1.unlocked); // Select decoys using spendable output from tx0 as the real, and make sure DSA selects // the freshly unlocked output from tx1 as a decoy - let (protocol, output_tx0): (Protocol, SpendableOutput) = state; + let (rct_type, output_tx0): (RctType, WalletOutput) = state; let mut selected_fresh_decoy = false; let mut attempts = 1000; while !selected_fresh_decoy && attempts > 0 { - let decoys = Decoys::select( + let decoys = OutputWithDecoys::new( &mut OsRng, // TODO: use a seeded RNG to consistently select the latest output &rpc, - protocol.ring_len(), + ring_len(rct_type), height, - &[output_tx0.clone()], + output_tx0.clone(), ) .await - .unwrap(); + .unwrap() + .decoys() + .clone(); - selected_fresh_decoy = decoys[0].indexes().contains(&output_tx1.global_index); + selected_fresh_decoy = decoys.positions().contains(&most_recent_o_index); attempts -= 1; } diff --git a/coins/monero/tests/eventuality.rs b/networks/monero/wallet/tests/eventuality.rs similarity index 58% rename from coins/monero/tests/eventuality.rs rename to networks/monero/wallet/tests/eventuality.rs index dfbc6f0d..c9e1d9eb 100644 --- a/coins/monero/tests/eventuality.rs +++ b/networks/monero/wallet/tests/eventuality.rs @@ -1,11 +1,9 @@ use curve25519_dalek::constants::ED25519_BASEPOINT_POINT; -use monero_serai::{ - transaction::Transaction, - wallet::{ - Eventuality, - address::{AddressType, AddressMeta, MoneroAddress}, - }, +use monero_serai::transaction::Transaction; +use monero_wallet::{ + rpc::Rpc, + address::{AddressType, MoneroAddress}, }; mod runner; @@ -18,7 +16,8 @@ test!( // Each have their own slight implications to eventualities builder.add_payment( MoneroAddress::new( - AddressMeta::new(Network::Mainnet, AddressType::Standard), + Network::Mainnet, + AddressType::Legacy, ED25519_BASEPOINT_POINT, ED25519_BASEPOINT_POINT, ), @@ -26,7 +25,8 @@ test!( ); builder.add_payment( MoneroAddress::new( - AddressMeta::new(Network::Mainnet, AddressType::Integrated([0xaa; 8])), + Network::Mainnet, + AddressType::LegacyIntegrated([0xaa; 8]), ED25519_BASEPOINT_POINT, ED25519_BASEPOINT_POINT, ), @@ -34,7 +34,8 @@ test!( ); builder.add_payment( MoneroAddress::new( - AddressMeta::new(Network::Mainnet, AddressType::Subaddress), + Network::Mainnet, + AddressType::Subaddress, ED25519_BASEPOINT_POINT, ED25519_BASEPOINT_POINT, ), @@ -42,38 +43,38 @@ test!( ); builder.add_payment( MoneroAddress::new( - AddressMeta::new( - Network::Mainnet, - AddressType::Featured { subaddress: false, payment_id: None, guaranteed: true }, - ), + Network::Mainnet, + AddressType::Featured { subaddress: false, payment_id: None, guaranteed: true }, ED25519_BASEPOINT_POINT, ED25519_BASEPOINT_POINT, ), 4, ); - builder.set_r_seed(Zeroizing::new([0xbb; 32])); let tx = builder.build().unwrap(); - let eventuality = tx.eventuality().unwrap(); + let eventuality = Eventuality::from(tx.clone()); assert_eq!( eventuality, Eventuality::read::<&[u8]>(&mut eventuality.serialize().as_ref()).unwrap() ); (tx, eventuality) }, - |_, mut tx: Transaction, _, eventuality: Eventuality| async move { + |_, _, mut tx: Transaction, _, eventuality: Eventuality| async move { // 4 explicitly outputs added and one change output - assert_eq!(tx.prefix.outputs.len(), 5); + assert_eq!(tx.prefix().outputs.len(), 5); // The eventuality's available extra should be the actual TX's - assert_eq!(tx.prefix.extra, eventuality.extra()); + assert_eq!(tx.prefix().extra, eventuality.extra()); // The TX should match - assert!(eventuality.matches(&tx)); + assert!(eventuality.matches(&tx.clone().into())); // Mutate the TX - tx.rct_signatures.base.commitments[0] += ED25519_BASEPOINT_POINT; + let Transaction::V2 { proofs: Some(ref mut proofs), .. } = tx else { + panic!("TX wasn't RingCT") + }; + proofs.base.commitments[0] += ED25519_BASEPOINT_POINT; // Verify it no longer matches - assert!(!eventuality.matches(&tx)); + assert!(!eventuality.matches(&tx.clone().into())); }, ), ); diff --git a/networks/monero/wallet/tests/runner/builder.rs b/networks/monero/wallet/tests/runner/builder.rs new file mode 100644 index 00000000..7e2abe1e --- /dev/null +++ b/networks/monero/wallet/tests/runner/builder.rs @@ -0,0 +1,82 @@ +use zeroize::{Zeroize, Zeroizing}; + +use monero_wallet::{ + ringct::RctType, + rpc::FeeRate, + address::MoneroAddress, + OutputWithDecoys, + send::{Change, SendError, SignableTransaction}, + extra::MAX_ARBITRARY_DATA_SIZE, +}; + +/// A builder for Monero transactions. +#[derive(Clone, PartialEq, Eq, Zeroize, Debug)] +pub struct SignableTransactionBuilder { + rct_type: RctType, + outgoing_view_key: Zeroizing<[u8; 32]>, + inputs: Vec, + payments: Vec<(MoneroAddress, u64)>, + change: Change, + data: Vec>, + fee_rate: FeeRate, +} + +impl SignableTransactionBuilder { + pub fn new( + rct_type: RctType, + outgoing_view_key: Zeroizing<[u8; 32]>, + change: Change, + fee_rate: FeeRate, + ) -> Self { + Self { + rct_type, + outgoing_view_key, + inputs: vec![], + payments: vec![], + change, + data: vec![], + fee_rate, + } + } + + pub fn add_input(&mut self, input: OutputWithDecoys) -> &mut Self { + self.inputs.push(input); + self + } + #[allow(unused)] + pub fn add_inputs(&mut self, inputs: &[OutputWithDecoys]) -> &mut Self { + self.inputs.extend(inputs.iter().cloned()); + self + } + + pub fn add_payment(&mut self, dest: MoneroAddress, amount: u64) -> &mut Self { + self.payments.push((dest, amount)); + self + } + #[allow(unused)] + pub fn add_payments(&mut self, payments: &[(MoneroAddress, u64)]) -> &mut Self { + self.payments.extend(payments); + self + } + + #[allow(unused)] + pub fn add_data(&mut self, data: Vec) -> Result<&mut Self, SendError> { + if data.len() > MAX_ARBITRARY_DATA_SIZE { + Err(SendError::TooMuchArbitraryData)?; + } + self.data.push(data); + Ok(self) + } + + pub fn build(self) -> Result { + SignableTransaction::new( + self.rct_type, + self.outgoing_view_key, + self.inputs, + self.payments, + self.change, + self.data, + self.fee_rate, + ) + } +} diff --git a/networks/monero/wallet/tests/runner/mod.rs b/networks/monero/wallet/tests/runner/mod.rs new file mode 100644 index 00000000..7fe6ac53 --- /dev/null +++ b/networks/monero/wallet/tests/runner/mod.rs @@ -0,0 +1,355 @@ +use core::ops::Deref; +use std_shims::sync::OnceLock; + +use zeroize::Zeroizing; +use rand_core::OsRng; + +use curve25519_dalek::{constants::ED25519_BASEPOINT_TABLE, scalar::Scalar}; + +use tokio::sync::Mutex; + +use monero_simple_request_rpc::SimpleRequestRpc; +use monero_wallet::{ + ringct::RctType, + transaction::Transaction, + block::Block, + rpc::{Rpc, FeeRate}, + address::{Network, AddressType, MoneroAddress}, + DEFAULT_LOCK_WINDOW, ViewPair, GuaranteedViewPair, WalletOutput, Scanner, +}; + +mod builder; +pub use builder::SignableTransactionBuilder; + +pub fn ring_len(rct_type: RctType) -> usize { + match rct_type { + RctType::ClsagBulletproof => 11, + RctType::ClsagBulletproofPlus => 16, + _ => panic!("ring size unknown for RctType"), + } +} + +pub fn random_address() -> (Scalar, ViewPair, MoneroAddress) { + let spend = Scalar::random(&mut OsRng); + let spend_pub = &spend * ED25519_BASEPOINT_TABLE; + let view = Zeroizing::new(Scalar::random(&mut OsRng)); + ( + spend, + ViewPair::new(spend_pub, view.clone()).unwrap(), + MoneroAddress::new( + Network::Mainnet, + AddressType::Legacy, + spend_pub, + view.deref() * ED25519_BASEPOINT_TABLE, + ), + ) +} + +#[allow(unused)] +pub fn random_guaranteed_address() -> (Scalar, GuaranteedViewPair, MoneroAddress) { + let spend = Scalar::random(&mut OsRng); + let spend_pub = &spend * ED25519_BASEPOINT_TABLE; + let view = Zeroizing::new(Scalar::random(&mut OsRng)); + ( + spend, + GuaranteedViewPair::new(spend_pub, view.clone()).unwrap(), + MoneroAddress::new( + Network::Mainnet, + AddressType::Legacy, + spend_pub, + view.deref() * ED25519_BASEPOINT_TABLE, + ), + ) +} + +// TODO: Support transactions already on-chain +// TODO: Don't have a side effect of mining blocks more blocks than needed under race conditions +pub async fn mine_until_unlocked( + rpc: &SimpleRequestRpc, + addr: &MoneroAddress, + tx_hash: [u8; 32], +) -> Block { + // mine until tx is in a block + let mut height = rpc.get_height().await.unwrap(); + let mut found = false; + let mut block = None; + while !found { + let inner_block = rpc.get_block_by_number(height - 1).await.unwrap(); + found = match inner_block.transactions.iter().find(|&&x| x == tx_hash) { + Some(_) => { + block = Some(inner_block); + true + } + None => { + height = rpc.generate_blocks(addr, 1).await.unwrap().1 + 1; + false + } + } + } + + // Mine until tx's outputs are unlocked + for _ in 0 .. (DEFAULT_LOCK_WINDOW - 1) { + rpc.generate_blocks(addr, 1).await.unwrap(); + } + + block.unwrap() +} + +// Mines 60 blocks and returns an unlocked miner TX output. +#[allow(dead_code)] +pub async fn get_miner_tx_output(rpc: &SimpleRequestRpc, view: &ViewPair) -> WalletOutput { + let mut scanner = Scanner::new(view.clone()); + + // Mine 60 blocks to unlock a miner TX + let start = rpc.get_height().await.unwrap(); + rpc.generate_blocks(&view.legacy_address(Network::Mainnet), 60).await.unwrap(); + + let block = rpc.get_block_by_number(start).await.unwrap(); + scanner.scan(rpc, &block).await.unwrap().ignore_additional_timelock().swap_remove(0) +} + +/// Make sure the weight and fee match the expected calculation. +pub fn check_weight_and_fee(tx: &Transaction, fee_rate: FeeRate) { + let Transaction::V2 { proofs: Some(ref proofs), .. } = tx else { panic!("TX wasn't RingCT") }; + let fee = proofs.base.fee; + + let weight = tx.weight(); + let expected_weight = fee_rate.calculate_weight_from_fee(fee); + assert_eq!(weight, expected_weight); + + let expected_fee = fee_rate.calculate_fee_from_weight(weight); + assert_eq!(fee, expected_fee); +} + +pub async fn rpc() -> SimpleRequestRpc { + let rpc = + SimpleRequestRpc::new("http://serai:seraidex@127.0.0.1:18081".to_string()).await.unwrap(); + + const BLOCKS_TO_MINE: usize = 110; + + // Only run once + if rpc.get_height().await.unwrap() > BLOCKS_TO_MINE { + return rpc; + } + + let addr = MoneroAddress::new( + Network::Mainnet, + AddressType::Legacy, + &Scalar::random(&mut OsRng) * ED25519_BASEPOINT_TABLE, + &Scalar::random(&mut OsRng) * ED25519_BASEPOINT_TABLE, + ); + + // Mine enough blocks to ensure decoy availability + rpc.generate_blocks(&addr, BLOCKS_TO_MINE).await.unwrap(); + + rpc +} + +pub static SEQUENTIAL: OnceLock> = OnceLock::new(); + +#[macro_export] +macro_rules! async_sequential { + ($(async fn $name: ident() $body: block)*) => { + $( + #[tokio::test] + async fn $name() { + let guard = runner::SEQUENTIAL.get_or_init(|| tokio::sync::Mutex::new(())).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; + } + )* + } +} + +#[macro_export] +macro_rules! test { + ( + $name: ident, + ( + $first_tx: expr, + $first_checks: expr, + ), + $(( + $tx: expr, + $checks: expr, + )$(,)?),* + ) => { + async_sequential! { + async fn $name() { + use core::{ops::Deref, any::Any}; + #[cfg(feature = "multisig")] + use std::collections::HashMap; + + use zeroize::Zeroizing; + use rand_core::{RngCore, OsRng}; + + use curve25519_dalek::{constants::ED25519_BASEPOINT_TABLE, scalar::Scalar}; + + #[cfg(feature = "multisig")] + use frost::{ + curve::Ed25519, + Participant, + tests::{THRESHOLD, key_gen}, + }; + + use monero_wallet::{ + ringct::RctType, + rpc::FeePriority, + address::Network, + ViewPair, Scanner, OutputWithDecoys, + send::{Change, SignableTransaction, Eventuality}, + }; + + use runner::{ + SignableTransactionBuilder, ring_len, random_address, rpc, mine_until_unlocked, + get_miner_tx_output, check_weight_and_fee, + }; + + type Builder = SignableTransactionBuilder; + + // Run each function as both a single signer and as a multisig + #[allow(clippy::redundant_closure_call)] + for multisig in [false, true] { + // Only run the multisig variant if multisig is enabled + if multisig { + #[cfg(not(feature = "multisig"))] + continue; + } + + let spend = Zeroizing::new(Scalar::random(&mut OsRng)); + #[cfg(feature = "multisig")] + let keys = key_gen::<_, Ed25519>(&mut OsRng); + + let spend_pub = if !multisig { + spend.deref() * ED25519_BASEPOINT_TABLE + } else { + #[cfg(not(feature = "multisig"))] + panic!("Multisig branch called without the multisig feature"); + #[cfg(feature = "multisig")] + keys[&Participant::new(1).unwrap()].group_key().0 + }; + + let rpc = rpc().await; + + let view_priv = Zeroizing::new(Scalar::random(&mut OsRng)); + let mut outgoing_view = Zeroizing::new([0; 32]); + OsRng.fill_bytes(outgoing_view.as_mut()); + let view = ViewPair::new(spend_pub, view_priv.clone()).unwrap(); + let addr = view.legacy_address(Network::Mainnet); + + let miner_tx = get_miner_tx_output(&rpc, &view).await; + + let rct_type = match rpc.get_hardfork_version().await.unwrap() { + 14 => RctType::ClsagBulletproof, + 15 | 16 => RctType::ClsagBulletproofPlus, + _ => panic!("unrecognized hardfork version"), + }; + + let builder = SignableTransactionBuilder::new( + rct_type, + outgoing_view, + Change::new( + ViewPair::new( + &Scalar::random(&mut OsRng) * ED25519_BASEPOINT_TABLE, + Zeroizing::new(Scalar::random(&mut OsRng)) + ).unwrap(), + None, + ), + rpc.get_fee_rate(FeePriority::Unimportant).await.unwrap(), + ); + + let sign = |tx: SignableTransaction| { + let spend = spend.clone(); + #[cfg(feature = "multisig")] + let keys = keys.clone(); + + assert_eq!(&SignableTransaction::read(&mut tx.serialize().as_slice()).unwrap(), &tx); + + let eventuality = Eventuality::from(tx.clone()); + + let tx = if !multisig { + tx.sign(&mut OsRng, &spend).unwrap() + } else { + #[cfg(not(feature = "multisig"))] + panic!("multisig branch called without the multisig feature"); + #[cfg(feature = "multisig")] + { + let mut machines = HashMap::new(); + for i in (1 ..= THRESHOLD).map(|i| Participant::new(i).unwrap()) { + machines.insert(i, tx.clone().multisig(&keys[&i]).unwrap()); + } + + frost::tests::sign_without_caching(&mut OsRng, machines, &[]) + } + }; + + assert_eq!(&eventuality.extra(), &tx.prefix().extra, "eventuality extra was distinct"); + assert!(eventuality.matches(&tx.clone().into()), "eventuality didn't match"); + + tx + }; + + // TODO: Generate a distinct wallet for each transaction to prevent overlap + let next_addr = addr; + + let temp = Box::new({ + let mut builder = builder.clone(); + + let input = OutputWithDecoys::fingerprintable_deterministic_new( + &mut OsRng, + &rpc, + ring_len(rct_type), + rpc.get_height().await.unwrap(), + miner_tx, + ).await.unwrap(); + builder.add_input(input); + + let (tx, state) = ($first_tx)(rpc.clone(), builder, next_addr).await; + let fee_rate = tx.fee_rate().clone(); + let signed = sign(tx); + rpc.publish_transaction(&signed).await.unwrap(); + let block = + mine_until_unlocked(&rpc, &random_address().2, signed.hash()).await; + let tx = rpc.get_transaction(signed.hash()).await.unwrap(); + check_weight_and_fee(&tx, fee_rate); + let scanner = Scanner::new(view.clone()); + ($first_checks)(rpc.clone(), block, tx, scanner, state).await + }); + #[allow(unused_variables, unused_mut, unused_assignments)] + let mut carried_state: Box = temp; + + $( + let (tx, state) = ($tx)( + rct_type, + rpc.clone(), + builder.clone(), + next_addr, + *carried_state.downcast().unwrap() + ).await; + let fee_rate = tx.fee_rate().clone(); + let signed = sign(tx); + rpc.publish_transaction(&signed).await.unwrap(); + let block = + mine_until_unlocked(&rpc, &random_address().2, signed.hash()).await; + let tx = rpc.get_transaction(signed.hash()).await.unwrap(); + if stringify!($name) != "spend_one_input_to_two_outputs_no_change" { + // Skip weight and fee check for the above test because when there is no change, + // the change is added to the fee + check_weight_and_fee(&tx, fee_rate); + } + #[allow(unused_assignments)] + { + let scanner = Scanner::new(view.clone()); + carried_state = Box::new(($checks)(rpc.clone(), block, tx, scanner, state).await); + } + )* + } + } + } + } +} diff --git a/networks/monero/wallet/tests/scan.rs b/networks/monero/wallet/tests/scan.rs new file mode 100644 index 00000000..b2a51c60 --- /dev/null +++ b/networks/monero/wallet/tests/scan.rs @@ -0,0 +1,166 @@ +use monero_serai::transaction::Transaction; +use monero_wallet::{rpc::Rpc, address::SubaddressIndex, extra::PaymentId, GuaranteedScanner}; + +mod runner; + +test!( + scan_standard_address, + ( + |_, mut builder: Builder, _| async move { + let view = runner::random_address().1; + let scanner = Scanner::new(view.clone()); + builder.add_payment(view.legacy_address(Network::Mainnet), 5); + (builder.build().unwrap(), scanner) + }, + |rpc, block, tx: Transaction, _, mut state: Scanner| async move { + let output = state.scan(&rpc, &block).await.unwrap().not_additionally_locked().swap_remove(0); + assert_eq!(output.transaction(), tx.hash()); + assert_eq!(output.commitment().amount, 5); + let dummy_payment_id = PaymentId::Encrypted([0u8; 8]); + assert_eq!(output.payment_id(), Some(dummy_payment_id)); + }, + ), +); + +test!( + scan_subaddress, + ( + |_, mut builder: Builder, _| async move { + let subaddress = SubaddressIndex::new(0, 1).unwrap(); + + let view = runner::random_address().1; + let mut scanner = Scanner::new(view.clone()); + scanner.register_subaddress(subaddress); + + builder.add_payment(view.subaddress(Network::Mainnet, subaddress), 5); + (builder.build().unwrap(), (scanner, subaddress)) + }, + |rpc, block, tx: Transaction, _, mut state: (Scanner, SubaddressIndex)| async move { + let output = + state.0.scan(&rpc, &block).await.unwrap().not_additionally_locked().swap_remove(0); + assert_eq!(output.transaction(), tx.hash()); + assert_eq!(output.commitment().amount, 5); + assert_eq!(output.subaddress(), Some(state.1)); + }, + ), +); + +test!( + scan_integrated_address, + ( + |_, mut builder: Builder, _| async move { + let view = runner::random_address().1; + let scanner = Scanner::new(view.clone()); + + let mut payment_id = [0u8; 8]; + OsRng.fill_bytes(&mut payment_id); + + builder.add_payment(view.legacy_integrated_address(Network::Mainnet, payment_id), 5); + (builder.build().unwrap(), (scanner, payment_id)) + }, + |rpc, block, tx: Transaction, _, mut state: (Scanner, [u8; 8])| async move { + let output = + state.0.scan(&rpc, &block).await.unwrap().not_additionally_locked().swap_remove(0); + assert_eq!(output.transaction(), tx.hash()); + assert_eq!(output.commitment().amount, 5); + assert_eq!(output.payment_id(), Some(PaymentId::Encrypted(state.1))); + }, + ), +); + +test!( + scan_guaranteed, + ( + |_, mut builder: Builder, _| async move { + let view = runner::random_guaranteed_address().1; + let scanner = GuaranteedScanner::new(view.clone()); + builder.add_payment(view.address(Network::Mainnet, None, None), 5); + (builder.build().unwrap(), scanner) + }, + |rpc, block, tx: Transaction, _, mut scanner: GuaranteedScanner| async move { + let output = + scanner.scan(&rpc, &block).await.unwrap().not_additionally_locked().swap_remove(0); + assert_eq!(output.transaction(), tx.hash()); + assert_eq!(output.commitment().amount, 5); + assert_eq!(output.subaddress(), None); + }, + ), +); + +test!( + scan_guaranteed_subaddress, + ( + |_, mut builder: Builder, _| async move { + let subaddress = SubaddressIndex::new(0, 2).unwrap(); + + let view = runner::random_guaranteed_address().1; + let mut scanner = GuaranteedScanner::new(view.clone()); + scanner.register_subaddress(subaddress); + + builder.add_payment(view.address(Network::Mainnet, Some(subaddress), None), 5); + (builder.build().unwrap(), (scanner, subaddress)) + }, + |rpc, block, tx: Transaction, _, mut state: (GuaranteedScanner, SubaddressIndex)| async move { + let output = + state.0.scan(&rpc, &block).await.unwrap().not_additionally_locked().swap_remove(0); + assert_eq!(output.transaction(), tx.hash()); + assert_eq!(output.commitment().amount, 5); + assert_eq!(output.subaddress(), Some(state.1)); + }, + ), +); + +test!( + scan_guaranteed_integrated, + ( + |_, mut builder: Builder, _| async move { + let view = runner::random_guaranteed_address().1; + let scanner = GuaranteedScanner::new(view.clone()); + let mut payment_id = [0u8; 8]; + OsRng.fill_bytes(&mut payment_id); + + builder.add_payment(view.address(Network::Mainnet, None, Some(payment_id)), 5); + (builder.build().unwrap(), (scanner, payment_id)) + }, + |rpc, block, tx: Transaction, _, mut state: (GuaranteedScanner, [u8; 8])| async move { + let output = + state.0.scan(&rpc, &block).await.unwrap().not_additionally_locked().swap_remove(0); + assert_eq!(output.transaction(), tx.hash()); + assert_eq!(output.commitment().amount, 5); + assert_eq!(output.payment_id(), Some(PaymentId::Encrypted(state.1))); + }, + ), +); + +#[rustfmt::skip] +test!( + scan_guaranteed_integrated_subaddress, + ( + |_, mut builder: Builder, _| async move { + let subaddress = SubaddressIndex::new(0, 3).unwrap(); + + let view = runner::random_guaranteed_address().1; + let mut scanner = GuaranteedScanner::new(view.clone()); + scanner.register_subaddress(subaddress); + + let mut payment_id = [0u8; 8]; + OsRng.fill_bytes(&mut payment_id); + + builder.add_payment(view.address(Network::Mainnet, Some(subaddress), Some(payment_id)), 5); + (builder.build().unwrap(), (scanner, payment_id, subaddress)) + }, + | + rpc, + block, + tx: Transaction, + _, + mut state: (GuaranteedScanner, [u8; 8], SubaddressIndex), + | async move { + let output = state.0.scan(&rpc, &block).await.unwrap().not_additionally_locked().swap_remove(0); + assert_eq!(output.transaction(), tx.hash()); + assert_eq!(output.commitment().amount, 5); + assert_eq!(output.payment_id(), Some(PaymentId::Encrypted(state.1))); + assert_eq!(output.subaddress(), Some(state.2)); + }, + ), +); diff --git a/networks/monero/wallet/tests/send.rs b/networks/monero/wallet/tests/send.rs new file mode 100644 index 00000000..86a801d7 --- /dev/null +++ b/networks/monero/wallet/tests/send.rs @@ -0,0 +1,394 @@ +use std::collections::HashSet; + +use rand_core::OsRng; + +use monero_simple_request_rpc::SimpleRequestRpc; +use monero_wallet::{ + ringct::RctType, transaction::Transaction, rpc::Rpc, address::SubaddressIndex, extra::Extra, + WalletOutput, OutputWithDecoys, +}; + +mod runner; +use runner::{SignableTransactionBuilder, ring_len}; + +// Set up inputs, select decoys, then add them to the TX builder +async fn add_inputs( + rct_type: RctType, + rpc: &SimpleRequestRpc, + outputs: Vec, + builder: &mut SignableTransactionBuilder, +) { + for output in outputs { + builder.add_input( + OutputWithDecoys::fingerprintable_deterministic_new( + &mut OsRng, + rpc, + ring_len(rct_type), + rpc.get_height().await.unwrap(), + output, + ) + .await + .unwrap(), + ); + } +} + +test!( + spend_miner_output, + ( + |_, mut builder: Builder, addr| async move { + builder.add_payment(addr, 5); + (builder.build().unwrap(), ()) + }, + |rpc, block, tx: Transaction, mut scanner: Scanner, ()| async move { + let output = + scanner.scan(&rpc, &block).await.unwrap().not_additionally_locked().swap_remove(0); + assert_eq!(output.transaction(), tx.hash()); + assert_eq!(output.commitment().amount, 5); + }, + ), +); + +test!( + spend_multiple_outputs, + ( + |_, mut builder: Builder, addr| async move { + builder.add_payment(addr, 1000000000000); + builder.add_payment(addr, 2000000000000); + (builder.build().unwrap(), ()) + }, + |rpc, block, tx: Transaction, mut scanner: Scanner, ()| async move { + let mut outputs = scanner.scan(&rpc, &block).await.unwrap().not_additionally_locked(); + assert_eq!(outputs.len(), 2); + assert_eq!(outputs[0].transaction(), tx.hash()); + assert_eq!(outputs[0].transaction(), tx.hash()); + outputs.sort_by(|x, y| x.commitment().amount.cmp(&y.commitment().amount)); + assert_eq!(outputs[0].commitment().amount, 1000000000000); + assert_eq!(outputs[1].commitment().amount, 2000000000000); + outputs + }, + ), + ( + |rct_type: RctType, rpc, mut builder: Builder, addr, outputs: Vec| async move { + add_inputs(rct_type, &rpc, outputs, &mut builder).await; + builder.add_payment(addr, 6); + (builder.build().unwrap(), ()) + }, + |rpc, block, tx: Transaction, mut scanner: Scanner, ()| async move { + let output = + scanner.scan(&rpc, &block).await.unwrap().not_additionally_locked().swap_remove(0); + assert_eq!(output.transaction(), tx.hash()); + assert_eq!(output.commitment().amount, 6); + }, + ), +); + +test!( + // Ideally, this would be single_R, yet it isn't feasible to apply allow(non_snake_case) here + single_r_subaddress_send, + ( + // Consume this builder for an output we can use in the future + // This is needed because we can't get the input from the passed in builder + |_, mut builder: Builder, addr| async move { + builder.add_payment(addr, 1000000000000); + (builder.build().unwrap(), ()) + }, + |rpc, block, tx: Transaction, mut scanner: Scanner, ()| async move { + let outputs = scanner.scan(&rpc, &block).await.unwrap().not_additionally_locked(); + assert_eq!(outputs.len(), 1); + assert_eq!(outputs[0].transaction(), tx.hash()); + assert_eq!(outputs[0].commitment().amount, 1000000000000); + outputs + }, + ), + ( + |rct_type, rpc: SimpleRequestRpc, _, _, outputs: Vec| async move { + use monero_wallet::rpc::FeePriority; + + let view_priv = Zeroizing::new(Scalar::random(&mut OsRng)); + let mut outgoing_view = Zeroizing::new([0; 32]); + OsRng.fill_bytes(outgoing_view.as_mut()); + let change_view = + ViewPair::new(&Scalar::random(&mut OsRng) * ED25519_BASEPOINT_TABLE, view_priv.clone()) + .unwrap(); + + let mut builder = SignableTransactionBuilder::new( + rct_type, + outgoing_view, + Change::new(change_view.clone(), None), + rpc.get_fee_rate(FeePriority::Unimportant).await.unwrap(), + ); + add_inputs(rct_type, &rpc, vec![outputs.first().unwrap().clone()], &mut builder).await; + + // Send to a subaddress + let sub_view = ViewPair::new( + &Scalar::random(&mut OsRng) * ED25519_BASEPOINT_TABLE, + Zeroizing::new(Scalar::random(&mut OsRng)), + ) + .unwrap(); + builder + .add_payment(sub_view.subaddress(Network::Mainnet, SubaddressIndex::new(0, 1).unwrap()), 1); + (builder.build().unwrap(), (change_view, sub_view)) + }, + |rpc, block, tx: Transaction, _, views: (ViewPair, ViewPair)| async move { + // Make sure the change can pick up its output + let mut change_scanner = Scanner::new(views.0); + assert!( + change_scanner.scan(&rpc, &block).await.unwrap().not_additionally_locked().len() == 1 + ); + + // Make sure the subaddress can pick up its output + let mut sub_scanner = Scanner::new(views.1); + sub_scanner.register_subaddress(SubaddressIndex::new(0, 1).unwrap()); + let sub_outputs = sub_scanner.scan(&rpc, &block).await.unwrap().not_additionally_locked(); + assert!(sub_outputs.len() == 1); + assert_eq!(sub_outputs[0].transaction(), tx.hash()); + assert_eq!(sub_outputs[0].commitment().amount, 1); + assert!(sub_outputs[0].subaddress().unwrap().account() == 0); + assert!(sub_outputs[0].subaddress().unwrap().address() == 1); + + // Make sure only one R was included in TX extra + assert!(Extra::read::<&[u8]>(&mut tx.prefix().extra.as_ref()) + .unwrap() + .keys() + .unwrap() + .1 + .is_none()); + }, + ), +); + +test!( + spend_one_input_to_one_output_plus_change, + ( + |_, mut builder: Builder, addr| async move { + builder.add_payment(addr, 2000000000000); + (builder.build().unwrap(), ()) + }, + |rpc, block, tx: Transaction, mut scanner: Scanner, ()| async move { + let outputs = scanner.scan(&rpc, &block).await.unwrap().not_additionally_locked(); + assert_eq!(outputs.len(), 1); + assert_eq!(outputs[0].transaction(), tx.hash()); + assert_eq!(outputs[0].commitment().amount, 2000000000000); + outputs + }, + ), + ( + |rct_type: RctType, rpc, mut builder: Builder, addr, outputs: Vec| async move { + add_inputs(rct_type, &rpc, outputs, &mut builder).await; + builder.add_payment(addr, 2); + (builder.build().unwrap(), ()) + }, + |rpc, block, tx: Transaction, mut scanner: Scanner, ()| async move { + let output = + scanner.scan(&rpc, &block).await.unwrap().not_additionally_locked().swap_remove(0); + assert_eq!(output.transaction(), tx.hash()); + assert_eq!(output.commitment().amount, 2); + }, + ), +); + +test!( + spend_max_outputs, + ( + |_, mut builder: Builder, addr| async move { + builder.add_payment(addr, 1000000000000); + (builder.build().unwrap(), ()) + }, + |rpc, block, tx: Transaction, mut scanner: Scanner, ()| async move { + let outputs = scanner.scan(&rpc, &block).await.unwrap().not_additionally_locked(); + assert_eq!(outputs.len(), 1); + assert_eq!(outputs[0].transaction(), tx.hash()); + assert_eq!(outputs[0].commitment().amount, 1000000000000); + outputs + }, + ), + ( + |rct_type: RctType, rpc, mut builder: Builder, addr, outputs: Vec| async move { + add_inputs(rct_type, &rpc, outputs, &mut builder).await; + + for i in 0 .. 15 { + builder.add_payment(addr, i + 1); + } + (builder.build().unwrap(), ()) + }, + |rpc, block, tx: Transaction, mut scanner: Scanner, ()| async move { + let mut scanned_tx = scanner.scan(&rpc, &block).await.unwrap().not_additionally_locked(); + + let mut output_amounts = HashSet::new(); + for i in 0 .. 15 { + output_amounts.insert(i + 1); + } + for _ in 0 .. 15 { + let output = scanned_tx.swap_remove(0); + assert_eq!(output.transaction(), tx.hash()); + let amount = output.commitment().amount; + assert!(output_amounts.remove(&amount)); + } + assert_eq!(output_amounts.len(), 0); + }, + ), +); + +test!( + spend_max_outputs_to_subaddresses, + ( + |_, mut builder: Builder, addr| async move { + builder.add_payment(addr, 1000000000000); + (builder.build().unwrap(), ()) + }, + |rpc, block, tx: Transaction, mut scanner: Scanner, ()| async move { + let outputs = scanner.scan(&rpc, &block).await.unwrap().not_additionally_locked(); + assert_eq!(outputs.len(), 1); + assert_eq!(outputs[0].transaction(), tx.hash()); + assert_eq!(outputs[0].commitment().amount, 1000000000000); + outputs + }, + ), + ( + |rct_type: RctType, rpc, mut builder: Builder, _, outputs: Vec| async move { + add_inputs(rct_type, &rpc, outputs, &mut builder).await; + + let view = runner::random_address().1; + let mut scanner = Scanner::new(view.clone()); + + let mut subaddresses = vec![]; + for i in 0 .. 15 { + let subaddress = SubaddressIndex::new(0, i + 1).unwrap(); + scanner.register_subaddress(subaddress); + + builder.add_payment(view.subaddress(Network::Mainnet, subaddress), u64::from(i + 1)); + subaddresses.push(subaddress); + } + + (builder.build().unwrap(), (scanner, subaddresses)) + }, + |rpc, block, tx: Transaction, _, mut state: (Scanner, Vec)| async move { + use std::collections::HashMap; + + let mut scanned_tx = state.0.scan(&rpc, &block).await.unwrap().not_additionally_locked(); + + let mut output_amounts_by_subaddress = HashMap::new(); + for i in 0 .. 15 { + output_amounts_by_subaddress.insert(u64::try_from(i + 1).unwrap(), state.1[i]); + } + for _ in 0 .. 15 { + let output = scanned_tx.swap_remove(0); + assert_eq!(output.transaction(), tx.hash()); + let amount = output.commitment().amount; + + assert_eq!( + output.subaddress().unwrap(), + output_amounts_by_subaddress.remove(&amount).unwrap() + ); + } + assert_eq!(output_amounts_by_subaddress.len(), 0); + }, + ), +); + +test!( + spend_one_input_to_two_outputs_no_change, + ( + |_, mut builder: Builder, addr| async move { + builder.add_payment(addr, 1000000000000); + (builder.build().unwrap(), ()) + }, + |rpc, block, tx: Transaction, mut scanner: Scanner, ()| async move { + let outputs = scanner.scan(&rpc, &block).await.unwrap().not_additionally_locked(); + assert_eq!(outputs.len(), 1); + assert_eq!(outputs[0].transaction(), tx.hash()); + assert_eq!(outputs[0].commitment().amount, 1000000000000); + outputs + }, + ), + ( + |rct_type, rpc: SimpleRequestRpc, _, addr, outputs: Vec| async move { + use monero_wallet::rpc::FeePriority; + + let mut outgoing_view = Zeroizing::new([0; 32]); + OsRng.fill_bytes(outgoing_view.as_mut()); + let mut builder = SignableTransactionBuilder::new( + rct_type, + outgoing_view, + Change::fingerprintable(None), + rpc.get_fee_rate(FeePriority::Unimportant).await.unwrap(), + ); + add_inputs(rct_type, &rpc, vec![outputs.first().unwrap().clone()], &mut builder).await; + builder.add_payment(addr, 10000); + builder.add_payment(addr, 50000); + + (builder.build().unwrap(), ()) + }, + |rpc, block, tx: Transaction, mut scanner: Scanner, ()| async move { + let mut outputs = scanner.scan(&rpc, &block).await.unwrap().not_additionally_locked(); + assert_eq!(outputs.len(), 2); + assert_eq!(outputs[0].transaction(), tx.hash()); + assert_eq!(outputs[1].transaction(), tx.hash()); + outputs.sort_by(|x, y| x.commitment().amount.cmp(&y.commitment().amount)); + assert_eq!(outputs[0].commitment().amount, 10000); + assert_eq!(outputs[1].commitment().amount, 50000); + + // The remainder should get shunted to fee, which is fingerprintable + let Transaction::V2 { proofs: Some(ref proofs), .. } = tx else { panic!("TX wasn't RingCT") }; + assert_eq!(proofs.base.fee, 1000000000000 - 10000 - 50000); + }, + ), +); + +test!( + subaddress_change, + ( + // Consume this builder for an output we can use in the future + // This is needed because we can't get the input from the passed in builder + |_, mut builder: Builder, addr| async move { + builder.add_payment(addr, 1000000000000); + (builder.build().unwrap(), ()) + }, + |rpc, block, tx: Transaction, mut scanner: Scanner, ()| async move { + let outputs = scanner.scan(&rpc, &block).await.unwrap().not_additionally_locked(); + assert_eq!(outputs.len(), 1); + assert_eq!(outputs[0].transaction(), tx.hash()); + assert_eq!(outputs[0].commitment().amount, 1000000000000); + outputs + }, + ), + ( + |rct_type, rpc: SimpleRequestRpc, _, _, outputs: Vec| async move { + use monero_wallet::rpc::FeePriority; + + let view_priv = Zeroizing::new(Scalar::random(&mut OsRng)); + let mut outgoing_view = Zeroizing::new([0; 32]); + OsRng.fill_bytes(outgoing_view.as_mut()); + let change_view = + ViewPair::new(&Scalar::random(&mut OsRng) * ED25519_BASEPOINT_TABLE, view_priv.clone()) + .unwrap(); + + let mut builder = SignableTransactionBuilder::new( + rct_type, + outgoing_view, + Change::new(change_view.clone(), Some(SubaddressIndex::new(0, 1).unwrap())), + rpc.get_fee_rate(FeePriority::Unimportant).await.unwrap(), + ); + add_inputs(rct_type, &rpc, vec![outputs.first().unwrap().clone()], &mut builder).await; + + // Send to a random address + let view = ViewPair::new( + &Scalar::random(&mut OsRng) * ED25519_BASEPOINT_TABLE, + Zeroizing::new(Scalar::random(&mut OsRng)), + ) + .unwrap(); + builder.add_payment(view.legacy_address(Network::Mainnet), 1); + (builder.build().unwrap(), change_view) + }, + |rpc, block, _, _, change_view: ViewPair| async move { + // Make sure the change can pick up its output + let mut change_scanner = Scanner::new(change_view); + change_scanner.register_subaddress(SubaddressIndex::new(0, 1).unwrap()); + let outputs = change_scanner.scan(&rpc, &block).await.unwrap().not_additionally_locked(); + assert!(outputs.len() == 1); + assert!(outputs[0].subaddress().unwrap().account() == 0); + assert!(outputs[0].subaddress().unwrap().address() == 1); + }, + ), +); diff --git a/coins/monero/tests/wallet2_compatibility.rs b/networks/monero/wallet/tests/wallet2_compatibility.rs similarity index 75% rename from coins/monero/tests/wallet2_compatibility.rs rename to networks/monero/wallet/tests/wallet2_compatibility.rs index c6b58978..e7815d70 100644 --- a/coins/monero/tests/wallet2_compatibility.rs +++ b/networks/monero/wallet/tests/wallet2_compatibility.rs @@ -1,23 +1,30 @@ -use std::collections::HashSet; - use rand_core::{OsRng, RngCore}; use serde::Deserialize; use serde_json::json; -use monero_serai::{ +use monero_simple_request_rpc::SimpleRequestRpc; +use monero_wallet::{ transaction::Transaction, - rpc::{EmptyResponse, HttpRpc, Rpc}, - wallet::{ - address::{Network, AddressSpec, SubaddressIndex, MoneroAddress}, - extra::{MAX_TX_EXTRA_NONCE_SIZE, Extra, PaymentId}, - Scanner, - }, + rpc::Rpc, + address::{Network, SubaddressIndex, MoneroAddress}, + extra::{MAX_ARBITRARY_DATA_SIZE, Extra, PaymentId}, + Scanner, }; mod runner; -async fn make_integrated_address(rpc: &Rpc, payment_id: [u8; 8]) -> String { +#[derive(Clone, Copy, PartialEq, Eq)] +enum AddressSpec { + Legacy, + LegacyIntegrated([u8; 8]), + Subaddress(SubaddressIndex), +} + +#[derive(Deserialize, Debug)] +struct EmptyResponse {} + +async fn make_integrated_address(rpc: &SimpleRequestRpc, payment_id: [u8; 8]) -> String { #[derive(Debug, Deserialize)] struct IntegratedAddressResponse { integrated_address: String, @@ -34,8 +41,8 @@ async fn make_integrated_address(rpc: &Rpc, payment_id: [u8; 8]) -> Str res.integrated_address } -async fn initialize_rpcs() -> (Rpc, Rpc, String) { - let wallet_rpc = HttpRpc::new("http://127.0.0.1:18082".to_string()).await.unwrap(); +async fn initialize_rpcs() -> (SimpleRequestRpc, SimpleRequestRpc, MoneroAddress) { + let wallet_rpc = SimpleRequestRpc::new("http://127.0.0.1:18082".to_string()).await.unwrap(); let daemon_rpc = runner::rpc().await; #[derive(Debug, Deserialize)] @@ -57,9 +64,10 @@ async fn initialize_rpcs() -> (Rpc, Rpc, String) { wallet_rpc.json_rpc_call("get_address", Some(json!({ "account_index": 0 }))).await.unwrap(); // Fund the new wallet - daemon_rpc.generate_blocks(&address.address, 70).await.unwrap(); + let address = MoneroAddress::from_str(Network::Mainnet, &address.address).unwrap(); + daemon_rpc.generate_blocks(&address, 70).await.unwrap(); - (wallet_rpc, daemon_rpc, address.address) + (wallet_rpc, daemon_rpc, address) } async fn from_wallet_rpc_to_self(spec: AddressSpec) { @@ -68,7 +76,13 @@ async fn from_wallet_rpc_to_self(spec: AddressSpec) { // make an addr let (_, view_pair, _) = runner::random_address(); - let addr = view_pair.address(Network::Mainnet, spec); + let addr = match spec { + AddressSpec::Legacy => view_pair.legacy_address(Network::Mainnet), + AddressSpec::LegacyIntegrated(payment_id) => { + view_pair.legacy_integrated_address(Network::Mainnet, payment_id) + } + AddressSpec::Subaddress(index) => view_pair.subaddress(Network::Mainnet, index), + }; // refresh & make a tx let _: EmptyResponse = wallet_rpc.json_rpc_call("refresh", None).await.unwrap(); @@ -90,38 +104,39 @@ async fn from_wallet_rpc_to_self(spec: AddressSpec) { // TODO: Needs https://github.com/monero-project/monero/pull/9260 // let fee_rate = daemon_rpc - // .get_fee(daemon_rpc.get_protocol().await.unwrap(), FeePriority::Unimportant) + // .get_fee_rate(FeePriority::Unimportant) // .await // .unwrap(); // unlock it - runner::mine_until_unlocked(&daemon_rpc, &wallet_rpc_addr, tx_hash).await; + let block = runner::mine_until_unlocked(&daemon_rpc, &wallet_rpc_addr, tx_hash).await; // Create the scanner - let mut scanner = Scanner::from_view(view_pair, Some(HashSet::new())); + let mut scanner = Scanner::new(view_pair); if let AddressSpec::Subaddress(index) = spec { scanner.register_subaddress(index); } // Retrieve it and scan it - let tx = daemon_rpc.get_transaction(tx_hash).await.unwrap(); - let output = scanner.scan_transaction(&tx).not_locked().swap_remove(0); + let output = + scanner.scan(&daemon_rpc, &block).await.unwrap().not_additionally_locked().swap_remove(0); + assert_eq!(output.transaction(), tx_hash); // TODO: Needs https://github.com/monero-project/monero/pull/9260 // runner::check_weight_and_fee(&tx, fee_rate); match spec { AddressSpec::Subaddress(index) => { - assert_eq!(output.metadata.subaddress, Some(index)); - assert_eq!(output.metadata.payment_id, Some(PaymentId::Encrypted([0u8; 8]))); + assert_eq!(output.subaddress(), Some(index)); + assert_eq!(output.payment_id(), Some(PaymentId::Encrypted([0u8; 8]))); } - AddressSpec::Integrated(payment_id) => { - assert_eq!(output.metadata.payment_id, Some(PaymentId::Encrypted(payment_id))); - assert_eq!(output.metadata.subaddress, None); + AddressSpec::LegacyIntegrated(payment_id) => { + assert_eq!(output.payment_id(), Some(PaymentId::Encrypted(payment_id))); + assert_eq!(output.subaddress(), None); } - AddressSpec::Standard | AddressSpec::Featured { .. } => { - assert_eq!(output.metadata.subaddress, None); - assert_eq!(output.metadata.payment_id, Some(PaymentId::Encrypted([0u8; 8]))); + AddressSpec::Legacy => { + assert_eq!(output.subaddress(), None); + assert_eq!(output.payment_id(), Some(PaymentId::Encrypted([0u8; 8]))); } } assert_eq!(output.commitment().amount, 1000000000000); @@ -129,7 +144,7 @@ async fn from_wallet_rpc_to_self(spec: AddressSpec) { async_sequential!( async fn receipt_of_wallet_rpc_tx_standard() { - from_wallet_rpc_to_self(AddressSpec::Standard).await; + from_wallet_rpc_to_self(AddressSpec::Legacy).await; } async fn receipt_of_wallet_rpc_tx_subaddress() { @@ -139,7 +154,7 @@ async_sequential!( async fn receipt_of_wallet_rpc_tx_integrated() { let mut payment_id = [0u8; 8]; OsRng.fill_bytes(&mut payment_id); - from_wallet_rpc_to_self(AddressSpec::Integrated(payment_id)).await; + from_wallet_rpc_to_self(AddressSpec::LegacyIntegrated(payment_id)).await; } ); @@ -170,11 +185,10 @@ test!( let (wallet_rpc, _, wallet_rpc_addr) = initialize_rpcs().await; // add destination - builder - .add_payment(MoneroAddress::from_str(Network::Mainnet, &wallet_rpc_addr).unwrap(), 1000000); + builder.add_payment(wallet_rpc_addr, 1000000); (builder.build().unwrap(), wallet_rpc) }, - |_, tx: Transaction, _, data: Rpc| async move { + |_, _, tx: Transaction, _, data: SimpleRequestRpc| async move { // confirm receipt let _: EmptyResponse = data.json_rpc_call("refresh", None).await.unwrap(); let transfer: TransfersResponse = data @@ -208,7 +222,7 @@ test!( .add_payment(MoneroAddress::from_str(Network::Mainnet, &addr.address).unwrap(), 1000000); (builder.build().unwrap(), (wallet_rpc, addr.account_index)) }, - |_, tx: Transaction, _, data: (Rpc, u32)| async move { + |_, _, tx: Transaction, _, data: (SimpleRequestRpc, u32)| async move { // confirm receipt let _: EmptyResponse = data.0.json_rpc_call("refresh", None).await.unwrap(); let transfer: TransfersResponse = data @@ -224,7 +238,7 @@ test!( assert_eq!(transfer.transfer.payment_id, hex::encode([0u8; 8])); // Make sure only one R was included in TX extra - assert!(Extra::read::<&[u8]>(&mut tx.prefix.extra.as_ref()) + assert!(Extra::read::<&[u8]>(&mut tx.prefix().extra.as_ref()) .unwrap() .keys() .unwrap() @@ -260,7 +274,7 @@ test!( ]); (builder.build().unwrap(), (wallet_rpc, daemon_rpc, addrs.address_index)) }, - |_, tx: Transaction, _, data: (Rpc, Rpc, u32)| async move { + |_, _, tx: Transaction, _, data: (SimpleRequestRpc, SimpleRequestRpc, u32)| async move { // confirm receipt let _: EmptyResponse = data.0.json_rpc_call("refresh", None).await.unwrap(); let transfer: TransfersResponse = data @@ -283,7 +297,7 @@ test!( // Make sure 3 additional pub keys are included in TX extra let keys = - Extra::read::<&[u8]>(&mut tx.prefix.extra.as_ref()).unwrap().keys().unwrap().1.unwrap(); + Extra::read::<&[u8]>(&mut tx.prefix().extra.as_ref()).unwrap().keys().unwrap().1.unwrap(); assert_eq!(keys.len(), 3); }, @@ -305,7 +319,7 @@ test!( builder.add_payment(MoneroAddress::from_str(Network::Mainnet, &addr).unwrap(), 1000000); (builder.build().unwrap(), (wallet_rpc, payment_id)) }, - |_, tx: Transaction, _, data: (Rpc, [u8; 8])| async move { + |_, _, tx: Transaction, _, data: (SimpleRequestRpc, [u8; 8])| async move { // confirm receipt let _: EmptyResponse = data.0.json_rpc_call("refresh", None).await.unwrap(); let transfer: TransfersResponse = data @@ -328,19 +342,17 @@ test!( let (wallet_rpc, _, wallet_rpc_addr) = initialize_rpcs().await; // add destination - builder - .add_payment(MoneroAddress::from_str(Network::Mainnet, &wallet_rpc_addr).unwrap(), 1000000); + builder.add_payment(wallet_rpc_addr, 1000000); // Make 2 data that is the full 255 bytes for _ in 0 .. 2 { - // Subtract 1 since we prefix data with 127 - let data = vec![b'a'; MAX_TX_EXTRA_NONCE_SIZE - 1]; + let data = vec![b'a'; MAX_ARBITRARY_DATA_SIZE]; builder.add_data(data).unwrap(); } (builder.build().unwrap(), wallet_rpc) }, - |_, tx: Transaction, _, data: Rpc| async move { + |_, _, tx: Transaction, _, data: SimpleRequestRpc| async move { // confirm receipt let _: EmptyResponse = data.json_rpc_call("refresh", None).await.unwrap(); let transfer: TransfersResponse = data diff --git a/networks/monero/wallet/util/Cargo.toml b/networks/monero/wallet/util/Cargo.toml new file mode 100644 index 00000000..a72f09bf --- /dev/null +++ b/networks/monero/wallet/util/Cargo.toml @@ -0,0 +1,51 @@ +[package] +name = "monero-wallet-util" +version = "0.1.0" +description = "Additional utility functions for monero-wallet" +license = "MIT" +repository = "https://github.com/serai-dex/serai/tree/develop/networks/monero/wallet/util" +authors = ["Luke Parker "] +edition = "2021" +rust-version = "1.79" + +[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 } + +thiserror = { version = "1", default-features = false, optional = true } + +zeroize = { version = "^1.5", default-features = false, features = ["zeroize_derive"] } +rand_core = { version = "0.6", default-features = false } + +monero-wallet = { path = "..", default-features = false } + +monero-seed = { path = "../seed", default-features = false } +polyseed = { path = "../polyseed", default-features = false } + +[dev-dependencies] +hex = { version = "0.4", default-features = false, features = ["std"] } +curve25519-dalek = { version = "4", default-features = false, features = ["alloc", "zeroize"] } + +[features] +std = [ + "std-shims/std", + + "thiserror", + + "zeroize/std", + "rand_core/std", + + "monero-wallet/std", + + "monero-seed/std", + "polyseed/std", +] +compile-time-generators = ["monero-wallet/compile-time-generators"] +multisig = ["monero-wallet/multisig", "std"] +default = ["std", "compile-time-generators"] diff --git a/networks/monero/wallet/util/LICENSE b/networks/monero/wallet/util/LICENSE new file mode 100644 index 00000000..91d893c1 --- /dev/null +++ b/networks/monero/wallet/util/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022-2024 Luke Parker + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/networks/monero/wallet/util/README.md b/networks/monero/wallet/util/README.md new file mode 100644 index 00000000..15d7c80c --- /dev/null +++ b/networks/monero/wallet/util/README.md @@ -0,0 +1,25 @@ +# Monero Wallet Utilities + +Additional utility functions for monero-wallet. + +This library is isolated as it adds a notable amount of dependencies to the +tree, and to be a subject to a distinct versioning policy. This library may +more frequently undergo breaking API changes. + +This library is usable under no-std when the `std` feature (on by default) is +disabled. + +### Features + +- Support for Monero's seed algorithm +- Support for Polyseed + +### Cargo Features + +- `std` (on by default): Enables `std` (and with it, more efficient internal + implementations). +- `compile-time-generators` (on by default): Derives the generators at + compile-time so they don't need to be derived at runtime. This is recommended + if program size doesn't need to be kept minimal. +- `multisig`: Adds support for creation of transactions using a threshold + multisignature wallet. diff --git a/networks/monero/wallet/util/src/lib.rs b/networks/monero/wallet/util/src/lib.rs new file mode 100644 index 00000000..e2aaa696 --- /dev/null +++ b/networks/monero/wallet/util/src/lib.rs @@ -0,0 +1,9 @@ +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![doc = include_str!("../README.md")] +#![deny(missing_docs)] +#![cfg_attr(not(feature = "std"), no_std)] + +pub use monero_wallet::*; + +/// Seed creation and parsing functionality. +pub mod seed; diff --git a/networks/monero/wallet/util/src/seed.rs b/networks/monero/wallet/util/src/seed.rs new file mode 100644 index 00000000..77ca3358 --- /dev/null +++ b/networks/monero/wallet/util/src/seed.rs @@ -0,0 +1,150 @@ +use core::fmt; +use std_shims::string::String; + +use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing}; +use rand_core::{RngCore, CryptoRng}; + +pub use monero_seed as original; +pub use polyseed; + +use original::{SeedError as OriginalSeedError, Seed as OriginalSeed}; +use polyseed::{PolyseedError, Polyseed}; + +/// An error from working with seeds. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +#[cfg_attr(feature = "std", derive(thiserror::Error))] +pub enum SeedError { + /// The seed was invalid. + #[cfg_attr(feature = "std", error("invalid seed"))] + InvalidSeed, + /// The entropy was invalid. + #[cfg_attr(feature = "std", error("invalid entropy"))] + InvalidEntropy, + /// The checksum did not match the data. + #[cfg_attr(feature = "std", error("invalid checksum"))] + InvalidChecksum, + /// Unsupported features were enabled. + #[cfg_attr(feature = "std", error("unsupported features"))] + UnsupportedFeatures, +} + +impl From for SeedError { + fn from(error: OriginalSeedError) -> SeedError { + match error { + OriginalSeedError::DeprecatedEnglishWithChecksum | OriginalSeedError::InvalidChecksum => { + SeedError::InvalidChecksum + } + OriginalSeedError::InvalidSeed => SeedError::InvalidSeed, + } + } +} + +impl From for SeedError { + fn from(error: PolyseedError) -> SeedError { + match error { + PolyseedError::UnsupportedFeatures => SeedError::UnsupportedFeatures, + PolyseedError::InvalidEntropy => SeedError::InvalidEntropy, + PolyseedError::InvalidSeed => SeedError::InvalidSeed, + PolyseedError::InvalidChecksum => SeedError::InvalidChecksum, + } + } +} + +/// The type of the seed. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum SeedType { + /// The seed format originally used by Monero, + Original(monero_seed::Language), + /// Polyseed. + Polyseed(polyseed::Language), +} + +/// A seed, internally either the original format or a Polyseed. +#[derive(Clone, PartialEq, Eq, Zeroize, ZeroizeOnDrop)] +pub enum Seed { + /// The originally formatted seed. + Original(OriginalSeed), + /// A Polyseed. + Polyseed(Polyseed), +} + +impl fmt::Debug for Seed { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Seed::Original(_) => f.debug_struct("Seed::Original").finish_non_exhaustive(), + Seed::Polyseed(_) => f.debug_struct("Seed::Polyseed").finish_non_exhaustive(), + } + } +} + +impl Seed { + /// Create a new seed. + pub fn new(rng: &mut R, seed_type: SeedType) -> Seed { + match seed_type { + SeedType::Original(lang) => Seed::Original(OriginalSeed::new(rng, lang)), + SeedType::Polyseed(lang) => Seed::Polyseed(Polyseed::new(rng, lang)), + } + } + + /// Parse a seed from a string. + pub fn from_string(seed_type: SeedType, words: Zeroizing) -> Result { + match seed_type { + SeedType::Original(lang) => Ok(OriginalSeed::from_string(lang, words).map(Seed::Original)?), + SeedType::Polyseed(lang) => Ok(Polyseed::from_string(lang, words).map(Seed::Polyseed)?), + } + } + + /// Create a seed from entropy. + /// + /// A birthday may be optionally provided, denoted in seconds since the epoch. For + /// SeedType::Original, it will be ignored. For SeedType::Polyseed, it'll be embedded into the + /// seed. + /// + /// For SeedType::Polyseed, the last 13 bytes of `entropy` must be 0. + // TODO: Return Result, not Option + pub fn from_entropy( + seed_type: SeedType, + entropy: Zeroizing<[u8; 32]>, + birthday: Option, + ) -> Option { + match seed_type { + SeedType::Original(lang) => OriginalSeed::from_entropy(lang, entropy).map(Seed::Original), + SeedType::Polyseed(lang) => { + Polyseed::from(lang, 0, birthday.unwrap_or(0), entropy).ok().map(Seed::Polyseed) + } + } + } + + /// Converts the seed to a string. + pub fn to_string(&self) -> Zeroizing { + match self { + Seed::Original(seed) => seed.to_string(), + Seed::Polyseed(seed) => seed.to_string(), + } + } + + /// Get the entropy for this seed. + pub fn entropy(&self) -> Zeroizing<[u8; 32]> { + match self { + Seed::Original(seed) => seed.entropy(), + Seed::Polyseed(seed) => seed.entropy().clone(), + } + } + + /// Get the key derived from this seed. + pub fn key(&self) -> Zeroizing<[u8; 32]> { + match self { + // Original does not differentiate between its entropy and its key + Seed::Original(seed) => seed.entropy(), + Seed::Polyseed(seed) => seed.key(), + } + } + + /// Get the birthday of this seed, denoted in seconds since the epoch. + pub fn birthday(&self) -> u64 { + match self { + Seed::Original(_) => 0, + Seed::Polyseed(seed) => seed.birthday(), + } + } +} diff --git a/networks/monero/wallet/util/tests/tests.rs b/networks/monero/wallet/util/tests/tests.rs new file mode 100644 index 00000000..7b6656f2 --- /dev/null +++ b/networks/monero/wallet/util/tests/tests.rs @@ -0,0 +1,3 @@ +// TODO +#[test] +fn test() {} diff --git a/orchestration/dev/coins/bitcoin/run.sh b/orchestration/dev/networks/bitcoin/run.sh similarity index 100% rename from orchestration/dev/coins/bitcoin/run.sh rename to orchestration/dev/networks/bitcoin/run.sh diff --git a/orchestration/dev/coins/monero/run.sh b/orchestration/dev/networks/ethereum-relayer/.folder old mode 100755 new mode 100644 similarity index 100% rename from orchestration/dev/coins/monero/run.sh rename to orchestration/dev/networks/ethereum-relayer/.folder diff --git a/orchestration/dev/coins/ethereum/run.sh b/orchestration/dev/networks/ethereum/run.sh similarity index 100% rename from orchestration/dev/coins/ethereum/run.sh rename to orchestration/dev/networks/ethereum/run.sh diff --git a/orchestration/dev/coins/monero-wallet-rpc/run.sh b/orchestration/dev/networks/monero-wallet-rpc/run.sh similarity index 100% rename from orchestration/dev/coins/monero-wallet-rpc/run.sh rename to orchestration/dev/networks/monero-wallet-rpc/run.sh diff --git a/orchestration/dev/coins/monero/hashes-v0.18.3.1.txt b/orchestration/dev/networks/monero/hashes-v0.18.3.1.txt similarity index 100% rename from orchestration/dev/coins/monero/hashes-v0.18.3.1.txt rename to orchestration/dev/networks/monero/hashes-v0.18.3.1.txt diff --git a/orchestration/dev/networks/monero/run.sh b/orchestration/dev/networks/monero/run.sh new file mode 100755 index 00000000..75a93e46 --- /dev/null +++ b/orchestration/dev/networks/monero/run.sh @@ -0,0 +1,11 @@ +#!/bin/sh + +RPC_USER="${RPC_USER:=serai}" +RPC_PASS="${RPC_PASS:=seraidex}" + +# Run Monero +monerod --non-interactive --regtest --offline --fixed-difficulty=1 \ + --no-zmq --rpc-bind-ip=0.0.0.0 --rpc-bind-port=18081 --confirm-external-bind \ + --rpc-access-control-origins "*" --disable-rpc-ban \ + --rpc-login=$RPC_USER:$RPC_PASS --log-level 2 \ + $1 diff --git a/orchestration/runtime/Dockerfile b/orchestration/runtime/Dockerfile index 2801f070..f3120758 100644 --- a/orchestration/runtime/Dockerfile +++ b/orchestration/runtime/Dockerfile @@ -1,5 +1,5 @@ -# rust:1.77.0-slim-bookworm as of March 22nd, 2024 (GMT) -FROM --platform=linux/amd64 rust@sha256:e785e4aa81f87bc1ee02fa2026ffbc491e0410bdaf6652cea74884373f452664 as deterministic +# rust:1.79.0-slim-bookworm as of June 14th, 2024 (GMT) +FROM --platform=linux/amd64 rust@sha256:fa189cd885739dd17fc6bb4e132687fce43f2bf42983c0ac39b60e4943201e9c as deterministic # Move to a Debian package snapshot RUN rm -rf /etc/apt/sources.list.d/debian.sources && \ @@ -19,7 +19,7 @@ FROM deterministic ADD patches /serai/patches ADD common /serai/common ADD crypto /serai/crypto -ADD coins /serai/coins +ADD networks /serai/networks ADD message-queue /serai/message-queue ADD processor /serai/processor ADD coordinator /serai/coordinator diff --git a/orchestration/src/coordinator.rs b/orchestration/src/coordinator.rs index 13fdff59..26058886 100644 --- a/orchestration/src/coordinator.rs +++ b/orchestration/src/coordinator.rs @@ -17,6 +17,7 @@ pub fn coordinator( let longer_reattempts = if network == Network::Dev { "longer-reattempts" } else { "" }; let setup = mimalloc(Os::Debian).to_string() + &build_serai_service( + "", network.release(), &format!("{db} {longer_reattempts}"), "serai-coordinator", diff --git a/orchestration/src/docker.rs b/orchestration/src/docker.rs index 2523bfaa..abe5834f 100644 --- a/orchestration/src/docker.rs +++ b/orchestration/src/docker.rs @@ -15,7 +15,7 @@ pub fn build(orchestration_path: &Path, network: Network, name: &str) { let mut dockerfile_path = orchestration_path.to_path_buf(); if HashSet::from(["bitcoin", "ethereum", "monero", "monero-wallet-rpc"]).contains(name) { - dockerfile_path = dockerfile_path.join("coins"); + dockerfile_path = dockerfile_path.join("networks"); } if name.contains("-processor") { dockerfile_path = diff --git a/orchestration/src/ethereum_relayer.rs b/orchestration/src/ethereum_relayer.rs new file mode 100644 index 00000000..c75f6847 --- /dev/null +++ b/orchestration/src/ethereum_relayer.rs @@ -0,0 +1,39 @@ +use std::path::Path; + +use crate::{Network, Os, mimalloc, os, build_serai_service, write_dockerfile}; + +pub fn ethereum_relayer(orchestration_path: &Path, network: Network) { + let setup = mimalloc(Os::Debian).to_string() + + &build_serai_service("", network.release(), network.db(), "serai-ethereum-relayer"); + + let env_vars = [ + ("DB_PATH", "/volume/ethereum-relayer-db".to_string()), + ("RUST_LOG", "info,serai_ethereum_relayer=trace".to_string()), + ]; + let mut env_vars_str = String::new(); + for (env_var, value) in env_vars { + env_vars_str += &format!(r#"{env_var}=${{{env_var}:="{value}"}} "#); + } + + let run_ethereum_relayer = format!( + r#" +# Copy the relayer server binary and relevant license +COPY --from=builder --chown=ethereumrelayer /serai/bin/serai-ethereum-relayer /bin + +# Run ethereum-relayer +EXPOSE 20830 +EXPOSE 20831 +CMD {env_vars_str} serai-ethereum-relayer +"# + ); + + let run = os(Os::Debian, "", "ethereumrelayer") + &run_ethereum_relayer; + let res = setup + &run; + + let mut ethereum_relayer_path = orchestration_path.to_path_buf(); + ethereum_relayer_path.push("networks"); + ethereum_relayer_path.push("ethereum-relayer"); + ethereum_relayer_path.push("Dockerfile"); + + write_dockerfile(ethereum_relayer_path, &res); +} diff --git a/orchestration/src/main.rs b/orchestration/src/main.rs index 0e6c7cb0..3185c107 100644 --- a/orchestration/src/main.rs +++ b/orchestration/src/main.rs @@ -29,8 +29,11 @@ use ciphersuite::{ mod mimalloc; use mimalloc::mimalloc; -mod coins; -use coins::*; +mod networks; +use networks::*; + +mod ethereum_relayer; +use ethereum_relayer::ethereum_relayer; mod message_queue; use message_queue::message_queue; @@ -137,13 +140,13 @@ WORKDIR /home/{user} } } -fn build_serai_service(release: bool, features: &str, package: &str) -> String { +fn build_serai_service(prelude: &str, release: bool, features: &str, package: &str) -> String { let profile = if release { "release" } else { "debug" }; let profile_flag = if release { "--release" } else { "" }; format!( r#" -FROM rust:1.77-slim-bookworm as builder +FROM rust:1.79-slim-bookworm as builder COPY --from=mimalloc-debian libmimalloc.so /usr/lib RUN echo "/usr/lib/libmimalloc.so" >> /etc/ld.so.preload @@ -159,11 +162,13 @@ RUN apt install -y make protobuf-compiler # Add the wasm toolchain RUN rustup target add wasm32-unknown-unknown +{prelude} + # Add files for build ADD patches /serai/patches ADD common /serai/common ADD crypto /serai/crypto -ADD coins /serai/coins +ADD networks /serai/networks ADD message-queue /serai/message-queue ADD processor /serai/processor ADD coordinator /serai/coordinator @@ -278,6 +283,8 @@ fn dockerfiles(network: Network) { let ethereum_key = infrastructure_keys.remove("ethereum").unwrap(); let monero_key = infrastructure_keys.remove("monero").unwrap(); + ethereum_relayer(&orchestration_path, network); + message_queue( &orchestration_path, network, @@ -361,6 +368,7 @@ fn start(network: Network, services: HashSet) { let name = match service.as_ref() { "serai" => "serai", "coordinator" => "coordinator", + "ethereum-relayer" => "ethereum-relayer", "message-queue" => "message-queue", "bitcoin-daemon" => "bitcoin", "bitcoin-processor" => "bitcoin-processor", @@ -374,23 +382,17 @@ fn start(network: Network, services: HashSet) { let serai_runtime_volume = format!("serai-{}-runtime-volume", network.label()); if name == "serai" { // Check if it's built by checking if the volume has the expected runtime file + let wasm_build_container_name = format!("serai-{}-runtime", network.label()); let built = || { - if let Ok(path) = Command::new("docker") - .arg("volume") + if let Ok(state_and_status) = Command::new("docker") .arg("inspect") .arg("-f") - .arg("{{ .Mountpoint }}") - .arg(&serai_runtime_volume) + .arg("{{.State.Status}}:{{.State.ExitCode}}") + .arg(&wasm_build_container_name) .output() { - if let Ok(path) = String::from_utf8(path.stdout) { - if let Ok(iter) = std::fs::read_dir(PathBuf::from(path.trim())) { - for item in iter.flatten() { - if item.file_name() == "serai.wasm" { - return true; - } - } - } + if let Ok(state_and_status) = String::from_utf8(state_and_status.stdout) { + return state_and_status.trim() == "exited:0"; } } false @@ -493,6 +495,10 @@ fn start(network: Network, services: HashSet) { command } } + "ethereum-relayer" => { + // Expose the router command fetch server + command.arg("-p").arg("20831:20831") + } "monero" => { // Expose the RPC for tests if network == Network::Dev { @@ -559,6 +565,9 @@ Commands: - `message-queue` - `bitcoin-daemon` - `bitcoin-processor` + - `ethereum-daemon` + - `ethereum-processor` + - `ethereum-relayer` - `monero-daemon` - `monero-processor` - `monero-wallet-rpc` (if "dev") @@ -591,6 +600,9 @@ Commands: Some("start") => { let mut services = HashSet::new(); for arg in args { + if arg == "ethereum-processor" { + services.insert("ethereum-relayer".to_string()); + } if let Some(ext_network) = arg.strip_suffix("-processor") { services.insert(ext_network.to_string() + "-daemon"); } diff --git a/orchestration/src/message_queue.rs b/orchestration/src/message_queue.rs index eb662b67..ea97a619 100644 --- a/orchestration/src/message_queue.rs +++ b/orchestration/src/message_queue.rs @@ -13,7 +13,7 @@ pub fn message_queue( monero_key: ::G, ) { let setup = mimalloc(Os::Debian).to_string() + - &build_serai_service(network.release(), network.db(), "serai-message-queue"); + &build_serai_service("", network.release(), network.db(), "serai-message-queue"); let env_vars = [ ("COORDINATOR_KEY", hex::encode(coordinator_key.to_bytes())), diff --git a/orchestration/src/coins/bitcoin.rs b/orchestration/src/networks/bitcoin.rs similarity index 93% rename from orchestration/src/coins/bitcoin.rs rename to orchestration/src/networks/bitcoin.rs index 94686244..128858ac 100644 --- a/orchestration/src/coins/bitcoin.rs +++ b/orchestration/src/networks/bitcoin.rs @@ -7,7 +7,7 @@ pub fn bitcoin(orchestration_path: &Path, network: Network) { const DOWNLOAD_BITCOIN: &str = r#" FROM alpine:latest as bitcoin -ENV BITCOIN_VERSION=27.0 +ENV BITCOIN_VERSION=27.1 RUN apk --no-cache add git gnupg @@ -37,7 +37,7 @@ COPY --from=bitcoin --chown=bitcoin bitcoind /bin EXPOSE 8332 8333 -ADD /orchestration/{}/coins/bitcoin/run.sh / +ADD /orchestration/{}/networks/bitcoin/run.sh / CMD ["/run.sh"] "#, network.label() @@ -47,7 +47,7 @@ CMD ["/run.sh"] let res = setup + &run; let mut bitcoin_path = orchestration_path.to_path_buf(); - bitcoin_path.push("coins"); + bitcoin_path.push("networks"); bitcoin_path.push("bitcoin"); bitcoin_path.push("Dockerfile"); diff --git a/orchestration/src/coins/ethereum/consensus/lighthouse.rs b/orchestration/src/networks/ethereum/consensus/lighthouse.rs similarity index 93% rename from orchestration/src/coins/ethereum/consensus/lighthouse.rs rename to orchestration/src/networks/ethereum/consensus/lighthouse.rs index add9728b..3434117d 100644 --- a/orchestration/src/coins/ethereum/consensus/lighthouse.rs +++ b/orchestration/src/networks/ethereum/consensus/lighthouse.rs @@ -27,7 +27,7 @@ RUN tar xvf lighthouse-v${LIGHTHOUSE_VERSION}-$(uname -m)-unknown-linux-gnu.tar. r#" COPY --from=lighthouse --chown=ethereum lighthouse /bin -ADD /orchestration/{}/coins/ethereum/consensus/lighthouse/run.sh /consensus_layer.sh +ADD /orchestration/{}/networks/ethereum/consensus/lighthouse/run.sh /consensus_layer.sh "#, network.label() ); diff --git a/orchestration/src/coins/ethereum/consensus/mod.rs b/orchestration/src/networks/ethereum/consensus/mod.rs similarity index 100% rename from orchestration/src/coins/ethereum/consensus/mod.rs rename to orchestration/src/networks/ethereum/consensus/mod.rs diff --git a/orchestration/src/coins/ethereum/consensus/nimbus.rs b/orchestration/src/networks/ethereum/consensus/nimbus.rs similarity index 95% rename from orchestration/src/coins/ethereum/consensus/nimbus.rs rename to orchestration/src/networks/ethereum/consensus/nimbus.rs index 07006aa9..94649e4f 100644 --- a/orchestration/src/coins/ethereum/consensus/nimbus.rs +++ b/orchestration/src/networks/ethereum/consensus/nimbus.rs @@ -40,7 +40,7 @@ RUN sha512sum nimbus | grep {checksum} r#" COPY --from=nimbus --chown=ethereum nimbus /bin -ADD /orchestration/{}/coins/ethereum/consensus/nimbus/run.sh /consensus_layer.sh +ADD /orchestration/{}/networks/ethereum/consensus/nimbus/run.sh /consensus_layer.sh "#, network.label() ); diff --git a/orchestration/src/coins/ethereum/execution/anvil.rs b/orchestration/src/networks/ethereum/execution/anvil.rs similarity index 100% rename from orchestration/src/coins/ethereum/execution/anvil.rs rename to orchestration/src/networks/ethereum/execution/anvil.rs diff --git a/orchestration/src/coins/ethereum/execution/mod.rs b/orchestration/src/networks/ethereum/execution/mod.rs similarity index 100% rename from orchestration/src/coins/ethereum/execution/mod.rs rename to orchestration/src/networks/ethereum/execution/mod.rs diff --git a/orchestration/src/coins/ethereum/execution/reth.rs b/orchestration/src/networks/ethereum/execution/reth.rs similarity index 92% rename from orchestration/src/coins/ethereum/execution/reth.rs rename to orchestration/src/networks/ethereum/execution/reth.rs index 8c80a9fa..65b096dd 100644 --- a/orchestration/src/coins/ethereum/execution/reth.rs +++ b/orchestration/src/networks/ethereum/execution/reth.rs @@ -29,7 +29,7 @@ COPY --from=reth --chown=ethereum reth /bin EXPOSE 30303 9001 8545 -ADD /orchestration/{}/coins/ethereum/execution/reth/run.sh /execution_layer.sh +ADD /orchestration/{}/networks/ethereum/execution/reth/run.sh /execution_layer.sh "#, network.label() ); diff --git a/orchestration/src/coins/ethereum/mod.rs b/orchestration/src/networks/ethereum/mod.rs similarity index 92% rename from orchestration/src/coins/ethereum/mod.rs rename to orchestration/src/networks/ethereum/mod.rs index a06318c0..8b5c5a85 100644 --- a/orchestration/src/coins/ethereum/mod.rs +++ b/orchestration/src/networks/ethereum/mod.rs @@ -21,7 +21,7 @@ pub fn ethereum(orchestration_path: &Path, network: Network) { let run = format!( r#" -ADD /orchestration/{}/coins/ethereum/run.sh /run.sh +ADD /orchestration/{}/networks/ethereum/run.sh /run.sh CMD ["/run.sh"] "#, network.label() @@ -35,7 +35,7 @@ CMD ["/run.sh"] let res = download + &run; let mut ethereum_path = orchestration_path.to_path_buf(); - ethereum_path.push("coins"); + ethereum_path.push("networks"); ethereum_path.push("ethereum"); ethereum_path.push("Dockerfile"); diff --git a/orchestration/src/coins/mod.rs b/orchestration/src/networks/mod.rs similarity index 100% rename from orchestration/src/coins/mod.rs rename to orchestration/src/networks/mod.rs diff --git a/orchestration/src/coins/monero.rs b/orchestration/src/networks/monero.rs similarity index 87% rename from orchestration/src/coins/monero.rs rename to orchestration/src/networks/monero.rs index c21bc610..dfac3013 100644 --- a/orchestration/src/coins/monero.rs +++ b/orchestration/src/networks/monero.rs @@ -30,7 +30,7 @@ RUN apk --no-cache add gnupg RUN wget https://downloads.getmonero.org/cli/monero-linux-{arch}-v{MONERO_VERSION}.tar.bz2 # Verify Binary -- fingerprint from https://github.com/monero-project/monero-site/issues/1949 -ADD orchestration/{}/coins/monero/hashes-v{MONERO_VERSION}.txt . +ADD orchestration/{}/networks/monero/hashes-v{MONERO_VERSION}.txt . RUN gpg --keyserver hkp://keyserver.ubuntu.com:80 --keyserver-options no-self-sigs-only --receive-keys 81AC591FE9C4B65C5806AFC3F0AF4D462A0BDF92 && \ gpg --verify hashes-v{MONERO_VERSION}.txt && \ grep "$(sha256sum monero-linux-{arch}-v{MONERO_VERSION}.tar.bz2 | cut -c 1-64)" hashes-v{MONERO_VERSION}.txt @@ -49,7 +49,7 @@ COPY --from=monero --chown=monero:nogroup {monero_binary} /bin EXPOSE {ports} -ADD /orchestration/{}/coins/{folder}/run.sh / +ADD /orchestration/{}/networks/{folder}/run.sh / CMD ["/run.sh"] "#, network.label(), @@ -61,7 +61,7 @@ CMD ["/run.sh"] let res = setup + &run; let mut monero_path = orchestration_path.to_path_buf(); - monero_path.push("coins"); + monero_path.push("networks"); monero_path.push(folder); monero_path.push("Dockerfile"); @@ -69,14 +69,7 @@ CMD ["/run.sh"] } pub fn monero(orchestration_path: &Path, network: Network) { - monero_internal( - network, - if network == Network::Dev { Os::Alpine } else { Os::Debian }, - orchestration_path, - "monero", - "monerod", - "18080 18081", - ) + monero_internal(network, Os::Debian, orchestration_path, "monero", "monerod", "18080 18081") } pub fn monero_wallet_rpc(orchestration_path: &Path) { diff --git a/orchestration/src/processor.rs b/orchestration/src/processor.rs index 8a2c8c77..cefe6455 100644 --- a/orchestration/src/processor.rs +++ b/orchestration/src/processor.rs @@ -17,6 +17,15 @@ pub fn processor( ) { let setup = mimalloc(Os::Debian).to_string() + &build_serai_service( + if coin == "ethereum" { + r#" +RUN cargo install svm-rs +RUN svm install 0.8.25 +RUN svm use 0.8.25 +"# + } else { + "" + }, network.release(), &format!("binaries {} {coin}", network.db()), "serai-processor", @@ -32,24 +41,32 @@ RUN apt install -y ca-certificates const RPC_PASS: &str = "seraidex"; // TODO: Isolate networks let hostname = format!("serai-{}-{coin}", network.label()); - let port = match coin { - "bitcoin" => 8332, - "ethereum" => return, // TODO - "monero" => 18081, - _ => panic!("unrecognized external network"), - }; + let port = format!( + "{}", + match coin { + "bitcoin" => 8332, + "ethereum" => 8545, + "monero" => 18081, + _ => panic!("unrecognized external network"), + } + ); - let env_vars = [ + let mut env_vars = vec![ ("MESSAGE_QUEUE_RPC", format!("serai-{}-message-queue", network.label())), ("MESSAGE_QUEUE_KEY", hex::encode(coin_key.to_repr())), ("ENTROPY", hex::encode(entropy.as_ref())), ("NETWORK", coin.to_string()), ("NETWORK_RPC_LOGIN", format!("{RPC_USER}:{RPC_PASS}")), ("NETWORK_RPC_HOSTNAME", hostname), - ("NETWORK_RPC_PORT", format!("{port}")), + ("NETWORK_RPC_PORT", port), ("DB_PATH", "/volume/processor-db".to_string()), ("RUST_LOG", "info,serai_processor=debug".to_string()), ]; + if coin == "ethereum" { + env_vars + .push(("ETHEREUM_RELAYER_HOSTNAME", format!("serai-{}-ethereum-relayer", network.label()))); + env_vars.push(("ETHEREUM_RELAYER_PORT", "20830".to_string())); + } let mut env_vars_str = String::new(); for (env_var, value) in env_vars { env_vars_str += &format!(r#"{env_var}=${{{env_var}:="{value}"}} "#); diff --git a/orchestration/src/serai.rs b/orchestration/src/serai.rs index 2e1e915c..e2f96f6a 100644 --- a/orchestration/src/serai.rs +++ b/orchestration/src/serai.rs @@ -11,9 +11,9 @@ pub fn serai( serai_key: &Zeroizing<::F>, ) { // Always builds in release for performance reasons - let setup = mimalloc(Os::Debian).to_string() + &build_serai_service(true, "", "serai-node"); + let setup = mimalloc(Os::Debian).to_string() + &build_serai_service("", true, "", "serai-node"); let setup_fast_epoch = - mimalloc(Os::Debian).to_string() + &build_serai_service(true, "fast-epoch", "serai-node"); + mimalloc(Os::Debian).to_string() + &build_serai_service("", true, "fast-epoch", "serai-node"); let env_vars = [("KEY", hex::encode(serai_key.to_repr()))]; let mut env_vars_str = String::new(); diff --git a/orchestration/testnet/coins/bitcoin/run.sh b/orchestration/testnet/networks/bitcoin/run.sh similarity index 100% rename from orchestration/testnet/coins/bitcoin/run.sh rename to orchestration/testnet/networks/bitcoin/run.sh diff --git a/orchestration/testnet/networks/ethereum-relayer/.folder b/orchestration/testnet/networks/ethereum-relayer/.folder new file mode 100644 index 00000000..675d4438 --- /dev/null +++ b/orchestration/testnet/networks/ethereum-relayer/.folder @@ -0,0 +1,11 @@ +#!/bin/sh + +RPC_USER="${RPC_USER:=serai}" +RPC_PASS="${RPC_PASS:=seraidex}" + +# Run Monero +monerod --non-interactive --regtest --offline --fixed-difficulty=1 \ + --no-zmq --rpc-bind-ip=0.0.0.0 --rpc-bind-port=18081 --confirm-external-bind \ + --rpc-access-control-origins "*" --disable-rpc-ban \ + --rpc-login=$RPC_USER:$RPC_PASS \ + $1 diff --git a/orchestration/testnet/coins/ethereum/consensus/lighthouse/run.sh b/orchestration/testnet/networks/ethereum/consensus/lighthouse/run.sh similarity index 100% rename from orchestration/testnet/coins/ethereum/consensus/lighthouse/run.sh rename to orchestration/testnet/networks/ethereum/consensus/lighthouse/run.sh diff --git a/orchestration/testnet/coins/ethereum/consensus/nimbus/run.sh b/orchestration/testnet/networks/ethereum/consensus/nimbus/run.sh similarity index 100% rename from orchestration/testnet/coins/ethereum/consensus/nimbus/run.sh rename to orchestration/testnet/networks/ethereum/consensus/nimbus/run.sh diff --git a/orchestration/testnet/coins/ethereum/execution/geth/run.sh b/orchestration/testnet/networks/ethereum/execution/geth/run.sh similarity index 100% rename from orchestration/testnet/coins/ethereum/execution/geth/run.sh rename to orchestration/testnet/networks/ethereum/execution/geth/run.sh diff --git a/orchestration/testnet/coins/ethereum/execution/reth/run.sh b/orchestration/testnet/networks/ethereum/execution/reth/run.sh similarity index 100% rename from orchestration/testnet/coins/ethereum/execution/reth/run.sh rename to orchestration/testnet/networks/ethereum/execution/reth/run.sh diff --git a/orchestration/testnet/coins/ethereum/run.sh b/orchestration/testnet/networks/ethereum/run.sh similarity index 100% rename from orchestration/testnet/coins/ethereum/run.sh rename to orchestration/testnet/networks/ethereum/run.sh diff --git a/orchestration/testnet/coins/monero/hashes-v0.18.3.1.txt b/orchestration/testnet/networks/monero/hashes-v0.18.3.1.txt similarity index 100% rename from orchestration/testnet/coins/monero/hashes-v0.18.3.1.txt rename to orchestration/testnet/networks/monero/hashes-v0.18.3.1.txt diff --git a/orchestration/testnet/coins/monero/run.sh b/orchestration/testnet/networks/monero/run.sh similarity index 100% rename from orchestration/testnet/coins/monero/run.sh rename to orchestration/testnet/networks/monero/run.sh diff --git a/patches/parking_lot/Cargo.toml b/patches/parking_lot/Cargo.toml new file mode 100644 index 00000000..957b19bf --- /dev/null +++ b/patches/parking_lot/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "parking_lot" +version = "0.11.2" +description = "parking_lot which patches to the latest update" +license = "MIT" +repository = "https://github.com/serai-dex/serai/tree/develop/patches/parking_lot" +authors = ["Luke Parker "] +keywords = [] +edition = "2021" +rust-version = "1.70" + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + +[dependencies] +parking_lot = "0.12" diff --git a/patches/parking_lot/src/lib.rs b/patches/parking_lot/src/lib.rs new file mode 100644 index 00000000..df10a74d --- /dev/null +++ b/patches/parking_lot/src/lib.rs @@ -0,0 +1 @@ +pub use parking_lot::*; diff --git a/patches/parking_lot_core/Cargo.toml b/patches/parking_lot_core/Cargo.toml new file mode 100644 index 00000000..37dcc703 --- /dev/null +++ b/patches/parking_lot_core/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "parking_lot_core" +version = "0.8.6" +description = "parking_lot_core which patches to the latest update" +license = "MIT" +repository = "https://github.com/serai-dex/serai/tree/develop/patches/parking_lot_core" +authors = ["Luke Parker "] +keywords = [] +edition = "2021" +rust-version = "1.70" + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + +[dependencies] +parking_lot_core = "0.9" diff --git a/patches/parking_lot_core/src/lib.rs b/patches/parking_lot_core/src/lib.rs new file mode 100644 index 00000000..bfecbfd8 --- /dev/null +++ b/patches/parking_lot_core/src/lib.rs @@ -0,0 +1 @@ +pub use parking_lot_core::*; diff --git a/processor/Cargo.toml b/processor/Cargo.toml index f90f6117..5ff7e94d 100644 --- a/processor/Cargo.toml +++ b/processor/Cargo.toml @@ -21,7 +21,6 @@ workspace = true async-trait = { version = "0.1", default-features = false } zeroize = { version = "1", default-features = false, features = ["std"] } thiserror = { version = "1", default-features = false } -serde = { version = "1", default-features = false, features = ["std", "derive"] } # Libs rand_core = { version = "0.6", default-features = false, features = ["std", "getrandom"] } @@ -45,15 +44,16 @@ frost-schnorrkel = { path = "../crypto/schnorrkel", default-features = false } k256 = { version = "^0.13.1", default-features = false, features = ["std"], optional = true } # Bitcoin -secp256k1 = { version = "0.28", default-features = false, features = ["std", "global-context", "rand-std"], optional = true } -bitcoin-serai = { path = "../coins/bitcoin", default-features = false, features = ["std"], optional = true } +secp256k1 = { version = "0.29", default-features = false, features = ["std", "global-context", "rand-std"], optional = true } +bitcoin-serai = { path = "../networks/bitcoin", default-features = false, features = ["std"], optional = true } # Ethereum -ethereum-serai = { path = "../coins/ethereum", default-features = false, optional = true } +ethereum-serai = { path = "../networks/ethereum", default-features = false, optional = true } # Monero dalek-ff-group = { path = "../crypto/dalek-ff-group", default-features = false, features = ["std"], optional = true } -monero-serai = { path = "../coins/monero", default-features = false, features = ["std", "http-rpc", "multisig"], optional = true } +monero-simple-request-rpc = { path = "../networks/monero/rpc/simple-request", default-features = false, optional = true } +monero-wallet = { path = "../networks/monero/wallet", default-features = false, features = ["std", "multisig", "compile-time-generators"], optional = true } # Application log = { version = "0.4", default-features = false, features = ["std"] } @@ -75,7 +75,7 @@ frost = { package = "modular-frost", path = "../crypto/frost", features = ["test sp-application-crypto = { git = "https://github.com/serai-dex/substrate", default-features = false, features = ["std"] } -ethereum-serai = { path = "../coins/ethereum", default-features = false, features = ["tests"] } +ethereum-serai = { path = "../networks/ethereum", default-features = false, features = ["tests"] } dockertest = "0.4" serai-docker-tests = { path = "../tests/docker" } @@ -87,7 +87,7 @@ bitcoin = ["dep:secp256k1", "secp256k1", "bitcoin-serai", "serai-client/bitcoin" ethereum = ["secp256k1", "ethereum-serai/tests"] ed25519 = ["dalek-ff-group", "frost/ed25519"] -monero = ["ed25519", "monero-serai", "serai-client/monero"] +monero = ["ed25519", "monero-simple-request-rpc", "monero-wallet", "serai-client/monero"] binaries = ["env_logger", "serai-env", "message-queue"] parity-db = ["serai-db/parity-db"] diff --git a/processor/src/key_gen.rs b/processor/src/key_gen.rs index f1a5b47c..297db194 100644 --- a/processor/src/key_gen.rs +++ b/processor/src/key_gen.rs @@ -10,7 +10,7 @@ use ciphersuite::group::GroupEncoding; use frost::{ curve::{Ciphersuite, Ristretto}, dkg::{ - DkgError, Participant, ThresholdParams, ThresholdCore, ThresholdKeys, encryption::*, frost::*, + DkgError, Participant, ThresholdParams, ThresholdCore, ThresholdKeys, encryption::*, pedpop::*, }, }; @@ -512,6 +512,7 @@ impl KeyGen { ProcessorMessage::GeneratedKeyPair { id, substrate_key: generated_substrate_key.unwrap().to_bytes(), + // TODO: This can be made more efficient since tweaked keys may be a subset of keys network_key: generated_network_key.unwrap().to_bytes().as_ref().to_vec(), } } diff --git a/processor/src/main.rs b/processor/src/main.rs index 1a50effa..e0d97aa6 100644 --- a/processor/src/main.rs +++ b/processor/src/main.rs @@ -748,7 +748,15 @@ async fn main() { #[cfg(feature = "bitcoin")] NetworkId::Bitcoin => run(db, Bitcoin::new(url).await, coordinator).await, #[cfg(feature = "ethereum")] - NetworkId::Ethereum => run(db.clone(), Ethereum::new(db, url).await, coordinator).await, + NetworkId::Ethereum => { + let relayer_hostname = env::var("ETHEREUM_RELAYER_HOSTNAME") + .expect("ethereum relayer hostname wasn't specified") + .to_string(); + let relayer_port = + env::var("ETHEREUM_RELAYER_PORT").expect("ethereum relayer port wasn't specified"); + let relayer_url = relayer_hostname + ":" + &relayer_port; + run(db.clone(), Ethereum::new(db, url, relayer_url).await, coordinator).await + } #[cfg(feature = "monero")] NetworkId::Monero => run(db, Monero::new(url).await, coordinator).await, _ => panic!("spawning a processor for an unsupported network"), diff --git a/processor/src/multisigs/db.rs b/processor/src/multisigs/db.rs index 339b7bdc..3d1d13bd 100644 --- a/processor/src/multisigs/db.rs +++ b/processor/src/multisigs/db.rs @@ -231,7 +231,7 @@ impl ForwardedOutputDb { let res = InInstructionWithBalance::decode(&mut outputs_ref).unwrap(); assert!(outputs_ref.len() < outputs.len()); if outputs_ref.is_empty() { - txn.del(&Self::key(balance)); + txn.del(Self::key(balance)); } else { Self::set(txn, balance, &outputs); } diff --git a/processor/src/multisigs/mod.rs b/processor/src/multisigs/mod.rs index 75c91675..12f01715 100644 --- a/processor/src/multisigs/mod.rs +++ b/processor/src/multisigs/mod.rs @@ -63,9 +63,22 @@ fn instruction_from_output( return (presumed_origin, None); } - let Ok(shorthand) = Shorthand::decode(&mut data) else { return (presumed_origin, None) }; - let Ok(instruction) = RefundableInInstruction::try_from(shorthand) else { - return (presumed_origin, None); + let shorthand = match Shorthand::decode(&mut data) { + Ok(shorthand) => shorthand, + Err(e) => { + info!("data in output {} wasn't valid shorthand: {e:?}", hex::encode(output.id())); + return (presumed_origin, None); + } + }; + let instruction = match RefundableInInstruction::try_from(shorthand) { + Ok(instruction) => instruction, + Err(e) => { + info!( + "shorthand in output {} wasn't convertible to a RefundableInInstruction: {e:?}", + hex::encode(output.id()) + ); + return (presumed_origin, None); + } }; let mut balance = output.balance(); diff --git a/processor/src/multisigs/scanner.rs b/processor/src/multisigs/scanner.rs index 3d28f3e8..1b25e108 100644 --- a/processor/src/multisigs/scanner.rs +++ b/processor/src/multisigs/scanner.rs @@ -279,6 +279,8 @@ impl ScannerHandle { activation_number: usize, key: ::G, ) { + info!("Registering key {} in scanner at {activation_number}", hex::encode(key.to_bytes())); + let mut scanner_lock = self.scanner.write().await; let scanner = scanner_lock.as_mut().unwrap(); assert!( @@ -286,8 +288,6 @@ impl ScannerHandle { "activation block of new keys was already scanned", ); - info!("Registering key {} in scanner at {activation_number}", hex::encode(key.to_bytes())); - if scanner.keys.is_empty() { assert!(scanner.ram_scanned.is_none()); scanner.ram_scanned = Some(activation_number); diff --git a/processor/src/multisigs/scheduler/smart_contract.rs b/processor/src/multisigs/scheduler/smart_contract.rs index 4f48e391..3da8acf4 100644 --- a/processor/src/multisigs/scheduler/smart_contract.rs +++ b/processor/src/multisigs/scheduler/smart_contract.rs @@ -116,7 +116,7 @@ impl> SchedulerTrait for Scheduler { assert!(self.coins.contains(&utxo.balance().coin)); } - let mut nonce = LastNonce::get(txn).map_or(1, |nonce| nonce + 1); + let mut nonce = LastNonce::get(txn).unwrap_or(1); let mut plans = vec![]; for chunk in payments.as_slice().chunks(N::MAX_OUTPUTS) { // Once we rotate, all further payments should be scheduled via the new multisig diff --git a/processor/src/multisigs/scheduler/utxo.rs b/processor/src/multisigs/scheduler/utxo.rs index e9aa3351..1865cab9 100644 --- a/processor/src/multisigs/scheduler/utxo.rs +++ b/processor/src/multisigs/scheduler/utxo.rs @@ -432,7 +432,7 @@ impl> Scheduler { } // If there's a UTXO to restore, restore it - // This is down now as if there is a to_restore output, and it was inserted into self.utxos + // This is done now as if there is a to_restore output, and it was inserted into self.utxos // earlier, self.utxos.len() may become `N::MAX_INPUTS + 1` // The prior block requires the len to be `<= N::MAX_INPUTS` if let Some(to_restore) = to_restore { @@ -442,9 +442,10 @@ impl> Scheduler { txn.put(scheduler_key::(&self.key), self.serialize()); log::info!( - "created {} plans containing {} payments to sign", + "created {} plans containing {} payments to sign, with {} payments pending scheduling", plans.len(), payments_at_start - self.payments.len(), + self.payments.len(), ); plans } @@ -589,7 +590,8 @@ impl> SchedulerTrait for Scheduler { output: N::Output, refund_to: N::Address, ) -> Plan { - Plan { + let output_id = output.id().as_ref().to_vec(); + let res = Plan { key: output.key(), // Uses a payment as this will still be successfully sent due to fee amortization, // and because change is currently always a Serai key @@ -597,7 +599,9 @@ impl> SchedulerTrait for Scheduler { inputs: vec![output], change: None, scheduler_addendum: (), - } + }; + log::info!("refund plan for {} has ID {}", hex::encode(output_id), hex::encode(res.id())); + res } fn shim_forward_plan(output: N::Output, to: ::G) -> Option> { diff --git a/processor/src/networks/bitcoin.rs b/processor/src/networks/bitcoin.rs index 3f8174e4..ae62f4c8 100644 --- a/processor/src/networks/bitcoin.rs +++ b/processor/src/networks/bitcoin.rs @@ -20,12 +20,11 @@ use bitcoin_serai::{ key::{Parity, XOnlyPublicKey}, consensus::{Encodable, Decodable}, script::Instruction, - address::{NetworkChecked, Address as BAddress}, - Transaction, Block, Network as BNetwork, ScriptBuf, + Transaction, Block, ScriptBuf, opcodes::all::{OP_SHA256, OP_EQUALVERIFY}, }, wallet::{ - tweak_keys, address_payload, ReceivedOutput, Scanner, TransactionError, + tweak_keys, p2tr_script_buf, ReceivedOutput, Scanner, TransactionError, SignableTransaction as BSignableTransaction, TransactionMachine, }, rpc::{RpcError, Rpc}, @@ -175,7 +174,7 @@ pub struct Fee(u64); impl TransactionTrait for Transaction { type Id = [u8; 32]; fn id(&self) -> Self::Id { - let mut hash = *self.txid().as_raw_hash().as_byte_array(); + let mut hash = *self.compute_txid().as_raw_hash().as_byte_array(); hash.reverse(); hash } @@ -243,7 +242,8 @@ impl EventualityTrait for Eventuality { buf } fn read_completion(reader: &mut R) -> io::Result { - Transaction::consensus_decode(reader).map_err(|e| io::Error::other(format!("{e}"))) + Transaction::consensus_decode(&mut io::BufReader::with_capacity(0, reader)) + .map_err(|e| io::Error::other(format!("{e}"))) } } @@ -405,7 +405,7 @@ impl Bitcoin { .to_sat(); } let out = tx.output.iter().map(|output| output.value.to_sat()).sum::(); - fees.push((in_value - out) / tx.weight().to_wu()); + fees.push((in_value - out) / u64::try_from(tx.vsize()).unwrap()); } } fees.sort(); @@ -413,11 +413,6 @@ impl Bitcoin { // The DUST constant documentation notes a relay rule practically enforcing a // 1000 sat/kilo-vbyte minimum fee. - // - // 1000 sat/kilo-vbyte is 1000 sat/4-kilo-weight (250 sat/kilo-weight). - // Since bitcoin-serai takes fee per weight, we'd need to pass 0.25 to achieve this fee rate. - // Accordingly, setting 1 is 4x the current relay rule minimum (and should be more than safe). - // TODO: Rewrite to fee_per_vbyte, not fee_per_weight? Ok(Fee(fee.max(1))) } @@ -453,7 +448,7 @@ impl Bitcoin { match BSignableTransaction::new( inputs.iter().map(|input| input.output.clone()).collect(), &payments, - change.as_ref().map(AsRef::as_ref), + change.clone().map(Into::into), None, fee.0, ) { @@ -465,7 +460,9 @@ impl Bitcoin { Err(TransactionError::NoOutputs | TransactionError::NotEnoughFunds) => Ok(None), // amortize_fee removes payments which fall below the dust threshold Err(TransactionError::DustPayment) => panic!("dust payment despite removing dust"), - Err(TransactionError::TooMuchData) => panic!("too much data despite not specifying data"), + Err(TransactionError::TooMuchData) => { + panic!("too much data despite not specifying data") + } Err(TransactionError::TooLowFee) => { panic!("created a transaction whose fee is below the minimum") } @@ -534,12 +531,14 @@ impl Bitcoin { input_index: usize, private_key: &PrivateKey, ) -> ScriptBuf { + use bitcoin_serai::bitcoin::{Network as BNetwork, Address as BAddress}; + let public_key = PublicKey::from_private_key(SECP256K1, private_key); - let main_addr = BAddress::p2pkh(&public_key, BNetwork::Regtest); + let main_addr = BAddress::p2pkh(public_key, BNetwork::Regtest); let mut der = SECP256K1 .sign_ecdsa_low_r( - &Message::from( + &Message::from_digest_slice( SighashCache::new(tx) .legacy_signature_hash( input_index, @@ -547,8 +546,10 @@ impl Bitcoin { EcdsaSighashType::All.to_u32(), ) .unwrap() - .to_raw_hash(), - ), + .to_raw_hash() + .as_ref(), + ) + .unwrap(), &private_key.inner, ) .serialize_der() @@ -577,8 +578,10 @@ const MAX_INPUTS: usize = 520; const MAX_OUTPUTS: usize = 520; fn address_from_key(key: ProjectivePoint) -> Address { - Address::new(BAddress::::new(BNetwork::Bitcoin, address_payload(key).unwrap())) - .unwrap() + Address::new( + p2tr_script_buf(key).expect("creating address from key which isn't properly tweaked"), + ) + .expect("couldn't create Serai-representable address for P2TR script") } #[async_trait] @@ -724,9 +727,7 @@ impl Network for Bitcoin { } tx.unwrap().output.swap_remove(usize::try_from(input.previous_output.vout).unwrap()) }; - BAddress::from_script(&spent_output.script_pubkey, BNetwork::Bitcoin) - .ok() - .and_then(Address::new) + Address::new(spent_output.script_pubkey) }; let data = Self::extract_serai_data(tx); for output in &mut outputs { @@ -858,7 +859,7 @@ impl Network for Bitcoin { Err(RpcError::ConnectionError) => Err(NetworkError::ConnectionError)?, // TODO: Distinguish already in pool vs double spend (other signing attempt succeeded) vs // invalid transaction - Err(e) => panic!("failed to publish TX {}: {e}", tx.txid()), + Err(e) => panic!("failed to publish TX {}: {e}", tx.compute_txid()), } Ok(()) } @@ -894,6 +895,8 @@ impl Network for Bitcoin { #[cfg(test)] async fn mine_block(&self) { + use bitcoin_serai::bitcoin::{Network as BNetwork, Address as BAddress}; + self .rpc .rpc_call::>( @@ -906,10 +909,12 @@ impl Network for Bitcoin { #[cfg(test)] async fn test_send(&self, address: Address) -> Block { + use bitcoin_serai::bitcoin::{Network as BNetwork, Address as BAddress}; + let secret_key = SecretKey::new(&mut rand_core::OsRng); let private_key = PrivateKey::new(secret_key, BNetwork::Regtest); let public_key = PublicKey::from_private_key(SECP256K1, &private_key); - let main_addr = BAddress::p2pkh(&public_key, BNetwork::Regtest); + let main_addr = BAddress::p2pkh(public_key, BNetwork::Regtest); let new_block = self.get_latest_block_number().await.unwrap() + 1; self @@ -923,14 +928,14 @@ impl Network for Bitcoin { version: Version(2), lock_time: LockTime::ZERO, input: vec![TxIn { - previous_output: OutPoint { txid: tx.txid(), vout: 0 }, + previous_output: OutPoint { txid: tx.compute_txid(), vout: 0 }, script_sig: Script::new().into(), sequence: Sequence(u32::MAX), witness: Witness::default(), }], output: vec![TxOut { value: tx.output[0].value - BAmount::from_sat(10000), - script_pubkey: address.as_ref().script_pubkey(), + script_pubkey: address.clone().into(), }], }; tx.input[0].script_sig = Self::sign_btc_input_for_p2pkh(&tx, 0, &private_key); diff --git a/processor/src/networks/ethereum.rs b/processor/src/networks/ethereum.rs index 7ffe7041..6a11b06d 100644 --- a/processor/src/networks/ethereum.rs +++ b/processor/src/networks/ethereum.rs @@ -13,7 +13,7 @@ use frost::ThresholdKeys; use ethereum_serai::{ alloy::{ primitives::U256, - rpc_types::{BlockNumberOrTag, Transaction}, + rpc_types::{BlockTransactionsKind, BlockNumberOrTag, Transaction}, simple_request_transport::SimpleRequest, rpc_client::ClientBuilder, provider::{Provider, RootProvider}, @@ -31,6 +31,11 @@ use tokio::{ time::sleep, sync::{RwLock, RwLockReadGuard}, }; +#[cfg(not(test))] +use tokio::{ + io::{AsyncReadExt, AsyncWriteExt}, + net::TcpStream, +}; use serai_client::{ primitives::{Coin, Amount, Balance, NetworkId}, @@ -290,6 +295,8 @@ pub struct Ethereum { // address. Accordingly, all methods present are consistent to a Serai chain with a finalized // first key (regardless of local state), and this is safe. db: D, + #[cfg_attr(test, allow(unused))] + relayer_url: String, provider: Arc>, deployer: Deployer, router: Arc>>, @@ -309,9 +316,9 @@ impl fmt::Debug for Ethereum { } } impl Ethereum { - pub async fn new(db: D, url: String) -> Self { + pub async fn new(db: D, daemon_url: String, relayer_url: String) -> Self { let provider = Arc::new(RootProvider::new( - ClientBuilder::default().transport(SimpleRequest::new(url), true), + ClientBuilder::default().transport(SimpleRequest::new(daemon_url), true), )); let mut deployer = Deployer::new(provider.clone()).await; @@ -322,7 +329,9 @@ impl Ethereum { } let deployer = deployer.unwrap().unwrap(); - Ethereum { db, provider, deployer, router: Arc::new(RwLock::new(None)) } + dbg!(&relayer_url); + dbg!(relayer_url.len()); + Ethereum { db, relayer_url, provider, deployer, router: Arc::new(RwLock::new(None)) } } // Obtain a reference to the Router, sleeping until it's deployed if it hasn't already been. @@ -423,10 +432,10 @@ impl Network for Ethereum { async fn get_latest_block_number(&self) -> Result { let actual_number = self .provider - .get_block(BlockNumberOrTag::Finalized.into(), false) + .get_block(BlockNumberOrTag::Finalized.into(), BlockTransactionsKind::Hashes) .await .map_err(|_| NetworkError::ConnectionError)? - .expect("no blocks were finalized") + .ok_or(NetworkError::ConnectionError)? .header .number .unwrap(); @@ -451,7 +460,7 @@ impl Network for Ethereum { } else { self .provider - .get_block(u64::try_from(start - 1).unwrap().into(), false) + .get_block(u64::try_from(start - 1).unwrap().into(), BlockTransactionsKind::Hashes) .await .ok() .flatten() @@ -464,7 +473,7 @@ impl Network for Ethereum { let end_header = self .provider - .get_block(u64::try_from(start + 31).unwrap().into(), false) + .get_block(u64::try_from(start + 31).unwrap().into(), BlockTransactionsKind::Hashes) .await .ok() .flatten() @@ -714,8 +723,32 @@ impl Network for Ethereum { // Publish this to the dedicated TX server for a solver to actually publish #[cfg(not(test))] { - let _ = completion; - todo!("TODO"); + let mut msg = vec![]; + match completion.command() { + RouterCommand::UpdateSeraiKey { nonce, .. } | RouterCommand::Execute { nonce, .. } => { + msg.extend(&u32::try_from(nonce).unwrap().to_le_bytes()); + } + } + completion.write(&mut msg).unwrap(); + + let Ok(mut socket) = TcpStream::connect(&self.relayer_url).await else { + log::warn!("couldn't connect to the relayer server"); + Err(NetworkError::ConnectionError)? + }; + let Ok(()) = socket.write_all(&u32::try_from(msg.len()).unwrap().to_le_bytes()).await else { + log::warn!("couldn't send the message's len to the relayer server"); + Err(NetworkError::ConnectionError)? + }; + let Ok(()) = socket.write_all(&msg).await else { + log::warn!("couldn't write the message to the relayer server"); + Err(NetworkError::ConnectionError)? + }; + if socket.read_u8().await.ok() != Some(1) { + log::warn!("didn't get the ack from the relayer server"); + Err(NetworkError::ConnectionError)?; + } + + Ok(()) } // Publish this using a dummy account we fund with magic RPC commands @@ -774,7 +807,7 @@ impl Network for Ethereum { async fn get_block_number(&self, id: &>::Id) -> usize { self .provider - .get_block(B256::from(*id).into(), false) + .get_block(B256::from(*id).into(), BlockTransactionsKind::Hashes) .await .unwrap() .unwrap() diff --git a/processor/src/networks/monero.rs b/processor/src/networks/monero.rs index 8d4d1760..54a3af24 100644 --- a/processor/src/networks/monero.rs +++ b/processor/src/networks/monero.rs @@ -13,19 +13,20 @@ use ciphersuite::group::{ff::Field, Group}; use dalek_ff_group::{Scalar, EdwardsPoint}; use frost::{curve::Ed25519, ThresholdKeys}; -use monero_serai::{ - Protocol, +use monero_simple_request_rpc::SimpleRequestRpc; +use monero_wallet::{ ringct::RctType, transaction::Transaction, block::Block, - rpc::{RpcError, HttpRpc, Rpc}, - wallet::{ - ViewPair, Scanner, - address::{Network as MoneroNetwork, SubaddressIndex, AddressSpec}, - Fee, SpendableOutput, Change, Decoys, TransactionError, - SignableTransaction as MSignableTransaction, Eventuality, TransactionMachine, + rpc::{FeeRate, RpcError, Rpc}, + address::{Network as MoneroNetwork, SubaddressIndex}, + ViewPair, GuaranteedViewPair, WalletOutput, OutputWithDecoys, GuaranteedScanner, + send::{ + SendError, Change, SignableTransaction as MSignableTransaction, Eventuality, TransactionMachine, }, }; +#[cfg(test)] +use monero_wallet::Scanner; use tokio::time::sleep; @@ -45,7 +46,7 @@ use crate::{ }; #[derive(Clone, PartialEq, Eq, Debug)] -pub struct Output(SpendableOutput, Vec); +pub struct Output(WalletOutput); const EXTERNAL_SUBADDRESS: Option = SubaddressIndex::new(0, 0); const BRANCH_SUBADDRESS: Option = SubaddressIndex::new(1, 0); @@ -59,7 +60,7 @@ impl OutputTrait for Output { type Id = [u8; 32]; fn kind(&self) -> OutputType { - match self.0.output.metadata.subaddress { + match self.0.subaddress() { EXTERNAL_SUBADDRESS => OutputType::External, BRANCH_SUBADDRESS => OutputType::Branch, CHANGE_SUBADDRESS => OutputType::Change, @@ -69,15 +70,15 @@ impl OutputTrait for Output { } fn id(&self) -> Self::Id { - self.0.output.data.key.compress().to_bytes() + self.0.key().compress().to_bytes() } fn tx_id(&self) -> [u8; 32] { - self.0.output.absolute.tx + self.0.transaction() } fn key(&self) -> EdwardsPoint { - EdwardsPoint(self.0.output.data.key - (EdwardsPoint::generator().0 * self.0.key_offset())) + EdwardsPoint(self.0.key() - (EdwardsPoint::generator().0 * self.0.key_offset())) } fn presumed_origin(&self) -> Option

{ @@ -89,29 +90,26 @@ impl OutputTrait for Output { } fn data(&self) -> &[u8] { - &self.1 + let Some(data) = self.0.arbitrary_data().first() else { return &[] }; + // If the data is too large, prune it + // This should cause decoding the instruction to fail, and trigger a refund as appropriate + if data.len() > usize::try_from(MAX_DATA_LEN).unwrap() { + return &[]; + } + data } fn write(&self, writer: &mut W) -> io::Result<()> { self.0.write(writer)?; - writer.write_all(&u16::try_from(self.1.len()).unwrap().to_le_bytes())?; - writer.write_all(&self.1)?; Ok(()) } fn read(reader: &mut R) -> io::Result { - let output = SpendableOutput::read(reader)?; - - let mut data_len = [0; 2]; - reader.read_exact(&mut data_len)?; - - let mut data = vec![0; usize::from(u16::from_le_bytes(data_len))]; - reader.read_exact(&mut data)?; - - Ok(Output(output, data)) + Ok(Output(WalletOutput::read(reader)?)) } } +// TODO: Consider ([u8; 32], TransactionPruned) #[async_trait] impl TransactionTrait for Transaction { type Id = [u8; 32]; @@ -121,7 +119,10 @@ impl TransactionTrait for Transaction { #[cfg(test)] async fn fee(&self, _: &Monero) -> u64 { - self.rct_signatures.base.fee + match self { + Transaction::V1 { .. } => panic!("v1 TX in test-only function"), + Transaction::V2 { ref proofs, .. } => proofs.as_ref().unwrap().base.fee, + } } } @@ -134,7 +135,7 @@ impl EventualityTrait for Eventuality { // Extra includess the one time keys which are derived from the plan ID, so a collision here is a // hash collision fn lookup(&self) -> Vec { - self.extra().to_vec() + self.extra() } fn read(reader: &mut R) -> io::Result { @@ -156,13 +157,10 @@ impl EventualityTrait for Eventuality { } #[derive(Clone, Debug)] -pub struct SignableTransaction { - transcript: RecommendedTranscript, - actual: MSignableTransaction, -} +pub struct SignableTransaction(MSignableTransaction); impl SignableTransactionTrait for SignableTransaction { fn fee(&self) -> u64 { - self.actual.fee() + self.0.necessary_fee() } } @@ -179,17 +177,17 @@ impl BlockTrait for Block { async fn time(&self, rpc: &Monero) -> u64 { // Constant from Monero - const BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW: u64 = 60; + const BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW: usize = 60; // If Monero doesn't have enough blocks to build a window, it doesn't define a network time if (self.number().unwrap() + 1) < BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW { // Use the block number as the time - return self.number().unwrap(); + return u64::try_from(self.number().unwrap()).unwrap(); } let mut timestamps = vec![self.header.timestamp]; let mut parent = self.parent(); - while u64::try_from(timestamps.len()).unwrap() < BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW { + while timestamps.len() < BLOCKCHAIN_TIMESTAMP_CHECK_WINDOW { let mut parent_block; while { parent_block = rpc.rpc.get_block(parent).await; @@ -220,13 +218,13 @@ impl BlockTrait for Block { // Monero also solely requires the block's time not be less than the median, it doesn't ensure // it advances the median forward // Ensure monotonicity despite both these issues by adding the block number to the median time - res + self.number().unwrap() + res + u64::try_from(self.number().unwrap()).unwrap() } } #[derive(Clone, Debug)] pub struct Monero { - rpc: Rpc, + rpc: SimpleRequestRpc, } // Shim required for testing/debugging purposes due to generic arguments also necessitating trait // bounds @@ -247,31 +245,32 @@ fn map_rpc_err(err: RpcError) -> NetworkError { NetworkError::ConnectionError } +enum MakeSignableTransactionResult { + Fee(u64), + SignableTransaction(MSignableTransaction), +} + impl Monero { pub async fn new(url: String) -> Monero { - let mut res = HttpRpc::new(url.clone()).await; + let mut res = SimpleRequestRpc::new(url.clone()).await; while let Err(e) = res { log::error!("couldn't connect to Monero node: {e:?}"); tokio::time::sleep(Duration::from_secs(5)).await; - res = HttpRpc::new(url.clone()).await; + res = SimpleRequestRpc::new(url.clone()).await; } Monero { rpc: res.unwrap() } } - fn view_pair(spend: EdwardsPoint) -> ViewPair { - ViewPair::new(spend.0, Zeroizing::new(additional_key::(0).0)) + fn view_pair(spend: EdwardsPoint) -> GuaranteedViewPair { + GuaranteedViewPair::new(spend.0, Zeroizing::new(additional_key::(0).0)).unwrap() } fn address_internal(spend: EdwardsPoint, subaddress: Option) -> Address { - Address::new(Self::view_pair(spend).address( - MoneroNetwork::Mainnet, - AddressSpec::Featured { subaddress, payment_id: None, guaranteed: true }, - )) - .unwrap() + Address::new(Self::view_pair(spend).address(MoneroNetwork::Mainnet, subaddress, None)).unwrap() } - fn scanner(spend: EdwardsPoint) -> Scanner { - let mut scanner = Scanner::from_view(Self::view_pair(spend), None); + fn scanner(spend: EdwardsPoint) -> GuaranteedScanner { + let mut scanner = GuaranteedScanner::new(Self::view_pair(spend)); debug_assert!(EXTERNAL_SUBADDRESS.is_none()); scanner.register_subaddress(BRANCH_SUBADDRESS.unwrap()); scanner.register_subaddress(CHANGE_SUBADDRESS.unwrap()); @@ -279,26 +278,24 @@ impl Monero { scanner } - async fn median_fee(&self, block: &Block) -> Result { + async fn median_fee(&self, block: &Block) -> Result { let mut fees = vec![]; - for tx_hash in &block.txs { + for tx_hash in &block.transactions { let tx = self.rpc.get_transaction(*tx_hash).await.map_err(|_| NetworkError::ConnectionError)?; // Only consider fees from RCT transactions, else the fee property read wouldn't be accurate - if tx.rct_signatures.rct_type() != RctType::Null { - continue; - } - // This isn't entirely accurate as Bulletproof TXs will have a higher weight than their - // serialization length - // It's likely 'good enough' - // TODO2: Improve - fees.push(tx.rct_signatures.base.fee / u64::try_from(tx.serialize().len()).unwrap()); + let fee = match &tx { + Transaction::V2 { proofs: Some(proofs), .. } => proofs.base.fee, + _ => continue, + }; + fees.push(fee / u64::try_from(tx.weight()).unwrap()); } fees.sort(); let fee = fees.get(fees.len() / 2).copied().unwrap_or(0); // TODO: Set a sane minimum fee - Ok(Fee { per_weight: fee.max(1500000), mask: 10000 }) + const MINIMUM_FEE: u64 = 1_500_000; + Ok(FeeRate::new(fee.max(MINIMUM_FEE), 10000).unwrap()) } async fn make_signable_transaction( @@ -309,7 +306,7 @@ impl Monero { payments: &[Payment], change: &Option
, calculating_fee: bool, - ) -> Result, NetworkError> { + ) -> Result, NetworkError> { for payment in payments { assert_eq!(payment.balance.coin, Coin::Monero); } @@ -318,28 +315,13 @@ impl Monero { let block_for_fee = self.get_block(block_number).await?; let fee_rate = self.median_fee(&block_for_fee).await?; - // Get the protocol for the specified block number - // For now, this should just be v16, the latest deployed protocol, since there's no upcoming - // hard fork to be mindful of - let get_protocol = || Protocol::v16; - - #[cfg(not(test))] - let protocol = get_protocol(); - // If this is a test, we won't be using a mainnet node and need a distinct protocol - // determination - // Just use whatever the node expects - #[cfg(test)] - let protocol = self.rpc.get_protocol().await.unwrap(); - - // Hedge against the above codegen failing by having an always included runtime check - if !cfg!(test) { - assert_eq!(protocol, get_protocol()); - } - - // Check a fork hasn't occurred which this processor hasn't been updated for - assert_eq!(protocol, self.rpc.get_protocol().await.map_err(map_rpc_err)?); - - let spendable_outputs = inputs.iter().map(|input| input.0.clone()).collect::>(); + // Determine the RCT proofs to make based off the hard fork + // TODO: Make a fn for this block which is duplicated with tests + let rct_type = match block_for_fee.header.hardfork_version { + 14 => RctType::ClsagBulletproof, + 15 | 16 => RctType::ClsagBulletproofPlus, + _ => panic!("Monero hard forked and the processor wasn't updated for it"), + }; let mut transcript = RecommendedTranscript::new(b"Serai Processor Monero Transaction Transcript"); @@ -347,17 +329,25 @@ impl Monero { // All signers need to select the same decoys // All signers use the same height and a seeded RNG to make sure they do so. - let decoys = Decoys::fingerprintable_canonical_select( - &mut ChaCha20Rng::from_seed(transcript.rng_seed(b"decoys")), - &self.rpc, - protocol.ring_len(), - block_number + 1, - &spendable_outputs, - ) - .await - .map_err(map_rpc_err)?; - - let inputs = spendable_outputs.into_iter().zip(decoys).collect::>(); + let mut inputs_actual = Vec::with_capacity(inputs.len()); + for input in inputs { + inputs_actual.push( + OutputWithDecoys::fingerprintable_deterministic_new( + &mut ChaCha20Rng::from_seed(transcript.rng_seed(b"decoys")), + &self.rpc, + // TODO: Have Decoys take RctType + match rct_type { + RctType::ClsagBulletproof => 11, + RctType::ClsagBulletproofPlus => 16, + _ => panic!("selecting decoys for an unsupported RctType"), + }, + block_number + 1, + input.0.clone(), + ) + .await + .map_err(map_rpc_err)?, + ); + } // Monero requires at least two outputs // If we only have one output planned, add a dummy payment @@ -369,7 +359,8 @@ impl Monero { payments.push(Payment { address: Address::new( ViewPair::new(EdwardsPoint::generator().0, Zeroizing::new(Scalar::ONE.0)) - .address(MoneroNetwork::Mainnet, AddressSpec::Standard), + .unwrap() + .legacy_address(MoneroNetwork::Mainnet), ) .unwrap(), balance: Balance { coin: Coin::Monero, amount: Amount(0) }, @@ -379,56 +370,69 @@ impl Monero { let payments = payments .into_iter() - // If we're solely estimating the fee, don't actually specify an amount - // This won't affect the fee calculation yet will ensure we don't hit an out of funds error - .map(|payment| { - (payment.address.into(), if calculating_fee { 0 } else { payment.balance.amount.0 }) - }) + .map(|payment| (payment.address.into(), payment.balance.amount.0)) .collect::>(); match MSignableTransaction::new( - protocol, - // Use the plan ID as the r_seed - // This perfectly binds the plan while simultaneously allowing verifying the plan was - // executed with no additional communication - Some(Zeroizing::new(*plan_id)), - inputs.clone(), + rct_type, + // Use the plan ID as the outgoing view key + Zeroizing::new(*plan_id), + inputs_actual, payments, - &Change::fingerprintable(change.as_ref().map(|change| change.clone().into())), + Change::fingerprintable(change.as_ref().map(|change| change.clone().into())), vec![], fee_rate, ) { - Ok(signable) => Ok(Some((transcript, signable))), + Ok(signable) => Ok(Some({ + if calculating_fee { + MakeSignableTransactionResult::Fee(signable.necessary_fee()) + } else { + MakeSignableTransactionResult::SignableTransaction(signable) + } + })), Err(e) => match e { - TransactionError::MultiplePaymentIds => { + SendError::UnsupportedRctType => { + panic!("trying to use an RctType unsupported by monero-wallet") + } + SendError::NoInputs | + SendError::InvalidDecoyQuantity | + SendError::NoOutputs | + SendError::TooManyOutputs | + SendError::NoChange | + SendError::TooMuchArbitraryData | + SendError::TooLargeTransaction | + SendError::WrongPrivateKey => { + panic!("created an invalid Monero transaction: {e}"); + } + SendError::MultiplePaymentIds => { panic!("multiple payment IDs despite not supporting integrated addresses"); } - TransactionError::NoInputs | - TransactionError::NoOutputs | - TransactionError::InvalidDecoyQuantity | - TransactionError::NoChange | - TransactionError::TooManyOutputs | - TransactionError::TooMuchData | - TransactionError::TooLargeTransaction | - TransactionError::WrongPrivateKey => { - panic!("created an Monero invalid transaction: {e}"); - } - TransactionError::ClsagError(_) | - TransactionError::InvalidTransaction(_) | - TransactionError::FrostError(_) => { - panic!("supposedly unreachable (at this time) Monero error: {e}"); - } - TransactionError::NotEnoughFunds { inputs, outputs, fee } => { + SendError::NotEnoughFunds { inputs, outputs, necessary_fee } => { log::debug!( - "Monero NotEnoughFunds. inputs: {:?}, outputs: {:?}, fee: {fee}", + "Monero NotEnoughFunds. inputs: {:?}, outputs: {:?}, necessary_fee: {necessary_fee:?}", inputs, outputs ); - Ok(None) + match necessary_fee { + Some(necessary_fee) => { + // If we're solely calculating the fee, return the fee this TX will cost + if calculating_fee { + Ok(Some(MakeSignableTransactionResult::Fee(necessary_fee))) + } else { + // If we're actually trying to make the TX, return None + Ok(None) + } + } + // We didn't have enough funds to even cover the outputs + None => { + // Ensure we're not misinterpreting this + assert!(outputs > inputs); + Ok(None) + } + } } - TransactionError::RpcError(e) => { - log::error!("RpcError when preparing transaction: {e:?}"); - Err(map_rpc_err(e)) + SendError::MaliciousSerialization | SendError::ClsagError(_) | SendError::FrostError(_) => { + panic!("supposedly unreachable (at this time) Monero error: {e}"); } }, } @@ -436,18 +440,17 @@ impl Monero { #[cfg(test)] fn test_view_pair() -> ViewPair { - ViewPair::new(*EdwardsPoint::generator(), Zeroizing::new(Scalar::ONE.0)) + ViewPair::new(*EdwardsPoint::generator(), Zeroizing::new(Scalar::ONE.0)).unwrap() } #[cfg(test)] fn test_scanner() -> Scanner { - Scanner::from_view(Self::test_view_pair(), Some(std::collections::HashSet::new())) + Scanner::new(Self::test_view_pair()) } #[cfg(test)] fn test_address() -> Address { - Address::new(Self::test_view_pair().address(MoneroNetwork::Mainnet, AddressSpec::Standard)) - .unwrap() + Address::new(Self::test_view_pair().legacy_address(MoneroNetwork::Mainnet)).unwrap() } } @@ -475,7 +478,6 @@ impl Network for Monero { const MAX_OUTPUTS: usize = 16; // 0.01 XMR - // TODO: Set a sane dust const DUST: u64 = 10000000000; // TODO @@ -528,34 +530,17 @@ impl Network for Monero { } }; - let mut txs = outputs - .iter() - .filter_map(|outputs| Some(outputs.not_locked()).filter(|outputs| !outputs.is_empty())) - .collect::>(); + // Miner transactions are required to explicitly state their timelock, so this does exclude + // those (which have an extended timelock we don't want to deal with) + let raw_outputs = outputs.not_additionally_locked(); + let mut outputs = Vec::with_capacity(raw_outputs.len()); + for output in raw_outputs { + // This should be pointless as we shouldn't be able to scan for any other subaddress + // This just helps ensures nothing invalid makes it through + assert!([EXTERNAL_SUBADDRESS, BRANCH_SUBADDRESS, CHANGE_SUBADDRESS, FORWARD_SUBADDRESS] + .contains(&output.subaddress())); - // This should be pointless as we shouldn't be able to scan for any other subaddress - // This just ensures nothing invalid makes it through - for tx_outputs in &txs { - for output in tx_outputs { - assert!([EXTERNAL_SUBADDRESS, BRANCH_SUBADDRESS, CHANGE_SUBADDRESS, FORWARD_SUBADDRESS] - .contains(&output.output.metadata.subaddress)); - } - } - - let mut outputs = Vec::with_capacity(txs.len()); - for mut tx_outputs in txs.drain(..) { - for output in tx_outputs.drain(..) { - let mut data = output.arbitrary_data().first().cloned().unwrap_or(vec![]); - - // The Output serialization code above uses u16 to represent length - data.truncate(u16::MAX.into()); - // Monero data segments should be <= 255 already, and MAX_DATA_LEN is currently 512 - // This just allows either Monero to change, or MAX_DATA_LEN to change, without introducing - // complicationso - data.truncate(MAX_DATA_LEN.try_into().unwrap()); - - outputs.push(Output(output, data)); - } + outputs.push(Output(output)); } outputs @@ -577,7 +562,7 @@ impl Network for Monero { block: &Block, res: &mut HashMap<[u8; 32], (usize, [u8; 32], Transaction)>, ) { - for hash in &block.txs { + for hash in &block.transactions { let tx = { let mut tx; while { @@ -590,23 +575,21 @@ impl Network for Monero { tx.unwrap() }; - if let Some((_, eventuality)) = eventualities.map.get(&tx.prefix.extra) { - if eventuality.matches(&tx) { + if let Some((_, eventuality)) = eventualities.map.get(&tx.prefix().extra) { + if eventuality.matches(&tx.clone().into()) { res.insert( - eventualities.map.remove(&tx.prefix.extra).unwrap().0, - (usize::try_from(block.number().unwrap()).unwrap(), tx.id(), tx), + eventualities.map.remove(&tx.prefix().extra).unwrap().0, + (block.number().unwrap(), tx.id(), tx), ); } } } eventualities.block_number += 1; - assert_eq!(eventualities.block_number, usize::try_from(block.number().unwrap()).unwrap()); + assert_eq!(eventualities.block_number, block.number().unwrap()); } - for block_num in - (eventualities.block_number + 1) .. usize::try_from(block.number().unwrap()).unwrap() - { + for block_num in (eventualities.block_number + 1) .. block.number().unwrap() { let block = { let mut block; while { @@ -624,7 +607,7 @@ impl Network for Monero { // Also check the current block check_block(self, eventualities, block, &mut res).await; - assert_eq!(eventualities.block_number, usize::try_from(block.number().unwrap()).unwrap()); + assert_eq!(eventualities.block_number, block.number().unwrap()); res } @@ -636,12 +619,14 @@ impl Network for Monero { payments: &[Payment], change: &Option
, ) -> Result, NetworkError> { - Ok( - self - .make_signable_transaction(block_number, &[0; 32], inputs, payments, change, true) - .await? - .map(|(_, signable)| signable.fee()), - ) + let res = self + .make_signable_transaction(block_number, &[0; 32], inputs, payments, change, true) + .await?; + let Some(res) = res else { return Ok(None) }; + let MakeSignableTransactionResult::Fee(fee) = res else { + panic!("told make_signable_transaction calculating_fee and got transaction") + }; + Ok(Some(fee)) } async fn signable_transaction( @@ -654,16 +639,17 @@ impl Network for Monero { change: &Option
, (): &(), ) -> Result, NetworkError> { - Ok( - self - .make_signable_transaction(block_number, plan_id, inputs, payments, change, false) - .await? - .map(|(transcript, signable)| { - let signable = SignableTransaction { transcript, actual: signable }; - let eventuality = signable.actual.eventuality().unwrap(); - (signable, eventuality) - }), - ) + let res = self + .make_signable_transaction(block_number, plan_id, inputs, payments, change, false) + .await?; + let Some(res) = res else { return Ok(None) }; + let MakeSignableTransactionResult::SignableTransaction(signable) = res else { + panic!("told make_signable_transaction not calculating_fee and got fee") + }; + + let signable = SignableTransaction(signable); + let eventuality = signable.0.clone().into(); + Ok(Some((signable, eventuality))) } async fn attempt_sign( @@ -671,7 +657,7 @@ impl Network for Monero { keys: ThresholdKeys, transaction: SignableTransaction, ) -> Result { - match transaction.actual.clone().multisig(&keys, transaction.transcript) { + match transaction.0.clone().multisig(&keys) { Ok(machine) => Ok(machine), Err(e) => panic!("failed to create a multisig machine for TX: {e}"), } @@ -696,7 +682,7 @@ impl Network for Monero { id: &[u8; 32], ) -> Result, NetworkError> { let tx = self.rpc.get_transaction(*id).await.map_err(map_rpc_err)?; - if eventuality.matches(&tx) { + if eventuality.matches(&tx.clone().into()) { Ok(Some(tx)) } else { Ok(None) @@ -705,7 +691,7 @@ impl Network for Monero { #[cfg(test)] async fn get_block_number(&self, id: &[u8; 32]) -> usize { - self.rpc.get_block(*id).await.unwrap().number().unwrap().try_into().unwrap() + self.rpc.get_block(*id).await.unwrap().number().unwrap() } #[cfg(test)] @@ -714,7 +700,7 @@ impl Network for Monero { eventuality: &Self::Eventuality, claim: &[u8; 32], ) -> bool { - return eventuality.matches(&self.rpc.get_transaction(*claim).await.unwrap()); + return eventuality.matches(&self.rpc.get_pruned_transaction(*claim).await.unwrap()); } #[cfg(test)] @@ -724,9 +710,9 @@ impl Network for Monero { eventuality: &Eventuality, ) -> Transaction { let block = self.rpc.get_block_by_number(block).await.unwrap(); - for tx in &block.txs { + for tx in &block.transactions { let tx = self.rpc.get_transaction(*tx).await.unwrap(); - if eventuality.matches(&tx) { + if eventuality.matches(&tx.clone().into()) { return tx; } } @@ -737,69 +723,59 @@ impl Network for Monero { async fn mine_block(&self) { // https://github.com/serai-dex/serai/issues/198 sleep(std::time::Duration::from_millis(100)).await; - - #[derive(Debug, serde::Deserialize)] - struct EmptyResponse {} - let _: EmptyResponse = self - .rpc - .rpc_call( - "json_rpc", - Some(serde_json::json!({ - "method": "generateblocks", - "params": { - "wallet_address": Self::test_address().to_string(), - "amount_of_blocks": 1 - }, - })), - ) - .await - .unwrap(); + self.rpc.generate_blocks(&Self::test_address().into(), 1).await.unwrap(); } #[cfg(test)] async fn test_send(&self, address: Address) -> Block { use zeroize::Zeroizing; - use rand_core::OsRng; - use monero_serai::wallet::FeePriority; + use rand_core::{RngCore, OsRng}; + use monero_wallet::rpc::FeePriority; let new_block = self.get_latest_block_number().await.unwrap() + 1; for _ in 0 .. 80 { self.mine_block().await; } - let outputs = Self::test_scanner() - .scan(&self.rpc, &self.rpc.get_block_by_number(new_block).await.unwrap()) - .await - .unwrap() - .swap_remove(0) - .ignore_timelock(); + let new_block = self.rpc.get_block_by_number(new_block).await.unwrap(); + let mut outputs = + Self::test_scanner().scan(&self.rpc, &new_block).await.unwrap().ignore_additional_timelock(); + let output = outputs.swap_remove(0); - let amount = outputs[0].commitment().amount; + let amount = output.commitment().amount; // The dust should always be sufficient for the fee let fee = Monero::DUST; - let protocol = self.rpc.get_protocol().await.unwrap(); + let rct_type = match new_block.header.hardfork_version { + 14 => RctType::ClsagBulletproof, + 15 | 16 => RctType::ClsagBulletproofPlus, + _ => panic!("Monero hard forked and the processor wasn't updated for it"), + }; - let decoys = Decoys::fingerprintable_canonical_select( + let output = OutputWithDecoys::fingerprintable_deterministic_new( &mut OsRng, &self.rpc, - protocol.ring_len(), + match rct_type { + RctType::ClsagBulletproof => 11, + RctType::ClsagBulletproofPlus => 16, + _ => panic!("selecting decoys for an unsupported RctType"), + }, self.rpc.get_height().await.unwrap(), - &outputs, + output, ) .await .unwrap(); - let inputs = outputs.into_iter().zip(decoys).collect::>(); - + let mut outgoing_view_key = Zeroizing::new([0; 32]); + OsRng.fill_bytes(outgoing_view_key.as_mut()); let tx = MSignableTransaction::new( - protocol, - None, - inputs, + rct_type, + outgoing_view_key, + vec![output], vec![(address.into(), amount - fee)], - &Change::fingerprintable(Some(Self::test_address().into())), + Change::fingerprintable(Some(Self::test_address().into())), vec![], - self.rpc.get_fee(protocol, FeePriority::Unimportant).await.unwrap(), + self.rpc.get_fee_rate(FeePriority::Unimportant).await.unwrap(), ) .unwrap() .sign(&mut OsRng, &Zeroizing::new(Scalar::ONE.0)) diff --git a/processor/src/tests/literal/mod.rs b/processor/src/tests/literal/mod.rs index 5c5f3203..d45649d5 100644 --- a/processor/src/tests/literal/mod.rs +++ b/processor/src/tests/literal/mod.rs @@ -70,7 +70,7 @@ mod bitcoin { // btc key pair to send from let private_key = PrivateKey::new(SecretKey::new(&mut rand_core::OsRng), BNetwork::Regtest); let public_key = PublicKey::from_private_key(SECP256K1, &private_key); - let main_addr = BAddress::p2pkh(&public_key, BNetwork::Regtest); + let main_addr = BAddress::p2pkh(public_key, BNetwork::Regtest); // get unlocked coins let new_block = btc.get_latest_block_number().await.unwrap() + 1; @@ -107,7 +107,7 @@ mod bitcoin { version: Version(2), lock_time: LockTime::ZERO, input: vec![TxIn { - previous_output: OutPoint { txid: tx.txid(), vout: 0 }, + previous_output: OutPoint { txid: tx.compute_txid(), vout: 0 }, script_sig: Script::new().into(), sequence: Sequence(u32::MAX), witness: Witness::default(), @@ -128,14 +128,14 @@ mod bitcoin { version: Version(2), lock_time: LockTime::ZERO, input: vec![TxIn { - previous_output: OutPoint { txid: tx.txid(), vout: 0 }, + previous_output: OutPoint { txid: tx.compute_txid(), vout: 0 }, script_sig: Script::new().into(), sequence: Sequence(u32::MAX), witness: Witness::new(), }], output: vec![TxOut { value: tx.output[0].value - BAmount::from_sat(10000), - script_pubkey: serai_btc_address.as_ref().script_pubkey(), + script_pubkey: serai_btc_address.into(), }], }; @@ -143,12 +143,14 @@ mod bitcoin { // This is the standard script with an extra argument of the InInstruction let mut sig = SECP256K1 .sign_ecdsa_low_r( - &Message::from( + &Message::from_digest_slice( SighashCache::new(&tx) .p2wsh_signature_hash(0, &script, initial_output_value, EcdsaSighashType::All) .unwrap() - .to_raw_hash(), - ), + .to_raw_hash() + .as_ref(), + ) + .unwrap(), &private_key.inner, ) .serialize_der() @@ -421,7 +423,7 @@ mod ethereum { }); } - Ethereum::new(db, url.clone()).await + Ethereum::new(db, url.clone(), String::new()).await }) } } diff --git a/processor/src/tests/scanner.rs b/processor/src/tests/scanner.rs index 078a07d5..6421c499 100644 --- a/processor/src/tests/scanner.rs +++ b/processor/src/tests/scanner.rs @@ -115,6 +115,12 @@ pub async fn test_scanner( pub async fn test_no_deadlock_in_multisig_completed( new_network: impl Fn(MemDb) -> Pin>>, ) { + // This test scans two blocks then acknowledges one, yet a network with one confirm won't scan + // two blocks before the first is acknowledged (due to the look-ahead limit) + if N::CONFIRMATIONS <= 1 { + return; + } + let mut db = MemDb::new(); let network = new_network(db.clone()).await; @@ -139,6 +145,10 @@ pub async fn test_no_deadlock_in_multisig_completed( let mut txn = db.txn(); NetworkKeyDb::set(&mut txn, Session(0), &key.to_bytes().as_ref().to_vec()); txn.commit(); + + // Sleep for 5 seconds as setting the Network key value will trigger an async task for + // Ethereum + tokio::time::sleep(Duration::from_secs(5)).await; } key }; @@ -158,6 +168,7 @@ pub async fn test_no_deadlock_in_multisig_completed( network.mine_block().await; } + // Block for the second set of keys registered let block_id = match timeout(Duration::from_secs(30), scanner.events.recv()).await.unwrap().unwrap() { ScannerEvent::Block { is_retirement_block, block, outputs: _ } => { @@ -170,6 +181,7 @@ pub async fn test_no_deadlock_in_multisig_completed( } }; + // Block for the third set of keys registered match timeout(Duration::from_secs(30), scanner.events.recv()).await.unwrap().unwrap() { ScannerEvent::Block { .. } => {} ScannerEvent::Completed(_, _, _, _, _) => { diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 77a0cea2..fe982784 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,5 +1,5 @@ [toolchain] -channel = "1.77" +channel = "1.79" targets = ["wasm32-unknown-unknown"] profile = "minimal" components = ["rust-src", "rustfmt", "clippy"] diff --git a/substrate/abi/Cargo.toml b/substrate/abi/Cargo.toml index e442e86c..c2947aaa 100644 --- a/substrate/abi/Cargo.toml +++ b/substrate/abi/Cargo.toml @@ -16,29 +16,50 @@ rustdoc-args = ["--cfg", "docsrs"] workspace = true [dependencies] -scale = { package = "parity-scale-codec", version = "3", features = ["derive"] } -scale-info = { version = "2", features = ["derive"] } +scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] } +scale-info = { version = "2", default-features = false, features = ["derive"] } -borsh = { version = "1", features = ["derive", "de_strict_order"], optional = true } -serde = { version = "1", features = ["derive", "alloc"], optional = true } +borsh = { version = "1", default-features = false, features = ["derive", "de_strict_order"], optional = true } +serde = { version = "1", default-features = false, features = ["derive", "alloc"], optional = true } -sp-core = { git = "https://github.com/serai-dex/substrate" } -sp-runtime = { git = "https://github.com/serai-dex/substrate" } +sp-core = { git = "https://github.com/serai-dex/substrate", default-features = false } +sp-runtime = { git = "https://github.com/serai-dex/substrate", default-features = false } -sp-consensus-babe = { git = "https://github.com/serai-dex/substrate" } -sp-consensus-grandpa = { git = "https://github.com/serai-dex/substrate" } +sp-consensus-babe = { git = "https://github.com/serai-dex/substrate", default-features = false } +sp-consensus-grandpa = { git = "https://github.com/serai-dex/substrate", default-features = false } -serai-primitives = { path = "../primitives", version = "0.1" } -serai-coins-primitives = { path = "../coins/primitives", version = "0.1" } -serai-validator-sets-primitives = { path = "../validator-sets/primitives", version = "0.1" } -serai-genesis-liquidity-primitives = { path = "../genesis-liquidity/primitives", version = "0.1" } -serai-emissions-primitives = { path = "../emissions/primitives", version = "0.1" } -serai-in-instructions-primitives = { path = "../in-instructions/primitives", version = "0.1" } -serai-signals-primitives = { path = "../signals/primitives", version = "0.1" } +frame-support = { git = "https://github.com/serai-dex/substrate", default-features = false } -frame-support = { git = "https://github.com/serai-dex/substrate" } +serai-primitives = { path = "../primitives", version = "0.1", default-features = false } +serai-coins-primitives = { path = "../coins/primitives", version = "0.1", default-features = false } +serai-validator-sets-primitives = { path = "../validator-sets/primitives", version = "0.1", default-features = false } +serai-genesis-liquidity-primitives = { path = "../genesis-liquidity/primitives", version = "0.1", default-features = false } +serai-in-instructions-primitives = { path = "../in-instructions/primitives", version = "0.1", default-features = false } +serai-signals-primitives = { path = "../signals/primitives", version = "0.1", default-features = false } [features] +std = [ + "scale/std", + "scale-info/std", + + "borsh?/std", + "serde?/std", + + "sp-core/std", + "sp-runtime/std", + + "sp-consensus-babe/std", + "sp-consensus-grandpa/std", + + "frame-support/std", + + "serai-primitives/std", + "serai-coins-primitives/std", + "serai-validator-sets-primitives/std", + "serai-genesis-liquidity-primitives/std", + "serai-in-instructions-primitives/std", + "serai-signals-primitives/std", +] borsh = [ "dep:borsh", "serai-primitives/borsh", @@ -57,3 +78,4 @@ serde = [ "serai-in-instructions-primitives/serde", "serai-signals-primitives/serde", ] +default = ["std"] diff --git a/substrate/abi/src/babe.rs b/substrate/abi/src/babe.rs index 29bbee9c..9bba63d9 100644 --- a/substrate/abi/src/babe.rs +++ b/substrate/abi/src/babe.rs @@ -4,7 +4,7 @@ use serai_primitives::{Header, SeraiAddress}; #[derive(Clone, PartialEq, Eq, Debug, scale::Encode, scale::Decode, scale_info::TypeInfo)] pub struct ReportEquivocation { - pub equivocation_proof: Box>, + pub equivocation_proof: alloc::boxed::Box>, pub key_owner_proof: SeraiAddress, } diff --git a/substrate/abi/src/coins.rs b/substrate/abi/src/coins.rs index c3fa2dad..9466db0f 100644 --- a/substrate/abi/src/coins.rs +++ b/substrate/abi/src/coins.rs @@ -5,7 +5,8 @@ use primitives::OutInstructionWithBalance; #[derive(Clone, PartialEq, Eq, Debug, scale::Encode, scale::Decode, scale_info::TypeInfo)] #[cfg_attr(feature = "borsh", derive(borsh::BorshSerialize, borsh::BorshDeserialize))] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", derive(serde::Serialize))] +#[cfg_attr(all(feature = "std", feature = "serde"), derive(serde::Deserialize))] pub enum Call { transfer { to: SeraiAddress, balance: Balance }, burn { balance: Balance }, @@ -14,7 +15,8 @@ pub enum Call { #[derive(Clone, PartialEq, Eq, Debug, scale::Encode, scale::Decode, scale_info::TypeInfo)] #[cfg_attr(feature = "borsh", derive(borsh::BorshSerialize, borsh::BorshDeserialize))] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", derive(serde::Serialize))] +#[cfg_attr(all(feature = "std", feature = "serde"), derive(serde::Deserialize))] pub enum Event { Mint { to: SeraiAddress, balance: Balance }, Burn { from: SeraiAddress, balance: Balance }, diff --git a/substrate/abi/src/dex.rs b/substrate/abi/src/dex.rs index 5136e974..2daa62f0 100644 --- a/substrate/abi/src/dex.rs +++ b/substrate/abi/src/dex.rs @@ -6,7 +6,8 @@ type PoolId = Coin; type MaxSwapPathLength = sp_core::ConstU32<3>; #[derive(Clone, PartialEq, Eq, Debug, scale::Encode, scale::Decode, scale_info::TypeInfo)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", derive(serde::Serialize))] +#[cfg_attr(all(feature = "std", feature = "serde"), derive(serde::Deserialize))] pub enum Call { add_liquidity { coin: Coin, @@ -38,7 +39,8 @@ pub enum Call { } #[derive(Clone, PartialEq, Eq, Debug, scale::Encode, scale::Decode, scale_info::TypeInfo)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", derive(serde::Serialize))] +#[cfg_attr(all(feature = "std", feature = "serde"), derive(serde::Deserialize))] pub enum Event { PoolCreated { pool_id: PoolId, diff --git a/substrate/abi/src/genesis_liquidity.rs b/substrate/abi/src/genesis_liquidity.rs index 2b0c208c..46128414 100644 --- a/substrate/abi/src/genesis_liquidity.rs +++ b/substrate/abi/src/genesis_liquidity.rs @@ -7,7 +7,7 @@ use primitives::*; #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum Call { remove_coin_liquidity { balance: Balance }, - set_initial_price { prices: Prices, signature: Signature }, + oraclize_values { values: Values, signature: Signature }, } #[derive(Clone, PartialEq, Eq, Debug, scale::Encode, scale::Decode, scale_info::TypeInfo)] diff --git a/substrate/abi/src/grandpa.rs b/substrate/abi/src/grandpa.rs index 54de8182..376b0b1d 100644 --- a/substrate/abi/src/grandpa.rs +++ b/substrate/abi/src/grandpa.rs @@ -4,7 +4,7 @@ use serai_primitives::{BlockNumber, SeraiAddress}; #[derive(Clone, PartialEq, Eq, Debug, scale::Encode, scale::Decode, scale_info::TypeInfo)] pub struct ReportEquivocation { - pub equivocation_proof: Box>, + pub equivocation_proof: alloc::boxed::Box>, pub key_owner_proof: SeraiAddress, } @@ -15,10 +15,10 @@ pub enum Call { } #[derive(Clone, PartialEq, Eq, Debug, scale::Encode, scale::Decode, scale_info::TypeInfo)] -#[cfg_attr(feature = "borsh", derive(borsh::BorshSerialize, borsh::BorshDeserialize))] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", derive(serde::Serialize))] +#[cfg_attr(all(feature = "std", feature = "serde"), derive(serde::Deserialize))] pub enum Event { - NewAuthorities { authority_set: Vec<(SeraiAddress, u64)> }, + NewAuthorities { authority_set: alloc::vec::Vec<(SeraiAddress, u64)> }, // TODO: Remove these Paused, Resumed, diff --git a/substrate/abi/src/in_instructions.rs b/substrate/abi/src/in_instructions.rs index 1e5d1bb5..d3ab5ca3 100644 --- a/substrate/abi/src/in_instructions.rs +++ b/substrate/abi/src/in_instructions.rs @@ -5,14 +5,16 @@ use primitives::SignedBatch; #[derive(Clone, PartialEq, Eq, Debug, scale::Encode, scale::Decode, scale_info::TypeInfo)] #[cfg_attr(feature = "borsh", derive(borsh::BorshSerialize, borsh::BorshDeserialize))] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", derive(serde::Serialize))] +#[cfg_attr(all(feature = "std", feature = "serde"), derive(serde::Deserialize))] pub enum Call { execute_batch { batch: SignedBatch }, } #[derive(Clone, PartialEq, Eq, Debug, scale::Encode, scale::Decode, scale_info::TypeInfo)] #[cfg_attr(feature = "borsh", derive(borsh::BorshSerialize, borsh::BorshDeserialize))] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", derive(serde::Serialize))] +#[cfg_attr(all(feature = "std", feature = "serde"), derive(serde::Deserialize))] pub enum Event { Batch { network: NetworkId, id: u32, block: BlockHash, instructions_hash: [u8; 32] }, InstructionFailure { network: NetworkId, id: u32, index: u32 }, diff --git a/substrate/abi/src/lib.rs b/substrate/abi/src/lib.rs index 593e5440..e193b51c 100644 --- a/substrate/abi/src/lib.rs +++ b/substrate/abi/src/lib.rs @@ -1,10 +1,18 @@ +#![cfg_attr(docsrs, feature(doc_cfg))] +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![cfg_attr(not(feature = "std"), no_std)] #![allow(non_camel_case_types)] +extern crate alloc; + +pub use serai_primitives as primitives; + pub mod system; pub mod timestamp; pub mod coins; +pub mod liquidity_tokens; pub mod dex; pub mod validator_sets; @@ -17,16 +25,15 @@ pub mod emissions; pub mod babe; pub mod grandpa; -pub use serai_primitives as primitives; +pub mod tx; #[derive(Clone, PartialEq, Eq, Debug, scale::Encode, scale::Decode, scale_info::TypeInfo)] pub enum Call { - System, Timestamp(timestamp::Call), - TransactionPayment, Coins(coins::Call), - LiquidityTokens(coins::Call), + LiquidityTokens(liquidity_tokens::Call), Dex(dex::Call), + GenesisLiquidity(genesis_liquidity::Call), ValidatorSets(validator_sets::Call), GenesisLiquidity(genesis_liquidity::Call), Emissions(emissions::Call), @@ -48,8 +55,9 @@ pub enum Event { Timestamp, TransactionPayment(TransactionPaymentEvent), Coins(coins::Event), - LiquidityTokens(coins::Event), + LiquidityTokens(liquidity_tokens::Event), Dex(dex::Event), + GenesisLiquidity(genesis_liquidity::Event), ValidatorSets(validator_sets::Event), GenesisLiquidity(genesis_liquidity::Event), Emissions(emissions::Event), @@ -60,16 +68,20 @@ pub enum Event { } #[derive(Clone, Copy, PartialEq, Eq, Debug, scale::Encode, scale::Decode, scale_info::TypeInfo)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", derive(serde::Serialize))] +#[cfg_attr(all(feature = "std", feature = "serde"), derive(serde::Deserialize))] pub struct Extra { pub era: sp_runtime::generic::Era, - pub nonce: scale::Compact, - pub tip: scale::Compact, + #[codec(compact)] + pub nonce: u32, + #[codec(compact)] + pub tip: u64, } #[derive(Clone, PartialEq, Eq, Debug, scale::Encode, scale::Decode, scale_info::TypeInfo)] #[cfg_attr(feature = "borsh", derive(borsh::BorshSerialize, borsh::BorshDeserialize))] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", derive(serde::Serialize))] +#[cfg_attr(all(feature = "std", feature = "serde"), derive(serde::Deserialize))] pub struct SignedPayloadExtra { pub spec_version: u32, pub tx_version: u32, @@ -77,4 +89,4 @@ pub struct SignedPayloadExtra { pub mortality_checkpoint: [u8; 32], } -pub type Transaction = primitives::Transaction; +pub type Transaction = tx::Transaction; diff --git a/substrate/abi/src/signals.rs b/substrate/abi/src/signals.rs index 2c8dd545..6a77672f 100644 --- a/substrate/abi/src/signals.rs +++ b/substrate/abi/src/signals.rs @@ -7,7 +7,8 @@ use primitives::SignalId; #[derive(Clone, PartialEq, Eq, Debug, scale::Encode, scale::Decode, scale_info::TypeInfo)] #[cfg_attr(feature = "borsh", derive(borsh::BorshSerialize, borsh::BorshDeserialize))] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", derive(serde::Serialize))] +#[cfg_attr(all(feature = "std", feature = "serde"), derive(serde::Deserialize))] pub enum Call { register_retirement_signal { in_favor_of: [u8; 32] }, revoke_retirement_signal { retirement_signal_id: [u8; 32] }, @@ -18,7 +19,8 @@ pub enum Call { #[derive(Clone, PartialEq, Eq, Debug, scale::Encode, scale::Decode, scale_info::TypeInfo)] #[cfg_attr(feature = "borsh", derive(borsh::BorshSerialize, borsh::BorshDeserialize))] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", derive(serde::Serialize))] +#[cfg_attr(all(feature = "std", feature = "serde"), derive(serde::Deserialize))] pub enum Event { RetirementSignalRegistered { signal_id: [u8; 32], diff --git a/substrate/abi/src/system.rs b/substrate/abi/src/system.rs index bb67c91c..d025e767 100644 --- a/substrate/abi/src/system.rs +++ b/substrate/abi/src/system.rs @@ -3,7 +3,6 @@ use frame_support::dispatch::{DispatchInfo, DispatchError}; use serai_primitives::SeraiAddress; #[derive(Clone, PartialEq, Eq, Debug, scale::Encode, scale::Decode, scale_info::TypeInfo)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum Event { ExtrinsicSuccess { dispatch_info: DispatchInfo }, ExtrinsicFailed { dispatch_error: DispatchError, dispatch_info: DispatchInfo }, diff --git a/substrate/abi/src/timestamp.rs b/substrate/abi/src/timestamp.rs index c6e7d8cd..af763928 100644 --- a/substrate/abi/src/timestamp.rs +++ b/substrate/abi/src/timestamp.rs @@ -1,5 +1,9 @@ #[derive(Clone, PartialEq, Eq, Debug, scale::Encode, scale::Decode, scale_info::TypeInfo)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", derive(serde::Serialize))] +#[cfg_attr(all(feature = "std", feature = "serde"), derive(serde::Deserialize))] pub enum Call { - set { now: scale::Compact }, + set { + #[codec(compact)] + now: u64, + }, } diff --git a/substrate/abi/src/tx.rs b/substrate/abi/src/tx.rs new file mode 100644 index 00000000..6c61535b --- /dev/null +++ b/substrate/abi/src/tx.rs @@ -0,0 +1,183 @@ +use scale::Encode; + +use sp_core::sr25519::{Public, Signature}; +use sp_runtime::traits::Verify; + +use serai_primitives::SeraiAddress; + +use frame_support::dispatch::GetDispatchInfo; + +pub trait TransactionMember: + Clone + PartialEq + Eq + core::fmt::Debug + scale::Encode + scale::Decode + scale_info::TypeInfo +{ +} +impl< + T: Clone + + PartialEq + + Eq + + core::fmt::Debug + + scale::Encode + + scale::Decode + + scale_info::TypeInfo, + > TransactionMember for T +{ +} + +type TransactionEncodeAs<'a, Extra> = + (&'a crate::Call, &'a Option<(SeraiAddress, Signature, Extra)>); +type TransactionDecodeAs = (crate::Call, Option<(SeraiAddress, Signature, Extra)>); + +// We use our own Transaction struct, over UncheckedExtrinsic, for more control, a bit more +// simplicity, and in order to be immune to https://github.com/paritytech/polkadot-sdk/issues/2947 +#[allow(private_bounds)] +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct Transaction< + Call: 'static + TransactionMember + From, + Extra: 'static + TransactionMember, +> { + call: crate::Call, + mapped_call: Call, + signature: Option<(SeraiAddress, Signature, Extra)>, +} + +impl, Extra: 'static + TransactionMember> + Transaction +{ + pub fn new(call: crate::Call, signature: Option<(SeraiAddress, Signature, Extra)>) -> Self { + Self { call: call.clone(), mapped_call: call.into(), signature } + } + + pub fn call(&self) -> &crate::Call { + &self.call + } +} + +impl, Extra: 'static + TransactionMember> + scale::Encode for Transaction +{ + fn using_encoded R>(&self, f: F) -> R { + let tx: TransactionEncodeAs = (&self.call, &self.signature); + tx.using_encoded(f) + } +} +impl, Extra: 'static + TransactionMember> + scale::Decode for Transaction +{ + fn decode(input: &mut I) -> Result { + let (call, signature) = TransactionDecodeAs::decode(input)?; + let mapped_call = Call::from(call.clone()); + Ok(Self { call, mapped_call, signature }) + } +} +impl, Extra: 'static + TransactionMember> + scale_info::TypeInfo for Transaction +{ + type Identity = TransactionDecodeAs; + + // Define the type info as the info of the type equivalent to what we encode as + fn type_info() -> scale_info::Type { + TransactionDecodeAs::::type_info() + } +} + +#[cfg(feature = "serde")] +mod _serde { + use scale::Encode; + use serde::{ser::*, de::*}; + use super::*; + impl, Extra: 'static + TransactionMember> + Serialize for Transaction + { + fn serialize(&self, serializer: S) -> Result { + let encoded = self.encode(); + serializer.serialize_bytes(&encoded) + } + } + #[cfg(feature = "std")] + impl< + 'a, + Call: 'static + TransactionMember + From, + Extra: 'static + TransactionMember, + > Deserialize<'a> for Transaction + { + fn deserialize>(de: D) -> Result { + let bytes = sp_core::bytes::deserialize(de)?; + ::decode(&mut &bytes[..]) + .map_err(|e| serde::de::Error::custom(format!("invalid transaction: {e}"))) + } + } +} + +impl< + Call: 'static + TransactionMember + From + TryInto, + Extra: 'static + TransactionMember, + > sp_runtime::traits::Extrinsic for Transaction +{ + type Call = Call; + type SignaturePayload = (SeraiAddress, Signature, Extra); + fn is_signed(&self) -> Option { + Some(self.signature.is_some()) + } + fn new(call: Call, signature: Option) -> Option { + Some(Self { call: call.clone().try_into().ok()?, mapped_call: call, signature }) + } +} + +impl< + Call: 'static + TransactionMember + From + TryInto, + Extra: 'static + TransactionMember, + > frame_support::traits::ExtrinsicCall for Transaction +{ + fn call(&self) -> &Call { + &self.mapped_call + } +} + +impl< + Call: 'static + TransactionMember + From, + Extra: 'static + TransactionMember + sp_runtime::traits::SignedExtension, + > sp_runtime::traits::ExtrinsicMetadata for Transaction +{ + type SignedExtensions = Extra; + + const VERSION: u8 = 0; +} + +impl< + Call: 'static + TransactionMember + From + GetDispatchInfo, + Extra: 'static + TransactionMember, + > GetDispatchInfo for Transaction +{ + fn get_dispatch_info(&self) -> frame_support::dispatch::DispatchInfo { + self.mapped_call.get_dispatch_info() + } +} + +impl< + Call: 'static + TransactionMember + From, + Extra: 'static + TransactionMember + sp_runtime::traits::SignedExtension, + > sp_runtime::traits::BlindCheckable for Transaction +{ + type Checked = sp_runtime::generic::CheckedExtrinsic; + + fn check( + self, + ) -> Result { + Ok(match self.signature { + Some((signer, signature, extra)) => { + if !signature.verify( + (&self.call, &extra, extra.additional_signed()?).encode().as_slice(), + &signer.into(), + ) { + Err(sp_runtime::transaction_validity::InvalidTransaction::BadProof)? + } + + sp_runtime::generic::CheckedExtrinsic { + signed: Some((signer.into(), extra)), + function: self.mapped_call, + } + } + None => sp_runtime::generic::CheckedExtrinsic { signed: None, function: self.mapped_call }, + }) + } +} diff --git a/substrate/abi/src/validator_sets.rs b/substrate/abi/src/validator_sets.rs index 1630f8ac..1e1e3359 100644 --- a/substrate/abi/src/validator_sets.rs +++ b/substrate/abi/src/validator_sets.rs @@ -1,4 +1,4 @@ -use sp_core::{ConstU32, bounded_vec::BoundedVec}; +use sp_core::{ConstU32, bounded::BoundedVec}; pub use serai_validator_sets_primitives as primitives; @@ -6,11 +6,12 @@ use serai_primitives::*; use serai_validator_sets_primitives::*; #[derive(Clone, PartialEq, Eq, Debug, scale::Encode, scale::Decode, scale_info::TypeInfo)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", derive(serde::Serialize))] +#[cfg_attr(all(feature = "std", feature = "serde"), derive(serde::Deserialize))] pub enum Call { set_keys { network: NetworkId, - removed_participants: Vec, + removed_participants: BoundedVec>, key_pair: KeyPair, signature: Signature, }, @@ -35,7 +36,8 @@ pub enum Call { #[derive(Clone, PartialEq, Eq, Debug, scale::Encode, scale::Decode, scale_info::TypeInfo)] #[cfg_attr(feature = "borsh", derive(borsh::BorshSerialize, borsh::BorshDeserialize))] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", derive(serde::Serialize))] +#[cfg_attr(all(feature = "std", feature = "serde"), derive(serde::Deserialize))] pub enum Event { NewSet { set: ValidatorSet, diff --git a/substrate/client/Cargo.toml b/substrate/client/Cargo.toml index f97e40fb..7bb252ce 100644 --- a/substrate/client/Cargo.toml +++ b/substrate/client/Cargo.toml @@ -36,10 +36,10 @@ async-lock = "3" simple-request = { path = "../../common/request", version = "0.1", optional = true } -bitcoin = { version = "0.31", optional = true } +bitcoin = { version = "0.32", optional = true } ciphersuite = { path = "../../crypto/ciphersuite", version = "0.4", optional = true } -monero-serai = { path = "../../coins/monero", version = "0.1.4-alpha", optional = true } +monero-wallet = { path = "../../networks/monero/wallet", version = "0.1.0", default-features = false, features = ["std"], optional = true } [dev-dependencies] rand_core = "0.6" @@ -62,7 +62,7 @@ borsh = ["serai-abi/borsh"] networks = [] bitcoin = ["networks", "dep:bitcoin"] -monero = ["networks", "ciphersuite/ed25519", "monero-serai"] +monero = ["networks", "ciphersuite/ed25519", "monero-wallet"] # Assumes the default usage is to use Serai as a DEX, which doesn't actually # require connecting to a Serai node diff --git a/substrate/client/src/networks/bitcoin.rs b/substrate/client/src/networks/bitcoin.rs index 5ea37898..502bfb44 100644 --- a/substrate/client/src/networks/bitcoin.rs +++ b/substrate/client/src/networks/bitcoin.rs @@ -6,38 +6,46 @@ use bitcoin::{ hashes::{Hash as HashTrait, hash160::Hash}, PubkeyHash, ScriptHash, network::Network, - WitnessVersion, WitnessProgram, - address::{Error, Payload, NetworkChecked, Address as BAddressGeneric}, + WitnessVersion, WitnessProgram, ScriptBuf, + address::{AddressType, NetworkChecked, Address as BAddress}, }; -type BAddress = BAddressGeneric; - #[derive(Clone, Eq, Debug)] -pub struct Address(BAddress); +pub struct Address(ScriptBuf); impl PartialEq for Address { fn eq(&self, other: &Self) -> bool { - // Since Serai defines the Bitcoin-address specification as a variant of the payload alone, - // define equivalency as the payload alone - self.0.payload() == other.0.payload() + // Since Serai defines the Bitcoin-address specification as a variant of the script alone, + // define equivalency as the script alone + self.0 == other.0 + } +} + +impl From
for ScriptBuf { + fn from(addr: Address) -> ScriptBuf { + addr.0 } } impl FromStr for Address { - type Err = Error; - fn from_str(str: &str) -> Result { + type Err = (); + fn from_str(str: &str) -> Result { Address::new( - BAddressGeneric::from_str(str) - .map_err(|_| Error::UnrecognizedScript)? - .require_network(Network::Bitcoin)?, + BAddress::from_str(str) + .map_err(|_| ())? + .require_network(Network::Bitcoin) + .map_err(|_| ())? + .script_pubkey(), ) - .ok_or(Error::UnrecognizedScript) + .ok_or(()) } } impl fmt::Display for Address { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.0.fmt(f) + BAddress::::from_script(&self.0, Network::Bitcoin) + .map_err(|_| fmt::Error)? + .fmt(f) } } @@ -54,55 +62,52 @@ enum EncodedAddress { impl TryFrom> for Address { type Error = (); fn try_from(data: Vec) -> Result { - Ok(Address(BAddress::new( - Network::Bitcoin, - match EncodedAddress::decode(&mut data.as_ref()).map_err(|_| ())? { - EncodedAddress::P2PKH(hash) => { - Payload::PubkeyHash(PubkeyHash::from_raw_hash(Hash::from_byte_array(hash))) - } - EncodedAddress::P2SH(hash) => { - Payload::ScriptHash(ScriptHash::from_raw_hash(Hash::from_byte_array(hash))) - } - EncodedAddress::P2WPKH(hash) => { - Payload::WitnessProgram(WitnessProgram::new(WitnessVersion::V0, hash).unwrap()) - } - EncodedAddress::P2WSH(hash) => { - Payload::WitnessProgram(WitnessProgram::new(WitnessVersion::V0, hash).unwrap()) - } - EncodedAddress::P2TR(key) => { - Payload::WitnessProgram(WitnessProgram::new(WitnessVersion::V1, key).unwrap()) - } - }, - ))) + Ok(Address(match EncodedAddress::decode(&mut data.as_ref()).map_err(|_| ())? { + EncodedAddress::P2PKH(hash) => { + ScriptBuf::new_p2pkh(&PubkeyHash::from_raw_hash(Hash::from_byte_array(hash))) + } + EncodedAddress::P2SH(hash) => { + ScriptBuf::new_p2sh(&ScriptHash::from_raw_hash(Hash::from_byte_array(hash))) + } + EncodedAddress::P2WPKH(hash) => { + ScriptBuf::new_witness_program(&WitnessProgram::new(WitnessVersion::V0, &hash).unwrap()) + } + EncodedAddress::P2WSH(hash) => { + ScriptBuf::new_witness_program(&WitnessProgram::new(WitnessVersion::V0, &hash).unwrap()) + } + EncodedAddress::P2TR(key) => { + ScriptBuf::new_witness_program(&WitnessProgram::new(WitnessVersion::V1, &key).unwrap()) + } + })) } } fn try_to_vec(addr: &Address) -> Result, ()> { + let parsed_addr = + BAddress::::from_script(&addr.0, Network::Bitcoin).map_err(|_| ())?; Ok( - (match addr.0.payload() { - Payload::PubkeyHash(hash) => EncodedAddress::P2PKH(*hash.as_raw_hash().as_byte_array()), - Payload::ScriptHash(hash) => EncodedAddress::P2SH(*hash.as_raw_hash().as_byte_array()), - Payload::WitnessProgram(program) => match program.version() { - WitnessVersion::V0 => { - let program = program.program(); - if program.len() == 20 { - let mut buf = [0; 20]; - buf.copy_from_slice(program.as_ref()); - EncodedAddress::P2WPKH(buf) - } else if program.len() == 32 { - let mut buf = [0; 32]; - buf.copy_from_slice(program.as_ref()); - EncodedAddress::P2WSH(buf) - } else { - Err(())? - } - } - WitnessVersion::V1 => { - let program_ref: &[u8] = program.program().as_ref(); - EncodedAddress::P2TR(program_ref.try_into().map_err(|_| ())?) - } - _ => Err(())?, - }, + (match parsed_addr.address_type() { + Some(AddressType::P2pkh) => { + EncodedAddress::P2PKH(*parsed_addr.pubkey_hash().unwrap().as_raw_hash().as_byte_array()) + } + Some(AddressType::P2sh) => { + EncodedAddress::P2SH(*parsed_addr.script_hash().unwrap().as_raw_hash().as_byte_array()) + } + Some(AddressType::P2wpkh) => { + let program = parsed_addr.witness_program().ok_or(())?; + let program = program.program().as_bytes(); + EncodedAddress::P2WPKH(program.try_into().map_err(|_| ())?) + } + Some(AddressType::P2wsh) => { + let program = parsed_addr.witness_program().ok_or(())?; + let program = program.program().as_bytes(); + EncodedAddress::P2WSH(program.try_into().map_err(|_| ())?) + } + Some(AddressType::P2tr) => { + let program = parsed_addr.witness_program().ok_or(())?; + let program = program.program().as_bytes(); + EncodedAddress::P2TR(program.try_into().map_err(|_| ())?) + } _ => Err(())?, }) .encode(), @@ -116,20 +121,8 @@ impl From
for Vec { } } -impl From
for BAddress { - fn from(addr: Address) -> BAddress { - addr.0 - } -} - -impl AsRef for Address { - fn as_ref(&self) -> &BAddress { - &self.0 - } -} - impl Address { - pub fn new(address: BAddress) -> Option { + pub fn new(address: ScriptBuf) -> Option { let res = Self(address); if try_to_vec(&res).is_ok() { return Some(res); diff --git a/substrate/client/src/networks/monero.rs b/substrate/client/src/networks/monero.rs index 5b43860e..bd5e0a15 100644 --- a/substrate/client/src/networks/monero.rs +++ b/substrate/client/src/networks/monero.rs @@ -4,7 +4,7 @@ use scale::{Encode, Decode}; use ciphersuite::{Ciphersuite, Ed25519}; -use monero_serai::wallet::address::{AddressError, Network, AddressType, AddressMeta, MoneroAddress}; +use monero_wallet::address::{AddressError, Network, AddressType, MoneroAddress}; #[derive(Clone, PartialEq, Eq, Debug)] pub struct Address(MoneroAddress); @@ -33,7 +33,7 @@ impl fmt::Display for Address { // SCALE-encoded variant of Monero addresses. #[derive(Clone, PartialEq, Eq, Debug, Encode, Decode)] enum EncodedAddressType { - Standard, + Legacy, Subaddress, Featured(u8), } @@ -52,22 +52,20 @@ impl TryFrom> for Address { let addr = EncodedAddress::decode(&mut data.as_ref()).map_err(|_| ())?; // Convert over Ok(Address(MoneroAddress::new( - AddressMeta::new( - Network::Mainnet, - match addr.kind { - EncodedAddressType::Standard => AddressType::Standard, - EncodedAddressType::Subaddress => AddressType::Subaddress, - EncodedAddressType::Featured(flags) => { - let subaddress = (flags & 1) != 0; - let integrated = (flags & (1 << 1)) != 0; - let guaranteed = (flags & (1 << 2)) != 0; - if integrated { - Err(())?; - } - AddressType::Featured { subaddress, payment_id: None, guaranteed } + Network::Mainnet, + match addr.kind { + EncodedAddressType::Legacy => AddressType::Legacy, + EncodedAddressType::Subaddress => AddressType::Subaddress, + EncodedAddressType::Featured(flags) => { + let subaddress = (flags & 1) != 0; + let integrated = (flags & (1 << 1)) != 0; + let guaranteed = (flags & (1 << 2)) != 0; + if integrated { + Err(())?; } - }, - ), + AddressType::Featured { subaddress, payment_id: None, guaranteed } + } + }, Ed25519::read_G::<&[u8]>(&mut addr.spend.as_ref()).map_err(|_| ())?.0, Ed25519::read_G::<&[u8]>(&mut addr.view.as_ref()).map_err(|_| ())?.0, ))) @@ -85,16 +83,19 @@ impl Into for Address { impl Into> for Address { fn into(self) -> Vec { EncodedAddress { - kind: match self.0.meta.kind { - AddressType::Standard => EncodedAddressType::Standard, + kind: match self.0.kind() { + AddressType::Legacy => EncodedAddressType::Legacy, + AddressType::LegacyIntegrated(_) => { + panic!("integrated address became Serai Monero address") + } AddressType::Subaddress => EncodedAddressType::Subaddress, - AddressType::Integrated(_) => panic!("integrated address became Serai Monero address"), - AddressType::Featured { subaddress, payment_id: _, guaranteed } => { - EncodedAddressType::Featured(u8::from(subaddress) + (u8::from(guaranteed) << 2)) + AddressType::Featured { subaddress, payment_id, guaranteed } => { + debug_assert!(payment_id.is_none()); + EncodedAddressType::Featured(u8::from(*subaddress) + (u8::from(*guaranteed) << 2)) } }, - spend: self.0.spend.compress().0, - view: self.0.view.compress().0, + spend: self.0.spend().compress().0, + view: self.0.view().compress().0, } .encode() } diff --git a/substrate/client/src/serai/dex.rs b/substrate/client/src/serai/dex.rs index 39699ba8..046fdb8f 100644 --- a/substrate/client/src/serai/dex.rs +++ b/substrate/client/src/serai/dex.rs @@ -3,7 +3,7 @@ use serai_abi::primitives::{SeraiAddress, Amount, Coin}; use scale::{decode_from_bytes, Encode}; -use crate::{SeraiError, hex_decode, TemporalSerai}; +use crate::{Serai, SeraiError, TemporalSerai}; pub type DexEvent = serai_abi::dex::Event; @@ -62,24 +62,19 @@ impl<'a> SeraiDex<'a> { }) } - pub async fn get_reserves( - &self, - coin1: Coin, - coin2: Coin, - ) -> Result, SeraiError> { - let hash = self + /// Returns the reserves of `coin:SRI` pool. + pub async fn get_reserves(&self, coin: Coin) -> Result, SeraiError> { + let reserves = self .0 .serai - .call("state_call", ["DexApi_get_reserves".to_string(), hex::encode((coin1, coin2).encode())]) + .call( + "state_call", + ["DexApi_get_reserves".to_string(), hex::encode((coin, Coin::Serai).encode())], + ) .await?; - let bytes = hex_decode(hash) - .map_err(|_| SeraiError::InvalidNode("expected hex from node wasn't hex".to_string()))?; - let resut = decode_from_bytes::>(bytes.into()) + let bytes = Serai::hex_decode(reserves)?; + let result = decode_from_bytes::>(bytes.into()) .map_err(|e| SeraiError::ErrorInResponse(e.to_string()))?; - Ok(resut.map(|amounts| (Amount(amounts.0), Amount(amounts.1)))) - } - - pub async fn oracle_value(&self, coin: Coin) -> Result, SeraiError> { - self.0.storage(PALLET, "SecurityOracleValue", coin).await + Ok(result.map(|amounts| (Amount(amounts.0), Amount(amounts.1)))) } } diff --git a/substrate/client/src/serai/genesis_liquidity.rs b/substrate/client/src/serai/genesis_liquidity.rs index b8882bd7..04e80d74 100644 --- a/substrate/client/src/serai/genesis_liquidity.rs +++ b/substrate/client/src/serai/genesis_liquidity.rs @@ -1,5 +1,5 @@ pub use serai_abi::genesis_liquidity::primitives; -use primitives::Prices; +use primitives::{Values, LiquidityAmount}; use serai_abi::primitives::*; @@ -29,27 +29,9 @@ impl<'a> SeraiGenesisLiquidity<'a> { .await } - pub async fn liquidity_tokens( - &self, - address: &SeraiAddress, - coin: Coin, - ) -> Result { - Ok( - self - .0 - .storage( - PALLET, - "LiquidityTokensPerAddress", - (coin, sp_core::hashing::blake2_128(&address.encode()), &address.0), - ) - .await? - .unwrap_or(Amount(0)), - ) - } - - pub fn set_initial_price(prices: Prices, signature: Signature) -> Transaction { + pub fn oraclize_values(values: Values, signature: Signature) -> Transaction { Serai::unsigned(serai_abi::Call::GenesisLiquidity( - serai_abi::genesis_liquidity::Call::set_initial_price { prices, signature }, + serai_abi::genesis_liquidity::Call::oraclize_values { values, signature }, )) } @@ -59,15 +41,25 @@ impl<'a> SeraiGenesisLiquidity<'a> { }) } - pub async fn liquidity(&self, address: &SeraiAddress, coin: Coin) -> Option { - self - .0 - .storage( - PALLET, - "Liquidity", - (coin, sp_core::hashing::blake2_128(&address.encode()), &address.0), - ) - .await - .unwrap() + pub async fn liquidity( + &self, + address: &SeraiAddress, + coin: Coin, + ) -> Result { + Ok( + self + .0 + .storage( + PALLET, + "Liquidity", + (coin, sp_core::hashing::blake2_128(&address.encode()), &address.0), + ) + .await? + .unwrap_or(LiquidityAmount::zero()), + ) + } + + pub async fn supply(&self, coin: Coin) -> Result { + Ok(self.0.storage(PALLET, "Supply", coin).await?.unwrap_or(LiquidityAmount::zero())) } } diff --git a/substrate/client/src/serai/liquidity_tokens.rs b/substrate/client/src/serai/liquidity_tokens.rs index 22fcd49e..3e9052b2 100644 --- a/substrate/client/src/serai/liquidity_tokens.rs +++ b/substrate/client/src/serai/liquidity_tokens.rs @@ -32,10 +32,10 @@ impl<'a> SeraiLiquidityTokens<'a> { } pub fn transfer(to: SeraiAddress, balance: Balance) -> serai_abi::Call { - serai_abi::Call::Coins(serai_abi::coins::Call::transfer { to, balance }) + serai_abi::Call::LiquidityTokens(serai_abi::liquidity_tokens::Call::transfer { to, balance }) } pub fn burn(balance: Balance) -> serai_abi::Call { - serai_abi::Call::Coins(serai_abi::coins::Call::burn { balance }) + serai_abi::Call::LiquidityTokens(serai_abi::liquidity_tokens::Call::burn { balance }) } } diff --git a/substrate/client/src/serai/mod.rs b/substrate/client/src/serai/mod.rs index fc4a9ea7..53dc7b67 100644 --- a/substrate/client/src/serai/mod.rs +++ b/substrate/client/src/serai/mod.rs @@ -4,7 +4,7 @@ use thiserror::Error; use async_lock::RwLock; use simple_request::{hyper, Request, Client}; -use scale::{Compact, Decode, Encode}; +use scale::{Decode, Encode}; use serde::{Serialize, Deserialize, de::DeserializeOwned}; pub use sp_core::{ @@ -48,8 +48,8 @@ impl Block { /// Returns the time of this block, set by its producer, in milliseconds since the epoch. pub fn time(&self) -> Result { for transaction in &self.transactions { - if let Call::Timestamp(timestamp::Call::set { now }) = &transaction.call { - return Ok(u64::from(*now)); + if let Call::Timestamp(timestamp::Call::set { now }) = transaction.call() { + return Ok(*now); } } Err(SeraiError::InvalidNode("no time was present in block".to_string())) @@ -167,15 +167,14 @@ impl Serai { } fn unsigned(call: Call) -> Transaction { - Transaction { call, signature: None } + Transaction::new(call, None) } pub fn sign(&self, signer: &Pair, call: Call, nonce: u32, tip: u64) -> Transaction { const SPEC_VERSION: u32 = 1; const TX_VERSION: u32 = 1; - let extra = - Extra { era: sp_runtime::generic::Era::Immortal, nonce: Compact(nonce), tip: Compact(tip) }; + let extra = Extra { era: sp_runtime::generic::Era::Immortal, nonce, tip }; let signature_payload = ( &call, &extra, @@ -189,7 +188,7 @@ impl Serai { .encode(); let signature = signer.sign(&signature_payload); - Transaction { call, signature: Some((signer.public().into(), signature, extra)) } + Transaction::new(call, Some((signer.public().into(), signature, extra))) } pub async fn publish(&self, tx: &Transaction) -> Result<(), SeraiError> { @@ -202,11 +201,10 @@ impl Serai { // TODO: move this into substrate/client/src/validator_sets.rs async fn active_network_validators(&self, network: NetworkId) -> Result, SeraiError> { - let hash: String = self + let validators: String = self .call("state_call", ["SeraiRuntimeApi_validators".to_string(), hex::encode(network.encode())]) .await?; - let bytes = hex_decode(hash) - .map_err(|_| SeraiError::InvalidNode("expected hex from node wasn't hex".to_string()))?; + let bytes = Self::hex_decode(validators)?; let r = Vec::::decode(&mut bytes.as_slice()) .map_err(|e| SeraiError::ErrorInResponse(e.to_string()))?; Ok(r) @@ -378,7 +376,10 @@ impl<'a> TemporalSerai<'a> { let res = hex_decode(res) .map_err(|_| SeraiError::InvalidNode("expected hex from node wasn't hex".to_string()))?; Ok(Some(R::decode(&mut res.as_slice()).map_err(|_| { - SeraiError::InvalidRuntime("different type present at storage location".to_string()) + SeraiError::InvalidRuntime(format!( + "different type present at storage location, raw value: {}", + hex::encode(res) + )) })?)) } diff --git a/substrate/client/src/serai/validator_sets.rs b/substrate/client/src/serai/validator_sets.rs index c4e29644..959f8ee6 100644 --- a/substrate/client/src/serai/validator_sets.rs +++ b/substrate/client/src/serai/validator_sets.rs @@ -180,7 +180,10 @@ impl<'a> SeraiValidatorSets<'a> { pub fn set_keys( network: NetworkId, - removed_participants: Vec, + removed_participants: sp_runtime::BoundedVec< + SeraiAddress, + sp_core::ConstU32<{ primitives::MAX_KEY_SHARES_PER_SET / 3 }>, + >, key_pair: KeyPair, signature: Signature, ) -> Transaction { diff --git a/substrate/client/tests/common/in_instructions.rs b/substrate/client/tests/common/in_instructions.rs index b4c24898..e335244a 100644 --- a/substrate/client/tests/common/in_instructions.rs +++ b/substrate/client/tests/common/in_instructions.rs @@ -31,7 +31,7 @@ pub async fn provide_batch(serai: &Serai, batch: Batch) -> [u8; 32] { keys } else { let keys = KeyPair(pair.public(), vec![].try_into().unwrap()); - set_keys(serai, set, keys.clone()).await; + set_keys(serai, set, keys.clone(), &[insecure_pair_from_name("Alice")]).await; keys }; assert_eq!(keys.0, pair.public()); diff --git a/substrate/client/tests/common/validator_sets.rs b/substrate/client/tests/common/validator_sets.rs index b7257a1c..3238501a 100644 --- a/substrate/client/tests/common/validator_sets.rs +++ b/substrate/client/tests/common/validator_sets.rs @@ -14,7 +14,6 @@ use frost::dkg::musig::musig; use schnorrkel::Schnorrkel; use serai_client::{ - primitives::insecure_pair_from_name, validator_sets::{ primitives::{ValidatorSet, KeyPair, musig_context, set_keys_message}, ValidatorSetsEvent, @@ -25,33 +24,52 @@ use serai_client::{ use crate::common::tx::publish_tx; #[allow(dead_code)] -pub async fn set_keys(serai: &Serai, set: ValidatorSet, key_pair: KeyPair) -> [u8; 32] { - let pair = insecure_pair_from_name("Alice"); - let public = pair.public(); +pub async fn set_keys( + serai: &Serai, + set: ValidatorSet, + key_pair: KeyPair, + pairs: &[Pair], +) -> [u8; 32] { + let mut pub_keys = vec![]; + for pair in pairs { + let public_key = + ::read_G::<&[u8]>(&mut pair.public().0.as_ref()).unwrap(); + pub_keys.push(public_key); + } - let public_key = ::read_G::<&[u8]>(&mut public.0.as_ref()).unwrap(); - let secret_key = ::read_F::<&[u8]>( - &mut pair.as_ref().secret.to_bytes()[.. 32].as_ref(), - ) - .unwrap(); - assert_eq!(Ristretto::generator() * secret_key, public_key); - let threshold_keys = - musig::(&musig_context(set), &Zeroizing::new(secret_key), &[public_key]).unwrap(); + let mut threshold_keys = vec![]; + for i in 0 .. pairs.len() { + let secret_key = ::read_F::<&[u8]>( + &mut pairs[i].as_ref().secret.to_bytes()[.. 32].as_ref(), + ) + .unwrap(); + assert_eq!(Ristretto::generator() * secret_key, pub_keys[i]); + + threshold_keys.push( + musig::(&musig_context(set), &Zeroizing::new(secret_key), &pub_keys).unwrap(), + ); + } + + let mut musig_keys = HashMap::new(); + for tk in threshold_keys { + musig_keys.insert(tk.params().i(), tk.into()); + } let sig = frost::tests::sign_without_caching( &mut OsRng, - frost::tests::algorithm_machines( - &mut OsRng, - &Schnorrkel::new(b"substrate"), - &HashMap::from([(threshold_keys.params().i(), threshold_keys.into())]), - ), + frost::tests::algorithm_machines(&mut OsRng, &Schnorrkel::new(b"substrate"), &musig_keys), &set_keys_message(&set, &[], &key_pair), ); // Set the key pair let block = publish_tx( serai, - &SeraiValidatorSets::set_keys(set.network, vec![], key_pair.clone(), Signature(sig.to_bytes())), + &SeraiValidatorSets::set_keys( + set.network, + vec![].try_into().unwrap(), + key_pair.clone(), + Signature(sig.to_bytes()), + ), ) .await; diff --git a/substrate/client/tests/genesis_liquidity.rs b/substrate/client/tests/genesis_liquidity.rs index c61d6e70..8edc87d3 100644 --- a/substrate/client/tests/genesis_liquidity.rs +++ b/substrate/client/tests/genesis_liquidity.rs @@ -1,10 +1,216 @@ -use serai_client::Serai; +use std::{time::Duration, collections::HashMap}; + +use rand_core::{RngCore, OsRng}; +use zeroize::Zeroizing; + +use ciphersuite::{Ciphersuite, Ristretto}; +use frost::dkg::musig::musig; +use schnorrkel::Schnorrkel; + +use serai_client::{ + genesis_liquidity::{ + primitives::{GENESIS_LIQUIDITY_ACCOUNT, INITIAL_GENESIS_LP_SHARES}, + SeraiGenesisLiquidity, + }, + validator_sets::primitives::{musig_context, Session, ValidatorSet}, +}; + +use serai_abi::{ + genesis_liquidity::primitives::{oraclize_values_message, Values}, + primitives::COINS, +}; + +use sp_core::{sr25519::Signature, Pair as PairTrait}; + +use serai_client::{ + primitives::{ + Amount, NetworkId, Coin, Balance, BlockHash, SeraiAddress, insecure_pair_from_name, GENESIS_SRI, + }, + in_instructions::primitives::{InInstruction, InInstructionWithBalance, Batch}, + Serai, +}; mod common; -use common::genesis_liquidity::test_genesis_liquidity; +use common::{in_instructions::provide_batch, tx::publish_tx}; serai_test_fast_epoch!( genesis_liquidity: (|serai: Serai| async move { test_genesis_liquidity(serai).await; }) ); + +async fn test_genesis_liquidity(serai: Serai) { + // all coins except the native + let coins = COINS.into_iter().filter(|c| *c != Coin::native()).collect::>(); + + // make accounts with amounts + let mut accounts = HashMap::new(); + for coin in coins.clone() { + // make 5 accounts per coin + let mut values = vec![]; + for _ in 0 .. 5 { + let mut address = SeraiAddress::new([0; 32]); + OsRng.fill_bytes(&mut address.0); + values.push((address, Amount(OsRng.next_u64() % 10u64.pow(coin.decimals())))); + } + accounts.insert(coin, values); + } + + // send a batch per coin + let mut batch_ids: HashMap = HashMap::new(); + for coin in coins.clone() { + // set up instructions + let instructions = accounts[&coin] + .iter() + .map(|(addr, amount)| InInstructionWithBalance { + instruction: InInstruction::GenesisLiquidity(*addr), + balance: Balance { coin, amount: *amount }, + }) + .collect::>(); + + // set up bloch hash + let mut block = BlockHash([0; 32]); + OsRng.fill_bytes(&mut block.0); + + // set up batch id + batch_ids + .entry(coin.network()) + .and_modify(|v| { + *v += 1; + }) + .or_insert(0); + + let batch = + Batch { network: coin.network(), id: batch_ids[&coin.network()], block, instructions }; + provide_batch(&serai, batch).await; + } + + // wait until genesis ends + let genesis_blocks = 10; // TODO + let block_time = 6; // TODO + tokio::time::timeout( + tokio::time::Duration::from_secs(3 * (genesis_blocks * block_time)), + async { + while serai.latest_finalized_block().await.unwrap().number() < 10 { + tokio::time::sleep(Duration::from_secs(6)).await; + } + }, + ) + .await + .unwrap(); + + // set values relative to each other + // TODO: Random values here + let values = Values { monero: 184100, ether: 4785000, dai: 1500 }; + set_values(&serai, &values).await; + let values_map = HashMap::from([ + (Coin::Monero, values.monero), + (Coin::Ether, values.ether), + (Coin::Dai, values.dai), + ]); + + // wait a little bit.. + tokio::time::sleep(Duration::from_secs(12)).await; + + // check total SRI supply is +100M + // there are 6 endowed accounts in dev-net. Take this into consideration when checking + // for the total sri minted at this time. + let serai = serai.as_of_latest_finalized_block().await.unwrap(); + let sri = serai.coins().coin_supply(Coin::Serai).await.unwrap(); + let endowed_amount: u64 = 1 << 60; + let total_sri = (6 * endowed_amount) + GENESIS_SRI; + assert_eq!(sri, Amount(total_sri)); + + // check genesis account has no coins, all transferred to pools. + for coin in COINS { + let amount = serai.coins().coin_balance(coin, GENESIS_LIQUIDITY_ACCOUNT).await.unwrap(); + assert_eq!(amount.0, 0); + } + + // check pools has proper liquidity + let mut pool_amounts = HashMap::new(); + let mut total_value = 0u128; + for coin in coins.clone() { + let total_coin = accounts[&coin].iter().fold(0u128, |acc, value| acc + u128::from(value.1 .0)); + let value = if coin != Coin::Bitcoin { + (total_coin * u128::from(values_map[&coin])) / 10u128.pow(coin.decimals()) + } else { + total_coin + }; + + total_value += value; + pool_amounts.insert(coin, (total_coin, value)); + } + + // check distributed SRI per pool + let mut total_sri_distributed = 0u128; + for coin in coins.clone() { + let sri = if coin == *COINS.last().unwrap() { + u128::from(GENESIS_SRI).checked_sub(total_sri_distributed).unwrap() + } else { + (pool_amounts[&coin].1 * u128::from(GENESIS_SRI)) / total_value + }; + total_sri_distributed += sri; + + let reserves = serai.dex().get_reserves(coin).await.unwrap().unwrap(); + assert_eq!(u128::from(reserves.0 .0), pool_amounts[&coin].0); // coin side + assert_eq!(u128::from(reserves.1 .0), sri); // SRI side + } + + // check each liquidity provider got liquidity tokens proportional to their value + for coin in coins { + let liq_supply = serai.genesis_liquidity().supply(coin).await.unwrap(); + for (acc, amount) in &accounts[&coin] { + let acc_liq_shares = serai.genesis_liquidity().liquidity(acc, coin).await.unwrap().shares; + + // since we can't test the ratios directly(due to integer division giving 0) + // we test whether they give the same result when multiplied by another constant. + // Following test ensures the account in fact has the right amount of shares. + let mut shares_ratio = (INITIAL_GENESIS_LP_SHARES * acc_liq_shares) / liq_supply.shares; + let amounts_ratio = + (INITIAL_GENESIS_LP_SHARES * amount.0) / u64::try_from(pool_amounts[&coin].0).unwrap(); + + // we can tolerate 1 unit diff between them due to integer division. + if shares_ratio.abs_diff(amounts_ratio) == 1 { + shares_ratio = amounts_ratio; + } + + assert_eq!(shares_ratio, amounts_ratio); + } + } + + // TODO: test remove the liq before/after genesis ended. +} + +async fn set_values(serai: &Serai, values: &Values) { + // prepare a Musig tx to oraclize the relative values + let pair = insecure_pair_from_name("Alice"); + let public = pair.public(); + // we publish the tx in set 4 + let set = ValidatorSet { session: Session(4), network: NetworkId::Serai }; + + let public_key = ::read_G::<&[u8]>(&mut public.0.as_ref()).unwrap(); + let secret_key = ::read_F::<&[u8]>( + &mut pair.as_ref().secret.to_bytes()[.. 32].as_ref(), + ) + .unwrap(); + + assert_eq!(Ristretto::generator() * secret_key, public_key); + let threshold_keys = + musig::(&musig_context(set), &Zeroizing::new(secret_key), &[public_key]).unwrap(); + + let sig = frost::tests::sign_without_caching( + &mut OsRng, + frost::tests::algorithm_machines( + &mut OsRng, + &Schnorrkel::new(b"substrate"), + &HashMap::from([(threshold_keys.params().i(), threshold_keys.into())]), + ), + &oraclize_values_message(&set, values), + ); + + // oraclize values + let _ = + publish_tx(serai, &SeraiGenesisLiquidity::oraclize_values(*values, Signature(sig.to_bytes()))) + .await; +} diff --git a/substrate/client/tests/validator_sets.rs b/substrate/client/tests/validator_sets.rs index a44a0ac4..8aa8174f 100644 --- a/substrate/client/tests/validator_sets.rs +++ b/substrate/client/tests/validator_sets.rs @@ -1,36 +1,71 @@ use rand_core::{RngCore, OsRng}; -use sp_core::{sr25519::Public, Pair}; +use sp_core::{ + sr25519::{Public, Pair}, + Pair as PairTrait, +}; use serai_client::{ - primitives::{NETWORKS, NetworkId, insecure_pair_from_name}, + primitives::{NETWORKS, NetworkId, BlockHash, insecure_pair_from_name}, validator_sets::{ primitives::{Session, ValidatorSet, KeyPair}, ValidatorSetsEvent, }, + in_instructions::{ + primitives::{Batch, SignedBatch, batch_message}, + SeraiInInstructions, + }, Amount, Serai, }; mod common; -use common::validator_sets::{set_keys, allocate_stake, deallocate_stake}; +use common::{ + tx::publish_tx, + validator_sets::{allocate_stake, deallocate_stake, set_keys}, +}; -const EPOCH_INTERVAL: u64 = 300; +fn get_random_key_pair() -> KeyPair { + let mut ristretto_key = [0; 32]; + OsRng.fill_bytes(&mut ristretto_key); + let mut external_key = vec![0; 33]; + OsRng.fill_bytes(&mut external_key); + KeyPair(Public(ristretto_key), external_key.try_into().unwrap()) +} + +async fn get_ordered_keys(serai: &Serai, network: NetworkId, accounts: &[Pair]) -> Vec { + // retrieve the current session validators so that we know the order of the keys + // that is necessary for the correct musig signature. + let validators = serai + .as_of_latest_finalized_block() + .await + .unwrap() + .validator_sets() + .active_network_validators(network) + .await + .unwrap(); + + // collect the pairs of the validators + let mut pairs = vec![]; + for v in validators { + let p = accounts.iter().find(|pair| pair.public() == v).unwrap().clone(); + pairs.push(p); + } + + pairs +} serai_test!( set_keys_test: (|serai: Serai| async move { let network = NetworkId::Bitcoin; let set = ValidatorSet { session: Session(0), network }; - let public = insecure_pair_from_name("Alice").public(); + let pair = insecure_pair_from_name("Alice"); + let public = pair.public(); // Neither of these keys are validated // The external key is infeasible to validate on-chain, the Ristretto key is feasible // TODO: Should the Ristretto key be validated? - let mut ristretto_key = [0; 32]; - OsRng.fill_bytes(&mut ristretto_key); - let mut external_key = vec![0; 33]; - OsRng.fill_bytes(&mut external_key); - let key_pair = KeyPair(Public(ristretto_key), external_key.try_into().unwrap()); + let key_pair = get_random_key_pair(); // Make sure the genesis is as expected assert_eq!( @@ -62,7 +97,7 @@ serai_test!( assert_eq!(participants_ref, [public].as_ref()); } - let block = set_keys(&serai, set, key_pair.clone()).await; + let block = set_keys(&serai, set, key_pair.clone(), &[pair]).await; // While the set_keys function should handle this, it's beneficial to // independently test it @@ -149,11 +184,13 @@ async fn validator_set_rotation() { ); // genesis accounts - let pair1 = insecure_pair_from_name("Alice"); - let pair2 = insecure_pair_from_name("Bob"); - let pair3 = insecure_pair_from_name("Charlie"); - let pair4 = insecure_pair_from_name("Dave"); - let pair5 = insecure_pair_from_name("Eve"); + let accounts = vec![ + insecure_pair_from_name("Alice"), + insecure_pair_from_name("Bob"), + insecure_pair_from_name("Charlie"), + insecure_pair_from_name("Dave"), + insecure_pair_from_name("Eve"), + ]; // amounts for single key share per network let key_shares = HashMap::from([ @@ -164,8 +201,9 @@ async fn validator_set_rotation() { ]); // genesis participants per network + #[allow(clippy::redundant_closure_for_method_calls)] let default_participants = - vec![pair1.public(), pair2.public(), pair3.public(), pair4.public()]; + accounts[.. 4].to_vec().iter().map(|pair| pair.public()).collect::>(); let mut participants = HashMap::from([ (NetworkId::Serai, default_participants.clone()), (NetworkId::Bitcoin, default_participants.clone()), @@ -181,28 +219,83 @@ async fn validator_set_rotation() { participants.sort(); verify_session_and_active_validators(&serai, network, 0, participants).await; - // add 1 participant & verify - let hash = - allocate_stake(&serai, network, key_shares[&network], &pair5, i.try_into().unwrap()) - .await; - participants.push(pair5.public()); - participants.sort(); - verify_session_and_active_validators( + // add 1 participant + let last_participant = accounts[4].clone(); + let hash = allocate_stake( &serai, network, - get_active_session(&serai, network, hash).await, - participants, + key_shares[&network], + &last_participant, + i.try_into().unwrap(), ) .await; + participants.push(last_participant.public()); + // the session at which set changes becomes active + let activation_session = get_session_at_which_changes_activate(&serai, network, hash).await; - // remove 1 participant & verify - let hash = - deallocate_stake(&serai, network, key_shares[&network], &pair2, i.try_into().unwrap()) - .await; - participants.swap_remove(participants.iter().position(|k| *k == pair2.public()).unwrap()); - let active_session = get_active_session(&serai, network, hash).await; + // set the keys if it is an external set + if network != NetworkId::Serai { + let set = ValidatorSet { session: Session(0), network }; + let key_pair = get_random_key_pair(); + let pairs = get_ordered_keys(&serai, network, &accounts).await; + set_keys(&serai, set, key_pair, &pairs).await; + } + + // verify participants.sort(); - verify_session_and_active_validators(&serai, network, active_session, participants).await; + verify_session_and_active_validators(&serai, network, activation_session, participants) + .await; + + // remove 1 participant + let participant_to_remove = accounts[1].clone(); + let hash = deallocate_stake( + &serai, + network, + key_shares[&network], + &participant_to_remove, + i.try_into().unwrap(), + ) + .await; + participants.swap_remove( + participants.iter().position(|k| *k == participant_to_remove.public()).unwrap(), + ); + let activation_session = get_session_at_which_changes_activate(&serai, network, hash).await; + + if network != NetworkId::Serai { + // set the keys if it is an external set + let set = ValidatorSet { session: Session(1), network }; + + // we need the whole substrate key pair to sign the batch + let (substrate_pair, key_pair) = { + let pair = insecure_pair_from_name("session-1-key-pair"); + let public = pair.public(); + + let mut external_key = vec![0; 33]; + OsRng.fill_bytes(&mut external_key); + + (pair, KeyPair(public, external_key.try_into().unwrap())) + }; + let pairs = get_ordered_keys(&serai, network, &accounts).await; + set_keys(&serai, set, key_pair, &pairs).await; + + // provide a batch to complete the handover and retire the previous set + let mut block_hash = BlockHash([0; 32]); + OsRng.fill_bytes(&mut block_hash.0); + let batch = Batch { network, id: 0, block: block_hash, instructions: vec![] }; + publish_tx( + &serai, + &SeraiInInstructions::execute_batch(SignedBatch { + batch: batch.clone(), + signature: substrate_pair.sign(&batch_message(&batch)), + }), + ) + .await; + } + + // verify + participants.sort(); + verify_session_and_active_validators(&serai, network, activation_session, participants) + .await; // check pending deallocations let pending = serai @@ -212,8 +305,8 @@ async fn validator_set_rotation() { .validator_sets() .pending_deallocations( network, - pair2.public(), - Session(u32::try_from(active_session + 1).unwrap()), + participant_to_remove.public(), + Session(activation_session + 1), ) .await .unwrap(); @@ -223,24 +316,39 @@ async fn validator_set_rotation() { .await; } +async fn session_for_block(serai: &Serai, block: [u8; 32], network: NetworkId) -> u32 { + serai.as_of(block).validator_sets().session(network).await.unwrap().unwrap().0 +} + async fn verify_session_and_active_validators( serai: &Serai, network: NetworkId, - session: u64, + session: u32, participants: &[Public], ) { - // wait untill the epoch block finalized - let epoch_block = (session * EPOCH_INTERVAL) + 1; - while serai.finalized_block_by_number(epoch_block).await.unwrap().is_none() { - // sleep 1 block - tokio::time::sleep(tokio::time::Duration::from_secs(6)).await; - } - let serai_for_block = - serai.as_of(serai.finalized_block_by_number(epoch_block).await.unwrap().unwrap().hash()); + // wait until the active session. This wait should be max 30 secs since the epoch time. + let block = tokio::time::timeout(core::time::Duration::from_secs(2 * 60), async move { + loop { + let mut block = serai.latest_finalized_block_hash().await.unwrap(); + if session_for_block(serai, block, network).await < session { + // Sleep a block + tokio::time::sleep(core::time::Duration::from_secs(6)).await; + continue; + } + while session_for_block(serai, block, network).await > session { + block = serai.block(block).await.unwrap().unwrap().header.parent_hash.0; + } + assert_eq!(session_for_block(serai, block, network).await, session); + break block; + } + }) + .await + .unwrap(); + let serai_for_block = serai.as_of(block); // verify session let s = serai_for_block.validator_sets().session(network).await.unwrap().unwrap(); - assert_eq!(u64::from(s.0), session); + assert_eq!(s.0, session); // verify participants let mut validators = @@ -249,10 +357,11 @@ async fn verify_session_and_active_validators( assert_eq!(validators, participants); // make sure finalization continues as usual after the changes - tokio::time::timeout(tokio::time::Duration::from_secs(60), async move { + let current_finalized_block = serai.latest_finalized_block().await.unwrap().header.number; + tokio::time::timeout(core::time::Duration::from_secs(60), async move { let mut finalized_block = serai.latest_finalized_block().await.unwrap().header.number; - while finalized_block <= epoch_block + 2 { - tokio::time::sleep(tokio::time::Duration::from_secs(6)).await; + while finalized_block <= current_finalized_block + 2 { + tokio::time::sleep(core::time::Duration::from_secs(6)).await; finalized_block = serai.latest_finalized_block().await.unwrap().header.number; } }) @@ -262,15 +371,18 @@ async fn verify_session_and_active_validators( // TODO: verify key shares as well? } -async fn get_active_session(serai: &Serai, network: NetworkId, hash: [u8; 32]) -> u64 { - let block_number = serai.block(hash).await.unwrap().unwrap().header.number; - let epoch = block_number / EPOCH_INTERVAL; +async fn get_session_at_which_changes_activate( + serai: &Serai, + network: NetworkId, + hash: [u8; 32], +) -> u32 { + let session = session_for_block(serai, hash, network).await; // changes should be active in the next session if network == NetworkId::Serai { // it takes 1 extra session for serai net to make the changes active. - epoch + 2 + session + 2 } else { - epoch + 1 + session + 1 } } diff --git a/substrate/coins/pallet/Cargo.toml b/substrate/coins/pallet/Cargo.toml index da9a27f6..2aba1fbd 100644 --- a/substrate/coins/pallet/Cargo.toml +++ b/substrate/coins/pallet/Cargo.toml @@ -49,6 +49,9 @@ std = [ "coins-primitives/std", ] +# TODO +try-runtime = [] + runtime-benchmarks = [ "frame-system/runtime-benchmarks", "frame-support/runtime-benchmarks", diff --git a/substrate/dex/pallet/src/tests.rs b/substrate/dex/pallet/src/tests.rs index 80b45464..b0014199 100644 --- a/substrate/dex/pallet/src/tests.rs +++ b/substrate/dex/pallet/src/tests.rs @@ -25,7 +25,7 @@ pub use coins_pallet as coins; use coins::Pallet as CoinsPallet; -use serai_primitives::*; +use serai_primitives::{Balance, COINS, PublicKey, system_address, Amount}; type LiquidityTokens = coins_pallet::Pallet; type LiquidityTokensError = coins_pallet::Error; diff --git a/substrate/genesis-liquidity/pallet/Cargo.toml b/substrate/genesis-liquidity/pallet/Cargo.toml index 293e77fb..99b71c4c 100644 --- a/substrate/genesis-liquidity/pallet/Cargo.toml +++ b/substrate/genesis-liquidity/pallet/Cargo.toml @@ -18,7 +18,6 @@ ignored = ["scale", "scale-info"] [lints] workspace = true - [dependencies] scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] } scale-info = { version = "2", default-features = false, features = ["derive"] } @@ -32,6 +31,7 @@ sp-application-crypto = { git = "https://github.com/serai-dex/substrate", defaul dex-pallet = { package = "serai-dex-pallet", path = "../../dex/pallet", default-features = false } coins-pallet = { package = "serai-coins-pallet", path = "../../coins/pallet", default-features = false } +validator-sets-pallet = { package = "serai-validator-sets-pallet", path = "../../validator-sets/pallet", default-features = false } serai-primitives = { path = "../../primitives", default-features = false } genesis-liquidity-primitives = { package = "serai-genesis-liquidity-primitives", path = "../primitives", default-features = false } @@ -51,11 +51,13 @@ std = [ "coins-pallet/std", "dex-pallet/std", + "validator-sets-pallet/std", "serai-primitives/std", "genesis-liquidity-primitives/std", "validator-sets-primitives/std", ] -fast-epoch = ["genesis-liquidity-primitives/fast-epoch"] +try-runtime = [] # TODO +fast-epoch = [] default = ["std"] diff --git a/substrate/genesis-liquidity/pallet/src/lib.rs b/substrate/genesis-liquidity/pallet/src/lib.rs index e4c11370..6ff84961 100644 --- a/substrate/genesis-liquidity/pallet/src/lib.rs +++ b/substrate/genesis-liquidity/pallet/src/lib.rs @@ -5,46 +5,36 @@ pub mod pallet { use super::*; use frame_system::{pallet_prelude::*, RawOrigin}; - use frame_support::{ - pallet_prelude::*, - sp_runtime::{self, SaturatedConversion}, - }; + use frame_support::{pallet_prelude::*, sp_runtime::SaturatedConversion}; - use sp_std::{vec, vec::Vec, collections::btree_map::BTreeMap}; + use sp_std::{vec, vec::Vec}; use sp_core::sr25519::Signature; use sp_application_crypto::RuntimePublic; use dex_pallet::{Pallet as Dex, Config as DexConfig}; use coins_pallet::{Config as CoinsConfig, Pallet as Coins, AllowMint}; + use validator_sets_pallet::{Config as VsConfig, Pallet as ValidatorSets}; use serai_primitives::*; - use validator_sets_primitives::{ValidatorSet, Session, musig_key}; + use validator_sets_primitives::{ValidatorSet, musig_key}; pub use genesis_liquidity_primitives as primitives; use primitives::*; + // TODO: Have a more robust way of accessing LiquidityTokens pallet. /// LiquidityTokens Pallet as an instance of coins pallet. pub type LiquidityTokens = coins_pallet::Pallet; #[pallet::config] pub trait Config: - frame_system::Config + DexConfig + CoinsConfig + coins_pallet::Config + frame_system::Config + + VsConfig + + DexConfig + + CoinsConfig + + coins_pallet::Config { type RuntimeEvent: From> + IsType<::RuntimeEvent>; } - #[pallet::genesis_config] - #[derive(Clone, PartialEq, Eq, Debug, Encode, Decode)] - pub struct GenesisConfig { - /// List of participants to place in the initial validator sets. - pub participants: Vec, - } - - impl Default for GenesisConfig { - fn default() -> Self { - GenesisConfig { participants: Default::default() } - } - } - #[pallet::error] pub enum Error { GenesisPeriodEnded, @@ -58,44 +48,46 @@ pub mod pallet { pub enum Event { GenesisLiquidityAdded { by: SeraiAddress, balance: Balance }, GenesisLiquidityRemoved { by: SeraiAddress, balance: Balance }, - GenesisLiquidityAddedToPool { coin1: Balance, coin2: Balance }, + GenesisLiquidityAddedToPool { coin1: Balance, sri: Amount }, EconomicSecurityReached { network: NetworkId }, } #[pallet::pallet] pub struct Pallet(PhantomData); + /// Keeps shares and the amount of coins per account. #[pallet::storage] pub(crate) type Liquidity = - StorageDoubleMap<_, Identity, Coin, Blake2_128Concat, PublicKey, SubstrateAmount, OptionQuery>; + StorageDoubleMap<_, Identity, Coin, Blake2_128Concat, PublicKey, LiquidityAmount, OptionQuery>; + /// Keeps the total shares and the total amount of coins per coin. #[pallet::storage] - pub(crate) type LiquidityTokensPerAddress = - StorageDoubleMap<_, Identity, Coin, Blake2_128Concat, PublicKey, SubstrateAmount, OptionQuery>; + pub(crate) type Supply = StorageMap<_, Identity, Coin, LiquidityAmount, OptionQuery>; #[pallet::storage] pub(crate) type EconomicSecurityReached = - StorageMap<_, Identity, NetworkId, BlockNumberFor, ValueQuery>; + StorageMap<_, Identity, NetworkId, BlockNumberFor, OptionQuery>; #[pallet::storage] - pub(crate) type Participants = - StorageMap<_, Identity, NetworkId, BoundedVec>, ValueQuery>; + pub(crate) type Oracle = StorageMap<_, Identity, Coin, u64, OptionQuery>; #[pallet::storage] - pub(crate) type Oracle = StorageMap<_, Identity, Coin, u64, ValueQuery>; - - #[pallet::genesis_build] - impl BuildGenesisConfig for GenesisConfig { - fn build(&self) { - Participants::::set(NetworkId::Serai, self.participants.clone().try_into().unwrap()); - } - } + pub(crate) type GenesisComplete = StorageValue<_, (), OptionQuery>; #[pallet::hooks] impl Hooks> for Pallet { - fn on_finalize(n: BlockNumberFor) { + fn on_initialize(n: BlockNumberFor) -> Weight { + #[cfg(feature = "fast-epoch")] + let final_block = 10u64; + + #[cfg(not(feature = "fast-epoch"))] + let final_block = MONTHS; + // Distribute the genesis sri to pools after a month - if n == GENESIS_PERIOD_BLOCKS.into() { + if (n.saturated_into::() >= final_block) && + Self::oraclization_is_done() && + GenesisComplete::::get().is_none() + { // mint the SRI Coins::::mint( GENESIS_LIQUIDITY_ACCOUNT.into(), @@ -103,8 +95,7 @@ pub mod pallet { ) .unwrap(); - // get coin values & total - let mut account_values = BTreeMap::new(); + // get pool & total values let mut pool_values = vec![]; let mut total_value: u128 = 0; for coin in COINS { @@ -113,22 +104,18 @@ pub mod pallet { } // initial coin value in terms of btc - let value = Oracle::::get(coin); + let Some(value) = Oracle::::get(coin) else { + continue; + }; - // get the pool & individual address values - account_values.insert(coin, vec![]); - let mut pool_amount: u128 = 0; - for (account, amount) in Liquidity::::iter_prefix(coin) { - pool_amount = pool_amount.saturating_add(amount.into()); - let value_this_addr = - u128::from(amount).saturating_mul(value.into()) / 10u128.pow(coin.decimals()); - account_values.get_mut(&coin).unwrap().push((account, value_this_addr)) - } - // sort, so that everyone has a consistent accounts vector per coin - account_values.get_mut(&coin).unwrap().sort(); - - let pool_value = pool_amount.saturating_mul(value.into()) / 10u128.pow(coin.decimals()); - total_value = total_value.saturating_add(pool_value); + let pool_amount = + u128::from(Supply::::get(coin).unwrap_or(LiquidityAmount::zero()).coins); + let pool_value = pool_amount + .checked_mul(value.into()) + .unwrap() + .checked_div(10u128.pow(coin.decimals())) + .unwrap(); + total_value = total_value.checked_add(pool_value).unwrap(); pool_values.push((coin, pool_amount, pool_value)); } @@ -138,20 +125,22 @@ pub mod pallet { for (i, (coin, pool_amount, pool_value)) in pool_values.into_iter().enumerate() { // whatever sri left for the last coin should be ~= it's ratio let sri_amount = if i == (pool_values_len - 1) { - GENESIS_SRI - total_sri_distributed + GENESIS_SRI.checked_sub(total_sri_distributed).unwrap() } else { - u64::try_from(u128::from(GENESIS_SRI).saturating_mul(pool_value) / total_value).unwrap() + u64::try_from( + u128::from(GENESIS_SRI) + .checked_mul(pool_value) + .unwrap() + .checked_div(total_value) + .unwrap(), + ) + .unwrap() }; - total_sri_distributed += sri_amount; - - // we can't add 0 liquidity - if !(pool_amount > 0 && sri_amount > 0) { - continue; - } + total_sri_distributed = total_sri_distributed.checked_add(sri_amount).unwrap(); // actually add the liquidity to dex let origin = RawOrigin::Signed(GENESIS_LIQUIDITY_ACCOUNT.into()); - Dex::::add_liquidity( + let Ok(()) = Dex::::add_liquidity( origin.into(), coin, u64::try_from(pool_amount).unwrap(), @@ -159,38 +148,15 @@ pub mod pallet { u64::try_from(pool_amount).unwrap(), sri_amount, GENESIS_LIQUIDITY_ACCOUNT.into(), - ) - .unwrap(); + ) else { + continue; + }; // let everyone know about the event Self::deposit_event(Event::GenesisLiquidityAddedToPool { coin1: Balance { coin, amount: Amount(u64::try_from(pool_amount).unwrap()) }, - coin2: Balance { coin: Coin::Serai, amount: Amount(sri_amount) }, + sri: Amount(sri_amount), }); - - // set liquidity tokens per account - let tokens = - u128::from(LiquidityTokens::::balance(GENESIS_LIQUIDITY_ACCOUNT.into(), coin).0); - let mut total_tokens_this_coin: u128 = 0; - - let accounts = account_values.get(&coin).unwrap(); - for (i, (acc, acc_value)) in accounts.iter().enumerate() { - // give whatever left to the last account not to have rounding errors. - let liq_tokens_this_acc = if i == accounts.len() - 1 { - tokens - total_tokens_this_coin - } else { - tokens.saturating_mul(*acc_value) / pool_value - }; - - total_tokens_this_coin = total_tokens_this_coin.saturating_add(liq_tokens_this_acc); - - LiquidityTokensPerAddress::::set( - coin, - acc, - Some(u64::try_from(liq_tokens_this_acc).unwrap()), - ); - } - assert_eq!(tokens, total_tokens_this_coin); } assert_eq!(total_sri_distributed, GENESIS_SRI); @@ -199,18 +165,23 @@ pub mod pallet { for coin in COINS { assert_eq!(Coins::::balance(GENESIS_LIQUIDITY_ACCOUNT.into(), coin), Amount(0)); } + + GenesisComplete::::set(Some(())); } // we accept we reached economic security once we can mint smallest amount of a network's coin + // TODO: move EconomicSecurity to a separate pallet for coin in COINS { let existing = EconomicSecurityReached::::get(coin.network()); - if existing == 0u32.into() && + if existing.is_none() && ::AllowMint::is_allowed(&Balance { coin, amount: Amount(1) }) { - EconomicSecurityReached::::set(coin.network(), n); + EconomicSecurityReached::::set(coin.network(), Some(n)); Self::deposit_event(Event::EconomicSecurityReached { network: coin.network() }); } } + + Weight::zero() // TODO } } @@ -223,39 +194,87 @@ pub mod pallet { Err(Error::::GenesisPeriodEnded)?; } - // mint the coins - Coins::::mint(GENESIS_LIQUIDITY_ACCOUNT.into(), balance)?; + // calculate new shares & supply + let (new_liquidity, new_supply) = if let Some(supply) = Supply::::get(balance.coin) { + // calculate amount of shares for this amount + let shares = Self::mul_div(supply.shares, balance.amount.0, supply.coins)?; + + // get new shares for this account + let existing = + Liquidity::::get(balance.coin, account).unwrap_or(LiquidityAmount::zero()); + ( + LiquidityAmount { + shares: existing.shares.checked_add(shares).ok_or(Error::::AmountOverflowed)?, + coins: existing + .coins + .checked_add(balance.amount.0) + .ok_or(Error::::AmountOverflowed)?, + }, + LiquidityAmount { + shares: supply.shares.checked_add(shares).ok_or(Error::::AmountOverflowed)?, + coins: supply + .coins + .checked_add(balance.amount.0) + .ok_or(Error::::AmountOverflowed)?, + }, + ) + } else { + let first_amount = + LiquidityAmount { shares: INITIAL_GENESIS_LP_SHARES, coins: balance.amount.0 }; + (first_amount, first_amount) + }; // save - let existing = Liquidity::::get(balance.coin, account).unwrap_or(0); - let new = existing.checked_add(balance.amount.0).ok_or(Error::::AmountOverflowed)?; - Liquidity::::set(balance.coin, account, Some(new)); - + Liquidity::::set(balance.coin, account, Some(new_liquidity)); + Supply::::set(balance.coin, Some(new_supply)); Self::deposit_event(Event::GenesisLiquidityAdded { by: account.into(), balance }); Ok(()) } - /// Returns the number of blocks since the coin's network reached economic security first time. - /// If the network is yet to be reached that threshold, 0 is returned. And maximum of - /// `GENESIS_SRI_TRICKLE_FEED` returned. - fn blocks_since_ec_security(coin: &Coin) -> u64 { - let ec_security_block = - EconomicSecurityReached::::get(coin.network()).saturated_into::(); - let current = >::block_number().saturated_into::(); - if ec_security_block > 0 { - let diff = current - ec_security_block; - if diff > GENESIS_SRI_TRICKLE_FEED { - return GENESIS_SRI_TRICKLE_FEED; - } - - return diff; + /// Returns the number of blocks since the all networks reached economic security first time. + /// If networks is yet to be reached that threshold, None is returned. + fn blocks_since_ec_security() -> Option { + let mut min = u64::MAX; + for n in NETWORKS { + let ec_security_block = EconomicSecurityReached::::get(n)?.saturated_into::(); + let current = >::block_number().saturated_into::(); + let diff = current.saturating_sub(ec_security_block); + min = diff.min(min); } - - 0 + Some(min) } fn genesis_ended() -> bool { - >::block_number() >= GENESIS_PERIOD_BLOCKS.into() + Self::oraclization_is_done() && + >::block_number().saturated_into::() >= MONTHS + } + + fn oraclization_is_done() -> bool { + for c in COINS { + if c == Coin::Serai { + continue; + } + + if Oracle::::get(c).is_none() { + return false; + } + } + + true + } + + fn mul_div(a: u64, b: u64, c: u64) -> Result> { + let a = u128::from(a); + let b = u128::from(b); + let c = u128::from(c); + + let result = a + .checked_mul(b) + .ok_or(Error::::AmountOverflowed)? + .checked_div(c) + .ok_or(Error::::AmountOverflowed)?; + + result.try_into().map_err(|_| Error::::AmountOverflowed) } } @@ -267,14 +286,21 @@ pub mod pallet { pub fn remove_coin_liquidity(origin: OriginFor, balance: Balance) -> DispatchResult { let account = ensure_signed(origin)?; let origin = RawOrigin::Signed(GENESIS_LIQUIDITY_ACCOUNT.into()); + let supply = Supply::::get(balance.coin).ok_or(Error::::NotEnoughLiquidity)?; // check we are still in genesis period - if Self::genesis_ended() { - // check user have enough to remove - let existing = LiquidityTokensPerAddress::::get(balance.coin, account).unwrap_or(0); - if balance.amount.0 > existing { - Err(Error::::NotEnoughLiquidity)?; - } + let (new_liquidity, new_supply) = if Self::genesis_ended() { + // see how much liq tokens we have + let total_liq_tokens = + LiquidityTokens::::balance(GENESIS_LIQUIDITY_ACCOUNT.into(), Coin::Serai).0; + + // get how much user wants to remove + let LiquidityAmount { shares, coins } = + Liquidity::::get(balance.coin, account).unwrap_or(LiquidityAmount::zero()); + let total_shares = Supply::::get(balance.coin).unwrap_or(LiquidityAmount::zero()).shares; + let user_liq_tokens = Self::mul_div(total_liq_tokens, shares, total_shares)?; + let amount_to_remove = + Self::mul_div(user_liq_tokens, balance.amount.0, INITIAL_GENESIS_LP_SHARES)?; // remove liquidity from pool let prev_sri = Coins::::balance(GENESIS_LIQUIDITY_ACCOUNT.into(), Coin::Serai); @@ -282,7 +308,7 @@ pub mod pallet { Dex::::remove_liquidity( origin.clone().into(), balance.coin, - balance.amount.0, + amount_to_remove, 1, 1, GENESIS_LIQUIDITY_ACCOUNT.into(), @@ -291,18 +317,26 @@ pub mod pallet { let current_coin = Coins::::balance(GENESIS_LIQUIDITY_ACCOUNT.into(), balance.coin); // burn the SRI if necessary - let mut sri = current_sri.0.saturating_sub(prev_sri.0); + // TODO: take into consideration movement between pools. + let mut sri: u64 = current_sri.0.saturating_sub(prev_sri.0); let distance_to_full_pay = - GENESIS_SRI_TRICKLE_FEED - Self::blocks_since_ec_security(&balance.coin); - let burn_sri_amount = sri.saturating_mul(distance_to_full_pay) / GENESIS_SRI_TRICKLE_FEED; + GENESIS_SRI_TRICKLE_FEED.saturating_sub(Self::blocks_since_ec_security().unwrap_or(0)); + let burn_sri_amount = u64::try_from( + u128::from(sri) + .checked_mul(u128::from(distance_to_full_pay)) + .ok_or(Error::::AmountOverflowed)? + .checked_div(u128::from(GENESIS_SRI_TRICKLE_FEED)) + .ok_or(Error::::AmountOverflowed)?, + ) + .map_err(|_| Error::::AmountOverflowed)?; Coins::::burn( origin.clone().into(), Balance { coin: Coin::Serai, amount: Amount(burn_sri_amount) }, )?; - sri -= burn_sri_amount; + sri = sri.checked_sub(burn_sri_amount).ok_or(Error::::AmountOverflowed)?; // transfer to owner - let coin_out = current_coin.0 - prev_coin.0; + let coin_out = current_coin.0.saturating_sub(prev_coin.0); Coins::::transfer( origin.clone().into(), account, @@ -314,46 +348,73 @@ pub mod pallet { Balance { coin: Coin::Serai, amount: Amount(sri) }, )?; - // save - let existing = LiquidityTokensPerAddress::::get(balance.coin, account).unwrap_or(0); - let new = existing.checked_sub(balance.amount.0).ok_or(Error::::AmountOverflowed)?; - LiquidityTokensPerAddress::::set(balance.coin, account, Some(new)); + // return new amounts + ( + LiquidityAmount { + shares: shares.checked_sub(amount_to_remove).ok_or(Error::::AmountOverflowed)?, + coins: coins.checked_sub(coin_out).ok_or(Error::::AmountOverflowed)?, + }, + LiquidityAmount { + shares: supply + .shares + .checked_sub(amount_to_remove) + .ok_or(Error::::AmountOverflowed)?, + coins: supply.coins.checked_sub(coin_out).ok_or(Error::::AmountOverflowed)?, + }, + ) } else { - let existing = Liquidity::::get(balance.coin, account).unwrap_or(0); - if balance.amount.0 > existing || balance.amount.0 == 0 { - Err(Error::::NotEnoughLiquidity)?; - } - if balance.amount.0 < existing { + if balance.amount.0 != INITIAL_GENESIS_LP_SHARES { Err(Error::::CanOnlyRemoveFullAmount)?; } + let existing = + Liquidity::::get(balance.coin, account).ok_or(Error::::NotEnoughLiquidity)?; - // TODO: do external transfer instead for making it easier for the user? - // or do we even want to make it easier? - Coins::::transfer(origin.into(), account, balance)?; + // transfer to the user + Coins::::transfer( + origin.into(), + account, + Balance { coin: balance.coin, amount: Amount(existing.coins) }, + )?; - // save + ( + LiquidityAmount::zero(), + LiquidityAmount { + shares: supply + .shares + .checked_sub(existing.shares) + .ok_or(Error::::AmountOverflowed)?, + coins: supply.coins.checked_sub(existing.coins).ok_or(Error::::AmountOverflowed)?, + }, + ) + }; + + // save + if new_liquidity == LiquidityAmount::zero() { Liquidity::::set(balance.coin, account, None); + } else { + Liquidity::::set(balance.coin, account, Some(new_liquidity)); } + Supply::::set(balance.coin, Some(new_supply)); Self::deposit_event(Event::GenesisLiquidityRemoved { by: account.into(), balance }); Ok(()) } - /// A call to submit the initial coi values. + /// A call to submit the initial coin values in terms of BTC. #[pallet::call_index(1)] #[pallet::weight((0, DispatchClass::Operational))] // TODO - pub fn set_initial_price( + pub fn oraclize_values( origin: OriginFor, - prices: Prices, + values: Values, _signature: Signature, ) -> DispatchResult { ensure_none(origin)?; - // set the prices - Oracle::::set(Coin::Bitcoin, prices.bitcoin); - Oracle::::set(Coin::Monero, prices.monero); - Oracle::::set(Coin::Ether, prices.ethereum); - Oracle::::set(Coin::Dai, prices.dai); + // set their relative values + Oracle::::set(Coin::Bitcoin, Some(10u64.pow(Coin::Bitcoin.decimals()))); + Oracle::::set(Coin::Monero, Some(values.monero)); + Oracle::::set(Coin::Ether, Some(values.ether)); + Oracle::::set(Coin::Dai, Some(values.dai)); Ok(()) } } @@ -364,11 +425,32 @@ pub mod pallet { fn validate_unsigned(_: TransactionSource, call: &Self::Call) -> TransactionValidity { match call { - Call::set_initial_price { ref prices, ref signature } => { - let set = ValidatorSet { network: NetworkId::Serai, session: Session(0) }; - let signers = Participants::::get(NetworkId::Serai); + Call::oraclize_values { ref values, ref signature } => { + let network = NetworkId::Serai; + let Some(session) = ValidatorSets::::session(network) else { + return Err(TransactionValidityError::from(InvalidTransaction::Custom(0))); + }; - if !musig_key(set, &signers).verify(&set_initial_price_message(&set, prices), signature) { + let set = ValidatorSet { network, session }; + let signers = ValidatorSets::::participants_for_latest_decided_set(network) + .expect("no participant in the current set") + .into_iter() + .map(|(p, _)| p) + .collect::>(); + + // check this didn't get called before + if Self::oraclization_is_done() { + Err(InvalidTransaction::Custom(1))?; + } + + // make sure signers settings the value at the end of the genesis period. + // we don't need this check for tests. + #[cfg(not(feature = "fast-epoch"))] + if >::block_number().saturated_into::() < MONTHS { + Err(InvalidTransaction::Custom(2))?; + } + + if !musig_key(set, &signers).verify(&oraclize_values_message(&set, values), signature) { Err(InvalidTransaction::BadProof)?; } diff --git a/substrate/genesis-liquidity/primitives/src/lib.rs b/substrate/genesis-liquidity/primitives/src/lib.rs index fce60d35..4e4c277c 100644 --- a/substrate/genesis-liquidity/primitives/src/lib.rs +++ b/substrate/genesis-liquidity/primitives/src/lib.rs @@ -18,34 +18,37 @@ use scale_info::TypeInfo; use serai_primitives::*; use validator_sets_primitives::ValidatorSet; -// amount of blocks in 30 days for 6s per block. -#[cfg(not(feature = "fast-epoch"))] -pub const GENESIS_PERIOD_BLOCKS: u32 = 10 * 60 * 24 * 30; - -#[cfg(feature = "fast-epoch")] -pub const GENESIS_PERIOD_BLOCKS: u32 = 25; - -/// 180 days of blocks -pub const GENESIS_SRI_TRICKLE_FEED: u64 = 10 * 60 * 24 * 180; - -// 100 Million SRI -pub const GENESIS_SRI: u64 = 100_000_000 * 10_u64.pow(8); +pub const INITIAL_GENESIS_LP_SHARES: u64 = 10_000; // This is the account to hold and manage the genesis liquidity. -pub const GENESIS_LIQUIDITY_ACCOUNT: SeraiAddress = system_address(b"Genesis-liquidity-account"); +pub const GENESIS_LIQUIDITY_ACCOUNT: SeraiAddress = system_address(b"GenesisLiquidity-account"); #[derive(Clone, Copy, PartialEq, Eq, Debug, Encode, Decode, MaxEncodedLen, TypeInfo)] #[cfg_attr(feature = "std", derive(Zeroize))] #[cfg_attr(feature = "borsh", derive(BorshSerialize, BorshDeserialize))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub struct Prices { - pub bitcoin: u64, +pub struct Values { pub monero: u64, - pub ethereum: u64, + pub ether: u64, pub dai: u64, } -/// The message for the set_initial_price signature. -pub fn set_initial_price_message(set: &ValidatorSet, prices: &Prices) -> Vec { - (b"GenesisLiquidity-set_initial_price", set, prices).encode() +#[derive(Clone, Copy, PartialEq, Eq, Debug, Encode, Decode, MaxEncodedLen, TypeInfo)] +#[cfg_attr(feature = "std", derive(Zeroize))] +#[cfg_attr(feature = "borsh", derive(BorshSerialize, BorshDeserialize))] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct LiquidityAmount { + pub shares: u64, + pub coins: u64, +} + +impl LiquidityAmount { + pub fn zero() -> Self { + LiquidityAmount { shares: 0, coins: 0 } + } +} + +/// The message for the oraclize_values signature. +pub fn oraclize_values_message(set: &ValidatorSet, values: &Values) -> Vec { + (b"GenesisLiquidity-oraclize_values", set, values).encode() } diff --git a/substrate/in-instructions/pallet/Cargo.toml b/substrate/in-instructions/pallet/Cargo.toml index f35ddf2c..9053f3bc 100644 --- a/substrate/in-instructions/pallet/Cargo.toml +++ b/substrate/in-instructions/pallet/Cargo.toml @@ -33,6 +33,7 @@ frame-support = { git = "https://github.com/serai-dex/substrate", default-featur serai-primitives = { path = "../../primitives", default-features = false } in-instructions-primitives = { package = "serai-in-instructions-primitives", path = "../primitives", default-features = false } +genesis-liquidity-primitives = { package = "serai-genesis-liquidity-primitives", path = "../../genesis-liquidity/primitives", default-features = false } coins-pallet = { package = "serai-coins-pallet", path = "../../coins/pallet", default-features = false } dex-pallet = { package = "serai-dex-pallet", path = "../../dex/pallet", default-features = false } @@ -56,6 +57,7 @@ std = [ "serai-primitives/std", "in-instructions-primitives/std", + "genesis-liquidity-primitives/std", "coins-pallet/std", "dex-pallet/std", @@ -64,3 +66,6 @@ std = [ "emissions-pallet/std", ] default = ["std"] + +# TODO +try-runtime = [] diff --git a/substrate/in-instructions/pallet/src/lib.rs b/substrate/in-instructions/pallet/src/lib.rs index a154405c..7c13db76 100644 --- a/substrate/in-instructions/pallet/src/lib.rs +++ b/substrate/in-instructions/pallet/src/lib.rs @@ -19,6 +19,7 @@ pub mod pallet { use sp_core::sr25519::Public; use serai_primitives::{Coin, Amount, Balance}; + use genesis_liquidity_primitives::GENESIS_LIQUIDITY_ACCOUNT; use frame_support::pallet_prelude::*; use frame_system::{pallet_prelude::*, RawOrigin}; @@ -211,6 +212,7 @@ pub mod pallet { } } InInstruction::GenesisLiquidity(address) => { + Coins::::mint(GENESIS_LIQUIDITY_ACCOUNT.into(), instruction.balance)?; GenesisLiq::::add_coin_liquidity(address.into(), instruction.balance)?; } InInstruction::SwapToStakedSRI(address, network) => { diff --git a/substrate/node/Cargo.toml b/substrate/node/Cargo.toml index 60f7dc0f..0e551c72 100644 --- a/substrate/node/Cargo.toml +++ b/substrate/node/Cargo.toml @@ -20,10 +20,11 @@ workspace = true name = "serai-node" [dependencies] +rand_core = "0.6" zeroize = "1" hex = "0.4" +log = "0.4" -rand_core = "0.6" schnorrkel = "0.11" libp2p = "0.52" diff --git a/substrate/node/src/chain_spec.rs b/substrate/node/src/chain_spec.rs index dd3b75ce..40feda61 100644 --- a/substrate/node/src/chain_spec.rs +++ b/substrate/node/src/chain_spec.rs @@ -19,9 +19,12 @@ fn account_from_name(name: &'static str) -> PublicKey { fn wasm_binary() -> Vec { // TODO: Accept a config of runtime path - if let Ok(binary) = std::fs::read("/runtime/serai.wasm") { + const WASM_PATH: &str = "/runtime/serai.wasm"; + if let Ok(binary) = std::fs::read(WASM_PATH) { + log::info!("using {WASM_PATH}"); return binary; } + log::info!("using built-in wasm"); WASM_BINARY.ok_or("compiled in wasm not available").unwrap().to_vec() } diff --git a/substrate/primitives/src/constants.rs b/substrate/primitives/src/constants.rs new file mode 100644 index 00000000..c5c53d75 --- /dev/null +++ b/substrate/primitives/src/constants.rs @@ -0,0 +1,29 @@ +use crate::BlockNumber; + +// 1 MB +pub const BLOCK_SIZE: u32 = 1024 * 1024; +// 6 seconds +pub const TARGET_BLOCK_TIME: u64 = 6; + +/// Measured in blocks. +pub const MINUTES: BlockNumber = 60 / TARGET_BLOCK_TIME; +pub const HOURS: BlockNumber = MINUTES * 60; +pub const DAYS: BlockNumber = HOURS * 24; +pub const WEEKS: BlockNumber = DAYS * 7; +pub const MONTHS: BlockNumber = WEEKS * 4; + +/// 6 months of blocks +pub const GENESIS_SRI_TRICKLE_FEED: u64 = MONTHS * 6; + +// 100 Million SRI +pub const GENESIS_SRI: u64 = 100_000_000 * 10_u64.pow(8); + +/// This needs to be long enough for arbitrage to occur and make holding any fake price up +/// sufficiently unrealistic. +#[allow(clippy::cast_possible_truncation)] +pub const ARBITRAGE_TIME: u16 = (2 * HOURS) as u16; + +/// Since we use the median price, double the window length. +/// +/// We additionally +1 so there is a true median. +pub const MEDIAN_PRICE_WINDOW_LENGTH: u16 = (2 * ARBITRAGE_TIME) + 1; diff --git a/substrate/primitives/src/lib.rs b/substrate/primitives/src/lib.rs index 970cf46e..d2c52219 100644 --- a/substrate/primitives/src/lib.rs +++ b/substrate/primitives/src/lib.rs @@ -37,8 +37,8 @@ pub use balance::*; mod account; pub use account::*; -mod tx; -pub use tx::*; +mod constants; +pub use constants::*; pub type BlockNumber = u64; pub type Header = sp_runtime::generic::Header; diff --git a/substrate/primitives/src/tx.rs b/substrate/primitives/src/tx.rs deleted file mode 100644 index b97ec3a2..00000000 --- a/substrate/primitives/src/tx.rs +++ /dev/null @@ -1,124 +0,0 @@ -use scale::Encode; - -use sp_core::sr25519::{Public, Signature}; -use sp_runtime::traits::Verify; - -use crate::SeraiAddress; - -trait TransactionMember: - Clone + PartialEq + Eq + core::fmt::Debug + scale::Encode + scale::Decode + scale_info::TypeInfo -{ -} -impl< - T: Clone - + PartialEq - + Eq - + core::fmt::Debug - + scale::Encode - + scale::Decode - + scale_info::TypeInfo, - > TransactionMember for T -{ -} - -// We use our own Transaction struct, over UncheckedExtrinsic, for more control, a bit more -// simplicity, and in order to be immune to https://github.com/paritytech/polkadot-sdk/issues/2947 -#[allow(private_bounds)] -#[derive(Clone, PartialEq, Eq, Debug, scale::Encode, scale::Decode, scale_info::TypeInfo)] -pub struct Transaction { - pub call: Call, - pub signature: Option<(SeraiAddress, Signature, Extra)>, -} - -#[cfg(feature = "serde")] -mod _serde { - use scale::Encode; - use serde::{ser::*, de::*}; - use super::*; - impl Serialize for Transaction { - fn serialize(&self, serializer: S) -> Result { - let encoded = self.encode(); - serializer.serialize_bytes(&encoded) - } - } - #[cfg(feature = "std")] - impl<'a, Call: TransactionMember, Extra: TransactionMember> Deserialize<'a> - for Transaction - { - fn deserialize>(de: D) -> Result { - let bytes = sp_core::bytes::deserialize(de)?; - scale::Decode::decode(&mut &bytes[..]) - .map_err(|e| serde::de::Error::custom(format!("invalid transaction: {e}"))) - } - } -} - -impl sp_runtime::traits::Extrinsic - for Transaction -{ - type Call = Call; - type SignaturePayload = (SeraiAddress, Signature, Extra); - fn is_signed(&self) -> Option { - Some(self.signature.is_some()) - } - fn new(call: Call, signature: Option) -> Option { - Some(Self { call, signature }) - } -} - -impl frame_support::traits::ExtrinsicCall - for Transaction -{ - fn call(&self) -> &Call { - &self.call - } -} - -impl sp_runtime::traits::ExtrinsicMetadata - for Transaction -where - Extra: sp_runtime::traits::SignedExtension, -{ - type SignedExtensions = Extra; - - const VERSION: u8 = 0; -} - -impl frame_support::dispatch::GetDispatchInfo - for Transaction -where - Call: frame_support::dispatch::GetDispatchInfo, -{ - fn get_dispatch_info(&self) -> frame_support::dispatch::DispatchInfo { - self.call.get_dispatch_info() - } -} - -impl sp_runtime::traits::BlindCheckable - for Transaction -where - Extra: sp_runtime::traits::SignedExtension, -{ - type Checked = sp_runtime::generic::CheckedExtrinsic; - - fn check( - self, - ) -> Result { - Ok(match self.signature { - Some((signer, signature, extra)) => { - if !signature.verify( - (&self.call, &extra, extra.additional_signed()?).encode().as_slice(), - &signer.into(), - ) { - Err(sp_runtime::transaction_validity::InvalidTransaction::BadProof)? - } - - sp_runtime::generic::CheckedExtrinsic { - signed: Some((signer.into(), extra)), - function: self.call, - } - } - None => sp_runtime::generic::CheckedExtrinsic { signed: None, function: self.call }, - }) - } -} diff --git a/substrate/runtime/Cargo.toml b/substrate/runtime/Cargo.toml index 9a59b4e4..6baee37e 100644 --- a/substrate/runtime/Cargo.toml +++ b/substrate/runtime/Cargo.toml @@ -49,6 +49,7 @@ frame-executive = { git = "https://github.com/serai-dex/substrate", default-feat frame-benchmarking = { git = "https://github.com/serai-dex/substrate", default-features = false, optional = true } serai-primitives = { path = "../primitives", default-features = false } +serai-abi = { path = "../abi", default-features = false, features = ["serde"] } pallet-timestamp = { git = "https://github.com/serai-dex/substrate", default-features = false } pallet-authorship = { git = "https://github.com/serai-dex/substrate", default-features = false } @@ -104,6 +105,8 @@ std = [ "frame-executive/std", "serai-primitives/std", + "serai-abi/std", + "serai-abi/serde", "pallet-timestamp/std", "pallet-authorship/std", diff --git a/substrate/runtime/src/abi.rs b/substrate/runtime/src/abi.rs new file mode 100644 index 00000000..b479036d --- /dev/null +++ b/substrate/runtime/src/abi.rs @@ -0,0 +1,383 @@ +use core::marker::PhantomData; + +use scale::{Encode, Decode}; + +use serai_abi::Call; + +use crate::{ + Vec, + primitives::{PublicKey, SeraiAddress}, + timestamp, coins, dex, genesis_liquidity, + validator_sets::{self, MembershipProof}, + in_instructions, signals, babe, grandpa, RuntimeCall, +}; + +impl From for RuntimeCall { + fn from(call: Call) -> RuntimeCall { + match call { + Call::Timestamp(serai_abi::timestamp::Call::set { now }) => { + RuntimeCall::Timestamp(timestamp::Call::set { now }) + } + Call::Coins(coins) => match coins { + serai_abi::coins::Call::transfer { to, balance } => { + RuntimeCall::Coins(coins::Call::transfer { to: to.into(), balance }) + } + serai_abi::coins::Call::burn { balance } => { + RuntimeCall::Coins(coins::Call::burn { balance }) + } + serai_abi::coins::Call::burn_with_instruction { instruction } => { + RuntimeCall::Coins(coins::Call::burn_with_instruction { instruction }) + } + }, + Call::LiquidityTokens(lt) => match lt { + serai_abi::liquidity_tokens::Call::transfer { to, balance } => { + RuntimeCall::LiquidityTokens(coins::Call::transfer { to: to.into(), balance }) + } + serai_abi::liquidity_tokens::Call::burn { balance } => { + RuntimeCall::LiquidityTokens(coins::Call::burn { balance }) + } + }, + Call::Dex(dex) => match dex { + serai_abi::dex::Call::add_liquidity { + coin, + coin_desired, + sri_desired, + coin_min, + sri_min, + mint_to, + } => RuntimeCall::Dex(dex::Call::add_liquidity { + coin, + coin_desired, + sri_desired, + coin_min, + sri_min, + mint_to: mint_to.into(), + }), + serai_abi::dex::Call::remove_liquidity { + coin, + lp_token_burn, + coin_min_receive, + sri_min_receive, + withdraw_to, + } => RuntimeCall::Dex(dex::Call::remove_liquidity { + coin, + lp_token_burn, + coin_min_receive, + sri_min_receive, + withdraw_to: withdraw_to.into(), + }), + serai_abi::dex::Call::swap_exact_tokens_for_tokens { + path, + amount_in, + amount_out_min, + send_to, + } => RuntimeCall::Dex(dex::Call::swap_exact_tokens_for_tokens { + path, + amount_in, + amount_out_min, + send_to: send_to.into(), + }), + serai_abi::dex::Call::swap_tokens_for_exact_tokens { + path, + amount_out, + amount_in_max, + send_to, + } => RuntimeCall::Dex(dex::Call::swap_tokens_for_exact_tokens { + path, + amount_out, + amount_in_max, + send_to: send_to.into(), + }), + }, + Call::GenesisLiquidity(gl) => match gl { + serai_abi::genesis_liquidity::Call::remove_coin_liquidity { balance } => { + RuntimeCall::GenesisLiquidity(genesis_liquidity::Call::remove_coin_liquidity { balance }) + } + serai_abi::genesis_liquidity::Call::oraclize_values { values, signature } => { + RuntimeCall::GenesisLiquidity(genesis_liquidity::Call::oraclize_values { + values, + signature, + }) + } + }, + Call::ValidatorSets(vs) => match vs { + serai_abi::validator_sets::Call::set_keys { + network, + removed_participants, + key_pair, + signature, + } => RuntimeCall::ValidatorSets(validator_sets::Call::set_keys { + network, + removed_participants: <_>::try_from( + removed_participants.into_iter().map(PublicKey::from).collect::>(), + ) + .unwrap(), + key_pair, + signature, + }), + serai_abi::validator_sets::Call::report_slashes { network, slashes, signature } => { + RuntimeCall::ValidatorSets(validator_sets::Call::report_slashes { + network, + slashes: <_>::try_from( + slashes + .into_iter() + .map(|(addr, slash)| (PublicKey::from(addr), slash)) + .collect::>(), + ) + .unwrap(), + signature, + }) + } + serai_abi::validator_sets::Call::allocate { network, amount } => { + RuntimeCall::ValidatorSets(validator_sets::Call::allocate { network, amount }) + } + serai_abi::validator_sets::Call::deallocate { network, amount } => { + RuntimeCall::ValidatorSets(validator_sets::Call::deallocate { network, amount }) + } + serai_abi::validator_sets::Call::claim_deallocation { network, session } => { + RuntimeCall::ValidatorSets(validator_sets::Call::claim_deallocation { network, session }) + } + }, + Call::InInstructions(ii) => match ii { + serai_abi::in_instructions::Call::execute_batch { batch } => { + RuntimeCall::InInstructions(in_instructions::Call::execute_batch { batch }) + } + }, + Call::Signals(signals) => match signals { + serai_abi::signals::Call::register_retirement_signal { in_favor_of } => { + RuntimeCall::Signals(signals::Call::register_retirement_signal { in_favor_of }) + } + serai_abi::signals::Call::revoke_retirement_signal { retirement_signal_id } => { + RuntimeCall::Signals(signals::Call::revoke_retirement_signal { retirement_signal_id }) + } + serai_abi::signals::Call::favor { signal_id, for_network } => { + RuntimeCall::Signals(signals::Call::favor { signal_id, for_network }) + } + serai_abi::signals::Call::revoke_favor { signal_id, for_network } => { + RuntimeCall::Signals(signals::Call::revoke_favor { signal_id, for_network }) + } + serai_abi::signals::Call::stand_against { signal_id, for_network } => { + RuntimeCall::Signals(signals::Call::stand_against { signal_id, for_network }) + } + }, + Call::Babe(babe) => match babe { + serai_abi::babe::Call::report_equivocation(report) => { + RuntimeCall::Babe(babe::Call::report_equivocation { + // TODO: Find a better way to go from Proof<[u8; 32]> to Proof + equivocation_proof: <_>::decode(&mut report.equivocation_proof.encode().as_slice()) + .unwrap(), + key_owner_proof: MembershipProof(report.key_owner_proof.into(), PhantomData), + }) + } + serai_abi::babe::Call::report_equivocation_unsigned(report) => { + RuntimeCall::Babe(babe::Call::report_equivocation_unsigned { + // TODO: Find a better way to go from Proof<[u8; 32]> to Proof + equivocation_proof: <_>::decode(&mut report.equivocation_proof.encode().as_slice()) + .unwrap(), + key_owner_proof: MembershipProof(report.key_owner_proof.into(), PhantomData), + }) + } + }, + Call::Grandpa(grandpa) => match grandpa { + serai_abi::grandpa::Call::report_equivocation(report) => { + RuntimeCall::Grandpa(grandpa::Call::report_equivocation { + // TODO: Find a better way to go from Proof<[u8; 32]> to Proof + equivocation_proof: <_>::decode(&mut report.equivocation_proof.encode().as_slice()) + .unwrap(), + key_owner_proof: MembershipProof(report.key_owner_proof.into(), PhantomData), + }) + } + serai_abi::grandpa::Call::report_equivocation_unsigned(report) => { + RuntimeCall::Grandpa(grandpa::Call::report_equivocation_unsigned { + // TODO: Find a better way to go from Proof<[u8; 32]> to Proof + equivocation_proof: <_>::decode(&mut report.equivocation_proof.encode().as_slice()) + .unwrap(), + key_owner_proof: MembershipProof(report.key_owner_proof.into(), PhantomData), + }) + } + }, + } + } +} + +impl TryInto for RuntimeCall { + type Error = (); + + fn try_into(self) -> Result { + Ok(match self { + RuntimeCall::Timestamp(timestamp::Call::set { now }) => { + Call::Timestamp(serai_abi::timestamp::Call::set { now }) + } + RuntimeCall::Coins(call) => Call::Coins(match call { + coins::Call::transfer { to, balance } => { + serai_abi::coins::Call::transfer { to: to.into(), balance } + } + coins::Call::burn { balance } => serai_abi::coins::Call::burn { balance }, + coins::Call::burn_with_instruction { instruction } => { + serai_abi::coins::Call::burn_with_instruction { instruction } + } + _ => Err(())?, + }), + RuntimeCall::LiquidityTokens(call) => Call::LiquidityTokens(match call { + coins::Call::transfer { to, balance } => { + serai_abi::liquidity_tokens::Call::transfer { to: to.into(), balance } + } + coins::Call::burn { balance } => serai_abi::liquidity_tokens::Call::burn { balance }, + _ => Err(())?, + }), + RuntimeCall::Dex(call) => Call::Dex(match call { + dex::Call::add_liquidity { + coin, + coin_desired, + sri_desired, + coin_min, + sri_min, + mint_to, + } => serai_abi::dex::Call::add_liquidity { + coin, + coin_desired, + sri_desired, + coin_min, + sri_min, + mint_to: mint_to.into(), + }, + dex::Call::remove_liquidity { + coin, + lp_token_burn, + coin_min_receive, + sri_min_receive, + withdraw_to, + } => serai_abi::dex::Call::remove_liquidity { + coin, + lp_token_burn, + coin_min_receive, + sri_min_receive, + withdraw_to: withdraw_to.into(), + }, + dex::Call::swap_exact_tokens_for_tokens { path, amount_in, amount_out_min, send_to } => { + serai_abi::dex::Call::swap_exact_tokens_for_tokens { + path, + amount_in, + amount_out_min, + send_to: send_to.into(), + } + } + dex::Call::swap_tokens_for_exact_tokens { path, amount_out, amount_in_max, send_to } => { + serai_abi::dex::Call::swap_tokens_for_exact_tokens { + path, + amount_out, + amount_in_max, + send_to: send_to.into(), + } + } + _ => Err(())?, + }), + RuntimeCall::GenesisLiquidity(call) => Call::GenesisLiquidity(match call { + genesis_liquidity::Call::remove_coin_liquidity { balance } => { + serai_abi::genesis_liquidity::Call::remove_coin_liquidity { balance } + } + genesis_liquidity::Call::oraclize_values { values, signature } => { + serai_abi::genesis_liquidity::Call::oraclize_values { values, signature } + } + _ => Err(())?, + }), + RuntimeCall::ValidatorSets(call) => Call::ValidatorSets(match call { + validator_sets::Call::set_keys { network, removed_participants, key_pair, signature } => { + serai_abi::validator_sets::Call::set_keys { + network, + removed_participants: <_>::try_from( + removed_participants.into_iter().map(SeraiAddress::from).collect::>(), + ) + .unwrap(), + key_pair, + signature, + } + } + validator_sets::Call::report_slashes { network, slashes, signature } => { + serai_abi::validator_sets::Call::report_slashes { + network, + slashes: <_>::try_from( + slashes + .into_iter() + .map(|(addr, slash)| (SeraiAddress::from(addr), slash)) + .collect::>(), + ) + .unwrap(), + signature, + } + } + validator_sets::Call::allocate { network, amount } => { + serai_abi::validator_sets::Call::allocate { network, amount } + } + validator_sets::Call::deallocate { network, amount } => { + serai_abi::validator_sets::Call::deallocate { network, amount } + } + validator_sets::Call::claim_deallocation { network, session } => { + serai_abi::validator_sets::Call::claim_deallocation { network, session } + } + _ => Err(())?, + }), + RuntimeCall::InInstructions(call) => Call::InInstructions(match call { + in_instructions::Call::execute_batch { batch } => { + serai_abi::in_instructions::Call::execute_batch { batch } + } + _ => Err(())?, + }), + RuntimeCall::Signals(call) => Call::Signals(match call { + signals::Call::register_retirement_signal { in_favor_of } => { + serai_abi::signals::Call::register_retirement_signal { in_favor_of } + } + signals::Call::revoke_retirement_signal { retirement_signal_id } => { + serai_abi::signals::Call::revoke_retirement_signal { retirement_signal_id } + } + signals::Call::favor { signal_id, for_network } => { + serai_abi::signals::Call::favor { signal_id, for_network } + } + signals::Call::revoke_favor { signal_id, for_network } => { + serai_abi::signals::Call::revoke_favor { signal_id, for_network } + } + signals::Call::stand_against { signal_id, for_network } => { + serai_abi::signals::Call::stand_against { signal_id, for_network } + } + _ => Err(())?, + }), + RuntimeCall::Babe(call) => Call::Babe(match call { + babe::Call::report_equivocation { equivocation_proof, key_owner_proof } => { + serai_abi::babe::Call::report_equivocation(serai_abi::babe::ReportEquivocation { + // TODO: Find a better way to go from Proof to Proof<[u8; 32]> + equivocation_proof: <_>::decode(&mut equivocation_proof.encode().as_slice()).unwrap(), + key_owner_proof: key_owner_proof.0.into(), + }) + } + babe::Call::report_equivocation_unsigned { equivocation_proof, key_owner_proof } => { + serai_abi::babe::Call::report_equivocation_unsigned(serai_abi::babe::ReportEquivocation { + // TODO: Find a better way to go from Proof to Proof<[u8; 32]> + equivocation_proof: <_>::decode(&mut equivocation_proof.encode().as_slice()).unwrap(), + key_owner_proof: key_owner_proof.0.into(), + }) + } + _ => Err(())?, + }), + RuntimeCall::Grandpa(call) => Call::Grandpa(match call { + grandpa::Call::report_equivocation { equivocation_proof, key_owner_proof } => { + serai_abi::grandpa::Call::report_equivocation(serai_abi::grandpa::ReportEquivocation { + // TODO: Find a better way to go from Proof to Proof<[u8; 32]> + equivocation_proof: <_>::decode(&mut equivocation_proof.encode().as_slice()).unwrap(), + key_owner_proof: key_owner_proof.0.into(), + }) + } + grandpa::Call::report_equivocation_unsigned { equivocation_proof, key_owner_proof } => { + serai_abi::grandpa::Call::report_equivocation_unsigned( + serai_abi::grandpa::ReportEquivocation { + // TODO: Find a better way to go from Proof to Proof<[u8; 32]> + equivocation_proof: <_>::decode(&mut equivocation_proof.encode().as_slice()).unwrap(), + key_owner_proof: key_owner_proof.0.into(), + }, + ) + } + _ => Err(())?, + }), + _ => Err(())?, + }) + } +} diff --git a/substrate/runtime/src/lib.rs b/substrate/runtime/src/lib.rs index 8d70c9fa..dde797d9 100644 --- a/substrate/runtime/src/lib.rs +++ b/substrate/runtime/src/lib.rs @@ -49,7 +49,11 @@ use sp_runtime::{ BoundedVec, Perbill, ApplyExtrinsicResult, }; -use primitives::{NetworkId, PublicKey, AccountLookup, SubstrateAmount, Coin, NETWORKS}; +#[allow(unused_imports)] +use primitives::{ + NetworkId, PublicKey, AccountLookup, SubstrateAmount, Coin, NETWORKS, MEDIAN_PRICE_WINDOW_LENGTH, + HOURS, DAYS, MINUTES, TARGET_BLOCK_TIME, BLOCK_SIZE, +}; use support::{ traits::{ConstU8, ConstU16, ConstU32, ConstU64, Contains}, @@ -66,6 +70,8 @@ use sp_authority_discovery::AuthorityId as AuthorityDiscoveryId; use babe::AuthorityId as BabeId; use grandpa::AuthorityId as GrandpaId; +mod abi; + /// Nonce of a transaction in the chain, for a given account. pub type Nonce = u32; @@ -83,7 +89,7 @@ pub type SignedExtra = ( transaction_payment::ChargeTransactionPayment, ); -pub type Transaction = serai_primitives::Transaction; +pub type Transaction = serai_abi::tx::Transaction; pub type Block = generic::Block; pub type BlockId = generic::BlockId; @@ -114,28 +120,7 @@ pub fn native_version() -> NativeVersion { NativeVersion { runtime_version: VERSION, can_author_with: Default::default() } } -// 1 MB -pub const BLOCK_SIZE: u32 = 1024 * 1024; -// 6 seconds -pub const TARGET_BLOCK_TIME: u64 = 6; - -/// Measured in blocks. -pub const MINUTES: BlockNumber = 60 / TARGET_BLOCK_TIME; -pub const HOURS: BlockNumber = MINUTES * 60; -pub const DAYS: BlockNumber = HOURS * 24; - pub const PRIMARY_PROBABILITY: (u64, u64) = (1, 4); - -/// This needs to be long enough for arbitrage to occur and make holding any fake price up -/// sufficiently unrealistic. -#[allow(clippy::cast_possible_truncation)] -pub const ARBITRAGE_TIME: u16 = (2 * HOURS) as u16; - -/// Since we use the median price, double the window length. -/// -/// We additionally +1 so there is a true median. -pub const MEDIAN_PRICE_WINDOW_LENGTH: u16 = (2 * ARBITRAGE_TIME) + 1; - pub const BABE_GENESIS_EPOCH_CONFIG: sp_consensus_babe::BabeEpochConfiguration = sp_consensus_babe::BabeEpochConfiguration { c: PRIMARY_PROBABILITY, @@ -163,38 +148,9 @@ parameter_types! { pub struct CallFilter; impl Contains for CallFilter { fn contains(call: &RuntimeCall) -> bool { - match call { - RuntimeCall::Timestamp(call) => match call { - timestamp::Call::set { .. } => true, - timestamp::Call::__Ignore(_, _) => false, - }, - - // All of these pallets are our own, and all of their written calls are intended to be called - RuntimeCall::Coins(call) => !matches!(call, coins::Call::__Ignore(_, _)), - RuntimeCall::LiquidityTokens(call) => match call { - coins::Call::transfer { .. } | coins::Call::burn { .. } => true, - coins::Call::burn_with_instruction { .. } | coins::Call::__Ignore(_, _) => false, - }, - RuntimeCall::Dex(call) => !matches!(call, dex::Call::__Ignore(_, _)), - RuntimeCall::ValidatorSets(call) => !matches!(call, validator_sets::Call::__Ignore(_, _)), - RuntimeCall::GenesisLiquidity(call) => { - !matches!(call, genesis_liquidity::Call::__Ignore(_, _)) - } - RuntimeCall::InInstructions(call) => !matches!(call, in_instructions::Call::__Ignore(_, _)), - RuntimeCall::Signals(call) => !matches!(call, signals::Call::__Ignore(_, _)), - - RuntimeCall::Babe(call) => match call { - babe::Call::report_equivocation { .. } | - babe::Call::report_equivocation_unsigned { .. } => true, - babe::Call::plan_config_change { .. } | babe::Call::__Ignore(_, _) => false, - }, - - RuntimeCall::Grandpa(call) => match call { - grandpa::Call::report_equivocation { .. } | - grandpa::Call::report_equivocation_unsigned { .. } => true, - grandpa::Call::note_stalled { .. } | grandpa::Call::__Ignore(_, _) => false, - }, - } + // If the call is defined in our ABI, it's allowed + let call: Result = call.clone().try_into(); + call.is_ok() } } @@ -375,6 +331,7 @@ construct_runtime!( Coins: coins, LiquidityTokens: coins::::{Pallet, Call, Storage, Event}, Dex: dex, + GenesisLiquidity: genesis_liquidity, ValidatorSets: validator_sets, GenesisLiquidity: genesis_liquidity, diff --git a/substrate/signals/pallet/Cargo.toml b/substrate/signals/pallet/Cargo.toml index 582a3e09..e06b5e6b 100644 --- a/substrate/signals/pallet/Cargo.toml +++ b/substrate/signals/pallet/Cargo.toml @@ -57,4 +57,7 @@ runtime-benchmarks = [ "frame-support/runtime-benchmarks", ] +# TODO +try-runtime = [] + default = ["std"] diff --git a/substrate/validator-sets/pallet/Cargo.toml b/substrate/validator-sets/pallet/Cargo.toml index 3b553788..dd67d1bc 100644 --- a/substrate/validator-sets/pallet/Cargo.toml +++ b/substrate/validator-sets/pallet/Cargo.toml @@ -70,6 +70,9 @@ std = [ "dex-pallet/std", ] +# TODO +try-runtime = [] + runtime-benchmarks = [ "frame-system/runtime-benchmarks", "frame-support/runtime-benchmarks", diff --git a/substrate/validator-sets/pallet/src/lib.rs b/substrate/validator-sets/pallet/src/lib.rs index 0f02661f..97e0fdb1 100644 --- a/substrate/validator-sets/pallet/src/lib.rs +++ b/substrate/validator-sets/pallet/src/lib.rs @@ -124,7 +124,9 @@ pub mod pallet { #[pallet::getter(fn allocation_per_key_share)] pub type AllocationPerKeyShare = StorageMap<_, Identity, NetworkId, Amount, OptionQuery>; - /// The validators selected to be in-set. + /// The validators selected to be in-set (and their key shares), regardless of if removed. + /// + /// This method allows iterating over all validators and their stake. #[pallet::storage] #[pallet::getter(fn participants_for_latest_decided_set)] pub(crate) type Participants = StorageMap< @@ -134,8 +136,10 @@ pub mod pallet { BoundedVec<(Public, u64), ConstU32<{ MAX_KEY_SHARES_PER_SET }>>, OptionQuery, >; - /// The validators selected to be in-set, regardless of if removed, with the ability to perform a - /// check for presence. + /// The validators selected to be in-set, regardless of if removed. + /// + /// This method allows quickly checking for presence in-set and looking up a validator's key + /// shares. // Uses Identity for NetworkId to avoid a hash of a severely limited fixed key-space. #[pallet::storage] pub(crate) type InSet = @@ -364,7 +368,6 @@ pub mod pallet { let allocation_per_key_share = Self::allocation_per_key_share(network).unwrap().0; let mut participants = vec![]; - let mut total_stake = 0; { let mut iter = SortedAllocationsIter::::new(network); let mut key_shares = 0; @@ -376,7 +379,6 @@ pub mod pallet { participants.push((key, these_key_shares)); key_shares += these_key_shares; - total_stake += amount.0; } amortize_excess_key_shares(&mut participants); } @@ -384,7 +386,6 @@ pub mod pallet { for (key, shares) in &participants { InSet::::set(network, key, Some(*shares)); } - TotalAllocatedStake::::set(network, Some(Amount(total_stake))); let set = ValidatorSet { network, session }; Pallet::::deposit_event(Event::NewSet { set }); @@ -524,11 +525,16 @@ pub mod pallet { Err(Error::::AllocationWouldPreventFaultTolerance)?; } - if InSet::::contains_key(network, account) { - TotalAllocatedStake::::set( - network, - Some(Amount(TotalAllocatedStake::::get(network).unwrap_or(Amount(0)).0 + amount.0)), - ); + // If they're in the current set, and the current set has completed its handover (so its + // currently being tracked by TotalAllocatedStake), update the TotalAllocatedStake + if let Some(session) = Self::session(network) { + if InSet::::contains_key(network, account) && Self::handover_completed(network, session) + { + TotalAllocatedStake::::set( + network, + Some(Amount(TotalAllocatedStake::::get(network).unwrap_or(Amount(0)).0 + amount.0)), + ); + } } Ok(()) @@ -644,8 +650,9 @@ pub mod pallet { // Checks if this session has completed the handover from the prior session. fn handover_completed(network: NetworkId, session: Session) -> bool { let Some(current_session) = Self::session(network) else { return false }; - // No handover occurs on genesis - if current_session.0 == 0 { + + // If the session we've been queried about is old, it must have completed its handover + if current_session.0 > session.0 { return true; } // If the session we've been queried about has yet to start, it can't have completed its @@ -653,19 +660,21 @@ pub mod pallet { if current_session.0 < session.0 { return false; } - if current_session.0 == session.0 { - // Handover is automatically complete for Serai as it doesn't have a handover protocol - // If not Serai, check the prior session had its keys cleared, which happens once its - // retired - return (network == NetworkId::Serai) || - (!Keys::::contains_key(ValidatorSet { - network, - session: Session(current_session.0 - 1), - })); + + // Handover is automatically complete for Serai as it doesn't have a handover protocol + if network == NetworkId::Serai { + return true; } - // We're currently in a future session, meaning this session definitely performed itself - // handover - true + + // The current session must have set keys for its handover to be completed + if !Keys::::contains_key(ValidatorSet { network, session }) { + return false; + } + + // This must be the first session (which has set keys) OR the prior session must have been + // retired (signified by its keys no longer being present) + (session.0 == 0) || + (!Keys::::contains_key(ValidatorSet { network, session: Session(session.0 - 1) })) } fn new_session() { @@ -683,6 +692,17 @@ pub mod pallet { } } + fn set_total_allocated_stake(network: NetworkId) { + let participants = Participants::::get(network) + .expect("setting TotalAllocatedStake for a network without participants"); + let total_stake = participants.iter().fold(0, |acc, (addr, _)| { + acc + Allocations::::get((network, addr)).unwrap_or(Amount(0)).0 + }); + TotalAllocatedStake::::set(network, Some(Amount(total_stake))); + } + + // TODO: This is called retire_set, yet just starts retiring the set + // Update the nomenclature within this function pub fn retire_set(set: ValidatorSet) { // If the prior prior set didn't report, emit they're retired now if PendingSlashReport::::get(set.network).is_some() { @@ -691,7 +711,7 @@ pub mod pallet { }); } - // Serai network slashes are handled by BABE/GRANDPA + // Serai doesn't set keys and network slashes are handled by BABE/GRANDPA if set.network != NetworkId::Serai { // This overwrites the prior value as the prior to-report set's stake presumably just // unlocked, making their report unenforceable @@ -703,6 +723,9 @@ pub mod pallet { Self::deposit_event(Event::AcceptedHandover { set: ValidatorSet { network: set.network, session: Session(set.session.0 + 1) }, }); + + // Update the total allocated stake to be for the current set + Self::set_total_allocated_stake(set.network); } /// Take the amount deallocatable. @@ -890,7 +913,7 @@ pub mod pallet { pub fn set_keys( origin: OriginFor, network: NetworkId, - removed_participants: Vec, + removed_participants: BoundedVec>, key_pair: KeyPair, signature: Signature, ) -> DispatchResult { @@ -905,6 +928,13 @@ pub mod pallet { Keys::::set(set, Some(key_pair.clone())); + // If this is the first ever set for this network, set TotalAllocatedStake now + // We generally set TotalAllocatedStake when the prior set retires, and the new set is fully + // active and liable. Since this is the first set, there is no prior set to wait to retire + if session == Session(0) { + Self::set_total_allocated_stake(network); + } + // This does not remove from TotalAllocatedStake or InSet in order to: // 1) Not decrease the stake present in this set. This means removed participants are // still liable for the economic security of the external network. This prevents diff --git a/tests/coordinator/src/lib.rs b/tests/coordinator/src/lib.rs index e6b0324d..c364128c 100644 --- a/tests/coordinator/src/lib.rs +++ b/tests/coordinator/src/lib.rs @@ -60,12 +60,18 @@ pub fn coordinator_instance( ) } -pub fn serai_composition(name: &str) -> TestBodySpecification { - serai_docker_tests::build("serai".to_string()); - - TestBodySpecification::with_image( - Image::with_repository("serai-dev-serai").pull_policy(PullPolicy::Never), - ) +pub fn serai_composition(name: &str, fast_epoch: bool) -> TestBodySpecification { + (if fast_epoch { + serai_docker_tests::build("serai-fast-epoch".to_string()); + TestBodySpecification::with_image( + Image::with_repository("serai-dev-serai-fast-epoch").pull_policy(PullPolicy::Never), + ) + } else { + serai_docker_tests::build("serai".to_string()); + TestBodySpecification::with_image( + Image::with_repository("serai-dev-serai").pull_policy(PullPolicy::Never), + ) + }) .replace_env( [("SERAI_NAME".to_string(), name.to_lowercase()), ("KEY".to_string(), " ".to_string())].into(), ) diff --git a/tests/coordinator/src/tests/batch.rs b/tests/coordinator/src/tests/batch.rs index ebba957b..bfe4e36e 100644 --- a/tests/coordinator/src/tests/batch.rs +++ b/tests/coordinator/src/tests/batch.rs @@ -260,21 +260,29 @@ pub async fn batch( #[tokio::test] async fn batch_test() { - new_test(|mut processors: Vec| async move { - let (processor_is, substrate_key, _) = key_gen::(&mut processors).await; - batch( - &mut processors, - &processor_is, - Session(0), - &substrate_key, - Batch { - network: NetworkId::Bitcoin, - id: 0, - block: BlockHash([0x22; 32]), - instructions: vec![], - }, - ) - .await; - }) + new_test( + |mut processors: Vec| async move { + // pop the last participant since genesis keygen has only 4 participants + processors.pop().unwrap(); + assert_eq!(processors.len(), COORDINATORS); + + let (processor_is, substrate_key, _) = + key_gen::(&mut processors, Session(0)).await; + batch( + &mut processors, + &processor_is, + Session(0), + &substrate_key, + Batch { + network: NetworkId::Bitcoin, + id: 0, + block: BlockHash([0x22; 32]), + instructions: vec![], + }, + ) + .await; + }, + false, + ) .await; } diff --git a/tests/coordinator/src/tests/key_gen.rs b/tests/coordinator/src/tests/key_gen.rs index 8250b3bf..8ea14cbc 100644 --- a/tests/coordinator/src/tests/key_gen.rs +++ b/tests/coordinator/src/tests/key_gen.rs @@ -23,10 +23,12 @@ use crate::tests::*; pub async fn key_gen( processors: &mut [Processor], + session: Session, ) -> (Vec, Zeroizing<::F>, Zeroizing) { + let coordinators = processors.len(); let mut participant_is = vec![]; - let set = ValidatorSet { session: Session(0), network: NetworkId::Bitcoin }; + let set = ValidatorSet { session, network: NetworkId::Bitcoin }; let id = KeyGenId { session: set.session, attempt: 0 }; for (i, processor) in processors.iter_mut().enumerate() { @@ -46,8 +48,8 @@ pub async fn key_gen( CoordinatorMessage::KeyGen(messages::key_gen::CoordinatorMessage::GenerateKey { id, params: ThresholdParams::new( - u16::try_from(((COORDINATORS * 2) / 3) + 1).unwrap(), - u16::try_from(COORDINATORS).unwrap(), + u16::try_from(((coordinators * 2) / 3) + 1).unwrap(), + u16::try_from(coordinators).unwrap(), participant_is[i], ) .unwrap(), @@ -65,7 +67,7 @@ pub async fn key_gen( wait_for_tributary().await; for (i, processor) in processors.iter_mut().enumerate() { - let mut commitments = (0 .. u8::try_from(COORDINATORS).unwrap()) + let mut commitments = (0 .. u8::try_from(coordinators).unwrap()) .map(|l| { ( participant_is[usize::from(l)], @@ -83,7 +85,7 @@ pub async fn key_gen( ); // Recipient it's for -> (Sender i, Recipient i) - let mut shares = (0 .. u8::try_from(COORDINATORS).unwrap()) + let mut shares = (0 .. u8::try_from(coordinators).unwrap()) .map(|l| { ( participant_is[usize::from(l)], @@ -118,7 +120,7 @@ pub async fn key_gen( CoordinatorMessage::KeyGen(messages::key_gen::CoordinatorMessage::Shares { id, shares: { - let mut shares = (0 .. u8::try_from(COORDINATORS).unwrap()) + let mut shares = (0 .. u8::try_from(coordinators).unwrap()) .map(|l| { ( participant_is[usize::from(l)], @@ -182,14 +184,14 @@ pub async fn key_gen( .unwrap() .as_secs() .abs_diff(context.serai_time) < - 70 + (60 * 60 * 3) // 3 hours, which should exceed the length of any test we run ); assert_eq!(context.network_latest_finalized_block.0, [0; 32]); assert_eq!(set.session, session); assert_eq!(key_pair.0 .0, substrate_key); assert_eq!(&key_pair.1, &network_key); } - _ => panic!("coordinator didn't respond with ConfirmKeyPair"), + _ => panic!("coordinator didn't respond with ConfirmKeyPair. msg: {msg:?}"), } message = Some(msg); } else { @@ -220,8 +222,15 @@ pub async fn key_gen( #[tokio::test] async fn key_gen_test() { - new_test(|mut processors: Vec| async move { - key_gen::(&mut processors).await; - }) + new_test( + |mut processors: Vec| async move { + // pop the last participant since genesis keygen has only 4 participants + processors.pop().unwrap(); + assert_eq!(processors.len(), COORDINATORS); + + key_gen::(&mut processors, Session(0)).await; + }, + false, + ) .await; } diff --git a/tests/coordinator/src/tests/mod.rs b/tests/coordinator/src/tests/mod.rs index b564a26b..ef67b0ac 100644 --- a/tests/coordinator/src/tests/mod.rs +++ b/tests/coordinator/src/tests/mod.rs @@ -22,6 +22,8 @@ mod sign; #[allow(unused_imports)] pub use sign::sign; +mod rotation; + pub(crate) const COORDINATORS: usize = 4; pub(crate) const THRESHOLD: usize = ((COORDINATORS * 2) / 3) + 1; @@ -39,13 +41,15 @@ impl) -> F> Test } } -pub(crate) async fn new_test(test_body: impl TestBody) { +pub(crate) async fn new_test(test_body: impl TestBody, fast_epoch: bool) { let mut unique_id_lock = UNIQUE_ID.get_or_init(|| Mutex::new(0)).lock().await; let mut coordinators = vec![]; let mut test = DockerTest::new().with_network(dockertest::Network::Isolated); let mut coordinator_compositions = vec![]; - for i in 0 .. COORDINATORS { + // Spawn one extra coordinator which isn't in-set + #[allow(clippy::range_plus_one)] + for i in 0 .. (COORDINATORS + 1) { let name = match i { 0 => "Alice", 1 => "Bob", @@ -55,7 +59,7 @@ pub(crate) async fn new_test(test_body: impl TestBody) { 5 => "Ferdie", _ => panic!("needed a 7th name for a serai node"), }; - let serai_composition = serai_composition(name); + let serai_composition = serai_composition(name, fast_epoch); let (processor_key, message_queue_keys, message_queue_composition) = serai_message_queue_tests::instance(); diff --git a/tests/coordinator/src/tests/rotation.rs b/tests/coordinator/src/tests/rotation.rs new file mode 100644 index 00000000..1ebeec16 --- /dev/null +++ b/tests/coordinator/src/tests/rotation.rs @@ -0,0 +1,169 @@ +use tokio::time::{sleep, Duration}; + +use ciphersuite::Secp256k1; + +use serai_client::{ + primitives::{insecure_pair_from_name, NetworkId}, + validator_sets::{ + self, + primitives::{Session, ValidatorSet}, + ValidatorSetsEvent, + }, + Amount, Pair, Transaction, +}; + +use crate::{*, tests::*}; + +// TODO: This is duplicated with serai-client's tests +async fn publish_tx(serai: &Serai, tx: &Transaction) -> [u8; 32] { + let mut latest = serai + .block(serai.latest_finalized_block_hash().await.unwrap()) + .await + .unwrap() + .unwrap() + .number(); + + serai.publish(tx).await.unwrap(); + + // Get the block it was included in + // TODO: Add an RPC method for this/check the guarantee on the subscription + let mut ticks = 0; + loop { + latest += 1; + + let block = { + let mut block; + while { + block = serai.finalized_block_by_number(latest).await.unwrap(); + block.is_none() + } { + sleep(Duration::from_secs(1)).await; + ticks += 1; + + if ticks > 60 { + panic!("60 seconds without inclusion in a finalized block"); + } + } + block.unwrap() + }; + + for transaction in &block.transactions { + if transaction == tx { + return block.hash(); + } + } + } +} + +#[allow(dead_code)] +async fn allocate_stake( + serai: &Serai, + network: NetworkId, + amount: Amount, + pair: &Pair, + nonce: u32, +) -> [u8; 32] { + // get the call + let tx = + serai.sign(pair, validator_sets::SeraiValidatorSets::allocate(network, amount), nonce, 0); + publish_tx(serai, &tx).await +} + +#[allow(dead_code)] +async fn deallocate_stake( + serai: &Serai, + network: NetworkId, + amount: Amount, + pair: &Pair, + nonce: u32, +) -> [u8; 32] { + // get the call + let tx = + serai.sign(pair, validator_sets::SeraiValidatorSets::deallocate(network, amount), nonce, 0); + publish_tx(serai, &tx).await +} + +async fn get_session(serai: &Serai, network: NetworkId) -> Session { + serai + .as_of_latest_finalized_block() + .await + .unwrap() + .validator_sets() + .session(network) + .await + .unwrap() + .unwrap() +} + +async fn wait_till_session_1(serai: &Serai, network: NetworkId) { + let mut current_session = get_session(serai, network).await; + + while current_session.0 < 1 { + sleep(Duration::from_secs(6)).await; + current_session = get_session(serai, network).await; + } +} + +async fn most_recent_new_set_event(serai: &Serai, network: NetworkId) -> ValidatorSetsEvent { + let mut current_block = serai.latest_finalized_block().await.unwrap(); + loop { + let events = serai.as_of(current_block.hash()).validator_sets().new_set_events().await.unwrap(); + for event in events { + match event { + ValidatorSetsEvent::NewSet { set } => { + if set.network == network { + return event; + } + } + _ => panic!("new_set_events gave non-NewSet event: {event:?}"), + } + } + current_block = serai.block(current_block.header.parent_hash.0).await.unwrap().unwrap(); + } +} + +#[tokio::test] +async fn set_rotation_test() { + new_test( + |mut processors: Vec| async move { + // exclude the last processor from keygen since we will add him later + let mut excluded = processors.pop().unwrap(); + assert_eq!(processors.len(), COORDINATORS); + + // excluded participant + let pair5 = insecure_pair_from_name("Eve"); + let network = NetworkId::Bitcoin; + let amount = Amount(1_000_000 * 10_u64.pow(8)); + let serai = processors[0].serai().await; + + // allocate now for the last participant so that it is guaranteed to be included into session + // 1 set. This doesn't affect the genesis set at all since that is a predetermined set. + allocate_stake(&serai, network, amount, &pair5, 0).await; + + // genesis keygen + let _ = key_gen::(&mut processors, Session(0)).await; + // Even the excluded processor should receive the key pair confirmation + match excluded.recv_message().await { + CoordinatorMessage::Substrate( + messages::substrate::CoordinatorMessage::ConfirmKeyPair { session, .. }, + ) => assert_eq!(session, Session(0)), + _ => panic!("excluded got message other than ConfirmKeyPair"), + } + + // wait until next session to see the effect on coordinator + wait_till_session_1(&serai, network).await; + + // Ensure the new validator was included in the new set + assert_eq!( + most_recent_new_set_event(&serai, network).await, + ValidatorSetsEvent::NewSet { set: ValidatorSet { session: Session(1), network } }, + ); + + // add the last participant & do the keygen + processors.push(excluded); + let _ = key_gen::(&mut processors, Session(1)).await; + }, + true, + ) + .await; +} diff --git a/tests/coordinator/src/tests/sign.rs b/tests/coordinator/src/tests/sign.rs index e46e8890..db8a7203 100644 --- a/tests/coordinator/src/tests/sign.rs +++ b/tests/coordinator/src/tests/sign.rs @@ -168,161 +168,172 @@ pub async fn sign( #[tokio::test] async fn sign_test() { - new_test(|mut processors: Vec| async move { - let (participant_is, substrate_key, _) = key_gen::(&mut processors).await; + new_test( + |mut processors: Vec| async move { + // pop the last participant since genesis keygen has only 4 participant. + processors.pop().unwrap(); + assert_eq!(processors.len(), COORDINATORS); - // 'Send' external coins into Serai - let serai = processors[0].serai().await; - let (serai_pair, serai_addr) = { - let mut name = [0; 4]; - OsRng.fill_bytes(&mut name); - let pair = insecure_pair_from_name(&hex::encode(name)); - let address = SeraiAddress::from(pair.public()); + let (participant_is, substrate_key, _) = + key_gen::(&mut processors, Session(0)).await; - // Fund the new account to pay for fees - let balance = Balance { coin: Coin::Serai, amount: Amount(1_000_000_000) }; + // 'Send' external coins into Serai + let serai = processors[0].serai().await; + let (serai_pair, serai_addr) = { + let mut name = [0; 4]; + OsRng.fill_bytes(&mut name); + let pair = insecure_pair_from_name(&hex::encode(name)); + let address = SeraiAddress::from(pair.public()); + + // Fund the new account to pay for fees + let balance = Balance { coin: Coin::Serai, amount: Amount(1_000_000_000) }; + serai + .publish(&serai.sign( + &insecure_pair_from_name("Ferdie"), + SeraiCoins::transfer(address, balance), + 0, + Default::default(), + )) + .await + .unwrap(); + + (pair, address) + }; + + #[allow(clippy::inconsistent_digit_grouping)] + let amount = Amount(1_000_000_00); + let balance = Balance { coin: Coin::Bitcoin, amount }; + + let coin_block = BlockHash([0x33; 32]); + let block_included_in = batch( + &mut processors, + &participant_is, + Session(0), + &substrate_key, + Batch { + network: NetworkId::Bitcoin, + id: 0, + block: coin_block, + instructions: vec![InInstructionWithBalance { + instruction: InInstruction::Transfer(serai_addr), + balance, + }], + }, + ) + .await; + + { + let block_included_in_hash = + serai.finalized_block_by_number(block_included_in).await.unwrap().unwrap().hash(); + + let serai = serai.as_of(block_included_in_hash); + let serai = serai.coins(); + assert_eq!( + serai.coin_balance(Coin::Serai, serai_addr).await.unwrap(), + Amount(1_000_000_000) + ); + + // Verify the mint occurred as expected + assert_eq!( + serai.mint_events().await.unwrap(), + vec![CoinsEvent::Mint { to: serai_addr, balance }] + ); + assert_eq!(serai.coin_supply(Coin::Bitcoin).await.unwrap(), amount); + assert_eq!(serai.coin_balance(Coin::Bitcoin, serai_addr).await.unwrap(), amount); + } + + // Trigger a burn + let out_instruction = OutInstructionWithBalance { + balance, + instruction: OutInstruction { + address: ExternalAddress::new(b"external".to_vec()).unwrap(), + data: None, + }, + }; serai .publish(&serai.sign( - &insecure_pair_from_name("Ferdie"), - SeraiCoins::transfer(address, balance), + &serai_pair, + SeraiCoins::burn_with_instruction(out_instruction.clone()), 0, Default::default(), )) .await .unwrap(); - (pair, address) - }; - - #[allow(clippy::inconsistent_digit_grouping)] - let amount = Amount(1_000_000_00); - let balance = Balance { coin: Coin::Bitcoin, amount }; - - let coin_block = BlockHash([0x33; 32]); - let block_included_in = batch( - &mut processors, - &participant_is, - Session(0), - &substrate_key, - Batch { - network: NetworkId::Bitcoin, - id: 0, - block: coin_block, - instructions: vec![InInstructionWithBalance { - instruction: InInstruction::Transfer(serai_addr), - balance, - }], - }, - ) - .await; - - { - let block_included_in_hash = - serai.finalized_block_by_number(block_included_in).await.unwrap().unwrap().hash(); - - let serai = serai.as_of(block_included_in_hash); - let serai = serai.coins(); - assert_eq!(serai.coin_balance(Coin::Serai, serai_addr).await.unwrap(), Amount(1_000_000_000)); - - // Verify the mint occurred as expected - assert_eq!( - serai.mint_events().await.unwrap(), - vec![CoinsEvent::Mint { to: serai_addr, balance }] - ); - assert_eq!(serai.coin_supply(Coin::Bitcoin).await.unwrap(), amount); - assert_eq!(serai.coin_balance(Coin::Bitcoin, serai_addr).await.unwrap(), amount); - } - - // Trigger a burn - let out_instruction = OutInstructionWithBalance { - balance, - instruction: OutInstruction { - address: ExternalAddress::new(b"external".to_vec()).unwrap(), - data: None, - }, - }; - serai - .publish(&serai.sign( - &serai_pair, - SeraiCoins::burn_with_instruction(out_instruction.clone()), - 0, - Default::default(), - )) - .await - .unwrap(); - - // TODO: We *really* need a helper for this pattern - let mut last_serai_block = block_included_in; - 'outer: for _ in 0 .. 20 { - tokio::time::sleep(Duration::from_secs(6)).await; - if std::env::var("GITHUB_CI") == Ok("true".to_string()) { + // TODO: We *really* need a helper for this pattern + let mut last_serai_block = block_included_in; + 'outer: for _ in 0 .. 20 { tokio::time::sleep(Duration::from_secs(6)).await; - } - - while last_serai_block <= serai.latest_finalized_block().await.unwrap().number() { - let burn_events = serai - .as_of(serai.finalized_block_by_number(last_serai_block).await.unwrap().unwrap().hash()) - .coins() - .burn_with_instruction_events() - .await - .unwrap(); - - if !burn_events.is_empty() { - assert_eq!(burn_events.len(), 1); - assert_eq!( - burn_events[0], - CoinsEvent::BurnWithInstruction { - from: serai_addr, - instruction: out_instruction.clone() - } - ); - break 'outer; + if std::env::var("GITHUB_CI") == Ok("true".to_string()) { + tokio::time::sleep(Duration::from_secs(6)).await; } - last_serai_block += 1; - } - } - let last_serai_block = - serai.finalized_block_by_number(last_serai_block).await.unwrap().unwrap(); - let last_serai_block_hash = last_serai_block.hash(); - let serai = serai.as_of(last_serai_block_hash); - let serai = serai.coins(); - assert_eq!(serai.coin_supply(Coin::Bitcoin).await.unwrap(), Amount(0)); - assert_eq!(serai.coin_balance(Coin::Bitcoin, serai_addr).await.unwrap(), Amount(0)); + while last_serai_block <= serai.latest_finalized_block().await.unwrap().number() { + let burn_events = serai + .as_of(serai.finalized_block_by_number(last_serai_block).await.unwrap().unwrap().hash()) + .coins() + .burn_with_instruction_events() + .await + .unwrap(); - let mut plan_id = [0; 32]; - OsRng.fill_bytes(&mut plan_id); - let plan_id = plan_id; - - // We should now get a SubstrateBlock - for processor in &mut processors { - assert_eq!( - processor.recv_message().await, - messages::CoordinatorMessage::Substrate( - messages::substrate::CoordinatorMessage::SubstrateBlock { - context: SubstrateContext { - serai_time: last_serai_block.time().unwrap() / 1000, - network_latest_finalized_block: coin_block, - }, - block: last_serai_block.number(), - burns: vec![out_instruction.clone()], - batches: vec![], + if !burn_events.is_empty() { + assert_eq!(burn_events.len(), 1); + assert_eq!( + burn_events[0], + CoinsEvent::BurnWithInstruction { + from: serai_addr, + instruction: out_instruction.clone() + } + ); + break 'outer; } - ) - ); + last_serai_block += 1; + } + } - // Send the ACK, claiming there's a plan to sign - processor - .send_message(messages::ProcessorMessage::Coordinator( - messages::coordinator::ProcessorMessage::SubstrateBlockAck { - block: last_serai_block.number(), - plans: vec![PlanMeta { session: Session(0), id: plan_id }], - }, - )) - .await; - } + let last_serai_block = + serai.finalized_block_by_number(last_serai_block).await.unwrap().unwrap(); + let last_serai_block_hash = last_serai_block.hash(); + let serai = serai.as_of(last_serai_block_hash); + let serai = serai.coins(); + assert_eq!(serai.coin_supply(Coin::Bitcoin).await.unwrap(), Amount(0)); + assert_eq!(serai.coin_balance(Coin::Bitcoin, serai_addr).await.unwrap(), Amount(0)); - sign(&mut processors, &participant_is, Session(0), plan_id).await; - }) + let mut plan_id = [0; 32]; + OsRng.fill_bytes(&mut plan_id); + let plan_id = plan_id; + + // We should now get a SubstrateBlock + for processor in &mut processors { + assert_eq!( + processor.recv_message().await, + messages::CoordinatorMessage::Substrate( + messages::substrate::CoordinatorMessage::SubstrateBlock { + context: SubstrateContext { + serai_time: last_serai_block.time().unwrap() / 1000, + network_latest_finalized_block: coin_block, + }, + block: last_serai_block.number(), + burns: vec![out_instruction.clone()], + batches: vec![], + } + ) + ); + + // Send the ACK, claiming there's a plan to sign + processor + .send_message(messages::ProcessorMessage::Coordinator( + messages::coordinator::ProcessorMessage::SubstrateBlockAck { + block: last_serai_block.number(), + plans: vec![PlanMeta { session: Session(0), id: plan_id }], + }, + )) + .await; + } + + sign(&mut processors, &participant_is, Session(0), plan_id).await; + }, + false, + ) .await; } diff --git a/tests/docker/src/lib.rs b/tests/docker/src/lib.rs index 3493d502..3db069d8 100644 --- a/tests/docker/src/lib.rs +++ b/tests/docker/src/lib.rs @@ -85,8 +85,8 @@ pub fn build(name: String) { } let mut dockerfile_path = orchestration_path.clone(); - if HashSet::from(["bitcoin", "ethereum", "monero"]).contains(name.as_str()) { - dockerfile_path = dockerfile_path.join("coins"); + if HashSet::from(["bitcoin", "ethereum", "ethereum-relayer", "monero"]).contains(name.as_str()) { + dockerfile_path = dockerfile_path.join("networks"); } if name.contains("-processor") { dockerfile_path = @@ -125,6 +125,9 @@ pub fn build(name: String) { let meta = |path: PathBuf| (path.clone(), fs::metadata(path)); let mut metadatas = match name.as_str() { "bitcoin" | "ethereum" | "monero" => vec![], + "ethereum-relayer" => { + vec![meta(repo_path.join("common")), meta(repo_path.join("networks"))] + } "message-queue" => vec![ meta(repo_path.join("common")), meta(repo_path.join("crypto")), @@ -134,7 +137,7 @@ pub fn build(name: String) { "bitcoin-processor" | "ethereum-processor" | "monero-processor" => vec![ meta(repo_path.join("common")), meta(repo_path.join("crypto")), - meta(repo_path.join("coins")), + meta(repo_path.join("networks")), meta(repo_path.join("substrate")), meta(repo_path.join("message-queue")), meta(repo_path.join("processor")), @@ -142,7 +145,7 @@ pub fn build(name: String) { "coordinator" => vec![ meta(repo_path.join("common")), meta(repo_path.join("crypto")), - meta(repo_path.join("coins")), + meta(repo_path.join("networks")), meta(repo_path.join("substrate")), meta(repo_path.join("message-queue")), meta(repo_path.join("coordinator")), diff --git a/tests/full-stack/Cargo.toml b/tests/full-stack/Cargo.toml index 58e6de28..4e684752 100644 --- a/tests/full-stack/Cargo.toml +++ b/tests/full-stack/Cargo.toml @@ -26,8 +26,9 @@ rand_core = { version = "0.6", default-features = false } curve25519-dalek = { version = "4", features = ["rand_core"] } -bitcoin-serai = { path = "../../coins/bitcoin" } -monero-serai = { path = "../../coins/monero" } +bitcoin-serai = { path = "../../networks/bitcoin" } +monero-simple-request-rpc = { path = "../../networks/monero/rpc/simple-request" } +monero-wallet = { path = "../../networks/monero/wallet" } scale = { package = "parity-scale-codec", version = "3" } serde = "1" diff --git a/tests/full-stack/src/lib.rs b/tests/full-stack/src/lib.rs index 5e39c70d..509a67fa 100644 --- a/tests/full-stack/src/lib.rs +++ b/tests/full-stack/src/lib.rs @@ -53,8 +53,9 @@ impl Handles { pub async fn monero( &self, ops: &DockerOperations, - ) -> monero_serai::rpc::Rpc { - use monero_serai::rpc::HttpRpc; + ) -> monero_simple_request_rpc::SimpleRequestRpc { + use monero_simple_request_rpc::SimpleRequestRpc; + use monero_wallet::rpc::Rpc; let rpc = ops.handle(&self.monero.0).host_port(self.monero.1).unwrap(); let rpc = format!("http://{RPC_USER}:{RPC_PASS}@{}:{}", rpc.0, rpc.1); @@ -62,7 +63,7 @@ impl Handles { // If the RPC server has yet to start, sleep for up to 60s until it does for _ in 0 .. 60 { tokio::time::sleep(Duration::from_secs(1)).await; - let Ok(client) = HttpRpc::new(rpc.clone()).await else { continue }; + let Ok(client) = SimpleRequestRpc::new(rpc.clone()).await else { continue }; if client.get_height().await.is_err() { continue; } diff --git a/tests/full-stack/src/tests/mint_and_burn.rs b/tests/full-stack/src/tests/mint_and_burn.rs index 51b8156c..3bb9e11f 100644 --- a/tests/full-stack/src/tests/mint_and_burn.rs +++ b/tests/full-stack/src/tests/mint_and_burn.rs @@ -1,7 +1,6 @@ use std::{ sync::{OnceLock, Arc, Mutex}, time::{Duration, Instant}, - collections::HashSet, }; use zeroize::Zeroizing; @@ -57,7 +56,7 @@ async fn mint_and_burn_test() { }; let addr = Address::p2pkh( - &PublicKey::from_private_key( + PublicKey::from_private_key( SECP256K1, &PrivateKey::new(SecretKey::from_slice(&[0x01; 32]).unwrap(), Network::Bitcoin), ), @@ -88,14 +87,11 @@ async fn mint_and_burn_test() { // Mine a Monero block let monero_blocks = { use curve25519_dalek::{constants::ED25519_BASEPOINT_POINT, scalar::Scalar}; - use monero_serai::wallet::{ - ViewPair, - address::{Network, AddressSpec}, - }; + use monero_wallet::{rpc::Rpc, ViewPair, address::Network}; let addr = ViewPair::new(ED25519_BASEPOINT_POINT, Zeroizing::new(Scalar::ONE)) - .address(Network::Mainnet, AddressSpec::Standard) - .to_string(); + .unwrap() + .legacy_address(Network::Mainnet); let rpc = producer_handles.monero(ops).await; let mut res = Vec::with_capacity(count); @@ -103,8 +99,8 @@ async fn mint_and_burn_test() { let block = rpc.get_block(rpc.generate_blocks(&addr, 1).await.unwrap().0[0]).await.unwrap(); - let mut txs = Vec::with_capacity(block.txs.len()); - for tx in &block.txs { + let mut txs = Vec::with_capacity(block.transactions.len()); + for tx in &block.transactions { txs.push(rpc.get_transaction(*tx).await.unwrap()); } res.push((serde_json::json!([hex::encode(block.serialize())]), txs)); @@ -128,6 +124,8 @@ async fn mint_and_burn_test() { } { + use monero_wallet::rpc::Rpc; + let rpc = handles.monero(ops).await; for (block, txs) in &monero_blocks { @@ -266,14 +264,13 @@ async fn mint_and_burn_test() { script::{PushBytesBuf, Script, ScriptBuf, Builder}, absolute::LockTime, transaction::{Version, Transaction}, - address::Payload, - Sequence, Witness, OutPoint, TxIn, Amount, TxOut, Network, + Sequence, Witness, OutPoint, TxIn, Amount, TxOut, Network, Address, }; let private_key = PrivateKey::new(SecretKey::from_slice(&[0x01; 32]).unwrap(), Network::Bitcoin); let public_key = PublicKey::from_private_key(SECP256K1, &private_key); - let addr = Payload::p2pkh(&public_key); + let addr = Address::p2pkh(public_key, Network::Bitcoin); // Use the first block's coinbase let rpc = handles[0].bitcoin(&ops).await; @@ -284,7 +281,7 @@ async fn mint_and_burn_test() { version: Version(2), lock_time: LockTime::ZERO, input: vec![TxIn { - previous_output: OutPoint { txid: tx.txid(), vout: 0 }, + previous_output: OutPoint { txid: tx.compute_txid(), vout: 0 }, script_sig: Script::new().into(), sequence: Sequence(u32::MAX), witness: Witness::default(), @@ -292,17 +289,23 @@ async fn mint_and_burn_test() { output: vec![ TxOut { value: Amount::from_sat(1_100_000_00), - script_pubkey: Payload::p2tr_tweaked(TweakedPublicKey::dangerous_assume_tweaked( - XOnlyPublicKey::from_slice(&bitcoin_key_pair.1[1 ..]).unwrap(), - )) + script_pubkey: Address::p2tr_tweaked( + TweakedPublicKey::dangerous_assume_tweaked( + XOnlyPublicKey::from_slice(&bitcoin_key_pair.1[1 ..]).unwrap(), + ), + Network::Bitcoin, + ) .script_pubkey(), }, TxOut { // change = amount spent - fee value: Amount::from_sat(tx.output[0].value.to_sat() - 1_100_000_00 - 1_000_00), - script_pubkey: Payload::p2tr_tweaked(TweakedPublicKey::dangerous_assume_tweaked( - XOnlyPublicKey::from_slice(&public_key.inner.serialize()[1 ..]).unwrap(), - )) + script_pubkey: Address::p2tr_tweaked( + TweakedPublicKey::dangerous_assume_tweaked( + XOnlyPublicKey::from_slice(&public_key.inner.serialize()[1 ..]).unwrap(), + ), + Network::Bitcoin, + ) .script_pubkey(), }, TxOut { @@ -316,12 +319,14 @@ async fn mint_and_burn_test() { let mut der = SECP256K1 .sign_ecdsa_low_r( - &Message::from( + &Message::from_digest_slice( SighashCache::new(&tx) .legacy_signature_hash(0, &addr.script_pubkey(), EcdsaSighashType::All.to_u32()) .unwrap() - .to_raw_hash(), - ), + .to_raw_hash() + .as_ref(), + ) + .unwrap(), &private_key.inner, ) .serialize_der() @@ -338,59 +343,55 @@ async fn mint_and_burn_test() { // Send in XMR { use curve25519_dalek::{constants::ED25519_BASEPOINT_POINT, scalar::Scalar}; - use monero_serai::{ - Protocol, - transaction::Timelock, - wallet::{ - ViewPair, Scanner, Decoys, Change, FeePriority, SignableTransaction, - address::{Network, AddressType, AddressMeta, MoneroAddress}, - }, - decompress_point, + use monero_wallet::{ + io::decompress_point, + ringct::RctType, + rpc::{FeePriority, Rpc}, + address::{Network, AddressType, MoneroAddress}, + ViewPair, Scanner, OutputWithDecoys, + send::{Change, SignableTransaction}, }; // Grab the first output on the chain let rpc = handles[0].monero(&ops).await; - let view_pair = ViewPair::new(ED25519_BASEPOINT_POINT, Zeroizing::new(Scalar::ONE)); - let mut scanner = Scanner::from_view(view_pair.clone(), Some(HashSet::new())); + let view_pair = ViewPair::new(ED25519_BASEPOINT_POINT, Zeroizing::new(Scalar::ONE)).unwrap(); + let mut scanner = Scanner::new(view_pair.clone()); let output = scanner .scan(&rpc, &rpc.get_block_by_number(1).await.unwrap()) .await .unwrap() - .swap_remove(0) - .unlocked(Timelock::Block(rpc.get_height().await.unwrap())) - .unwrap() + .additional_timelock_satisfied_by(rpc.get_height().await.unwrap(), 0) .swap_remove(0); - let decoys = Decoys::fingerprintable_canonical_select( + let input = OutputWithDecoys::fingerprintable_deterministic_new( &mut OsRng, &rpc, - Protocol::v16.ring_len(), + 16, rpc.get_height().await.unwrap(), - &[output.clone()], + output.clone(), ) .await - .unwrap() - .swap_remove(0); + .unwrap(); + let mut outgoing_view_key = Zeroizing::new([0; 32]); + OsRng.fill_bytes(outgoing_view_key.as_mut()); let tx = SignableTransaction::new( - Protocol::v16, - None, - vec![(output, decoys)], + RctType::ClsagBulletproofPlus, + outgoing_view_key, + vec![input], vec![( MoneroAddress::new( - AddressMeta::new( - Network::Mainnet, - AddressType::Featured { guaranteed: true, subaddress: false, payment_id: None }, - ), + Network::Mainnet, + AddressType::Featured { guaranteed: true, subaddress: false, payment_id: None }, decompress_point(monero_key_pair.1.to_vec().try_into().unwrap()).unwrap(), ED25519_BASEPOINT_POINT * processor::additional_key::(0).0, ), 1_100_000_000_000, )], - &Change::new(&view_pair, false), + Change::new(view_pair.clone(), None), vec![Shorthand::transfer(None, serai_addr).encode()], - rpc.get_fee(Protocol::v16, FeePriority::Unimportant).await.unwrap(), + rpc.get_fee_rate(FeePriority::Unimportant).await.unwrap(), ) .unwrap() .sign(&mut OsRng, &Zeroizing::new(Scalar::ONE)) @@ -447,19 +448,17 @@ async fn mint_and_burn_test() { // Create a random Bitcoin/Monero address let bitcoin_addr = { - use bitcoin_serai::bitcoin::{network::Network, key::PublicKey, address::Address}; - // Uses Network::Bitcoin since it doesn't actually matter, Serai strips it out - // TODO: Move Serai to Payload from Address - Address::p2pkh( - &loop { + use bitcoin_serai::bitcoin::{key::PublicKey, ScriptBuf}; + ScriptBuf::new_p2pkh( + &(loop { let mut bytes = [0; 33]; OsRng.fill_bytes(&mut bytes); bytes[0] %= 4; if let Ok(key) = PublicKey::from_slice(&bytes) { break key; } - }, - Network::Bitcoin, + }) + .pubkey_hash(), ) }; @@ -468,9 +467,10 @@ async fn mint_and_burn_test() { let spend = ED25519_BASEPOINT_TABLE * &Scalar::random(&mut OsRng); let view = Scalar::random(&mut OsRng); - use monero_serai::wallet::address::{Network, AddressType, AddressMeta, MoneroAddress}; + use monero_wallet::address::{Network, AddressType, MoneroAddress}; let addr = MoneroAddress::new( - AddressMeta::new(Network::Mainnet, AddressType::Standard), + Network::Mainnet, + AddressType::Legacy, spend, ED25519_BASEPOINT_TABLE * &view, ); @@ -481,7 +481,10 @@ async fn mint_and_burn_test() { // Get the current blocks let mut start_bitcoin_block = handles[0].bitcoin(&ops).await.get_latest_block_number().await.unwrap(); - let mut start_monero_block = handles[0].monero(&ops).await.get_height().await.unwrap(); + let mut start_monero_block = { + use monero_wallet::rpc::Rpc; + handles[0].monero(&ops).await.get_height().await.unwrap() + }; // Burn the sriBTC/sriXMR { @@ -552,7 +555,7 @@ async fn mint_and_burn_test() { let received_output = block.txdata[1] .output .iter() - .find(|output| output.script_pubkey == bitcoin_addr.script_pubkey()) + .find(|output| output.script_pubkey == bitcoin_addr) .unwrap(); let tx_fee = 1_100_000_00 - @@ -573,12 +576,10 @@ async fn mint_and_burn_test() { // Verify the received Monero TX { - use monero_serai::wallet::{ViewPair, Scanner}; + use monero_wallet::{transaction::Transaction, rpc::Rpc, ViewPair, Scanner}; let rpc = handles[0].monero(&ops).await; - let mut scanner = Scanner::from_view( - ViewPair::new(monero_spend, Zeroizing::new(monero_view)), - Some(HashSet::new()), - ); + let mut scanner = + Scanner::new(ViewPair::new(monero_spend, Zeroizing::new(monero_view)).unwrap()); // Check for up to 5 minutes let mut found = false; @@ -586,15 +587,16 @@ async fn mint_and_burn_test() { while i < (5 * 6) { if let Ok(block) = rpc.get_block_by_number(start_monero_block).await { start_monero_block += 1; - let outputs = scanner.scan(&rpc, &block).await.unwrap(); + let outputs = scanner.scan(&rpc, &block).await.unwrap().not_additionally_locked(); if !outputs.is_empty() { assert_eq!(outputs.len(), 1); - let outputs = outputs[0].not_locked(); - assert_eq!(outputs.len(), 1); - assert_eq!(block.txs.len(), 1); - let tx = rpc.get_transaction(block.txs[0]).await.unwrap(); - let tx_fee = tx.rct_signatures.base.fee; + assert_eq!(block.transactions.len(), 1); + let tx = rpc.get_transaction(block.transactions[0]).await.unwrap(); + let tx_fee = match &tx { + Transaction::V2 { proofs: Some(proofs), .. } => proofs.base.fee, + _ => panic!("fetched TX wasn't a signed V2 TX"), + }; assert_eq!(outputs[0].commitment().amount, 1_000_000_000_000 - tx_fee); found = true; diff --git a/tests/full-stack/src/tests/mod.rs b/tests/full-stack/src/tests/mod.rs index 1fae8c48..7d92070e 100644 --- a/tests/full-stack/src/tests/mod.rs +++ b/tests/full-stack/src/tests/mod.rs @@ -57,15 +57,19 @@ pub(crate) async fn new_test(test_body: impl TestBody) { let (coord_key, message_queue_keys, message_queue_composition) = message_queue_instance(); let (bitcoin_composition, bitcoin_port) = network_instance(NetworkId::Bitcoin); - let bitcoin_processor_composition = + let mut bitcoin_processor_composition = processor_instance(NetworkId::Bitcoin, bitcoin_port, message_queue_keys[&NetworkId::Bitcoin]); + assert_eq!(bitcoin_processor_composition.len(), 1); + let bitcoin_processor_composition = bitcoin_processor_composition.swap_remove(0); let (monero_composition, monero_port) = network_instance(NetworkId::Monero); - let monero_processor_composition = + let mut monero_processor_composition = processor_instance(NetworkId::Monero, monero_port, message_queue_keys[&NetworkId::Monero]); + assert_eq!(monero_processor_composition.len(), 1); + let monero_processor_composition = monero_processor_composition.swap_remove(0); let coordinator_composition = coordinator_instance(name, coord_key); - let serai_composition = serai_composition(name); + let serai_composition = serai_composition(name, false); // Give every item in this stack a unique ID // Uses a Mutex as we can't generate a 8-byte random ID without hitting hostname length limits diff --git a/tests/no-std/Cargo.toml b/tests/no-std/Cargo.toml index dc128786..5023a80e 100644 --- a/tests/no-std/Cargo.toml +++ b/tests/no-std/Cargo.toml @@ -33,7 +33,6 @@ dkg = { path = "../../crypto/dkg", default-features = false } # modular-frost = { path = "../../crypto/frost", default-features = false } # frost-schnorrkel = { path = "../../crypto/schnorrkel", default-features = false } -bitcoin-serai = { path = "../../coins/bitcoin", default-features = false, features = ["hazmat"] } +bitcoin-serai = { path = "../../networks/bitcoin", default-features = false, features = ["hazmat"] } -monero-generators = { path = "../../coins/monero/generators", default-features = false } -monero-serai = { path = "../../coins/monero", default-features = false, features = ["cache-distribution"] } +monero-wallet-util = { path = "../../networks/monero/wallet/util", default-features = false, features = ["compile-time-generators"] } diff --git a/tests/no-std/src/lib.rs b/tests/no-std/src/lib.rs index 183fd40e..fa9da268 100644 --- a/tests/no-std/src/lib.rs +++ b/tests/no-std/src/lib.rs @@ -20,5 +20,4 @@ pub use frost_schnorrkel; pub use bitcoin_serai; -pub use monero_generators; -pub use monero_serai; +pub use monero_wallet_util; diff --git a/tests/processor/Cargo.toml b/tests/processor/Cargo.toml index be27e7de..ecf97fc1 100644 --- a/tests/processor/Cargo.toml +++ b/tests/processor/Cargo.toml @@ -23,12 +23,16 @@ zeroize = { version = "1", default-features = false } rand_core = { version = "0.6", default-features = false, features = ["getrandom"] } curve25519-dalek = "4" -ciphersuite = { path = "../../crypto/ciphersuite", default-features = false, features = ["ristretto"] } +ciphersuite = { path = "../../crypto/ciphersuite", default-features = false, features = ["secp256k1", "ristretto"] } dkg = { path = "../../crypto/dkg", default-features = false, features = ["tests"] } -bitcoin-serai = { path = "../../coins/bitcoin" } -ethereum-serai = { path = "../../coins/ethereum" } -monero-serai = { path = "../../coins/monero" } +bitcoin-serai = { path = "../../networks/bitcoin" } + +k256 = "0.13" +ethereum-serai = { path = "../../networks/ethereum" } + +monero-simple-request-rpc = { path = "../../networks/monero/rpc/simple-request" } +monero-wallet = { path = "../../networks/monero/wallet" } messages = { package = "serai-processor-messages", path = "../../processor/messages" } @@ -43,7 +47,7 @@ serde_json = { version = "1", default-features = false } tokio = { version = "1", features = ["time"] } -processor = { package = "serai-processor", path = "../../processor", features = ["bitcoin", "monero"] } +processor = { package = "serai-processor", path = "../../processor", features = ["bitcoin", "ethereum", "monero"] } dockertest = "0.4" serai-docker-tests = { path = "../docker" } diff --git a/tests/processor/src/lib.rs b/tests/processor/src/lib.rs index 5e854272..789d8679 100644 --- a/tests/processor/src/lib.rs +++ b/tests/processor/src/lib.rs @@ -28,7 +28,7 @@ pub fn processor_instance( network: NetworkId, port: u32, message_queue_key: ::F, -) -> TestBodySpecification { +) -> Vec { let mut entropy = [0; 32]; OsRng.fill_bytes(&mut entropy); @@ -41,7 +41,7 @@ pub fn processor_instance( let image = format!("{network_str}-processor"); serai_docker_tests::build(image.clone()); - TestBodySpecification::with_image( + let mut res = vec![TestBodySpecification::with_image( Image::with_repository(format!("serai-dev-{image}")).pull_policy(PullPolicy::Never), ) .replace_env( @@ -55,19 +55,40 @@ pub fn processor_instance( ("RUST_LOG".to_string(), "serai_processor=trace,".to_string()), ] .into(), - ) + )]; + + if network == NetworkId::Ethereum { + serai_docker_tests::build("ethereum-relayer".to_string()); + res.push( + TestBodySpecification::with_image( + Image::with_repository("serai-dev-ethereum-relayer".to_string()) + .pull_policy(PullPolicy::Never), + ) + .replace_env( + [ + ("DB_PATH".to_string(), "./ethereum-relayer-db".to_string()), + ("RUST_LOG".to_string(), "serai_ethereum_relayer=trace,".to_string()), + ] + .into(), + ) + .set_publish_all_ports(true), + ); + } + + res } -pub type Handles = (String, String, String); +pub type Handles = (String, String, String, String); pub fn processor_stack( network: NetworkId, + network_hostname_override: Option, ) -> (Handles, ::F, Vec) { let (network_composition, network_rpc_port) = network_instance(network); let (coord_key, message_queue_keys, message_queue_composition) = serai_message_queue_tests::instance(); - let processor_composition = + let mut processor_compositions = processor_instance(network, network_rpc_port, message_queue_keys[&network]); // Give every item in this stack a unique ID @@ -83,7 +104,7 @@ pub fn processor_stack( let mut compositions = vec![]; let mut handles = vec![]; for (name, composition) in [ - ( + Some(( match network { NetworkId::Serai => unreachable!(), NetworkId::Bitcoin => "bitcoin", @@ -91,10 +112,14 @@ pub fn processor_stack( NetworkId::Monero => "monero", }, network_composition, - ), - ("message_queue", message_queue_composition), - ("processor", processor_composition), - ] { + )), + Some(("message_queue", message_queue_composition)), + Some(("processor", processor_compositions.remove(0))), + processor_compositions.pop().map(|composition| ("relayer", composition)), + ] + .into_iter() + .flatten() + { let handle = format!("processor-{name}-{unique_id}"); compositions.push( composition.set_start_policy(StartPolicy::Strict).set_handle(handle.clone()).set_log_options( @@ -112,11 +137,27 @@ pub fn processor_stack( handles.push(handle); } - let processor_composition = compositions.last_mut().unwrap(); - processor_composition.inject_container_name(handles[0].clone(), "NETWORK_RPC_HOSTNAME"); + let processor_composition = compositions.get_mut(2).unwrap(); + processor_composition.inject_container_name( + network_hostname_override.unwrap_or_else(|| handles[0].clone()), + "NETWORK_RPC_HOSTNAME", + ); + if let Some(hostname) = handles.get(3) { + processor_composition.inject_container_name(hostname, "ETHEREUM_RELAYER_HOSTNAME"); + processor_composition.modify_env("ETHEREUM_RELAYER_PORT", "20830"); + } processor_composition.inject_container_name(handles[1].clone(), "MESSAGE_QUEUE_RPC"); - ((handles[0].clone(), handles[1].clone(), handles[2].clone()), coord_key, compositions) + ( + ( + handles[0].clone(), + handles[1].clone(), + handles[2].clone(), + handles.get(3).cloned().unwrap_or(String::new()), + ), + coord_key, + compositions, + ) } #[derive(serde::Deserialize, Debug)] @@ -130,6 +171,7 @@ pub struct Coordinator { message_queue_handle: String, #[allow(unused)] processor_handle: String, + relayer_handle: String, next_send_id: u64, next_recv_id: u64, @@ -140,7 +182,7 @@ impl Coordinator { pub fn new( network: NetworkId, ops: &DockerOperations, - handles: (String, String, String), + handles: Handles, coord_key: ::F, ) -> Coordinator { let rpc = ops.handle(&handles.1).host_port(2287).unwrap(); @@ -152,6 +194,7 @@ impl Coordinator { network_handle: handles.0, message_queue_handle: handles.1, processor_handle: handles.2, + relayer_handle: handles.3, next_send_id: 0, next_recv_id: 0, @@ -182,33 +225,61 @@ impl Coordinator { } } NetworkId::Ethereum => { - use ethereum_serai::alloy::{ - simple_request_transport::SimpleRequest, - rpc_client::ClientBuilder, - provider::{Provider, RootProvider}, - network::Ethereum, + use std::sync::Arc; + use ethereum_serai::{ + alloy::{ + simple_request_transport::SimpleRequest, + rpc_client::ClientBuilder, + provider::{Provider, RootProvider}, + network::Ethereum, + }, + deployer::Deployer, }; - let provider = RootProvider::<_, Ethereum>::new( + let provider = Arc::new(RootProvider::<_, Ethereum>::new( ClientBuilder::default().transport(SimpleRequest::new(rpc_url.clone()), true), - ); + )); - loop { - if handle - .block_on(provider.raw_request::<_, ()>("evm_setAutomine".into(), [false])) - .is_ok() - { - break; - } - handle.block_on(tokio::time::sleep(core::time::Duration::from_secs(1))); + if handle + .block_on(provider.raw_request::<_, ()>("evm_setAutomine".into(), [false])) + .is_ok() + { + handle.block_on(async { + // Deploy the deployer + let tx = Deployer::deployment_tx(); + let signer = tx.recover_signer().unwrap(); + let (tx, sig, _) = tx.into_parts(); + + provider + .raw_request::<_, ()>( + "anvil_setBalance".into(), + [signer.to_string(), (tx.gas_limit * tx.gas_price).to_string()], + ) + .await + .unwrap(); + + let mut bytes = vec![]; + tx.encode_with_signature_fields(&sig, &mut bytes); + let _ = provider.send_raw_transaction(&bytes).await.unwrap(); + + provider.raw_request::<_, ()>("anvil_mine".into(), [96]).await.unwrap(); + + let _ = Deployer::new(provider.clone()).await.unwrap().unwrap(); + + // Sleep until the actual time is ahead of whatever time is in the epoch we just + // mined + tokio::time::sleep(core::time::Duration::from_secs(30)).await; + }); + break; } } NetworkId::Monero => { - use monero_serai::rpc::HttpRpc; + use monero_simple_request_rpc::SimpleRequestRpc; + use monero_wallet::rpc::Rpc; // Monero's won't, so call get_height if handle - .block_on(HttpRpc::new(rpc_url.clone())) + .block_on(SimpleRequestRpc::new(rpc_url.clone())) .ok() .and_then(|rpc| handle.block_on(rpc.get_height()).ok()) .is_some() @@ -295,7 +366,7 @@ impl Coordinator { NetworkId::Ethereum => { use ethereum_serai::alloy::{ simple_request_transport::SimpleRequest, - rpc_types::BlockNumberOrTag, + rpc_types::{BlockTransactionsKind, BlockNumberOrTag}, rpc_client::ClientBuilder, provider::{Provider, RootProvider}, network::Ethereum, @@ -305,7 +376,7 @@ impl Coordinator { ClientBuilder::default().transport(SimpleRequest::new(rpc_url.clone()), true), ); let start = provider - .get_block(BlockNumberOrTag::Latest.into(), false) + .get_block(BlockNumberOrTag::Latest.into(), BlockTransactionsKind::Hashes) .await .unwrap() .unwrap() @@ -316,7 +387,7 @@ impl Coordinator { provider.raw_request::<_, ()>("anvil_mine".into(), [96]).await.unwrap(); let end_of_epoch = start + 31; let hash = provider - .get_block(BlockNumberOrTag::Number(end_of_epoch).into(), false) + .get_block(BlockNumberOrTag::Number(end_of_epoch).into(), BlockTransactionsKind::Hashes) .await .unwrap() .unwrap() @@ -333,25 +404,16 @@ impl Coordinator { } NetworkId::Monero => { use curve25519_dalek::{constants::ED25519_BASEPOINT_POINT, scalar::Scalar}; - use monero_serai::{ - wallet::{ - ViewPair, - address::{Network, AddressSpec}, - }, - rpc::HttpRpc, - }; + use monero_simple_request_rpc::SimpleRequestRpc; + use monero_wallet::{rpc::Rpc, address::Network, ViewPair}; - let rpc = HttpRpc::new(rpc_url).await.expect("couldn't connect to the Monero RPC"); - let _: EmptyResponse = rpc - .json_rpc_call( - "generateblocks", - Some(serde_json::json!({ - "wallet_address": ViewPair::new( - ED25519_BASEPOINT_POINT, - Zeroizing::new(Scalar::ONE), - ).address(Network::Mainnet, AddressSpec::Standard).to_string(), - "amount_of_blocks": 1, - })), + let rpc = SimpleRequestRpc::new(rpc_url).await.expect("couldn't connect to the Monero RPC"); + rpc + .generate_blocks( + &ViewPair::new(ED25519_BASEPOINT_POINT, Zeroizing::new(Scalar::ONE)) + .unwrap() + .legacy_address(Network::Mainnet), + 1, ) .await .unwrap(); @@ -371,7 +433,10 @@ impl Coordinator { let rpc = Rpc::new(rpc_url).await.expect("couldn't connect to the Bitcoin RPC"); let to = rpc.get_latest_block_number().await.unwrap(); for coordinator in others { - let from = rpc.get_latest_block_number().await.unwrap() + 1; + let other_rpc = Rpc::new(network_rpc(self.network, ops, &coordinator.network_handle)) + .await + .expect("couldn't connect to the Bitcoin RPC"); + let from = other_rpc.get_latest_block_number().await.unwrap() + 1; for b in from ..= to { let mut buf = vec![]; @@ -382,12 +447,10 @@ impl Coordinator { .consensus_encode(&mut buf) .unwrap(); - let rpc_url = network_rpc(coordinator.network, ops, &coordinator.network_handle); - let rpc = - Rpc::new(rpc_url).await.expect("couldn't connect to the coordinator's Bitcoin RPC"); - - let res: Option = - rpc.rpc_call("submitblock", serde_json::json!([hex::encode(buf)])).await.unwrap(); + let res: Option = other_rpc + .rpc_call("submitblock", serde_json::json!([hex::encode(buf)])) + .await + .unwrap(); if let Some(err) = res { panic!("submitblock failed: {err}"); } @@ -397,45 +460,75 @@ impl Coordinator { NetworkId::Ethereum => { use ethereum_serai::alloy::{ simple_request_transport::SimpleRequest, + rpc_types::{BlockTransactionsKind, BlockNumberOrTag}, rpc_client::ClientBuilder, provider::{Provider, RootProvider}, network::Ethereum, }; - let provider = RootProvider::<_, Ethereum>::new( - ClientBuilder::default().transport(SimpleRequest::new(rpc_url.clone()), true), - ); - let state = provider.raw_request::<_, String>("anvil_dumpState".into(), ()).await.unwrap(); + let (expected_number, state) = { + let provider = RootProvider::<_, Ethereum>::new( + ClientBuilder::default().transport(SimpleRequest::new(rpc_url.clone()), true), + ); + + let expected_number = provider + .get_block(BlockNumberOrTag::Latest.into(), BlockTransactionsKind::Hashes) + .await + .unwrap() + .unwrap() + .header + .number; + ( + expected_number, + provider.raw_request::<_, String>("anvil_dumpState".into(), ()).await.unwrap(), + ) + }; for coordinator in others { let rpc_url = network_rpc(coordinator.network, ops, &coordinator.network_handle); let provider = RootProvider::<_, Ethereum>::new( ClientBuilder::default().transport(SimpleRequest::new(rpc_url.clone()), true), ); - provider.raw_request::<_, ()>("anvil_loadState".into(), &state).await.unwrap(); + assert!(provider + .raw_request::<_, bool>("anvil_loadState".into(), &[&state]) + .await + .unwrap()); + + let new_number = provider + .get_block(BlockNumberOrTag::Latest.into(), BlockTransactionsKind::Hashes) + .await + .unwrap() + .unwrap() + .header + .number; + + // TODO: https://github.com/foundry-rs/foundry/issues/7955 + let _ = expected_number; + let _ = new_number; + //assert_eq!(expected_number, new_number); } } NetworkId::Monero => { - use monero_serai::rpc::HttpRpc; + use monero_simple_request_rpc::SimpleRequestRpc; + use monero_wallet::rpc::Rpc; - let rpc = HttpRpc::new(rpc_url).await.expect("couldn't connect to the Monero RPC"); + let rpc = SimpleRequestRpc::new(rpc_url).await.expect("couldn't connect to the Monero RPC"); let to = rpc.get_height().await.unwrap(); for coordinator in others { - let from = HttpRpc::new(network_rpc(self.network, ops, &coordinator.network_handle)) - .await - .expect("couldn't connect to the Monero RPC") - .get_height() - .await - .unwrap(); + let other_rpc = SimpleRequestRpc::new(network_rpc( + coordinator.network, + ops, + &coordinator.network_handle, + )) + .await + .expect("couldn't connect to the Monero RPC"); + + let from = other_rpc.get_height().await.unwrap(); for b in from .. to { let block = rpc.get_block(rpc.get_block_hash(b).await.unwrap()).await.unwrap().serialize(); - let rpc_url = network_rpc(coordinator.network, ops, &coordinator.network_handle); - let rpc = HttpRpc::new(rpc_url) - .await - .expect("couldn't connect to the coordinator's Monero RPC"); - let res: serde_json::Value = rpc + let res: serde_json::Value = other_rpc .json_rpc_call("submit_block", Some(serde_json::json!([hex::encode(block)]))) .await .unwrap(); @@ -450,7 +543,7 @@ impl Coordinator { } } - pub async fn publish_transacton(&self, ops: &DockerOperations, tx: &[u8]) { + pub async fn publish_transaction(&self, ops: &DockerOperations, tx: &[u8]) { let rpc_url = network_rpc(self.network, ops, &self.network_handle); match self.network { NetworkId::Bitcoin => { @@ -477,16 +570,26 @@ impl Coordinator { let _ = provider.send_raw_transaction(tx).await.unwrap(); } NetworkId::Monero => { - use monero_serai::{transaction::Transaction, rpc::HttpRpc}; + use monero_simple_request_rpc::SimpleRequestRpc; + use monero_wallet::{transaction::Transaction, rpc::Rpc}; - let rpc = - HttpRpc::new(rpc_url).await.expect("couldn't connect to the coordinator's Monero RPC"); + let rpc = SimpleRequestRpc::new(rpc_url) + .await + .expect("couldn't connect to the coordinator's Monero RPC"); rpc.publish_transaction(&Transaction::read(&mut &*tx).unwrap()).await.unwrap(); } NetworkId::Serai => panic!("processor tests broadcasting block to Serai"), } } + pub async fn publish_eventuality_completion(&self, ops: &DockerOperations, tx: &[u8]) { + match self.network { + NetworkId::Bitcoin | NetworkId::Monero => self.publish_transaction(ops, tx).await, + NetworkId::Ethereum => (), + NetworkId::Serai => panic!("processor tests broadcasting block to Serai"), + } + } + pub async fn get_published_transaction( &self, ops: &DockerOperations, @@ -517,14 +620,7 @@ impl Coordinator { } } NetworkId::Ethereum => { - use ethereum_serai::alloy::{ - consensus::{TxLegacy, Signed}, - simple_request_transport::SimpleRequest, - rpc_client::ClientBuilder, - provider::{Provider, RootProvider}, - network::Ethereum, - }; - + /* let provider = RootProvider::<_, Ethereum>::new( ClientBuilder::default().transport(SimpleRequest::new(rpc_url.clone()), true), ); @@ -535,12 +631,51 @@ impl Coordinator { let mut bytes = vec![]; tx.encode_with_signature_fields(&sig, &mut bytes); Some(bytes) + */ + + // This is being passed a signature. We need to check the relayer has a TX with this + // signature + + use tokio::{ + io::{AsyncReadExt, AsyncWriteExt}, + net::TcpStream, + }; + + let (ip, port) = ops.handle(&self.relayer_handle).host_port(20831).unwrap(); + let relayer_url = format!("{ip}:{port}"); + + let mut socket = TcpStream::connect(&relayer_url).await.unwrap(); + // Iterate over every published command + for i in 1 .. u32::MAX { + socket.write_all(&i.to_le_bytes()).await.unwrap(); + + let mut recvd_len = [0; 4]; + socket.read_exact(&mut recvd_len).await.unwrap(); + if recvd_len == [0; 4] { + break; + } + + let mut msg = vec![0; usize::try_from(u32::from_le_bytes(recvd_len)).unwrap()]; + socket.read_exact(&mut msg).await.unwrap(); + for start_pos in 0 .. msg.len() { + if (start_pos + tx.len()) > msg.len() { + break; + } + if &msg[start_pos .. (start_pos + tx.len())] == tx { + return Some(msg); + } + } + } + + None } NetworkId::Monero => { - use monero_serai::rpc::HttpRpc; + use monero_simple_request_rpc::SimpleRequestRpc; + use monero_wallet::rpc::Rpc; - let rpc = - HttpRpc::new(rpc_url).await.expect("couldn't connect to the coordinator's Monero RPC"); + let rpc = SimpleRequestRpc::new(rpc_url) + .await + .expect("couldn't connect to the coordinator's Monero RPC"); let mut hash = [0; 32]; hash.copy_from_slice(tx); if let Ok(tx) = rpc.get_transaction(hash).await { diff --git a/tests/processor/src/networks.rs b/tests/processor/src/networks.rs index 5b54cc01..d6ccb1f5 100644 --- a/tests/processor/src/networks.rs +++ b/tests/processor/src/networks.rs @@ -1,5 +1,3 @@ -use std::collections::HashSet; - use zeroize::Zeroizing; use rand_core::{RngCore, OsRng}; @@ -96,14 +94,15 @@ pub enum Wallet { input_tx: bitcoin_serai::bitcoin::Transaction, }, Ethereum { + rpc_url: String, key: ::F, nonce: u64, }, Monero { handle: String, spend_key: Zeroizing, - view_pair: monero_serai::wallet::ViewPair, - inputs: Vec, + view_pair: monero_wallet::ViewPair, + last_tx: (usize, [u8; 32]), }, } @@ -125,7 +124,7 @@ impl Wallet { let secret_key = SecretKey::new(&mut rand_core::OsRng); let private_key = PrivateKey::new(secret_key, Network::Regtest); let public_key = PublicKey::from_private_key(SECP256K1, &private_key); - let main_addr = Address::p2pkh(&public_key, Network::Regtest); + let main_addr = Address::p2pkh(public_key, Network::Regtest); let rpc = Rpc::new(rpc_url).await.expect("couldn't connect to the Bitcoin RPC"); @@ -155,7 +154,7 @@ impl Wallet { } NetworkId::Ethereum => { - use ciphersuite::{group::ff::Field, Ciphersuite, Secp256k1}; + use ciphersuite::{group::ff::Field, Secp256k1}; use ethereum_serai::alloy::{ primitives::{U256, Address}, simple_request_transport::SimpleRequest, @@ -183,60 +182,32 @@ impl Wallet { .await .unwrap(); - Wallet::Ethereum { key, nonce: 0 } + Wallet::Ethereum { rpc_url: rpc_url.clone(), key, nonce: 0 } } NetworkId::Monero => { use curve25519_dalek::{constants::ED25519_BASEPOINT_POINT, scalar::Scalar}; - use monero_serai::{ - wallet::{ - ViewPair, Scanner, - address::{Network, AddressSpec}, - }, - rpc::HttpRpc, - }; + use monero_simple_request_rpc::SimpleRequestRpc; + use monero_wallet::{rpc::Rpc, address::Network, ViewPair}; - let mut bytes = [0; 64]; - OsRng.fill_bytes(&mut bytes); - let spend_key = Scalar::from_bytes_mod_order_wide(&bytes); - OsRng.fill_bytes(&mut bytes); - let view_key = Scalar::from_bytes_mod_order_wide(&bytes); + let spend_key = Scalar::random(&mut OsRng); + let view_key = Scalar::random(&mut OsRng); let view_pair = - ViewPair::new(ED25519_BASEPOINT_POINT * spend_key, Zeroizing::new(view_key)); + ViewPair::new(ED25519_BASEPOINT_POINT * spend_key, Zeroizing::new(view_key)).unwrap(); - let rpc = HttpRpc::new(rpc_url).await.expect("couldn't connect to the Monero RPC"); + let rpc = SimpleRequestRpc::new(rpc_url).await.expect("couldn't connect to the Monero RPC"); let height = rpc.get_height().await.unwrap(); // Mines 200 blocks so sufficient decoys exist, as only 60 is needed for maturity - let _: EmptyResponse = rpc - .json_rpc_call( - "generateblocks", - Some(serde_json::json!({ - "wallet_address": view_pair.address( - Network::Mainnet, - AddressSpec::Standard - ).to_string(), - "amount_of_blocks": 200, - })), - ) - .await - .unwrap(); + rpc.generate_blocks(&view_pair.legacy_address(Network::Mainnet), 200).await.unwrap(); let block = rpc.get_block(rpc.get_block_hash(height).await.unwrap()).await.unwrap(); - let output = Scanner::from_view(view_pair.clone(), Some(HashSet::new())) - .scan(&rpc, &block) - .await - .unwrap() - .remove(0) - .ignore_timelock() - .remove(0); - Wallet::Monero { handle, spend_key: Zeroizing::new(spend_key), view_pair, - inputs: vec![output.output.clone()], + last_tx: (height, block.miner_transaction.hash()), } } NetworkId::Serai => panic!("creating a wallet for for Serai"), @@ -257,7 +228,6 @@ impl Wallet { consensus::Encodable, sighash::{EcdsaSighashType, SighashCache}, script::{PushBytesBuf, Script, ScriptBuf, Builder}, - address::Payload, OutPoint, Sequence, Witness, TxIn, Amount, TxOut, absolute::LockTime, transaction::{Version, Transaction}, @@ -268,7 +238,7 @@ impl Wallet { version: Version(2), lock_time: LockTime::ZERO, input: vec![TxIn { - previous_output: OutPoint { txid: input_tx.txid(), vout: 0 }, + previous_output: OutPoint { txid: input_tx.compute_txid(), vout: 0 }, script_sig: Script::new().into(), sequence: Sequence(u32::MAX), witness: Witness::default(), @@ -280,10 +250,11 @@ impl Wallet { }, TxOut { value: Amount::from_sat(AMOUNT), - script_pubkey: Payload::p2tr_tweaked(TweakedPublicKey::dangerous_assume_tweaked( - XOnlyPublicKey::from_slice(&to[1 ..]).unwrap(), - )) - .script_pubkey(), + script_pubkey: ScriptBuf::new_p2tr_tweaked( + TweakedPublicKey::dangerous_assume_tweaked( + XOnlyPublicKey::from_slice(&to[1 ..]).unwrap(), + ), + ), }, ], }; @@ -302,7 +273,7 @@ impl Wallet { let mut der = SECP256K1 .sign_ecdsa_low_r( - &Message::from( + &Message::from_digest_slice( SighashCache::new(&tx) .legacy_signature_hash( 0, @@ -310,8 +281,10 @@ impl Wallet { EcdsaSighashType::All.to_u32(), ) .unwrap() - .to_raw_hash(), - ), + .to_raw_hash() + .as_ref(), + ) + .unwrap(), &private_key.inner, ) .serialize_der() @@ -328,67 +301,162 @@ impl Wallet { (buf, Balance { coin: Coin::Bitcoin, amount: Amount(AMOUNT) }) } - Wallet::Ethereum { key, ref mut nonce } => { - /* - use ethereum_serai::alloy::primitives::U256; + Wallet::Ethereum { rpc_url, key, ref mut nonce } => { + use std::sync::Arc; + use ethereum_serai::{ + alloy::{ + primitives::{U256, TxKind}, + sol_types::SolCall, + simple_request_transport::SimpleRequest, + consensus::{TxLegacy, SignableTransaction}, + rpc_client::ClientBuilder, + provider::{Provider, RootProvider}, + network::Ethereum, + }, + crypto::PublicKey, + deployer::Deployer, + }; let eight_decimals = U256::from(100_000_000u64); let nine_decimals = eight_decimals * U256::from(10u64); let eighteen_decimals = nine_decimals * nine_decimals; + let one_eth = eighteen_decimals; - let tx = todo!("send to router"); + let provider = Arc::new(RootProvider::<_, Ethereum>::new( + ClientBuilder::default().transport(SimpleRequest::new(rpc_url.clone()), true), + )); + + let to_as_key = PublicKey::new( + ::read_G(&mut to.as_slice()).unwrap(), + ) + .unwrap(); + let router_addr = { + // Find the deployer + let deployer = Deployer::new(provider.clone()).await.unwrap().unwrap(); + + // Find the router, deploying if non-existent + let router = if let Some(router) = + deployer.find_router(provider.clone(), &to_as_key).await.unwrap() + { + router + } else { + let mut tx = deployer.deploy_router(&to_as_key); + tx.gas_price = 1_000_000_000u64.into(); + let tx = ethereum_serai::crypto::deterministically_sign(&tx); + let signer = tx.recover_signer().unwrap(); + let (tx, sig, _) = tx.into_parts(); + + provider + .raw_request::<_, ()>( + "anvil_setBalance".into(), + [signer.to_string(), (tx.gas_limit * tx.gas_price).to_string()], + ) + .await + .unwrap(); + + let mut bytes = vec![]; + tx.encode_with_signature_fields(&sig, &mut bytes); + let _ = provider.send_raw_transaction(&bytes).await.unwrap(); + + provider.raw_request::<_, ()>("anvil_mine".into(), [96]).await.unwrap(); + + deployer.find_router(provider.clone(), &to_as_key).await.unwrap().unwrap() + }; + + router.address() + }; + + let tx = TxLegacy { + chain_id: None, + nonce: *nonce, + gas_price: 1_000_000_000u128, + gas_limit: 200_000u128, + to: TxKind::Call(router_addr.into()), + // 1 ETH + value: one_eth, + input: ethereum_serai::router::abi::inInstructionCall::new(( + [0; 20].into(), + one_eth, + if let Some(instruction) = instruction { + Shorthand::Raw(RefundableInInstruction { origin: None, instruction }).encode().into() + } else { + vec![].into() + }, + )) + .abi_encode() + .into(), + }; *nonce += 1; - (tx, Balance { coin: Coin::Ether, amount: Amount(u64::try_from(eight_decimals).unwrap()) }) - */ - let _ = key; - let _ = nonce; - todo!() + + let sig = + k256::ecdsa::SigningKey::from(k256::elliptic_curve::NonZeroScalar::new(*key).unwrap()) + .sign_prehash_recoverable(tx.signature_hash().as_ref()) + .unwrap(); + + let mut bytes = vec![]; + tx.encode_with_signature_fields(&sig.into(), &mut bytes); + + // We drop the bottom 10 decimals + ( + bytes, + Balance { coin: Coin::Ether, amount: Amount(u64::try_from(eight_decimals).unwrap()) }, + ) } - Wallet::Monero { handle, ref spend_key, ref view_pair, ref mut inputs } => { + Wallet::Monero { handle, ref spend_key, ref view_pair, ref mut last_tx } => { use curve25519_dalek::constants::ED25519_BASEPOINT_POINT; - use monero_serai::{ - Protocol, - wallet::{ - address::{Network, AddressType, AddressMeta, Address}, - SpendableOutput, Decoys, Change, FeePriority, Scanner, SignableTransaction, - }, - rpc::HttpRpc, - decompress_point, + use monero_simple_request_rpc::SimpleRequestRpc; + use monero_wallet::{ + io::decompress_point, + ringct::RctType, + rpc::{FeePriority, Rpc}, + address::{Network, AddressType, Address}, + Scanner, OutputWithDecoys, + send::{Change, SignableTransaction}, }; use processor::{additional_key, networks::Monero}; let rpc_url = network_rpc(NetworkId::Monero, ops, handle); - let rpc = HttpRpc::new(rpc_url).await.expect("couldn't connect to the Monero RPC"); + let rpc = SimpleRequestRpc::new(rpc_url).await.expect("couldn't connect to the Monero RPC"); // Prepare inputs - let outputs = std::mem::take(inputs); - let mut these_inputs = vec![]; - for output in outputs { - these_inputs.push( - SpendableOutput::from(&rpc, output) + let current_height = rpc.get_height().await.unwrap(); + let mut outputs = vec![]; + for block in last_tx.0 .. current_height { + let block = rpc.get_block_by_number(block).await.unwrap(); + if (block.miner_transaction.hash() == last_tx.1) || + block.transactions.contains(&last_tx.1) + { + outputs = Scanner::new(view_pair.clone()) + .scan(&rpc, &block) .await - .expect("prior transaction was never published"), + .unwrap() + .ignore_additional_timelock(); + } + } + assert!(!outputs.is_empty()); + + let mut inputs = Vec::with_capacity(outputs.len()); + for output in outputs { + inputs.push( + OutputWithDecoys::fingerprintable_deterministic_new( + &mut OsRng, + &rpc, + 16, + rpc.get_height().await.unwrap(), + output, + ) + .await + .unwrap(), ); } - let mut decoys = Decoys::fingerprintable_canonical_select( - &mut OsRng, - &rpc, - Protocol::v16.ring_len(), - rpc.get_height().await.unwrap(), - &these_inputs, - ) - .await - .unwrap(); let to_spend_key = decompress_point(<[u8; 32]>::try_from(to.as_ref()).unwrap()).unwrap(); let to_view_key = additional_key::(0); let to_addr = Address::new( - AddressMeta::new( - Network::Mainnet, - AddressType::Featured { subaddress: false, payment_id: None, guaranteed: true }, - ), + Network::Mainnet, + AddressType::Featured { subaddress: false, payment_id: None, guaranteed: true }, to_spend_key, ED25519_BASEPOINT_POINT * to_view_key.0, ); @@ -399,26 +467,24 @@ impl Wallet { if let Some(instruction) = instruction { data.push(Shorthand::Raw(RefundableInInstruction { origin: None, instruction }).encode()); } + let mut outgoing_view_key = Zeroizing::new([0; 32]); + OsRng.fill_bytes(outgoing_view_key.as_mut()); let tx = SignableTransaction::new( - Protocol::v16, - None, - these_inputs.drain(..).zip(decoys.drain(..)).collect(), + RctType::ClsagBulletproofPlus, + outgoing_view_key, + inputs, vec![(to_addr, AMOUNT)], - &Change::new(view_pair, false), + Change::new(view_pair.clone(), None), data, - rpc.get_fee(Protocol::v16, FeePriority::Unimportant).await.unwrap(), + rpc.get_fee_rate(FeePriority::Unimportant).await.unwrap(), ) .unwrap() .sign(&mut OsRng, spend_key) .unwrap(); - // Push the change output - inputs.push( - Scanner::from_view(view_pair.clone(), Some(HashSet::new())) - .scan_transaction(&tx) - .ignore_timelock() - .remove(0), - ); + // Update the last TX to track the change output + last_tx.0 = current_height; + last_tx.1 = tx.hash(); (tx.serialize(), Balance { coin: Coin::Monero, amount: Amount(AMOUNT) }) } @@ -430,29 +496,24 @@ impl Wallet { match self { Wallet::Bitcoin { public_key, .. } => { - use bitcoin_serai::bitcoin::{Network, Address}; + use bitcoin_serai::bitcoin::ScriptBuf; ExternalAddress::new( - networks::bitcoin::Address::new(Address::p2pkh(public_key, Network::Regtest)) + networks::bitcoin::Address::new(ScriptBuf::new_p2pkh(&public_key.pubkey_hash())) .unwrap() .into(), ) .unwrap() } - Wallet::Ethereum { key, .. } => { - use ciphersuite::{Ciphersuite, Secp256k1}; - ExternalAddress::new( - ethereum_serai::crypto::address(&(Secp256k1::generator() * key)).into(), - ) - .unwrap() - } + Wallet::Ethereum { key, .. } => ExternalAddress::new( + ethereum_serai::crypto::address(&(ciphersuite::Secp256k1::generator() * key)).into(), + ) + .unwrap(), Wallet::Monero { view_pair, .. } => { - use monero_serai::wallet::address::{Network, AddressSpec}; + use monero_wallet::address::Network; ExternalAddress::new( - networks::monero::Address::new( - view_pair.address(Network::Mainnet, AddressSpec::Standard), - ) - .unwrap() - .into(), + networks::monero::Address::new(view_pair.legacy_address(Network::Mainnet)) + .unwrap() + .into(), ) .unwrap() } diff --git a/tests/processor/src/tests/batch.rs b/tests/processor/src/tests/batch.rs index 5729fd73..6170270a 100644 --- a/tests/processor/src/tests/batch.rs +++ b/tests/processor/src/tests/batch.rs @@ -17,7 +17,8 @@ use serai_client::{ validator_sets::primitives::Session, }; -use processor::networks::{Network, Bitcoin, Monero}; +use serai_db::MemDb; +use processor::networks::{Network, Bitcoin, Ethereum, Monero}; use crate::{*, tests::*}; @@ -188,7 +189,7 @@ pub(crate) async fn substrate_block( #[test] fn batch_test() { - for network in [NetworkId::Bitcoin, NetworkId::Monero] { + for network in [NetworkId::Bitcoin, NetworkId::Ethereum, NetworkId::Monero] { let (coordinators, test) = new_test(network); test.run(|ops| async move { @@ -228,7 +229,7 @@ fn batch_test() { let (tx, balance_sent) = wallet.send_to_address(&ops, &key_pair.1, instruction.clone()).await; for coordinator in &mut coordinators { - coordinator.publish_transacton(&ops, &tx).await; + coordinator.publish_transaction(&ops, &tx).await; } // Put the TX past the confirmation depth @@ -245,6 +246,8 @@ fn batch_test() { // The scanner works on a 5s interval, so this leaves a few s for any processing/latency tokio::time::sleep(Duration::from_secs(10)).await; + println!("sent in transaction. with in instruction: {}", instruction.is_some()); + let expected_batch = Batch { network, id: i, @@ -256,10 +259,11 @@ fn batch_test() { coin: balance_sent.coin, amount: Amount( balance_sent.amount.0 - - (2 * if network == NetworkId::Bitcoin { - Bitcoin::COST_TO_AGGREGATE - } else { - Monero::COST_TO_AGGREGATE + (2 * match network { + NetworkId::Bitcoin => Bitcoin::COST_TO_AGGREGATE, + NetworkId::Ethereum => Ethereum::::COST_TO_AGGREGATE, + NetworkId::Monero => Monero::COST_TO_AGGREGATE, + NetworkId::Serai => panic!("minted for Serai?"), }), ), }, @@ -272,6 +276,8 @@ fn batch_test() { }, }; + println!("receiving batch preprocesses..."); + // Make sure the processors picked it up by checking they're trying to sign a batch for it let (mut id, mut preprocesses) = recv_batch_preprocesses(&mut coordinators, Session(0), &expected_batch, 0).await; @@ -291,6 +297,8 @@ fn batch_test() { recv_batch_preprocesses(&mut coordinators, Session(0), &expected_batch, attempt).await; } + println!("signing batch..."); + // Continue with signing the batch let batch = sign_batch(&mut coordinators, key_pair.0 .0, id, preprocesses).await; diff --git a/tests/processor/src/tests/key_gen.rs b/tests/processor/src/tests/key_gen.rs index d50c12b7..7dea0bfd 100644 --- a/tests/processor/src/tests/key_gen.rs +++ b/tests/processor/src/tests/key_gen.rs @@ -144,7 +144,7 @@ pub(crate) async fn key_gen(coordinators: &mut [Coordinator]) -> KeyPair { #[test] fn key_gen_test() { - for network in [NetworkId::Bitcoin, NetworkId::Monero] { + for network in [NetworkId::Bitcoin, NetworkId::Ethereum, NetworkId::Monero] { let (coordinators, test) = new_test(network); test.run(|ops| async move { diff --git a/tests/processor/src/tests/mod.rs b/tests/processor/src/tests/mod.rs index 54a17020..afda97d5 100644 --- a/tests/processor/src/tests/mod.rs +++ b/tests/processor/src/tests/mod.rs @@ -20,8 +20,14 @@ pub(crate) const THRESHOLD: usize = ((COORDINATORS * 2) / 3) + 1; fn new_test(network: NetworkId) -> (Vec<(Handles, ::F)>, DockerTest) { let mut coordinators = vec![]; let mut test = DockerTest::new().with_network(dockertest::Network::Isolated); + let mut eth_handle = None; for _ in 0 .. COORDINATORS { - let (handles, coord_key, compositions) = processor_stack(network); + let (handles, coord_key, compositions) = processor_stack(network, eth_handle.clone()); + // TODO: Remove this once https://github.com/foundry-rs/foundry/issues/7955 + // This has all processors share an Ethereum node until we can sync controlled nodes + if network == NetworkId::Ethereum { + eth_handle = eth_handle.or_else(|| Some(handles.0.clone())); + } coordinators.push((handles, coord_key)); for composition in compositions { test.provide_container(composition); diff --git a/tests/processor/src/tests/send.rs b/tests/processor/src/tests/send.rs index 4d0d3cd6..62e80c09 100644 --- a/tests/processor/src/tests/send.rs +++ b/tests/processor/src/tests/send.rs @@ -8,12 +8,15 @@ use dkg::{Participant, tests::clone_without}; use messages::{sign::SignId, SubstrateContext}; use serai_client::{ - primitives::{BlockHash, NetworkId}, + primitives::{BlockHash, NetworkId, Amount, Balance, SeraiAddress}, coins::primitives::{OutInstruction, OutInstructionWithBalance}, - in_instructions::primitives::Batch, + in_instructions::primitives::{InInstruction, InInstructionWithBalance, Batch}, validator_sets::primitives::Session, }; +use serai_db::MemDb; +use processor::networks::{Network, Bitcoin, Ethereum, Monero}; + use crate::{*, tests::*}; #[allow(unused)] @@ -144,7 +147,7 @@ pub(crate) async fn sign_tx( #[test] fn send_test() { - for network in [NetworkId::Bitcoin, NetworkId::Monero] { + for network in [NetworkId::Bitcoin, NetworkId::Ethereum, NetworkId::Monero] { let (coordinators, test) = new_test(network); test.run(|ops| async move { @@ -173,9 +176,13 @@ fn send_test() { coordinators[0].sync(&ops, &coordinators[1 ..]).await; // Send into the processor's wallet - let (tx, balance_sent) = wallet.send_to_address(&ops, &key_pair.1, None).await; + let mut serai_address = [0; 32]; + OsRng.fill_bytes(&mut serai_address); + let instruction = InInstruction::Transfer(SeraiAddress(serai_address)); + let (tx, balance_sent) = + wallet.send_to_address(&ops, &key_pair.1, Some(instruction.clone())).await; for coordinator in &mut coordinators { - coordinator.publish_transacton(&ops, &tx).await; + coordinator.publish_transaction(&ops, &tx).await; } // Put the TX past the confirmation depth @@ -192,8 +199,25 @@ fn send_test() { // The scanner works on a 5s interval, so this leaves a few s for any processing/latency tokio::time::sleep(Duration::from_secs(10)).await; - let expected_batch = - Batch { network, id: 0, block: BlockHash(block_with_tx.unwrap()), instructions: vec![] }; + let amount_minted = Amount( + balance_sent.amount.0 - + (2 * match network { + NetworkId::Bitcoin => Bitcoin::COST_TO_AGGREGATE, + NetworkId::Ethereum => Ethereum::::COST_TO_AGGREGATE, + NetworkId::Monero => Monero::COST_TO_AGGREGATE, + NetworkId::Serai => panic!("minted for Serai?"), + }), + ); + + let expected_batch = Batch { + network, + id: 0, + block: BlockHash(block_with_tx.unwrap()), + instructions: vec![InInstructionWithBalance { + instruction, + balance: Balance { coin: balance_sent.coin, amount: amount_minted }, + }], + }; // Make sure the proceessors picked it up by checking they're trying to sign a batch for it let (id, preprocesses) = @@ -221,7 +245,7 @@ fn send_test() { block: substrate_block_num, burns: vec![OutInstructionWithBalance { instruction: OutInstruction { address: wallet.address(), data: None }, - balance: balance_sent, + balance: Balance { coin: balance_sent.coin, amount: amount_minted }, }], batches: vec![batch.batch.id], }, @@ -271,7 +295,7 @@ fn send_test() { .unwrap(); for (i, coordinator) in coordinators.iter_mut().enumerate() { if !participating.contains(&i) { - coordinator.publish_transacton(&ops, &tx).await; + coordinator.publish_eventuality_completion(&ops, &tx).await; // Tell them of it as a completion of the relevant signing nodes coordinator .send_message(messages::sign::CoordinatorMessage::Completed {