28 Commits

Author SHA1 Message Date
Luke Parker
0849d60f28 Run Bitcoin, Monero nodes on Alpine
While prior this didn't work well, presumably due to stack size limitations,
a shell script is included to raise the default stack size limit. This should
be tried again.
2025-12-08 02:30:34 -05:00
Luke Parker
3a792f9ce5 Update documentation on the serai-runtime build.rs 2025-12-08 02:22:29 -05:00
Luke Parker
50959fa0e3 Update the polkadot-sdk used
Removes `parity-wasm` as a dependency, closing
https://github.com/serai-dex/issues/227 and tidying our `deny.toml`.

This removes the `import-memory` flag from the linker as part of
`parity-wasm`'s usage was to map imports into exports
(5a1128b94b/substrate/client/executor/common/src/runtime_blob/runtime_blob.rs (L91-L142)).
2025-12-08 02:22:25 -05:00
Luke Parker
2fb90ebe55 Extend crates we patch to be empty from the Ethereum ecosystem
`ruint` pulls in many versions of many crates. This has it pull in less.
2025-12-06 08:27:34 -05:00
Luke Parker
b24adcbd14 Add panic-on-poison to no-std std_shims::sync::Mutex
We already had this behavior on `std`. It was omitted when no-`std` due to
deferring to `spin::Mutex`, which does not track poisoning at all. This
increases the parity of the two.

Part of https://github.com/serai-dex/serai/issues/698.
2025-12-06 08:06:38 -05:00
Luke Parker
b791256648 Remove substrate-wasm-builder
By defining our own build script, we gain complete clarity and control over how
the WASM is built. This also removes the need to patch the upstream due to it
allowing pollution of the environment variables from the host.

Notable appreciation is given to
https://github.com/rust-lang/rust/issues/145491 for identifying an issue
encountered here, with the associated PR clarifying the necessary flags for the
linker to fix this.
2025-12-04 23:23:38 -05:00
Luke Parker
36ac9c56a4 Remove workaround for lack of musl-dev now that musl-dev is provided in Rust Alpine images
Additionally, optimizes the build process a bit via leaving only the runtime
(and `busybox`) in the final image, and additionally building the runtime
without `std` (as we solely need the WASM blob from this process).
2025-12-04 11:58:38 -05:00
Luke Parker
57bf4984f8 panic = "abort"
`panic = "unwind"` was originally a requirement of Substrate, notably due to
its [native runtime](https://github.com/paritytech/substrate/issues/10874).
This does not mean all of Serai should use this setting however.

As the native runtime has been removed, we do no longer need this for the
Substrate node. With a review of our derivative, a panic guard is only used
when fetching the version from the runtime, causing an error on boot if a
panic occurs. Accordingly, we shouldn't have a need for `panic = "unwind"`
within the node, and the runtime itself should be fine.

The rest of Serai's services already registered bespoke hooks to ensure any
panic caused the process to exit. Those are left as-is, even though they're
now unnecessary.
2025-12-04 11:58:38 -05:00
Luke Parker
87750407de cargo-deny 0.18.8, remove bip39 git dependency
The former is necessary due to `cargo-deny` misinterpreting select licenses.
The latter is finally possible with the recent 2.2.1 release 🎉
2025-12-04 11:58:28 -05:00
Luke Parker
3ce90c55d9 Define a 512 KiB block size limit 2025-12-02 21:24:05 -05:00
Luke Parker
ff95c58341 Round out the runtime
Ensures the block's size limit is respected.

Defines a policy for weights. While I'm unsure I want to commit to this
forever, I do want to acknowledge it's valid and well-defined.

Cleans up the `serai-runtime` crate a bit with further modules in the `wasm`
folder.
2025-12-02 21:16:34 -05:00
Luke Parker
98044f93b1 Stub the in-instructions pallet 2025-12-02 16:46:10 -05:00
Luke Parker
eb04f873d5 Stub the genesis-liquidity pallet 2025-12-02 16:46:06 -05:00
Luke Parker
af74c318aa Add event emissions to the DEX pallet 2025-12-02 13:31:33 -05:00
Luke Parker
d711d8915f Update docs Ruby/gem versions 2025-12-02 13:20:17 -05:00
Luke Parker
3d549564a8 Misc tweaks in the style of the last commit
Notably removes the `kvdb-rocksdb` patch via updating the Substrate version
used to one which disables the `jemalloc` feature itself.

Simplifies the path of the built WASM file within the Dockerfile to consumers.
This also ensures if the image is built, the path of the WASM file is as
expected (prior unasserted).
2025-12-02 09:10:44 -05:00
Luke Parker
9a75f92864 Thoroughly update versions and methodology
For hash-pinned dependencies, adds comments documenting the associated
versions.

Adds a pin to `slither-analyzer` which was prior missing.

Updates to Monero 0.18.4.4.

`mimalloc` now has the correct option set when building for `musl`. A C++
compiler is no longer required in its Docker image.

The runtime's `Dockerfile` now symlinks a `libc.so` already present on the
image instead of creating one itself. It also builds the runtime within the
image to ensure it only happens once. The test to ensure the methodology is
reproducible has been updated to not simply create containers from the image,
yet rebuild the image entirely, accordingly. This also is more robust and
arguably should have already been done.

The pin to the exact hash of the `patch-polkadot-sdk` repo in every
`Cargo.toml` has been removed. The lockfile already serves that role,
simplifying updating in the future.

The latest Rust nightly is adopted as well (superseding
https://github.com/serai-dex/serai/pull/697).

The `librocksdb-sys` patch is replaced with a `kvdb-rocksdb` patch, removing a
git dependency, thanks to https://github.com/paritytech/parity-common/pull/950.
2025-12-01 18:17:01 -05:00
Luke Parker
30ea9d9a06 Tidy the DEX pallet 2025-11-30 21:42:27 -05:00
Luke Parker
c45c973ca1 Remove musl-dev from runtime/Dockerfile
It wasn't pinned with a hash yet with a version tag. This ensures we are
deterministic to the image (specified by hash), `Cargo.lock`, and source code
alone.

Unfortunately, this was incredibly annoying to do, the exact process uncovering
a SIGSEGV in stable Rust. The extensive documentation details the solution.
Thankfully, it works now.
2025-11-27 03:37:37 -05:00
Luke Parker
6e37ac030d Add patch for alloy-eip2124 to an empty crate
Removes the `crc` dependency which had a unique author associated.
2025-11-26 17:01:03 -05:00
Luke Parker
e7c759c468 Improve substrate-median tests
The use of a dedicated test module ensures the API doesn't hide anything which
needs to be public. There's also now explicit tests for when the median is the
popped value.
2025-11-25 23:46:12 -05:00
Luke Parker
8ec0582237 Add module to calculate medians 2025-11-25 22:39:52 -05:00
Luke Parker
8d8e8a7a77 Remove unnecessary MSRVs from patches/ 2025-11-25 17:05:30 -05:00
Luke Parker
028ec3cce0 borsh 1.6.0
Bumps th MSRV for some of our crates, which is fine.
2025-11-25 16:58:19 -05:00
Luke Parker
c49215805f Update Substrate 2025-11-25 00:06:54 -05:00
Luke Parker
2ffdd2a01d Update monero-oxide, Substrate 2025-11-22 11:49:25 -05:00
Luke Parker
e1e6e67d4a Ensure desired pruning behavior is held within the node 2025-11-18 21:46:58 -05:00
Luke Parker
6b19780c7b Remove historical state access from Serai
Resolves https://github.com/serai-dex/serai/issues/694.
2025-11-18 21:27:47 -05:00
156 changed files with 4363 additions and 7569 deletions

View File

@@ -12,7 +12,7 @@ runs:
steps: steps:
- name: Bitcoin Daemon Cache - name: Bitcoin Daemon Cache
id: cache-bitcoind id: cache-bitcoind
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # 4.2.4
with: with:
path: bitcoin.tar.gz path: bitcoin.tar.gz
key: bitcoind-${{ runner.os }}-${{ runner.arch }}-${{ inputs.version }} key: bitcoind-${{ runner.os }}-${{ runner.arch }}-${{ inputs.version }}

View File

@@ -52,7 +52,7 @@ runs:
- name: Install solc - name: Install solc
shell: bash shell: bash
run: | run: |
cargo +1.91 install svm-rs --version =0.5.19 cargo +1.91.1 install svm-rs --version =0.5.22
svm install 0.8.29 svm install 0.8.29
svm use 0.8.29 svm use 0.8.29
@@ -75,11 +75,8 @@ runs:
if: runner.os == 'Linux' if: runner.os == 'Linux'
- name: Install rootless Docker - name: Install rootless Docker
uses: docker/setup-docker-action@b60f85385d03ac8acfca6d9996982511d8620a19 uses: docker/setup-docker-action@e61617a16c407a86262fb923c35a616ddbe070b3 # 4.6.0
with: with:
rootless: true rootless: true
set-host: true set-host: true
if: runner.os == 'Linux' if: runner.os == 'Linux'
# - name: Cache Rust
# uses: Swatinem/rust-cache@a95ba195448af2da9b00fb742d14ffaaf3c21f43

View File

@@ -5,14 +5,14 @@ inputs:
version: version:
description: "Version to download and run" description: "Version to download and run"
required: false required: false
default: v0.18.4.3 default: v0.18.4.4
runs: runs:
using: "composite" using: "composite"
steps: steps:
- name: Monero Wallet RPC Cache - name: Monero Wallet RPC Cache
id: cache-monero-wallet-rpc id: cache-monero-wallet-rpc
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # 4.2.4
with: with:
path: monero-wallet-rpc path: monero-wallet-rpc
key: monero-wallet-rpc-${{ runner.os }}-${{ runner.arch }}-${{ inputs.version }} key: monero-wallet-rpc-${{ runner.os }}-${{ runner.arch }}-${{ inputs.version }}

View File

@@ -5,14 +5,14 @@ inputs:
version: version:
description: "Version to download and run" description: "Version to download and run"
required: false required: false
default: v0.18.4.3 default: v0.18.4.4
runs: runs:
using: "composite" using: "composite"
steps: steps:
- name: Monero Daemon Cache - name: Monero Daemon Cache
id: cache-monerod id: cache-monerod
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # 4.2.4
with: with:
path: /usr/bin/monerod path: /usr/bin/monerod
key: monerod-${{ runner.os }}-${{ runner.arch }}-${{ inputs.version }} key: monerod-${{ runner.os }}-${{ runner.arch }}-${{ inputs.version }}

View File

@@ -5,7 +5,7 @@ inputs:
monero-version: monero-version:
description: "Monero version to download and run as a regtest node" description: "Monero version to download and run as a regtest node"
required: false required: false
default: v0.18.4.3 default: v0.18.4.4
bitcoin-version: bitcoin-version:
description: "Bitcoin version to download and run as a regtest node" description: "Bitcoin version to download and run as a regtest node"
@@ -19,9 +19,9 @@ runs:
uses: ./.github/actions/build-dependencies uses: ./.github/actions/build-dependencies
- name: Install Foundry - name: Install Foundry
uses: foundry-rs/foundry-toolchain@8f1998e9878d786675189ef566a2e4bf24869773 uses: foundry-rs/foundry-toolchain@50d5a8956f2e319df19e6b57539d7e2acb9f8c1e # 1.5.0
with: with:
version: nightly-f625d0fa7c51e65b4bf1e8f7931cd1c6e2e285e9 version: v1.5.0
cache: false cache: false
- name: Run a Monero Regtest Node - name: Run a Monero Regtest Node

View File

@@ -1 +1 @@
nightly-2025-11-11 nightly-2025-12-01

View File

@@ -17,7 +17,7 @@ jobs:
test-common: test-common:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # 6.0.0
- name: Build Dependencies - name: Build Dependencies
uses: ./.github/actions/build-dependencies uses: ./.github/actions/build-dependencies

View File

@@ -31,7 +31,7 @@ jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # 6.0.0
- name: Install Build Dependencies - name: Install Build Dependencies
uses: ./.github/actions/build-dependencies uses: ./.github/actions/build-dependencies

View File

@@ -19,7 +19,7 @@ jobs:
test-crypto: test-crypto:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # 6.0.0
- name: Build Dependencies - name: Build Dependencies
uses: ./.github/actions/build-dependencies uses: ./.github/actions/build-dependencies

View File

@@ -9,16 +9,10 @@ jobs:
name: Run cargo deny name: Run cargo deny
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # 6.0.0
- name: Advisory Cache
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809
with:
path: ~/.cargo/advisory-db
key: rust-advisory-db
- name: Install cargo deny - name: Install cargo deny
run: cargo +1.91 install cargo-deny --version =0.18.5 run: cargo +1.91.1 install cargo-deny --version =0.18.8
- name: Run cargo deny - name: Run cargo deny
run: cargo deny -L error --all-features check --hide-inclusion-graph run: cargo deny -L error --all-features check --hide-inclusion-graph

View File

@@ -13,7 +13,7 @@ jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # 6.0.0
- name: Install Build Dependencies - name: Install Build Dependencies
uses: ./.github/actions/build-dependencies uses: ./.github/actions/build-dependencies

View File

@@ -15,7 +15,7 @@ jobs:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # 6.0.0
- name: Get nightly version to use - name: Get nightly version to use
id: nightly id: nightly
@@ -43,16 +43,10 @@ jobs:
deny: deny:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # 6.0.0
- name: Advisory Cache
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809
with:
path: ~/.cargo/advisory-db
key: rust-advisory-db
- name: Install cargo deny - name: Install cargo deny
run: cargo +1.91 install cargo-deny --version =0.18.5 run: cargo +1.91.1 install cargo-deny --version =0.18.8
- name: Run cargo deny - name: Run cargo deny
run: cargo deny -L error --all-features check --hide-inclusion-graph run: cargo deny -L error --all-features check --hide-inclusion-graph
@@ -60,7 +54,7 @@ jobs:
fmt: fmt:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # 6.0.0
- name: Get nightly version to use - name: Get nightly version to use
id: nightly id: nightly
@@ -73,10 +67,10 @@ jobs:
- name: Run rustfmt - name: Run rustfmt
run: cargo +${{ steps.nightly.outputs.version }} fmt -- --check run: cargo +${{ steps.nightly.outputs.version }} fmt -- --check
- name: Install foundry - name: Install Foundry
uses: foundry-rs/foundry-toolchain@8f1998e9878d786675189ef566a2e4bf24869773 uses: foundry-rs/foundry-toolchain@50d5a8956f2e319df19e6b57539d7e2acb9f8c1e # 1.5.0
with: with:
version: nightly-41d4e5437107f6f42c7711123890147bc736a609 version: v1.5.0
cache: false cache: false
- name: Run forge fmt - name: Run forge fmt
@@ -85,20 +79,20 @@ jobs:
machete: machete:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # 6.0.0
- name: Verify all dependencies are in use - name: Verify all dependencies are in use
run: | run: |
cargo +1.91 install cargo-machete --version =0.9.1 cargo +1.91.1 install cargo-machete --version =0.9.1
cargo +1.91 machete cargo +1.91.1 machete
msrv: msrv:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # 6.0.0
- name: Verify claimed `rust-version` - name: Verify claimed `rust-version`
shell: bash shell: bash
run: | run: |
cargo +1.91 install cargo-msrv --version =0.18.4 cargo +1.91.1 install cargo-msrv --version =0.18.4
function check_msrv { function check_msrv {
# We `cd` into the directory passed as the first argument, but will return to the # We `cd` into the directory passed as the first argument, but will return to the
@@ -189,16 +183,16 @@ jobs:
slither: slither:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # 6.0.0
- name: Build Dependencies - name: Build Dependencies
uses: ./.github/actions/build-dependencies uses: ./.github/actions/build-dependencies
- name: Slither - name: Slither
run: | run: |
python3 -m pip install slither-analyzer python3 -m pip install slither-analyzer==0.11.3
slither --include-paths ./networks/ethereum/schnorr/contracts/Schnorr.sol slither ./networks/ethereum/schnorr/contracts/Schnorr.sol
slither --include-paths ./networks/ethereum/schnorr/contracts ./networks/ethereum/schnorr/contracts/tests/Schnorr.sol slither --include-paths ./networks/ethereum/schnorr/contracts ./networks/ethereum/schnorr/contracts/tests/Schnorr.sol
slither processor/ethereum/deployer/contracts/Deployer.sol slither processor/ethereum/deployer/contracts/Deployer.sol
slither processor/ethereum/erc20/contracts/IERC20.sol slither processor/ethereum/erc20/contracts/IERC20.sol

View File

@@ -27,7 +27,7 @@ jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # 6.0.0
- name: Install Build Dependencies - name: Install Build Dependencies
uses: ./.github/actions/build-dependencies uses: ./.github/actions/build-dependencies

View File

@@ -17,7 +17,7 @@ jobs:
test-common: test-common:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # 6.0.0
- name: Build Dependencies - name: Build Dependencies
uses: ./.github/actions/build-dependencies uses: ./.github/actions/build-dependencies

View File

@@ -9,7 +9,7 @@ jobs:
name: Update nightly name: Update nightly
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # 6.0.0
with: with:
submodules: "recursive" submodules: "recursive"

View File

@@ -21,7 +21,7 @@ jobs:
test-networks: test-networks:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # 6.0.0
- name: Test Dependencies - name: Test Dependencies
uses: ./.github/actions/test-dependencies uses: ./.github/actions/test-dependencies

View File

@@ -23,7 +23,7 @@ jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # 6.0.0
- name: Install Build Dependencies - name: Install Build Dependencies
uses: ./.github/actions/build-dependencies uses: ./.github/actions/build-dependencies

View File

@@ -46,16 +46,16 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # 6.0.0
- name: Setup Ruby - name: Setup Ruby
uses: ruby/setup-ruby@44511735964dcb71245e7e55f72539531f7bc0eb uses: ruby/setup-ruby@8aeb6ff8030dd539317f8e1769a044873b56ea71 # 1.268.0
with: with:
bundler-cache: true bundler-cache: true
cache-version: 0 cache-version: 0
working-directory: "${{ github.workspace }}/docs" working-directory: "${{ github.workspace }}/docs"
- name: Setup Pages - name: Setup Pages
id: pages id: pages
uses: actions/configure-pages@983d7736d9b0ae728b81ab479565c72886d7745b uses: actions/configure-pages@983d7736d9b0ae728b81ab479565c72886d7745b # 5.0.0
- name: Build with Jekyll - name: Build with Jekyll
run: cd ${{ github.workspace }}/docs && bundle exec jekyll build --baseurl "${{ steps.pages.outputs.base_path }}" run: cd ${{ github.workspace }}/docs && bundle exec jekyll build --baseurl "${{ steps.pages.outputs.base_path }}"
env: env:
@@ -74,7 +74,7 @@ jobs:
mv target/doc docs/_site/rust mv target/doc docs/_site/rust
- name: Upload artifact - name: Upload artifact
uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # 4.0.0
with: with:
path: "docs/_site/" path: "docs/_site/"
@@ -88,4 +88,4 @@ jobs:
steps: steps:
- name: Deploy to GitHub Pages - name: Deploy to GitHub Pages
id: deployment id: deployment
uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # 4.0.5

View File

@@ -31,7 +31,7 @@ jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # 6.0.0
- name: Install Build Dependencies - name: Install Build Dependencies
uses: ./.github/actions/build-dependencies uses: ./.github/actions/build-dependencies

View File

@@ -27,10 +27,10 @@ jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # 6.0.0
- name: Install Build Dependencies - name: Install Build Dependencies
uses: ./.github/actions/build-dependencies uses: ./.github/actions/build-dependencies
- name: Run Reproducible Runtime tests - name: Run Reproducible Runtime tests
run: GITHUB_CI=true RUST_BACKTRACE=1 cargo test --all-features -p serai-reproducible-runtime-tests run: GITHUB_CI=true RUST_BACKTRACE=1 cargo test --all-features -p serai-reproducible-runtime-tests -- --nocapture

View File

@@ -29,7 +29,7 @@ jobs:
test-infra: test-infra:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # 6.0.0
- name: Build Dependencies - name: Build Dependencies
uses: ./.github/actions/build-dependencies uses: ./.github/actions/build-dependencies
@@ -74,7 +74,7 @@ jobs:
test-substrate: test-substrate:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # 6.0.0
- name: Build Dependencies - name: Build Dependencies
uses: ./.github/actions/build-dependencies uses: ./.github/actions/build-dependencies
@@ -84,6 +84,7 @@ jobs:
GITHUB_CI=true RUST_BACKTRACE=1 cargo test --all-features \ GITHUB_CI=true RUST_BACKTRACE=1 cargo test --all-features \
-p serai-primitives \ -p serai-primitives \
-p serai-abi \ -p serai-abi \
-p substrate-median \
-p serai-core-pallet \ -p serai-core-pallet \
-p serai-coins-pallet \ -p serai-coins-pallet \
-p serai-validator-sets-pallet \ -p serai-validator-sets-pallet \
@@ -100,7 +101,7 @@ jobs:
test-serai-client: test-serai-client:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # 6.0.0
- name: Build Dependencies - name: Build Dependencies
uses: ./.github/actions/build-dependencies uses: ./.github/actions/build-dependencies

1298
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -80,6 +80,8 @@ members = [
"substrate/primitives", "substrate/primitives",
"substrate/abi", "substrate/abi",
"substrate/median",
"substrate/core", "substrate/core",
"substrate/coins", "substrate/coins",
"substrate/validator-sets", "substrate/validator-sets",
@@ -114,6 +116,19 @@ members = [
"tests/reproducible-runtime", "tests/reproducible-runtime",
] ]
[profile.dev]
panic = "abort"
overflow-checks = true
[profile.release]
panic = "abort"
overflow-checks = true
# These do not respect the `panic` configuration value, so we don't provide them
[profile.test]
# panic = "abort" # https://github.com/rust-lang/issues/67650
overflow-checks = true
[profile.bench]
overflow-checks = true
[profile.dev.package] [profile.dev.package]
# Always compile Monero (and a variety of dependencies) with optimizations due # Always compile Monero (and a variety of dependencies) with optimizations due
# to the extensive operations required for Bulletproofs # to the extensive operations required for Bulletproofs
@@ -131,11 +146,14 @@ dalek-ff-group = { opt-level = 3 }
multiexp = { opt-level = 3 } multiexp = { opt-level = 3 }
monero-generators = { opt-level = 3 } monero-io = { opt-level = 3 }
monero-borromean = { opt-level = 3 } monero-primitives = { opt-level = 3 }
monero-bulletproofs = { opt-level = 3 } monero-ed25519 = { opt-level = 3 }
monero-mlsag = { opt-level = 3 } monero-mlsag = { opt-level = 3 }
monero-clsag = { opt-level = 3 } monero-clsag = { opt-level = 3 }
monero-borromean = { opt-level = 3 }
monero-bulletproofs-generators = { opt-level = 3 }
monero-bulletproofs = {opt-level = 3 }
monero-oxide = { opt-level = 3 } monero-oxide = { opt-level = 3 }
# Always compile the eVRF DKG tree with optimizations as well # Always compile the eVRF DKG tree with optimizations as well
@@ -160,16 +178,17 @@ revm-precompile = { opt-level = 3 }
revm-primitives = { opt-level = 3 } revm-primitives = { opt-level = 3 }
revm-state = { opt-level = 3 } revm-state = { opt-level = 3 }
[profile.release]
panic = "unwind"
overflow-checks = true
[patch.crates-io] [patch.crates-io]
# Point to empty crates for crates unused within in our tree # Point to empty crates for crates unused within in our tree
alloy-eip2124 = { path = "patches/ethereum/alloy-eip2124" }
ark-ff-3 = { package = "ark-ff", path = "patches/ethereum/ark-ff-0.3" } ark-ff-3 = { package = "ark-ff", path = "patches/ethereum/ark-ff-0.3" }
ark-ff-4 = { package = "ark-ff", path = "patches/ethereum/ark-ff-0.4" } ark-ff-4 = { package = "ark-ff", path = "patches/ethereum/ark-ff-0.4" }
c-kzg = { path = "patches/ethereum/c-kzg" } c-kzg = { path = "patches/ethereum/c-kzg" }
secp256k1-30 = { package = "secp256k1", path = "patches/ethereum/secp256k1-30" } fastrlp-3 = { package = "fastrlp", path = "patches/ethereum/fastrlp-0.3" }
fastrlp-4 = { package = "fastrlp", path = "patches/ethereum/fastrlp-0.4" }
primitive-types-12 = { package = "primitive-types", path = "patches/ethereum/primitive-types-0.12" }
rlp = { path = "patches/ethereum/rlp" }
secp256k1-30 = { package = "secp256k1", path = "patches/ethereum/secp256k1-0.30" }
# Dependencies from monero-oxide which originate from within our own tree, potentially shimmed to account for deviations since publishing # Dependencies from monero-oxide which originate from within our own tree, potentially shimmed to account for deviations since publishing
std-shims = { path = "patches/std-shims" } std-shims = { path = "patches/std-shims" }
@@ -208,9 +227,6 @@ parity-bip39 = { path = "patches/parity-bip39" }
k256 = { git = "https://github.com/kayabaNerve/elliptic-curves", rev = "4994c9ab163781a88cd4a49beae812a89a44e8c3" } k256 = { git = "https://github.com/kayabaNerve/elliptic-curves", rev = "4994c9ab163781a88cd4a49beae812a89a44e8c3" }
p256 = { git = "https://github.com/kayabaNerve/elliptic-curves", rev = "4994c9ab163781a88cd4a49beae812a89a44e8c3" } p256 = { git = "https://github.com/kayabaNerve/elliptic-curves", rev = "4994c9ab163781a88cd4a49beae812a89a44e8c3" }
# `jemalloc` conflicts with `mimalloc`, so patch to a `rocksdb` which never uses `jemalloc`
librocksdb-sys = { path = "patches/librocksdb-sys" }
[workspace.lints.clippy] [workspace.lints.clippy]
incompatible_msrv = "allow" # Manually verified with a GitHub workflow incompatible_msrv = "allow" # Manually verified with a GitHub workflow
manual_is_multiple_of = "allow" manual_is_multiple_of = "allow"

View File

@@ -7,7 +7,7 @@ repository = "https://github.com/serai-dex/serai/tree/develop/common/db"
authors = ["Luke Parker <lukeparker5132@gmail.com>"] authors = ["Luke Parker <lukeparker5132@gmail.com>"]
keywords = [] keywords = []
edition = "2021" edition = "2021"
rust-version = "1.65" rust-version = "1.77"
[package.metadata.docs.rs] [package.metadata.docs.rs]
all-features = true all-features = true

View File

@@ -6,12 +6,63 @@ pub use std::sync::{Arc, Weak};
mod mutex_shim { mod mutex_shim {
#[cfg(not(feature = "std"))] #[cfg(not(feature = "std"))]
pub use spin::{Mutex, MutexGuard}; mod spin_mutex {
use core::ops::{Deref, DerefMut};
// We wrap this in an `Option` so we can consider `None` as poisoned
pub(super) struct Mutex<T>(spin::Mutex<Option<T>>);
/// An acquired view of a `Mutex`.
pub struct MutexGuard<'mutex, T> {
mutex: spin::MutexGuard<'mutex, Option<T>>,
// This is `Some` for the lifetime of this guard, and is only represented as an `Option` due
// to needing to move it on `Drop` (which solely gives us a mutable reference to `self`)
value: Option<T>,
}
impl<T> Mutex<T> {
pub(super) const fn new(value: T) -> Self {
Self(spin::Mutex::new(Some(value)))
}
pub(super) fn lock(&self) -> MutexGuard<'_, T> {
let mut mutex = self.0.lock();
// Take from the `Mutex` so future acquisitions will see `None` unless this is restored
let value = mutex.take();
// Check the prior acquisition did in fact restore the value
if value.is_none() {
panic!("locking a `spin::Mutex` held by a thread which panicked");
}
MutexGuard { mutex, value }
}
}
impl<T> Deref for MutexGuard<'_, T> {
type Target = T;
fn deref(&self) -> &T {
self.value.as_ref().expect("no value yet checked upon lock acquisition")
}
}
impl<T> DerefMut for MutexGuard<'_, T> {
fn deref_mut(&mut self) -> &mut T {
self.value.as_mut().expect("no value yet checked upon lock acquisition")
}
}
impl<'mutex, T> Drop for MutexGuard<'mutex, T> {
fn drop(&mut self) {
// Restore the value
*self.mutex = self.value.take();
}
}
}
#[cfg(not(feature = "std"))]
pub use spin_mutex::*;
#[cfg(feature = "std")] #[cfg(feature = "std")]
pub use std::sync::{Mutex, MutexGuard}; pub use std::sync::{Mutex, MutexGuard};
/// A shimmed `Mutex` with an API mutual to `spin` and `std`. /// A shimmed `Mutex` with an API mutual to `spin` and `std`.
#[derive(Default, Debug)]
pub struct ShimMutex<T>(Mutex<T>); pub struct ShimMutex<T>(Mutex<T>);
impl<T> ShimMutex<T> { impl<T> ShimMutex<T> {
/// Construct a new `Mutex`. /// Construct a new `Mutex`.
@@ -21,8 +72,9 @@ mod mutex_shim {
/// Acquire a lock on the contents of the `Mutex`. /// Acquire a lock on the contents of the `Mutex`.
/// ///
/// On no-`std` environments, this may spin until the lock is acquired. On `std` environments, /// This will panic if the `Mutex` was poisoned.
/// this may panic if the `Mutex` was poisoned. ///
/// On no-`std` environments, the implementation presumably defers to that of a spin lock.
pub fn lock(&self) -> MutexGuard<'_, T> { pub fn lock(&self) -> MutexGuard<'_, T> {
#[cfg(feature = "std")] #[cfg(feature = "std")]
let res = self.0.lock().unwrap(); let res = self.0.lock().unwrap();

View File

@@ -4,11 +4,18 @@ use std::{sync::Arc, collections::HashMap};
use blake2::{Digest, Blake2b256}; use blake2::{Digest, Blake2b256};
use serai_client_serai::{ use serai_client_serai::{
abi::primitives::{ abi::{
balance::Amount, validator_sets::ExternalValidatorSet, address::SeraiAddress, primitives::{
merkle::IncrementalUnbalancedMerkleTree, network_id::{ExternalNetworkId, NetworkId},
balance::Amount,
crypto::Public,
validator_sets::{Session, ExternalValidatorSet},
address::SeraiAddress,
merkle::IncrementalUnbalancedMerkleTree,
},
validator_sets::Event,
}, },
Serai, Serai, Events,
}; };
use serai_db::*; use serai_db::*;
@@ -16,10 +23,20 @@ use serai_task::ContinuallyRan;
use crate::*; use crate::*;
#[derive(BorshSerialize, BorshDeserialize)]
struct Set {
session: Session,
key: Public,
stake: Amount,
}
create_db!( create_db!(
CosignIntend { CosignIntend {
ScanCosignFrom: () -> u64, ScanCosignFrom: () -> u64,
BuildsUpon: () -> IncrementalUnbalancedMerkleTree, BuildsUpon: () -> IncrementalUnbalancedMerkleTree,
Stakes: (network: ExternalNetworkId, validator: SeraiAddress) -> Amount,
Validators: (set: ExternalValidatorSet) -> Vec<SeraiAddress>,
LatestSet: (network: ExternalNetworkId) -> Set,
} }
); );
@@ -40,23 +57,38 @@ db_channel! {
async fn block_has_events_justifying_a_cosign( async fn block_has_events_justifying_a_cosign(
serai: &Serai, serai: &Serai,
block_number: u64, block_number: u64,
) -> Result<(Block, HasEvents), String> { ) -> Result<(Block, Events, HasEvents), String> {
let block = serai let block = serai
.block_by_number(block_number) .block_by_number(block_number)
.await .await
.map_err(|e| format!("{e:?}"))? .map_err(|e| format!("{e:?}"))?
.ok_or_else(|| "couldn't get block which should've been finalized".to_string())?; .ok_or_else(|| "couldn't get block which should've been finalized".to_string())?;
let serai = serai.as_of(block.header.hash()).await.map_err(|e| format!("{e:?}"))?; let events = serai.events(block.header.hash()).await.map_err(|e| format!("{e:?}"))?;
if !serai.validator_sets().set_keys_events().await.map_err(|e| format!("{e:?}"))?.is_empty() { if events.validator_sets().set_keys_events().next().is_some() {
return Ok((block, HasEvents::Notable)); return Ok((block, events, HasEvents::Notable));
} }
if !serai.coins().burn_with_instruction_events().await.map_err(|e| format!("{e:?}"))?.is_empty() { if events.coins().burn_with_instruction_events().next().is_some() {
return Ok((block, HasEvents::NonNotable)); return Ok((block, events, HasEvents::NonNotable));
} }
Ok((block, HasEvents::No)) Ok((block, events, HasEvents::No))
}
// Fetch the `ExternalValidatorSet`s, and their associated keys, used for cosigning as of this
// block.
fn cosigning_sets(getter: &impl Get) -> Vec<(ExternalValidatorSet, Public, Amount)> {
let mut sets = vec![];
for network in ExternalNetworkId::all() {
let Some(Set { session, key, stake }) = LatestSet::get(getter, network) else {
// If this network doesn't have usable keys, move on
continue;
};
sets.push((ExternalValidatorSet { network, session }, key, stake));
}
sets
} }
/// A task to determine which blocks we should intend to cosign. /// A task to determine which blocks we should intend to cosign.
@@ -77,7 +109,7 @@ impl<D: Db> ContinuallyRan for CosignIntendTask<D> {
for block_number in start_block_number ..= latest_block_number { for block_number in start_block_number ..= latest_block_number {
let mut txn = self.db.txn(); let mut txn = self.db.txn();
let (block, mut has_events) = let (block, events, mut has_events) =
block_has_events_justifying_a_cosign(&self.serai, block_number) block_has_events_justifying_a_cosign(&self.serai, block_number)
.await .await
.map_err(|e| format!("{e:?}"))?; .map_err(|e| format!("{e:?}"))?;
@@ -105,32 +137,75 @@ impl<D: Db> ContinuallyRan for CosignIntendTask<D> {
); );
BuildsUpon::set(&mut txn, &builds_upon); BuildsUpon::set(&mut txn, &builds_upon);
// Update the stakes
for event in events.validator_sets().allocation_events() {
let Event::Allocation { validator, network, amount } = event else {
panic!("event from `allocation_events` wasn't `Event::Allocation`")
};
let Ok(network) = ExternalNetworkId::try_from(*network) else { continue };
let existing = Stakes::get(&txn, network, *validator).unwrap_or(Amount(0));
Stakes::set(&mut txn, network, *validator, &Amount(existing.0 + amount.0));
}
for event in events.validator_sets().deallocation_events() {
let Event::Deallocation { validator, network, amount, timeline: _ } = event else {
panic!("event from `deallocation_events` wasn't `Event::Deallocation`")
};
let Ok(network) = ExternalNetworkId::try_from(*network) else { continue };
let existing = Stakes::get(&txn, network, *validator).unwrap_or(Amount(0));
Stakes::set(&mut txn, network, *validator, &Amount(existing.0 - amount.0));
}
// Handle decided sets
for event in events.validator_sets().set_decided_events() {
let Event::SetDecided { set, validators } = event else {
panic!("event from `set_decided_events` wasn't `Event::SetDecided`")
};
let Ok(set) = ExternalValidatorSet::try_from(*set) else { continue };
Validators::set(
&mut txn,
set,
&validators.iter().map(|(validator, _key_shares)| *validator).collect(),
);
}
// Handle declarations of the latest set
for event in events.validator_sets().set_keys_events() {
let Event::SetKeys { set, key_pair } = event else {
panic!("event from `set_keys_events` wasn't `Event::SetKeys`")
};
let mut stake = 0;
for validator in
Validators::take(&mut txn, *set).expect("set which wasn't decided set keys")
{
stake += Stakes::get(&txn, set.network, validator).unwrap_or(Amount(0)).0;
}
LatestSet::set(
&mut txn,
set.network,
&Set { session: set.session, key: key_pair.0, stake: Amount(stake) },
);
}
let global_session_for_this_block = LatestGlobalSessionIntended::get(&txn); let global_session_for_this_block = LatestGlobalSessionIntended::get(&txn);
// If this is notable, it creates a new global session, which we index into the database // If this is notable, it creates a new global session, which we index into the database
// now // now
if has_events == HasEvents::Notable { if has_events == HasEvents::Notable {
let serai = self.serai.as_of(block_hash).await.map_err(|e| format!("{e:?}"))?; let sets_and_keys_and_stakes = cosigning_sets(&txn);
let sets_and_keys = cosigning_sets(&serai).await?; let global_session = GlobalSession::id(
let global_session = sets_and_keys_and_stakes.iter().map(|(set, _key, _stake)| *set).collect(),
GlobalSession::id(sets_and_keys.iter().map(|(set, _key)| *set).collect()); );
let mut sets = Vec::with_capacity(sets_and_keys.len()); let mut sets = Vec::with_capacity(sets_and_keys_and_stakes.len());
let mut keys = HashMap::with_capacity(sets_and_keys.len()); let mut keys = HashMap::with_capacity(sets_and_keys_and_stakes.len());
let mut stakes = HashMap::with_capacity(sets_and_keys.len()); let mut stakes = HashMap::with_capacity(sets_and_keys_and_stakes.len());
let mut total_stake = 0; let mut total_stake = 0;
for (set, key) in &sets_and_keys { for (set, key, stake) in sets_and_keys_and_stakes {
sets.push(*set); sets.push(set);
keys.insert(set.network, SeraiAddress::from(*key)); keys.insert(set.network, key);
let stake = serai stakes.insert(set.network, stake.0);
.validator_sets() total_stake += stake.0;
.current_stake(set.network.into())
.await
.map_err(|e| format!("{e:?}"))?
.unwrap_or(Amount(0))
.0;
stakes.insert(set.network, stake);
total_stake += stake;
} }
if total_stake == 0 { if total_stake == 0 {
Err(format!("cosigning sets for block #{block_number} had 0 stake in total"))?; Err(format!("cosigning sets for block #{block_number} had 0 stake in total"))?;

View File

@@ -20,7 +20,7 @@ use serai_client_serai::{
}, },
Block, Block,
}, },
Serai, TemporalSerai, Serai, State,
}; };
use serai_db::*; use serai_db::*;
@@ -59,7 +59,7 @@ use delay::LatestCosignedBlockNumber;
pub(crate) struct GlobalSession { pub(crate) struct GlobalSession {
pub(crate) start_block_number: u64, pub(crate) start_block_number: u64,
pub(crate) sets: Vec<ExternalValidatorSet>, pub(crate) sets: Vec<ExternalValidatorSet>,
pub(crate) keys: HashMap<ExternalNetworkId, SeraiAddress>, pub(crate) keys: HashMap<ExternalNetworkId, Public>,
pub(crate) stakes: HashMap<ExternalNetworkId, u64>, pub(crate) stakes: HashMap<ExternalNetworkId, u64>,
pub(crate) total_stake: u64, pub(crate) total_stake: u64,
} }
@@ -121,60 +121,6 @@ create_db! {
} }
} }
/// Fetch the keys used for cosigning by a specific network.
async fn keys_for_network(
serai: &TemporalSerai<'_>,
network: ExternalNetworkId,
) -> Result<Option<(Session, KeyPair)>, String> {
let Some(latest_session) =
serai.validator_sets().current_session(network.into()).await.map_err(|e| format!("{e:?}"))?
else {
// If this network hasn't had a session declared, move on
return Ok(None);
};
// Get the keys for the latest session
if let Some(keys) = serai
.validator_sets()
.keys(ExternalValidatorSet { network, session: latest_session })
.await
.map_err(|e| format!("{e:?}"))?
{
return Ok(Some((latest_session, keys)));
}
// If the latest session has yet to set keys, use the prior session
if let Some(prior_session) = latest_session.0.checked_sub(1).map(Session) {
if let Some(keys) = serai
.validator_sets()
.keys(ExternalValidatorSet { network, session: prior_session })
.await
.map_err(|e| format!("{e:?}"))?
{
return Ok(Some((prior_session, keys)));
}
}
Ok(None)
}
/// Fetch the `ExternalValidatorSet`s, and their associated keys, used for cosigning as of this
/// block.
async fn cosigning_sets(
serai: &TemporalSerai<'_>,
) -> Result<Vec<(ExternalValidatorSet, Public)>, String> {
let mut sets = vec![];
for network in ExternalNetworkId::all() {
let Some((session, keys)) = keys_for_network(serai, network).await? else {
// If this network doesn't have usable keys, move on
continue;
};
sets.push((ExternalValidatorSet { network, session }, keys.0));
}
Ok(sets)
}
/// An object usable to request notable cosigns for a block. /// An object usable to request notable cosigns for a block.
pub trait RequestNotableCosigns: 'static + Send { pub trait RequestNotableCosigns: 'static + Send {
/// The error type which may be encountered when requesting notable cosigns. /// The error type which may be encountered when requesting notable cosigns.
@@ -379,13 +325,8 @@ impl<D: Db> Cosigning<D> {
// Check the cosign's signature // Check the cosign's signature
{ {
let key = Public::from({ let key =
let Some(key) = global_session.keys.get(&network) else { *global_session.keys.get(&network).ok_or(IntakeCosignError::NonParticipatingNetwork)?;
Err(IntakeCosignError::NonParticipatingNetwork)?
};
*key
});
if !signed_cosign.verify_signature(key) { if !signed_cosign.verify_signature(key) {
Err(IntakeCosignError::InvalidSignature)?; Err(IntakeCosignError::InvalidSignature)?;
} }

View File

@@ -60,8 +60,7 @@ impl Validators {
Besides, we can't connect to historical validators, only the current validators. Besides, we can't connect to historical validators, only the current validators.
*/ */
let temporal_serai = serai.borrow().as_of_latest_finalized_block().await?; let serai = serai.borrow().state().await?;
let temporal_serai = temporal_serai.validator_sets();
let mut session_changes = vec![]; let mut session_changes = vec![];
{ {
@@ -70,9 +69,9 @@ impl Validators {
let mut futures = FuturesUnordered::new(); let mut futures = FuturesUnordered::new();
for network in ExternalNetworkId::all() { for network in ExternalNetworkId::all() {
let sessions = sessions.borrow(); let sessions = sessions.borrow();
let temporal_serai = temporal_serai.borrow(); let serai = serai.borrow();
futures.push(async move { futures.push(async move {
let session = match temporal_serai.current_session(network.into()).await { let session = match serai.current_session(network.into()).await {
Ok(Some(session)) => session, Ok(Some(session)) => session,
Ok(None) => return Ok(None), Ok(None) => return Ok(None),
Err(e) => return Err(e), Err(e) => return Err(e),
@@ -81,7 +80,7 @@ impl Validators {
if sessions.get(&network) == Some(&session) { if sessions.get(&network) == Some(&session) {
Ok(None) Ok(None)
} else { } else {
match temporal_serai.current_validators(network.into()).await { match serai.current_validators(network.into()).await {
Ok(Some(validators)) => Ok(Some(( Ok(Some(validators)) => Ok(Some((
network, network,
session, session,

View File

@@ -50,7 +50,6 @@ fn tributary_db_folder(set: ExternalValidatorSet) -> String {
ExternalNetworkId::Bitcoin => "Bitcoin", ExternalNetworkId::Bitcoin => "Bitcoin",
ExternalNetworkId::Ethereum => "Ethereum", ExternalNetworkId::Ethereum => "Ethereum",
ExternalNetworkId::Monero => "Monero", ExternalNetworkId::Monero => "Monero",
_ => panic!("unrecognized `ExternalNetworkId`"),
}; };
format!("{root_path}/tributary-{network}-{}", set.session.0) format!("{root_path}/tributary-{network}-{}", set.session.0)
} }

View File

@@ -29,7 +29,6 @@ serai-client-serai = { path = "../../substrate/client/serai", default-features =
log = { version = "0.4", default-features = false, features = ["std"] } log = { version = "0.4", default-features = false, features = ["std"] }
futures = { version = "0.3", default-features = false, features = ["std"] } futures = { version = "0.3", default-features = false, features = ["std"] }
tokio = { version = "1", default-features = false }
serai-db = { path = "../../common/db", version = "0.1.1" } serai-db = { path = "../../common/db", version = "0.1.1" }
serai-task = { path = "../../common/task", version = "0.1" } serai-task = { path = "../../common/task", version = "0.1" }

View File

@@ -73,21 +73,13 @@ impl<D: Db> ContinuallyRan for CanonicalEventStream<D> {
} }
Err(serai_cosign::Faulted) => return Err("cosigning process faulted".to_string()), Err(serai_cosign::Faulted) => return Err("cosigning process faulted".to_string()),
}; };
let temporal_serai = serai.as_of(block_hash).await.map_err(|e| format!("{e}"))?; let events = serai.events(block_hash).await.map_err(|e| format!("{e}"))?;
let temporal_serai_validators = temporal_serai.validator_sets(); let set_keys_events = events.validator_sets().set_keys_events().cloned().collect();
let temporal_serai_instructions = temporal_serai.in_instructions(); let slash_report_events =
let temporal_serai_coins = temporal_serai.coins(); events.validator_sets().slash_report_events().cloned().collect();
let batch_events = events.in_instructions().batch_events().cloned().collect();
let (block, set_keys_events, slash_report_events, batch_events, burn_events) = let burn_events = events.coins().burn_with_instruction_events().cloned().collect();
tokio::try_join!( let Some(block) = serai.block(block_hash).await.map_err(|e| format!("{e:?}"))? else {
serai.block(block_hash),
temporal_serai_validators.set_keys_events(),
temporal_serai_validators.slash_report_events(),
temporal_serai_instructions.batch_events(),
temporal_serai_coins.burn_with_instruction_events(),
)
.map_err(|e| format!("{e:?}"))?;
let Some(block) = block else {
Err(format!("Serai node didn't have cosigned block #{block_number}"))? Err(format!("Serai node didn't have cosigned block #{block_number}"))?
}; };

View File

@@ -6,7 +6,7 @@ use futures::stream::{StreamExt, FuturesOrdered};
use serai_client_serai::{ use serai_client_serai::{
abi::primitives::{ abi::primitives::{
BlockHash, BlockHash,
crypto::EmbeddedEllipticCurveKeys, crypto::EmbeddedEllipticCurveKeys as EmbeddedEllipticCurveKeysStruct,
network_id::ExternalNetworkId, network_id::ExternalNetworkId,
validator_sets::{KeyShares, ExternalValidatorSet}, validator_sets::{KeyShares, ExternalValidatorSet},
address::SeraiAddress, address::SeraiAddress,
@@ -24,6 +24,10 @@ use crate::NewSetInformation;
create_db!( create_db!(
CoordinatorSubstrateEphemeral { CoordinatorSubstrateEphemeral {
NextBlock: () -> u64, NextBlock: () -> u64,
EmbeddedEllipticCurveKeys: (
network: ExternalNetworkId,
validator: SeraiAddress
) -> EmbeddedEllipticCurveKeysStruct,
} }
); );
@@ -56,6 +60,7 @@ impl<D: Db> ContinuallyRan for EphemeralEventStream<D> {
struct EphemeralEvents { struct EphemeralEvents {
block_hash: BlockHash, block_hash: BlockHash,
time: u64, time: u64,
embedded_elliptic_curve_keys_events: Vec<serai_client_serai::abi::validator_sets::Event>,
set_decided_events: Vec<serai_client_serai::abi::validator_sets::Event>, set_decided_events: Vec<serai_client_serai::abi::validator_sets::Event>,
accepted_handover_events: Vec<serai_client_serai::abi::validator_sets::Event>, accepted_handover_events: Vec<serai_client_serai::abi::validator_sets::Event>,
} }
@@ -76,15 +81,17 @@ impl<D: Db> ContinuallyRan for EphemeralEventStream<D> {
Err(serai_cosign::Faulted) => return Err("cosigning process faulted".to_string()), Err(serai_cosign::Faulted) => return Err("cosigning process faulted".to_string()),
}; };
let temporal_serai = serai.as_of(block_hash).await.map_err(|e| format!("{e}"))?; let events = serai.events(block_hash).await.map_err(|e| format!("{e}"))?;
let temporal_serai_validators = temporal_serai.validator_sets(); let embedded_elliptic_curve_keys_events = events
let (block, set_decided_events, accepted_handover_events) = tokio::try_join!( .validator_sets()
serai.block(block_hash), .set_embedded_elliptic_curve_keys_events()
temporal_serai_validators.set_decided_events(), .cloned()
temporal_serai_validators.accepted_handover_events(), .collect::<Vec<_>>();
) let set_decided_events =
.map_err(|e| format!("{e:?}"))?; events.validator_sets().set_decided_events().cloned().collect::<Vec<_>>();
let Some(block) = block else { let accepted_handover_events =
events.validator_sets().accepted_handover_events().cloned().collect::<Vec<_>>();
let Some(block) = serai.block(block_hash).await.map_err(|e| format!("{e:?}"))? else {
Err(format!("Serai node didn't have cosigned block #{block_number}"))? Err(format!("Serai node didn't have cosigned block #{block_number}"))?
}; };
@@ -92,7 +99,13 @@ impl<D: Db> ContinuallyRan for EphemeralEventStream<D> {
let time = block.header.unix_time_in_millis() / 1000; let time = block.header.unix_time_in_millis() / 1000;
Ok(( Ok((
block_number, block_number,
EphemeralEvents { block_hash, time, set_decided_events, accepted_handover_events }, EphemeralEvents {
block_hash,
time,
embedded_elliptic_curve_keys_events,
set_decided_events,
accepted_handover_events,
},
)) ))
} }
} }
@@ -123,30 +136,39 @@ impl<D: Db> ContinuallyRan for EphemeralEventStream<D> {
let mut txn = self.db.txn(); let mut txn = self.db.txn();
for event in block.embedded_elliptic_curve_keys_events {
let serai_client_serai::abi::validator_sets::Event::SetEmbeddedEllipticCurveKeys {
validator,
keys,
} = &event
else {
panic!(
"{}: {event:?}",
"`SetEmbeddedEllipticCurveKeys` event wasn't a `SetEmbeddedEllipticCurveKeys` event"
);
};
EmbeddedEllipticCurveKeys::set(&mut txn, keys.network(), *validator, keys);
}
for set_decided in block.set_decided_events { for set_decided in block.set_decided_events {
let serai_client_serai::abi::validator_sets::Event::SetDecided { set, validators } = let serai_client_serai::abi::validator_sets::Event::SetDecided { set, validators } =
&set_decided &set_decided
else { else {
panic!("`SetDecided` event wasn't a `SetDecided` event: {set_decided:?}"); panic!("`SetDecided` event wasn't a `SetDecided` event: {set_decided:?}");
}; };
// We only coordinate over external networks // We only coordinate over external networks
let Ok(set) = ExternalValidatorSet::try_from(*set) else { continue }; let Ok(set) = ExternalValidatorSet::try_from(*set) else { continue };
let serai = self.serai.as_of(block.block_hash).await.map_err(|e| format!("{e}"))?;
let serai = serai.validator_sets();
let validators = let validators =
validators.iter().map(|(validator, weight)| (*validator, weight)).collect::<Vec<_>>(); validators.iter().map(|(validator, weight)| (*validator, weight.0)).collect::<Vec<_>>();
let in_set = validators.iter().any(|(validator, _)| *validator == self.validator); let in_set = validators.iter().any(|(validator, _)| *validator == self.validator);
if in_set { if in_set {
if u16::try_from(validators.len()).is_err() { if u16::try_from(validators.len()).is_err() {
Err("more than u16::MAX validators sent")?; Err("more than u16::MAX validators sent")?;
} }
let validators = validators
.into_iter()
.map(|(validator, weight)| (validator, weight.0))
.collect::<Vec<_>>();
// Do the summation in u32 so we don't risk a u16 overflow // Do the summation in u32 so we don't risk a u16 overflow
let total_weight = validators.iter().map(|(_, weight)| u32::from(*weight)).sum::<u32>(); let total_weight = validators.iter().map(|(_, weight)| u32::from(*weight)).sum::<u32>();
if total_weight > u32::from(KeyShares::MAX_PER_SET) { if total_weight > u32::from(KeyShares::MAX_PER_SET) {
@@ -159,50 +181,26 @@ impl<D: Db> ContinuallyRan for EphemeralEventStream<D> {
.expect("value smaller than `u16` constant but doesn't fit in `u16`"); .expect("value smaller than `u16` constant but doesn't fit in `u16`");
// Fetch all of the validators' embedded elliptic curve keys // Fetch all of the validators' embedded elliptic curve keys
let mut embedded_elliptic_curve_keys = FuturesOrdered::new();
for (validator, _) in &validators {
let validator = *validator;
// try_join doesn't return a future so we need to wrap it in this additional async
// block
embedded_elliptic_curve_keys.push_back({
let serai = serai.clone();
async move {
match serai.embedded_elliptic_curve_keys(validator, set.network).await {
Ok(Some(keys)) => Ok(Some((
validator,
match keys {
EmbeddedEllipticCurveKeys::Bitcoin(substrate, external) => {
assert_eq!(set.network, ExternalNetworkId::Bitcoin);
(substrate, external.as_slice().to_vec())
}
EmbeddedEllipticCurveKeys::Ethereum(substrate, external) => {
assert_eq!(set.network, ExternalNetworkId::Ethereum);
(substrate, external.as_slice().to_vec())
}
EmbeddedEllipticCurveKeys::Monero(substrate) => {
assert_eq!(set.network, ExternalNetworkId::Monero);
(substrate, substrate.as_slice().to_vec())
}
},
))),
Ok(None) => Ok(None),
Err(e) => Err(e),
}
}
});
}
let mut evrf_public_keys = Vec::with_capacity(usize::from(total_weight)); let mut evrf_public_keys = Vec::with_capacity(usize::from(total_weight));
for (validator, weight) in &validators { for (validator, weight) in &validators {
let Some((future_validator, (substrate_embedded_key, external_embedded_key))) = let keys = match EmbeddedEllipticCurveKeys::get(&txn, set.network, *validator)
embedded_elliptic_curve_keys.next().await.unwrap().map_err(|e| format!("{e:?}"))? .expect("selected validator lacked embedded elliptic curve keys")
else { {
Err("`SetDecided` with validator missing an embedded key".to_string())? EmbeddedEllipticCurveKeysStruct::Bitcoin(substrate, external) => {
assert_eq!(set.network, ExternalNetworkId::Bitcoin);
(substrate, external.to_vec())
}
EmbeddedEllipticCurveKeysStruct::Ethereum(substrate, external) => {
assert_eq!(set.network, ExternalNetworkId::Ethereum);
(substrate, external.to_vec())
}
EmbeddedEllipticCurveKeysStruct::Monero(substrate) => {
assert_eq!(set.network, ExternalNetworkId::Monero);
(substrate, substrate.to_vec())
}
}; };
assert_eq!(*validator, future_validator);
for _ in 0 .. *weight { for _ in 0 .. *weight {
evrf_public_keys.push((substrate_embedded_key, external_embedded_key.clone())); evrf_public_keys.push(keys.clone());
} }
} }
@@ -213,6 +211,7 @@ impl<D: Db> ContinuallyRan for EphemeralEventStream<D> {
// TODO: This should be inlined into the Processor's key gen code // TODO: This should be inlined into the Processor's key gen code
// It's legacy from when we removed participants from the key gen // It's legacy from when we removed participants from the key gen
threshold: ((total_weight * 2) / 3) + 1, threshold: ((total_weight * 2) / 3) + 1,
// TODO: Why are `validators` and `evrf_public_keys` two separate fields?
validators, validators,
evrf_public_keys, evrf_public_keys,
participant_indexes: Default::default(), participant_indexes: Default::default(),

View File

@@ -36,8 +36,7 @@ impl<D: Db> PublishSlashReportTask<D> {
// This uses the latest finalized block, not the latest cosigned block, which should be // This uses the latest finalized block, not the latest cosigned block, which should be
// fine as in the worst case, the only impact is no longer attempting TX publication // fine as in the worst case, the only impact is no longer attempting TX publication
let serai = self.serai.as_of_latest_finalized_block().await.map_err(|e| format!("{e:?}"))?; let serai = self.serai.state().await.map_err(|e| format!("{e:?}"))?;
let serai = serai.validator_sets();
let session_after_slash_report = Session(session.0 + 1); let session_after_slash_report = Session(session.0 + 1);
let current_session = let current_session =
serai.current_session(network.into()).await.map_err(|e| format!("{e:?}"))?; serai.current_session(network.into()).await.map_err(|e| format!("{e:?}"))?;

View File

@@ -40,9 +40,7 @@ impl<D: Db> ContinuallyRan for SetKeysTask<D> {
// This uses the latest finalized block, not the latest cosigned block, which should be // This uses the latest finalized block, not the latest cosigned block, which should be
// fine as in the worst case, the only impact is no longer attempting TX publication // fine as in the worst case, the only impact is no longer attempting TX publication
let serai = let serai = self.serai.state().await.map_err(|e| format!("{e:?}"))?;
self.serai.as_of_latest_finalized_block().await.map_err(|e| format!("{e:?}"))?;
let serai = serai.validator_sets();
let current_session = let current_session =
serai.current_session(network.into()).await.map_err(|e| format!("{e:?}"))?; serai.current_session(network.into()).await.map_err(|e| format!("{e:?}"))?;
let current_session = current_session.map(|session| session.0); let current_session = current_session.map(|session| session.0);

View File

@@ -6,7 +6,7 @@ license = "MIT"
repository = "https://github.com/serai-dex/serai/tree/develop/coordinator/tendermint" repository = "https://github.com/serai-dex/serai/tree/develop/coordinator/tendermint"
authors = ["Luke Parker <lukeparker5132@gmail.com>"] authors = ["Luke Parker <lukeparker5132@gmail.com>"]
edition = "2021" edition = "2021"
rust-version = "1.75" rust-version = "1.77"
[package.metadata.docs.rs] [package.metadata.docs.rs]
all-features = true all-features = true

View File

@@ -575,14 +575,9 @@ impl<TD: Db, TDT: DbTxn, P: P2p> ScanBlock<'_, TD, TDT, P> {
}; };
let msgs = ( let msgs = (
decode_signed_message::<TendermintNetwork<TD, Transaction, P>>(&data.0).unwrap(), decode_signed_message::<TendermintNetwork<TD, Transaction, P>>(&data.0).unwrap(),
if data.1.is_some() { data.1.as_ref().map(|data| {
Some( decode_signed_message::<TendermintNetwork<TD, Transaction, P>>(data).unwrap()
decode_signed_message::<TendermintNetwork<TD, Transaction, P>>(&data.1.unwrap()) }),
.unwrap(),
)
} else {
None
},
); );
// Since anything with evidence is fundamentally faulty behavior, not just temporal // Since anything with evidence is fundamentally faulty behavior, not just temporal

View File

@@ -7,8 +7,7 @@ db-urls = ["https://github.com/rustsec/advisory-db"]
yanked = "deny" yanked = "deny"
ignore = [ ignore = [
"RUSTSEC-2022-0061", # https://github.com/serai-dex/serai/227 "RUSTSEC-2024-0370", # `proc-macro-error` is unmaintained, in-tree due to Substrate/`litep2p`
"RUSTSEC-2024-0370", # proc-macro-error is unmaintained
"RUSTSEC-2024-0436", # paste is unmaintained "RUSTSEC-2024-0436", # paste is unmaintained
] ]
@@ -79,7 +78,7 @@ exceptions = [
{ allow = ["AGPL-3.0-only"], name = "serai-coordinator-libp2p-p2p" }, { allow = ["AGPL-3.0-only"], name = "serai-coordinator-libp2p-p2p" },
{ allow = ["AGPL-3.0-only"], name = "serai-coordinator" }, { allow = ["AGPL-3.0-only"], name = "serai-coordinator" },
{ allow = ["AGPL-3.0-only"], name = "pallet-session" }, { allow = ["AGPL-3.0-only"], name = "substrate-median" },
{ allow = ["AGPL-3.0-only"], name = "serai-core-pallet" }, { allow = ["AGPL-3.0-only"], name = "serai-core-pallet" },
{ allow = ["AGPL-3.0-only"], name = "serai-coins-pallet" }, { allow = ["AGPL-3.0-only"], name = "serai-coins-pallet" },
@@ -151,8 +150,5 @@ allow-git = [
"https://github.com/rust-lang-nursery/lazy-static.rs", "https://github.com/rust-lang-nursery/lazy-static.rs",
"https://github.com/kayabaNerve/elliptic-curves", "https://github.com/kayabaNerve/elliptic-curves",
"https://github.com/monero-oxide/monero-oxide", "https://github.com/monero-oxide/monero-oxide",
"https://github.com/kayabaNerve/monero-oxide",
"https://github.com/rust-bitcoin/rust-bip39",
"https://github.com/rust-rocksdb/rust-rocksdb",
"https://github.com/serai-dex/patch-polkadot-sdk", "https://github.com/serai-dex/patch-polkadot-sdk",
] ]

View File

@@ -1 +1 @@
3.3.4 3.3.10

View File

@@ -1,4 +1,4 @@
source 'https://rubygems.org' source 'https://rubygems.org'
gem "jekyll", "~> 4.3.3" gem "jekyll", "~> 4.4"
gem "just-the-docs", "0.8.2" gem "just-the-docs", "0.10.1"

View File

@@ -1,34 +1,39 @@
GEM GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
addressable (2.8.7) addressable (2.8.8)
public_suffix (>= 2.0.2, < 7.0) public_suffix (>= 2.0.2, < 8.0)
bigdecimal (3.1.8) base64 (0.3.0)
bigdecimal (3.3.1)
colorator (1.1.0) colorator (1.1.0)
concurrent-ruby (1.3.4) concurrent-ruby (1.3.5)
csv (3.3.5)
em-websocket (0.5.3) em-websocket (0.5.3)
eventmachine (>= 0.12.9) eventmachine (>= 0.12.9)
http_parser.rb (~> 0) http_parser.rb (~> 0)
eventmachine (1.2.7) eventmachine (1.2.7)
ffi (1.17.0-x86_64-linux-gnu) ffi (1.17.2-x86_64-linux-gnu)
forwardable-extended (2.6.0) forwardable-extended (2.6.0)
google-protobuf (4.28.2-x86_64-linux) google-protobuf (4.33.1-x86_64-linux-gnu)
bigdecimal bigdecimal
rake (>= 13) rake (>= 13)
http_parser.rb (0.8.0) http_parser.rb (0.8.0)
i18n (1.14.6) i18n (1.14.7)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
jekyll (4.3.4) jekyll (4.4.1)
addressable (~> 2.4) addressable (~> 2.4)
base64 (~> 0.2)
colorator (~> 1.0) colorator (~> 1.0)
csv (~> 3.0)
em-websocket (~> 0.5) em-websocket (~> 0.5)
i18n (~> 1.0) i18n (~> 1.0)
jekyll-sass-converter (>= 2.0, < 4.0) jekyll-sass-converter (>= 2.0, < 4.0)
jekyll-watch (~> 2.0) jekyll-watch (~> 2.0)
json (~> 2.6)
kramdown (~> 2.3, >= 2.3.1) kramdown (~> 2.3, >= 2.3.1)
kramdown-parser-gfm (~> 1.0) kramdown-parser-gfm (~> 1.0)
liquid (~> 4.0) liquid (~> 4.0)
mercenary (>= 0.3.6, < 0.5) mercenary (~> 0.3, >= 0.3.6)
pathutil (~> 0.9) pathutil (~> 0.9)
rouge (>= 3.0, < 5.0) rouge (>= 3.0, < 5.0)
safe_yaml (~> 1.0) safe_yaml (~> 1.0)
@@ -36,19 +41,20 @@ GEM
webrick (~> 1.7) webrick (~> 1.7)
jekyll-include-cache (0.2.1) jekyll-include-cache (0.2.1)
jekyll (>= 3.7, < 5.0) jekyll (>= 3.7, < 5.0)
jekyll-sass-converter (3.0.0) jekyll-sass-converter (3.1.0)
sass-embedded (~> 1.54) sass-embedded (~> 1.75)
jekyll-seo-tag (2.8.0) jekyll-seo-tag (2.8.0)
jekyll (>= 3.8, < 5.0) jekyll (>= 3.8, < 5.0)
jekyll-watch (2.2.1) jekyll-watch (2.2.1)
listen (~> 3.0) listen (~> 3.0)
just-the-docs (0.8.2) json (2.16.0)
just-the-docs (0.10.1)
jekyll (>= 3.8.5) jekyll (>= 3.8.5)
jekyll-include-cache jekyll-include-cache
jekyll-seo-tag (>= 2.0) jekyll-seo-tag (>= 2.0)
rake (>= 12.3.1) rake (>= 12.3.1)
kramdown (2.4.0) kramdown (2.5.1)
rexml rexml (>= 3.3.9)
kramdown-parser-gfm (1.1.0) kramdown-parser-gfm (1.1.0)
kramdown (~> 2.0) kramdown (~> 2.0)
liquid (4.0.4) liquid (4.0.4)
@@ -58,27 +64,27 @@ GEM
mercenary (0.4.0) mercenary (0.4.0)
pathutil (0.16.2) pathutil (0.16.2)
forwardable-extended (~> 2.6) forwardable-extended (~> 2.6)
public_suffix (6.0.1) public_suffix (7.0.0)
rake (13.2.1) rake (13.3.1)
rb-fsevent (0.11.2) rb-fsevent (0.11.2)
rb-inotify (0.11.1) rb-inotify (0.11.1)
ffi (~> 1.0) ffi (~> 1.0)
rexml (3.3.7) rexml (3.4.4)
rouge (4.4.0) rouge (4.6.1)
safe_yaml (1.0.5) safe_yaml (1.0.5)
sass-embedded (1.79.3-x86_64-linux-gnu) sass-embedded (1.94.2-x86_64-linux-gnu)
google-protobuf (~> 4.27) google-protobuf (~> 4.31)
terminal-table (3.0.2) terminal-table (3.0.2)
unicode-display_width (>= 1.1.1, < 3) unicode-display_width (>= 1.1.1, < 3)
unicode-display_width (2.6.0) unicode-display_width (2.6.0)
webrick (1.8.2) webrick (1.9.2)
PLATFORMS PLATFORMS
x86_64-linux x86_64-linux
DEPENDENCIES DEPENDENCIES
jekyll (~> 4.3.3) jekyll (~> 4.4)
just-the-docs (= 0.8.2) just-the-docs (= 0.10.1)
BUNDLED WITH BUNDLED WITH
2.5.11 2.5.22

View File

@@ -219,7 +219,6 @@ async fn main() {
ExternalNetworkId::Bitcoin => "BITCOIN_KEY", ExternalNetworkId::Bitcoin => "BITCOIN_KEY",
ExternalNetworkId::Ethereum => "ETHEREUM_KEY", ExternalNetworkId::Ethereum => "ETHEREUM_KEY",
ExternalNetworkId::Monero => "MONERO_KEY", ExternalNetworkId::Monero => "MONERO_KEY",
_ => panic!("unrecognized network"),
}) else { }) else {
continue; continue;
}; };

View File

@@ -1,50 +0,0 @@
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA256
# This GPG-signed message exists to confirm the SHA256 sums of Monero binaries.
#
# Please verify the signature against the key for binaryFate in the
# source code repository (/utils/gpg_keys).
#
#
## CLI
4e1481835824b9233f204553d4a19645274824f3f6185d8a4b50198470752f54 monero-android-armv7-v0.18.4.3.tar.bz2
1aebd24aaaec3d1e87a64163f2e30ab2cd45f3902a7a859413f6870944775c21 monero-android-armv8-v0.18.4.3.tar.bz2
ff7b9c5cf2cb3d602c3dff1902ac0bc3394768cefc260b6003a9ad4bcfb7c6a4 monero-freebsd-x64-v0.18.4.3.tar.bz2
3ac83049bc565fb5238501f0fa629cdd473bbe94d5fb815088af8e6ff1d761cd monero-linux-armv7-v0.18.4.3.tar.bz2
b1cc5f135de3ba8512d56deb4b536b38c41addde922b2a53bf443aeaf2a5a800 monero-linux-armv8-v0.18.4.3.tar.bz2
95baaa6e8957b92caeaed7fb19b5c2659373df8dd5f4de2601ed3dae7b17ce2f monero-linux-riscv64-v0.18.4.3.tar.bz2
3a7b36ae4da831a4e9913e0a891728f4c43cd320f9b136cdb6686b1d0a33fafa monero-linux-x64-v0.18.4.3.tar.bz2
e0b51ca71934c33cb83cfa8535ffffebf431a2fc9efe3acf2baad96fb6ce21ec monero-linux-x86-v0.18.4.3.tar.bz2
bab9a6d3c2ca519386cff5ff0b5601642a495ed1a209736acaf354468cba1145 monero-mac-armv8-v0.18.4.3.tar.bz2
a8d8273b14f31569f5b7aa3063fbd322e3caec3d63f9f51e287dfc539c7f7d61 monero-mac-x64-v0.18.4.3.tar.bz2
bd9f615657c35d2d7dd9a5168ad54f1547dbf9a335dee7f12fab115f6f394e36 monero-win-x64-v0.18.4.3.zip
e642ed7bbfa34c30b185387fa553aa9c3ea608db1f3fc0e9332afa9b522c9c1a monero-win-x86-v0.18.4.3.zip
6ba5e082c8fa25216aba7aea8198f3e23d4b138df15c512457081e1eb3d03ff6 monero-source-v0.18.4.3.tar.bz2
#
## GUI
7b9255c696a462a00a810d9c8f94e60400a9e7d6438e8d6a8b693e9c13dca9ab monero-gui-install-win-x64-v0.18.4.3.exe
0bd84de0a7c18b2a3ea8e8eff2194ae000cf1060045badfd4ab48674bc1b9325 monero-gui-linux-x64-v0.18.4.3.tar.bz2
68ea30db32efb4a0671ec723297b6629d932fa188edf76edb38a37adaa3528e6 monero-gui-mac-armv8-v0.18.4.3.dmg
27243b01f030fdae68c59cae1daf21f530bbadeaf10579d2908db9a834191cee monero-gui-mac-x64-v0.18.4.3.dmg
dc9531cb4319b37b2c2dea4126e44a0fe6e7b6f34d278ccf5dd9ba693e3031e0 monero-gui-win-x64-v0.18.4.3.zip
0d44687644db9b1824f324416e98f4a46b3bb0a5ed09af54b2835b6facaa0cdd monero-gui-source-v0.18.4.3.tar.bz2
#
#
# ~binaryFate
-----BEGIN PGP SIGNATURE-----
iQIzBAEBCAAdFiEEgaxZH+nEtlxYBq/D8K9NRioL35IFAmjntxwACgkQ8K9NRioL
35J57g//dUOY1KAoLNaV7XLJyGbNk1lT6c2+A8h1wkK6iNQhXsmnc6rcigsHXrG0
LQyVUuZJ6ELhNb6BnH5V0zbcB72t8XjkSEqlYhStUfMnaUvj1VdXtL/OnSs3fEvt
Zwz6QBTxIKDDYEYyXrvCK96cYaYlIOgK3IVn/zoHdrHRUqTXqRJkFoTHum5l783y
vr9BwMFFWYePUrilphjIiLyJDl+eB5al8PaJkqK2whxBUHoA2jF1edJOSq2mZajI
+L2fBYClePS8oqwoKGquqCh2RVcmdtXtQTVzRIoNx14qFzP8ymqa+6z1Ygkri7bV
qMCJk7KQ8ND7uU9NShpaCIqrZpr5GZ4Al6SRkcpK/7mipQcy2QpKJR3iOpcfiTX1
YmYGVmLB3zmHu2kiS0kogZv6Ob7+tVFzOQ8NZX4FVnpB0N0phqMfNFOfHzdQZrsZ
qg29HNc9sHlUmsOVmE5w+7Oq+s79yvQB3034XXi/9wQu+f8fKRhqZboe0fe77FLf
QXoAYrZZ7LnGz0Z75Q9O4RB7uxM0Ug5imvyEFus4iuBVyBWjgcfyLnbkKJtbXmfn
BZBbTProhPJfVa/VffBxW9HZB27W7O14oGWVpUkGWnVMZfVY/78XTUHwxaScQsPO
SGawjobQsB3pTMNr/kra1XTjkti70si8Fcs5ueYWGB3yfc6r3hU=
=5HRY
-----END PGP SIGNATURE-----

View File

@@ -0,0 +1,50 @@
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA256
# This GPG-signed message exists to confirm the SHA256 sums of Monero binaries.
#
# Please verify the signature against the key for binaryFate in the
# source code repository (/utils/gpg_keys).
#
#
## CLI
7c2ad18ca3a1ad5bc603630ca935a753537a38a803e98d645edd6a3b94a5f036 monero-android-armv7-v0.18.4.4.tar.bz2
eb81b71f029884ab5fec76597be583982c95fd7dc3fc5f5083a422669cee311e monero-android-armv8-v0.18.4.4.tar.bz2
bc539178df23d1ae8b69569d9c328b5438ae585c0aacbebe12d8e7d387a745b0 monero-freebsd-x64-v0.18.4.4.tar.bz2
2040dc22748ef39ed8a755324d2515261b65315c67b91f449fa1617c5978910b monero-linux-armv7-v0.18.4.4.tar.bz2
b9daede195a24bdd05bba68cb5cb21e42c2e18b82d4d134850408078a44231c5 monero-linux-armv8-v0.18.4.4.tar.bz2
c939ea6e8002798f24a56ac03cbfc4ff586f70d7d9c3321b7794b3bcd1fa4c45 monero-linux-riscv64-v0.18.4.4.tar.bz2
7fe45ee9aade429ccdcfcad93b905ba45da5d3b46d2dc8c6d5afc48bd9e7f108 monero-linux-x64-v0.18.4.4.tar.bz2
8c174b756e104534f3d3a69fe68af66d6dc4d66afa97dfe31735f8d069d20570 monero-linux-x86-v0.18.4.4.tar.bz2
645e9bbae0275f555b2d72a9aa30d5f382df787ca9528d531521750ce2da9768 monero-mac-armv8-v0.18.4.4.tar.bz2
af3d98f09da94632db3e2f53c62cc612e70bf94aa5942d2a5200b4393cd9c842 monero-mac-x64-v0.18.4.4.tar.bz2
7eb3b87a105b3711361dd2b3e492ad14219d21ed8fd3dd726573a6cbd96e83a6 monero-win-x64-v0.18.4.4.zip
a148a2bd2b14183fb36e2cf917fce6f33fb687564db2ed53193b8432097ab398 monero-win-x86-v0.18.4.4.zip
84570eee26238d8f686605b5e31d59569488a3406f32e7045852de91f35508a2 monero-source-v0.18.4.4.tar.bz2
#
## GUI
4c81c8e97bd542daa453776d888557db1ceb2a718d43f6135ad68b12c8119948 monero-gui-install-win-x64-v0.18.4.4.exe
e45cb3fa9d972d67628cfed6463fb7604ae1414a11ba449f5e2f901c769ac788 monero-gui-linux-x64-v0.18.4.4.tar.bz2
a6f071719c401df339dba2d43ec6fffe103fda3e1df46f354b2496f34bb61cc4 monero-gui-mac-armv8-v0.18.4.4.dmg
811df70811a25f31289f24ebc0edc8f7648670384698d4c768bac5c2acbf2026 monero-gui-mac-x64-v0.18.4.4.dmg
b96faa56aa77cabed1f31f3fc9496e756a8da8c1124da2b9cb0b3730a8b6fbd9 monero-gui-win-x64-v0.18.4.4.zip
a7f6b91bc9efaa83173a397614626bf7612123e0017a48f66137ac397f7d19f8 monero-gui-source-v0.18.4.4.tar.bz2
#
#
# ~binaryFate
-----BEGIN PGP SIGNATURE-----
iQIzBAEBCAAdFiEEgaxZH+nEtlxYBq/D8K9NRioL35IFAmkbGLgACgkQ8K9NRioL
35LWYRAAnPeUu7TADV9Nly2gBlwu7bMK6l7pcUzs3hHhCMpg/Zb7wF8lx4D/r/hT
3wf3gNVK6tYl5GMPpF7GSKvK35SSzNN+8khRd7vhRByG75LGLnrNlcBsQU2wOzUv
Rmm2R8L8GP0B/+zXO92uJDMZ7Q7x72O+3fVX05217HBwz2kvzE1NpXe+EJPnUukA
Tr5CRnxKhxPbilvIhoEHdwkScMZqHMfsbdrefrB3KpO3xEaUz+gO9wESp7nzr4vp
Du6gJYBPK25Z2heZHCRsGN4WQP4QQv4MC0IFczc9fkVDBjywsJeNRRUbGtxR/BNt
vNJGI/kS+7KV140j6GkqAh/leZcaVJ5LRyCaHAwEQNA2T5okhrM0WZpoOAsZMi5K
bW4lNOXfWSw6/tokEPeuoi49yw0f9z0C8a4VLNOZGWKqmHcsA8WE6oVfmvVk6xWu
BqTU1Z9LJqL17GWRAReSX1ZuNA0Q0Pb/klUwP4X2afJcCVZ2YeBNr4jr21u3dYXY
QiLj0Gv7gg7a/GiMpVglNn5GzCu6mT0D94sbMNK+U5Tbve7aOtijJZ8JR62eO/mR
h+oNEys/xEcP9PQ5p74cNL71hNSfWSOcNi+GLSgXC75vsOGr7i96uaamilsHnsYB
p8PZMHzOf1pi6i/L5oOEuRgaujd9IjyCbxoYh3bbxxjBOhNEMqU=
=CVLA
-----END PGP SIGNATURE-----

View File

@@ -0,0 +1,166 @@
# Raises `PT_GNU_STACK`'s memory to be at least 8 MB.
#
# This causes `musl` to use a 8 MB default for new threads, resolving the primary
# compatibility issue faced when executing a program on a `musl` system.
#
# See https://wiki.musl-libc.org/functional-differences-from-glibc.html#Thread-stack-size
# for reference. This differs that instead of setting at time of link, it
# patches the binary as an already-linked ELF executable.
#!/bin/bash
set -eo pipefail
ELF="$1"
if [ ! -f "$ELF" ]; then
echo "\`increase_default_stack_size.sh\` [ELF binary]"
echo ""
echo "Sets the \`PT_GNU_STACK\` program header to its existing value or 8 MB,"
echo "whichever is greater."
exit 1
fi
function hex {
hexdump -e '1 1 "%.2x"' -v
}
function read_bytes {
dd status=none bs=1 skip=$1 count=$2 if="$ELF" | hex
}
function write_bytes {
POS=$1
BYTES=$2
while [ ! $BYTES = "" ]; do
printf "\x$(printf $BYTES | head -c2)" | dd status=none conv=notrunc bs=1 seek=$POS of="$ELF"
# Start with the third byte, as in, after the first two bytes
BYTES=$(printf $BYTES | tail -c+3)
POS=$(($POS + 1))
done
}
# Magic
MAGIC=$(read_bytes 0 4)
if [ ! $MAGIC = $(printf "\x7fELF" | hex) ]; then
echo "Not ELF"
exit 2
fi
# 1 if 32-bit, 2 if 64-bit
BITS=$(read_bytes 4 1)
case $BITS in
"01") BITS=32;;
"02") BITS=64;;
*)
echo "Not 32- or 64- bit"
exit 3
;;
esac
# For `value_per_bits a b`, `a` if 32-bit and `b` if 64-bit
function value_per_bits {
RESULT=$(($1))
if [ $BITS = 64 ]; then
RESULT=$(($2))
fi
printf $RESULT
}
# Read an integer by its offset, differing depending on if 32- or 64-bit
function read_integer_by_offset {
OFFSET=$(value_per_bits $1 $2)
printf $(( 0x$(swap_native_endian $(read_bytes $OFFSET $3)) ))
}
# 1 if little-endian, 2 if big-endian
LITTLE_ENDIAN=$(read_bytes 5 1)
case $LITTLE_ENDIAN in
"01") LITTLE_ENDIAN=1;;
"02") LITTLE_ENDIAN=0;;
*)
echo "Not little- or big- endian"
exit 4
;;
esac
# While this script is written in big-endian, we need to work with the file in
# its declared endian. This function swaps from big to native, or vice versa,
# as necessary.
function swap_native_endian {
BYTES="$1"
if [ "$BYTES" = "" ]; then
read BYTES
fi
if [ $LITTLE_ENDIAN -eq 0 ]; then
printf $BYTES
return
fi
while [ ! $BYTES = "" ]; do
printf $(printf $BYTES | tail -c2)
BYTES=$(printf $BYTES | head -c-2)
done
}
ELF_VERSION=$(read_bytes 6 1)
if [ ! $ELF_VERSION = "01" ]; then
echo "Unknown ELF Version ($ELF_VERSION)"
exit 5
fi
ELF_VERSION_2=$(read_bytes $((0x14)) 4)
if [ ! $ELF_VERSION_2 = $(swap_native_endian 00000001) ]; then
echo "Unknown secondary ELF Version ($ELF_VERSION_2)"
exit 6
fi
# Find where the program headers are
PROGRAM_HEADERS_OFFSET=$(read_integer_by_offset 0x1c 0x20 $(value_per_bits 4 8))
PROGRAM_HEADER_SIZE=$(value_per_bits 0x20 0x38)
DECLARED_PROGRAM_HEADER_SIZE=$(read_integer_by_offset 0x2a 0x36 2)
if [ ! $PROGRAM_HEADER_SIZE -eq $DECLARED_PROGRAM_HEADER_SIZE ]; then
echo "Unexpected size of a program header ($DECLARED_PROGRAM_HEADER_SIZE)"
exit 7
fi
function program_header_start {
printf $(($PROGRAM_HEADERS_OFFSET + ($1 * $PROGRAM_HEADER_SIZE)))
}
function read_program_header {
read_bytes $(program_header_start $1) $PROGRAM_HEADER_SIZE
}
# Iterate over each program header
PROGRAM_HEADERS=$(read_integer_by_offset 0x2c 0x38 2)
NEXT_PROGRAM_HEADER=$(( $PROGRAM_HEADERS - 1 ))
FOUND=0
while [ $NEXT_PROGRAM_HEADER -ne -1 ]; do
THIS_PROGRAM_HEADER=$NEXT_PROGRAM_HEADER
NEXT_PROGRAM_HEADER=$(( $NEXT_PROGRAM_HEADER - 1 ))
PROGRAM_HEADER=$(read_program_header $THIS_PROGRAM_HEADER)
HEADER_TYPE=$(printf $PROGRAM_HEADER | head -c8)
# `PT_GNU_STACK`
# https://github.com/torvalds/linux/blob/c2f2b01b74be8b40a2173372bcd770723f87e7b2/include/uapi/linux/elf.h#L41
if [ ! "$(swap_native_endian $HEADER_TYPE)" = "6474e551" ]; then
continue
fi
FOUND=1
MEMSZ_OFFSET=$(( $(program_header_start $THIS_PROGRAM_HEADER) + $(value_per_bits 0x14 0x28) ))
MEMSZ_LEN=$(value_per_bits 4 8)
# `MEMSZ_OFFSET MEMSZ_OFFSET` as we've already derived it depending on the amount of bits
MEMSZ=$(read_integer_by_offset $MEMSZ_OFFSET $MEMSZ_OFFSET $MEMSZ_LEN)
DESIRED_STACK_SIZE=$((8 * 1024 * 1024))
# Only run if the inherent value is _smaller_
if [ $MEMSZ -lt $DESIRED_STACK_SIZE ]; then
# `2 *`, as this is its length in hexadecimal
HEX_MEMSZ=$(printf %."$((2 * $MEMSZ_LEN))"x $DESIRED_STACK_SIZE)
write_bytes $MEMSZ_OFFSET $(swap_native_endian $HEX_MEMSZ)
fi
done
if [ $FOUND -eq 0 ]; then
echo "\`PT_GNU_STACK\` program header not found"
exit 8
fi
echo "All instances of \`PT_GNU_STACK\` patched to be at least 8 MB"
exit 0

View File

@@ -1,13 +1,12 @@
# rust:1.91.1-alpine as of November 11th, 2025 (GMT) #check=skip=FromPlatformFlagConstDisallowed
FROM --platform=linux/amd64 rust@sha256:700c0959b23445f69c82676b72caa97ca4359decd075dca55b13339df27dc4d3 AS deterministic # We want to explicitly set the platform to ensure a constant host environment
RUN apk add musl-dev=1.2.5-r10 # rust:1.91.1-alpine as of December 4th, 2025 (GMT)
FROM --platform=linux/amd64 rust@sha256:84f263251b0ada72c1913d82a824d47be15a607f3faf015d8bdae48db544cdf2 AS builder
# Add the wasm toolchain # Add the WASM toolchain
RUN rustup target add wasm32v1-none RUN rustup target add wasm32v1-none
FROM deterministic
# Add files for build # Add files for build
ADD patches /serai/patches ADD patches /serai/patches
ADD common /serai/common ADD common /serai/common
@@ -27,8 +26,17 @@ ADD AGPL-3.0 /serai
WORKDIR /serai WORKDIR /serai
# Build the runtime, copying it to the volume if it exists # Build the runtime
ENV RUSTFLAGS="-Ctarget-feature=-crt-static" RUN cargo build --release -p serai-runtime --no-default-features
CMD cargo build --release -p serai-runtime && \
mkdir -p /volume && \ # Copy the artifact to its own image which solely exists to further export it
cp /serai/target/release/wbuild/serai-runtime/serai_runtime.wasm /volume/serai.wasm FROM scratch
# Copy `busybox`, including the necessary shared libraries, from the builder for a functioning `cp`
COPY --from=builder /lib/ld-musl-x86_64.so.1 /lib/libc.musl-x86_64.so.1 /lib/
COPY --from=builder /bin/busybox /bin/
ENV LD_LIBRARY_PATH=/lib/
ENV PATH=/bin
# Copy the artifact itself
COPY --from=builder /serai/target/release/serai_runtime.wasm /serai.wasm
# By default, copy the artifact to `/volume`, presumably a provided volume
CMD ["busybox", "cp", "/serai.wasm", "/volume/serai.wasm"]

View File

@@ -16,7 +16,7 @@ pub fn coordinator(
) { ) {
let db = network.db(); let db = network.db();
let longer_reattempts = if network == Network::Dev { "longer-reattempts" } else { "" }; let longer_reattempts = if network == Network::Dev { "longer-reattempts" } else { "" };
let setup = mimalloc(Os::Debian).to_string() + let setup = mimalloc(Os::Debian) +
&build_serai_service( &build_serai_service(
"", "",
Os::Debian, Os::Debian,

View File

@@ -3,7 +3,7 @@ use std::path::Path;
use crate::{Network, Os, mimalloc, os, build_serai_service, write_dockerfile}; use crate::{Network, Os, mimalloc, os, build_serai_service, write_dockerfile};
pub fn ethereum_relayer(orchestration_path: &Path, network: Network) { pub fn ethereum_relayer(orchestration_path: &Path, network: Network) {
let setup = mimalloc(Os::Debian).to_string() + let setup = mimalloc(Os::Debian) +
&build_serai_service( &build_serai_service(
"", "",
Os::Debian, Os::Debian,

View File

@@ -13,7 +13,7 @@ pub fn message_queue(
ethereum_key: <Ristretto as WrappedGroup>::G, ethereum_key: <Ristretto as WrappedGroup>::G,
monero_key: <Ristretto as WrappedGroup>::G, monero_key: <Ristretto as WrappedGroup>::G,
) { ) {
let setup = mimalloc(Os::Alpine).to_string() + let setup = mimalloc(Os::Alpine) +
&build_serai_service("", Os::Alpine, network.release(), network.db(), "serai-message-queue"); &build_serai_service("", Os::Alpine, network.release(), network.db(), "serai-message-queue");
let env_vars = [ let env_vars = [

View File

@@ -1,36 +1,85 @@
use crate::Os; use crate::Os;
pub fn mimalloc(os: Os) -> &'static str { // 2.2.4
const ALPINE_MIMALLOC: &str = r#" const MIMALLOC_VERSION: &str = "fbd8b99c2b828428947d70fdc046bb55609be93e";
const FLAGS: &str =
"-DMI_SECURE=ON -DMI_GUARDED=ON -DMI_BUILD_STATIC=OFF -DMI_BUILD_OBJECT=OFF -DMI_BUILD_TESTS=OFF";
pub fn mimalloc(os: Os) -> String {
let build_script = |env, flags| {
format!(
r#"
#!/bin/sh
set -e
git clone https://github.com/microsoft/mimalloc
cd mimalloc
git checkout {MIMALLOC_VERSION}
# For some reason, `mimalloc` contains binary blobs in the repository, so we remove those now
rm -rf .git ./bin
mkdir -p out/secure
cd out/secure
# `CMakeLists.txt` requires a C++ compiler but `mimalloc` does not use one by default. We claim
# there is a working C++ compiler so CMake doesn't complain, allowing us to not unnecessarily
# install one. If it was ever invoked, our choice of `false` would immediately let us know.
# https://github.com/microsoft/mimalloc/issues/1179
{env} CXX=false cmake -DCMAKE_CXX_COMPILER_WORKS=1 {FLAGS} ../..
make
cd ../..
# Copy the built library to the original directory
cd ..
cp mimalloc/out/secure/libmimalloc-secure.so ./libmimalloc.so
# Clean up the source directory
rm -rf ./mimalloc
"#
)
};
let build_commands = |env, flags| {
let mut result = String::new();
for line in build_script(env, flags)
.lines()
.map(|line| {
assert!(!line.contains('"'));
format!(r#"RUN echo "{line}" >> ./mimalloc.sh"#)
})
.chain(["RUN /bin/sh ./mimalloc.sh", "RUN rm ./mimalloc.sh"].into_iter().map(str::to_string))
{
result.push_str(&line);
result.push('\n');
}
result
};
let alpine_build = build_commands("CC=$(uname -m)-alpine-linux-musl-gcc", "-DMI_LIBC_MUSL=ON");
let debian_build = build_commands("", "");
let alpine_mimalloc = format!(
r#"
FROM alpine:latest AS mimalloc-alpine FROM alpine:latest AS mimalloc-alpine
RUN apk update && apk upgrade && apk --no-cache add gcc g++ libc-dev make cmake git RUN apk update && apk upgrade && apk --no-cache add musl-dev gcc make cmake git
RUN git clone https://github.com/microsoft/mimalloc && \
cd mimalloc && \
git checkout fbd8b99c2b828428947d70fdc046bb55609be93e && \
mkdir -p out/secure && \
cd out/secure && \
cmake -DMI_SECURE=ON -DMI_GUARDED=on ../.. && \
make && \
cp ./libmimalloc-secure.so ../../../libmimalloc.so
"#;
const DEBIAN_MIMALLOC: &str = r#" {alpine_build}
"#
);
let debian_mimalloc = format!(
r#"
FROM debian:trixie-slim AS mimalloc-debian FROM debian:trixie-slim AS mimalloc-debian
RUN apt update && apt upgrade -y && apt install -y gcc g++ make cmake git RUN apt update && apt upgrade -y && apt install -y gcc make cmake git
RUN git clone https://github.com/microsoft/mimalloc && \
cd mimalloc && \ {debian_build}
git checkout fbd8b99c2b828428947d70fdc046bb55609be93e && \ "#
mkdir -p out/secure && \ );
cd out/secure && \
cmake -DMI_SECURE=ON -DMI_GUARDED=on ../.. && \
make && \
cp ./libmimalloc-secure.so ../../../libmimalloc.so
"#;
match os { match os {
Os::Alpine => ALPINE_MIMALLOC, Os::Alpine => alpine_mimalloc,
Os::Debian => DEBIAN_MIMALLOC, Os::Debian => debian_mimalloc,
} }
} }

View File

@@ -29,7 +29,7 @@ RUN tar xzvf bitcoin-${BITCOIN_VERSION}-$(uname -m)-linux-gnu.tar.gz
RUN mv bitcoin-${BITCOIN_VERSION}/bin/bitcoind . RUN mv bitcoin-${BITCOIN_VERSION}/bin/bitcoind .
"#; "#;
let setup = mimalloc(Os::Debian).to_string() + DOWNLOAD_BITCOIN; let setup = mimalloc(Os::Alpine) + DOWNLOAD_BITCOIN;
let run_bitcoin = format!( let run_bitcoin = format!(
r#" r#"
@@ -43,7 +43,7 @@ CMD ["/run.sh"]
network.label() network.label()
); );
let run = os(Os::Debian, "", "bitcoin") + &run_bitcoin; let run = os(Os::Alpine, "", "bitcoin") + &run_bitcoin;
let res = setup + &run; let res = setup + &run;
let mut bitcoin_path = orchestration_path.to_path_buf(); let mut bitcoin_path = orchestration_path.to_path_buf();

View File

@@ -17,7 +17,7 @@ pub fn ethereum(orchestration_path: &Path, network: Network) {
(reth(network), nimbus(network)) (reth(network), nimbus(network))
}; };
let download = mimalloc(Os::Alpine).to_string() + &el_download + &cl_download; let download = mimalloc(Os::Alpine) + &el_download + &cl_download;
let run = format!( let run = format!(
r#" r#"
@@ -26,7 +26,7 @@ CMD ["/run.sh"]
"#, "#,
network.label() network.label()
); );
let run = mimalloc(Os::Debian).to_string() + let run = mimalloc(Os::Debian) +
&os(Os::Debian, &(el_run_as_root + "\r\n" + &cl_run_as_root), "ethereum") + &os(Os::Debian, &(el_run_as_root + "\r\n" + &cl_run_as_root), "ethereum") +
&el_run + &el_run +
&cl_run + &cl_run +

View File

@@ -10,7 +10,7 @@ fn monero_internal(
monero_binary: &str, monero_binary: &str,
ports: &str, ports: &str,
) { ) {
const MONERO_VERSION: &str = "0.18.4.3"; const MONERO_VERSION: &str = "0.18.4.4";
let arch = match std::env::consts::ARCH { let arch = match std::env::consts::ARCH {
// We probably would run this without issues yet it's not worth needing to provide support for // We probably would run this without issues yet it's not worth needing to provide support for
@@ -21,7 +21,7 @@ fn monero_internal(
}; };
#[rustfmt::skip] #[rustfmt::skip]
let download_monero = format!(r#" let mut download_monero = format!(r#"
FROM alpine:latest AS monero FROM alpine:latest AS monero
RUN apk --no-cache add wget gnupg RUN apk --no-cache add wget gnupg
@@ -41,7 +41,17 @@ RUN tar -xvjf monero-linux-{arch}-v{MONERO_VERSION}.tar.bz2 --strip-components=1
network.label(), network.label(),
); );
let setup = mimalloc(os).to_string() + &download_monero; if os == Os::Alpine {
// Increase the default stack size, as Monero does heavily use its stack
download_monero += &format!(
r#"
ADD orchestration/increase_default_stack_size.sh .
RUN ./increase_default_stack_size.sh {monero_binary}
"#
);
}
let setup = mimalloc(os) + &download_monero;
let run_monero = format!( let run_monero = format!(
r#" r#"
@@ -69,13 +79,13 @@ CMD ["/run.sh"]
} }
pub fn monero(orchestration_path: &Path, network: Network) { pub fn monero(orchestration_path: &Path, network: Network) {
monero_internal(network, Os::Debian, orchestration_path, "monero", "monerod", "18080 18081") monero_internal(network, Os::Alpine, orchestration_path, "monero", "monerod", "18080 18081")
} }
pub fn monero_wallet_rpc(orchestration_path: &Path) { pub fn monero_wallet_rpc(orchestration_path: &Path) {
monero_internal( monero_internal(
Network::Dev, Network::Dev,
Os::Debian, Os::Alpine,
orchestration_path, orchestration_path,
"monero-wallet-rpc", "monero-wallet-rpc",
"monero-wallet-rpc", "monero-wallet-rpc",

View File

@@ -17,7 +17,7 @@ pub fn processor(
substrate_evrf_key: Zeroizing<Vec<u8>>, substrate_evrf_key: Zeroizing<Vec<u8>>,
network_evrf_key: Zeroizing<Vec<u8>>, network_evrf_key: Zeroizing<Vec<u8>>,
) { ) {
let setup = mimalloc(Os::Debian).to_string() + let setup = mimalloc(Os::Debian) +
&build_serai_service( &build_serai_service(
if coin == "ethereum" { if coin == "ethereum" {
r#" r#"

View File

@@ -12,8 +12,7 @@ pub fn serai(
serai_key: &Zeroizing<<Ristretto as WrappedGroup>::F>, serai_key: &Zeroizing<<Ristretto as WrappedGroup>::F>,
) { ) {
// Always builds in release for performance reasons // Always builds in release for performance reasons
let setup = let setup = mimalloc(Os::Debian) + &build_serai_service("", Os::Debian, true, "", "serai-node");
mimalloc(Os::Debian).to_string() + &build_serai_service("", Os::Debian, true, "", "serai-node");
let env_vars = [("KEY", hex::encode(serai_key.to_repr()))]; let env_vars = [("KEY", hex::encode(serai_key.to_repr()))];
let mut env_vars_str = String::new(); let mut env_vars_str = String::new();

View File

@@ -7,7 +7,6 @@ repository = "https://github.com/serai-dex/serai/tree/develop/crypto/dalek-ff-gr
authors = ["Luke Parker <lukeparker5132@gmail.com>"] authors = ["Luke Parker <lukeparker5132@gmail.com>"]
keywords = ["curve25519", "ed25519", "ristretto", "dalek", "group"] keywords = ["curve25519", "ed25519", "ristretto", "dalek", "group"]
edition = "2021" edition = "2021"
rust-version = "1.85"
[package.metadata.docs.rs] [package.metadata.docs.rs]
all-features = true all-features = true

View File

@@ -0,0 +1,19 @@
[package]
name = "alloy-eip2124"
version = "0.2.99"
description = "Patch to an empty crate"
license = "MIT"
repository = "https://github.com/serai-dex/serai/tree/develop/patches/ethereum/alloy-eip2124"
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
keywords = []
edition = "2021"
[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
[workspace]
[features]
std = []
serde = []

View File

@@ -0,0 +1,19 @@
[package]
name = "fastrlp"
version = "0.3.99"
description = "Patch to an empty crate"
license = "MIT"
repository = "https://github.com/serai-dex/serai/tree/develop/patches/ethereum/fastrlp-0.3"
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
keywords = []
edition = "2021"
[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
[workspace]
[features]
alloc = []
std = []

View File

@@ -0,0 +1,19 @@
[package]
name = "fastrlp"
version = "0.4.99"
description = "Patch to an empty crate"
license = "MIT"
repository = "https://github.com/serai-dex/serai/tree/develop/patches/ethereum/fastrlp-0.4"
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
keywords = []
edition = "2021"
[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
[workspace]
[features]
alloc = []
std = []

View File

@@ -0,0 +1 @@
const _NEVER_COMPILED: [(); 0 - 1] = [(); 0 - 1];

View File

@@ -0,0 +1,18 @@
[package]
name = "primitive-types"
version = "0.12.99"
description = "Patch to an empty crate"
license = "MIT"
repository = "https://github.com/serai-dex/serai/tree/develop/patches/ethereum/primitive-types"
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
keywords = []
edition = "2021"
[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
[workspace]
[features]
std = []

View File

@@ -0,0 +1 @@
const _NEVER_COMPILED: [(); 0 - 1] = [(); 0 - 1];

View File

@@ -0,0 +1,18 @@
[package]
name = "rlp"
version = "0.5.99"
description = "Patch to an empty crate"
license = "MIT"
repository = "https://github.com/serai-dex/serai/tree/develop/patches/ethereum/rlp"
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
keywords = []
edition = "2021"
[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
[workspace]
[features]
std = []

View File

@@ -0,0 +1 @@
const _NEVER_COMPILED: [(); 0 - 1] = [(); 0 - 1];

View File

@@ -3,7 +3,7 @@ name = "secp256k1"
version = "0.30.99" version = "0.30.99"
description = "Patch to an empty crate" description = "Patch to an empty crate"
license = "MIT" license = "MIT"
repository = "https://github.com/serai-dex/serai/tree/develop/patches/ethereum/secp256k1-30" repository = "https://github.com/serai-dex/serai/tree/develop/patches/ethereum/secp256k1-0.30"
authors = ["Luke Parker <lukeparker5132@gmail.com>"] authors = ["Luke Parker <lukeparker5132@gmail.com>"]
keywords = [] keywords = []
edition = "2021" edition = "2021"

View File

@@ -0,0 +1 @@
const _NEVER_COMPILED: [(); 0 - 1] = [(); 0 - 1];

View File

@@ -1,35 +0,0 @@
[package]
name = "librocksdb-sys"
version = "0.17.99"
description = "Replacement for `librocksdb-sys` which removes the `jemalloc` feature"
license = "MIT"
repository = "https://github.com/serai-dex/serai/tree/develop/patches/librocksdb-sys"
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
keywords = []
edition = "2018"
[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
[workspace]
[dependencies]
# librocksdb-sys 0.17.3+10.4.2, with the commit provided by crates.io in `cargo_vcs_info.json`
librocksdb-sys = { git = "https://github.com/rust-rocksdb/rust-rocksdb", commit = "bb7d2168eab1bc7849f23adbcb825e3aba1bd2f4", default-features = false }
[features]
default = ["librocksdb-sys/default"]
jemalloc = []
static = ["librocksdb-sys/static"]
bindgen-runtime = ["librocksdb-sys/bindgen-runtime"]
bindgen-static = ["librocksdb-sys/bindgen-static"]
mt_static = ["librocksdb-sys/mt_static"]
io-uring = ["librocksdb-sys/io-uring"]
snappy = ["librocksdb-sys/snappy"]
lz4 = ["librocksdb-sys/lz4"]
zstd = ["librocksdb-sys/zstd"]
zlib = ["librocksdb-sys/zlib"]
bzip2 = ["librocksdb-sys/bzip2"]
rtti = ["librocksdb-sys/rtti"]
lto = ["librocksdb-sys/lto"]

View File

@@ -1 +0,0 @@
pub use librocksdb_sys::*;

View File

@@ -7,6 +7,7 @@ repository = "https://github.com/serai-dex/serai/tree/develop/patches/option-ext
authors = ["Luke Parker <lukeparker5132@gmail.com>"] authors = ["Luke Parker <lukeparker5132@gmail.com>"]
keywords = [] keywords = []
edition = "2021" edition = "2021"
rust-version = "1.56"
[package.metadata.docs.rs] [package.metadata.docs.rs]
all-features = true all-features = true

View File

@@ -1,8 +1,8 @@
pub trait OptionExt<T: PartialEq> { pub trait OptionExt<T> {
fn contains(&self, x: &T) -> bool; fn contains(&self, x: &T) -> bool where T: PartialEq;
} }
impl<T: PartialEq> OptionExt<T> for Option<T> { impl<T> OptionExt<T> for Option<T> {
fn contains(&self, x: &T) -> bool { fn contains(&self, x: &T) -> bool where T: PartialEq {
self.as_ref() == Some(x) self.as_ref() == Some(x)
} }
} }

View File

@@ -15,7 +15,7 @@ rustdoc-args = ["--cfg", "docsrs"]
[workspace] [workspace]
[dependencies] [dependencies]
bip39 = { git = "https://github.com/rust-bitcoin/rust-bip39", commit = "f735e2559f30049f6738d1bf68c69a0b7bd7b858", default-features = false } bip39 = { version = "2.2.1", default-features = false }
[features] [features]
default = ["bip39/default"] default = ["bip39/default"]

View File

@@ -1,3 +1,3 @@
#![cfg_attr(not(feature = "std"), no_std)] #![no_std]
pub use bip39::*; pub use bip39::*;

View File

@@ -7,7 +7,6 @@ repository = "https://github.com/serai-dex/serai/tree/develop/patches/simple-req
authors = ["Luke Parker <lukeparker5132@gmail.com>"] authors = ["Luke Parker <lukeparker5132@gmail.com>"]
keywords = ["nostd", "no_std", "alloc", "io"] keywords = ["nostd", "no_std", "alloc", "io"]
edition = "2021" edition = "2021"
rust-version = "1.65"
[package.metadata.docs.rs] [package.metadata.docs.rs]
all-features = true all-features = true

View File

@@ -7,7 +7,6 @@ repository = "https://github.com/serai-dex/serai/tree/develop/patches/std-shims"
authors = ["Luke Parker <lukeparker5132@gmail.com>"] authors = ["Luke Parker <lukeparker5132@gmail.com>"]
keywords = ["nostd", "no_std", "alloc", "io"] keywords = ["nostd", "no_std", "alloc", "io"]
edition = "2021" edition = "2021"
rust-version = "1.65"
[package.metadata.docs.rs] [package.metadata.docs.rs]
all-features = true all-features = true

View File

@@ -28,8 +28,8 @@ dalek-ff-group = { path = "../../crypto/dalek-ff-group", default-features = fals
dkg = { package = "dkg-evrf", path = "../../crypto/dkg/evrf", default-features = false, features = ["std", "ed25519"] } dkg = { package = "dkg-evrf", path = "../../crypto/dkg/evrf", default-features = false, features = ["std", "ed25519"] }
frost = { package = "modular-frost", path = "../../crypto/frost", default-features = false } frost = { package = "modular-frost", path = "../../crypto/frost", default-features = false }
monero-wallet = { git = "https://github.com/monero-oxide/monero-oxide", rev = "030c60974f0f0306849c1795bca854a3bbb757b4", default-features = false, features = ["std", "multisig"] } monero-wallet = { git = "https://github.com/monero-oxide/monero-oxide", rev = "af0368e16395dbdda47cd53e54ccdb78a80b87bf", default-features = false, features = ["std", "multisig"] }
monero-simple-request-rpc = { git = "https://github.com/monero-oxide/monero-oxide", rev = "030c60974f0f0306849c1795bca854a3bbb757b4", default-features = false } monero-simple-request-rpc = { git = "https://github.com/monero-oxide/monero-oxide", rev = "af0368e16395dbdda47cd53e54ccdb78a80b87bf", default-features = false }
serai-primitives = { path = "../../substrate/primitives", default-features = false, features = ["std"] } serai-primitives = { path = "../../substrate/primitives", default-features = false, features = ["std"] }
serai-client-monero = { path = "../../substrate/client/monero", default-features = false } serai-client-monero = { path = "../../substrate/client/monero", default-features = false }

View File

@@ -1,9 +1,13 @@
use zeroize::Zeroizing; use zeroize::Zeroizing;
use ciphersuite::*; use ciphersuite::{group::ff::PrimeField, WrappedGroup, GroupIo};
use dalek_ff_group::Ed25519; use dalek_ff_group::Ed25519;
use monero_wallet::{address::SubaddressIndex, ViewPairError, GuaranteedViewPair}; use monero_wallet::{
ed25519::{Scalar, CompressedPoint},
address::SubaddressIndex,
ViewPairError, GuaranteedViewPair,
};
use view_keys::view_key; use view_keys::view_key;
@@ -29,7 +33,10 @@ pub(crate) const FORWARDED_SUBADDRESS: SubaddressIndex = match SubaddressIndex::
}; };
pub(crate) fn view_pair(key: <Ed25519 as WrappedGroup>::G) -> GuaranteedViewPair { pub(crate) fn view_pair(key: <Ed25519 as WrappedGroup>::G) -> GuaranteedViewPair {
match GuaranteedViewPair::new(key.0, Zeroizing::new(view_key::<Ed25519>(0))) { match GuaranteedViewPair::new(
CompressedPoint::from(key.0.compress().to_bytes()).decompress().unwrap(),
Zeroizing::new(Scalar::read(&mut view_key::<Ed25519>(0).to_repr().as_slice()).unwrap()),
) {
Ok(view_pair) => view_pair, Ok(view_pair) => view_pair,
Err(ViewPairError::TorsionedSpendKey) => { Err(ViewPairError::TorsionedSpendKey) => {
unreachable!("dalek_ff_group::EdwardsPoint had torsion") unreachable!("dalek_ff_group::EdwardsPoint had torsion")

View File

@@ -1,6 +1,9 @@
use std::io; use std::io;
use ciphersuite::WrappedGroup; use ciphersuite::{
group::{ff::PrimeField, GroupEncoding},
WrappedGroup,
};
use dalek_ff_group::Ed25519; use dalek_ff_group::Ed25519;
use monero_wallet::WalletOutput; use monero_wallet::WalletOutput;
@@ -65,9 +68,11 @@ impl ReceivedOutput<<Ed25519 as WrappedGroup>::G, Address> for Output {
// The spend key will be a key we generated, so it'll be in the prime-order subgroup // The spend key will be a key we generated, so it'll be in the prime-order subgroup
// The output's key is the spend key + (key_offset * G), so it's in the prime-order subgroup if // The output's key is the spend key + (key_offset * G), so it's in the prime-order subgroup if
// the spend key is // the spend key is
dalek_ff_group::EdwardsPoint( dalek_ff_group::EdwardsPoint::from_bytes(&self.0.key().compress().to_bytes()).unwrap() -
self.0.key() - (*<Ed25519 as WrappedGroup>::generator() * self.0.key_offset()), dalek_ff_group::EdwardsPoint(
) *<Ed25519 as WrappedGroup>::generator() *
dalek_ff_group::Scalar::from_repr(<[u8; 32]>::from(self.0.key_offset())).unwrap(),
)
} }
fn presumed_origin(&self) -> Option<Address> { fn presumed_origin(&self) -> Option<Address> {

View File

@@ -7,7 +7,7 @@ use rand_chacha::ChaCha20Rng;
use ciphersuite::*; use ciphersuite::*;
use dalek_ff_group::Ed25519; use dalek_ff_group::Ed25519;
use monero_wallet::interface::prelude::*; use monero_wallet::{ed25519::CompressedPoint, interface::prelude::*};
use serai_primitives::{coin::ExternalCoin, balance::Amount}; use serai_primitives::{coin::ExternalCoin, balance::Amount};
use serai_client_monero::Address; use serai_client_monero::Address;
@@ -116,8 +116,8 @@ async fn signable_transaction(
MoneroAddress::new( MoneroAddress::new(
Network::Mainnet, Network::Mainnet,
AddressType::Legacy, AddressType::Legacy,
<Ed25519 as WrappedGroup>::generator().0, CompressedPoint::G.decompress().unwrap(),
<Ed25519 as WrappedGroup>::generator().0, CompressedPoint::G.decompress().unwrap(),
), ),
0, 0,
)); ));

View File

@@ -22,12 +22,12 @@ workspace = true
borsh = { version = "1", default-features = false, features = ["derive", "de_strict_order"] } borsh = { version = "1", default-features = false, features = ["derive", "de_strict_order"] }
bitvec = { version = "1", default-features = false, features = ["alloc"] } bitvec = { version = "1", default-features = false, features = ["alloc"] }
sp-core = { git = "https://github.com/serai-dex/patch-polkadot-sdk", rev = "e01101b68c5b0f588dd4cdee48f801a2c1f75b84", default-features = false } sp-core = { git = "https://github.com/serai-dex/patch-polkadot-sdk", default-features = false }
serde = { version = "1", default-features = false, features = ["derive"], optional = true } serde = { version = "1", default-features = false, features = ["derive"], optional = true }
scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"], optional = true } scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"], optional = true }
sp-runtime = { git = "https://github.com/serai-dex/patch-polkadot-sdk", rev = "e01101b68c5b0f588dd4cdee48f801a2c1f75b84", default-features = false, features = ["serde"], optional = true } sp-runtime = { git = "https://github.com/serai-dex/patch-polkadot-sdk", default-features = false, features = ["serde"], optional = true }
frame-support = { git = "https://github.com/serai-dex/patch-polkadot-sdk", rev = "e01101b68c5b0f588dd4cdee48f801a2c1f75b84", default-features = false, optional = true } frame-support = { git = "https://github.com/serai-dex/patch-polkadot-sdk", default-features = false, optional = true }
serai-primitives = { path = "../primitives", version = "0.1", default-features = false } serai-primitives = { path = "../primitives", version = "0.1", default-features = false }

View File

@@ -63,6 +63,11 @@ pub struct HeaderV1 {
pub consensus_commitment: [u8; 32], pub consensus_commitment: [u8; 32],
} }
impl HeaderV1 {
/// The size of a serialized V1 header.
pub const SIZE: usize = 8 + 32 + 8 + 32 + 32 + 32;
}
/// A header for a block. /// A header for a block.
#[derive(Clone, Copy, PartialEq, Eq, Debug, BorshSerialize, BorshDeserialize)] #[derive(Clone, Copy, PartialEq, Eq, Debug, BorshSerialize, BorshDeserialize)]
pub enum Header { pub enum Header {
@@ -71,6 +76,9 @@ pub enum Header {
} }
impl Header { impl Header {
/// The size of a serialized header.
pub const SIZE: usize = 1 + HeaderV1::SIZE;
/// Get the hash of the header. /// Get the hash of the header.
pub fn number(&self) -> u64 { pub fn number(&self) -> u64 {
match self { match self {
@@ -109,8 +117,8 @@ impl Header {
/// A block. /// A block.
/// ///
/// This does not guarantee consistency. The header's `transactions_root` may not match the /// This does not guarantee consistency nor validity. The header's `transactions_root` may not
/// contained transactions. /// match the contained transactions, among other ill effects.
#[derive(Clone, PartialEq, Eq, Debug, BorshSerialize, BorshDeserialize)] #[derive(Clone, PartialEq, Eq, Debug, BorshSerialize, BorshDeserialize)]
pub struct Block { pub struct Block {
/// The block's header. /// The block's header.
@@ -119,6 +127,13 @@ pub struct Block {
pub transactions: Vec<Transaction>, pub transactions: Vec<Transaction>,
} }
impl Block {
/// The size limit for a block.
///
/// This is not enforced upon deserialization. Be careful accordingly.
pub const SIZE_LIMIT: usize = 512 * 1024;
}
#[cfg(feature = "substrate")] #[cfg(feature = "substrate")]
mod substrate { mod substrate {
use core::fmt::Debug; use core::fmt::Debug;
@@ -133,7 +148,7 @@ mod substrate {
use super::*; use super::*;
// Add `serde` implementations which treat self as a `Vec<u8>` // Add `serde` implementations which treat `self` as a `Vec<u8>`
impl sp_core::serde::Serialize for Transaction { impl sp_core::serde::Serialize for Transaction {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where where

View File

@@ -8,21 +8,26 @@ use serai_primitives::{
balance::{Amount, ExternalBalance, Balance}, balance::{Amount, ExternalBalance, Balance},
}; };
/// The address used for a liquidity pool by the DEX.
pub fn address(coin: ExternalCoin) -> SeraiAddress {
SeraiAddress::system(borsh::to_vec(&(b"DEX", coin)).unwrap())
}
/// A call to the DEX. /// A call to the DEX.
#[derive(Clone, PartialEq, Eq, Debug, BorshSerialize, BorshDeserialize)] #[derive(Clone, PartialEq, Eq, Debug, BorshSerialize, BorshDeserialize)]
pub enum Call { pub enum Call {
/// Add liquidity. /// Add liquidity.
add_liquidity { add_liquidity {
/// The coin to add liquidity for. /// The pool to add liquidity to, specified by its external coin.
coin: ExternalCoin, external_coin: ExternalCoin,
/// The intended amount of SRI to add as liquidity. /// The intended amount of SRI to add as liquidity.
sri_intended: Amount, sri_intended: Amount,
/// The intended amount of the coin to add as liquidity. /// The intended amount of the coin to add as liquidity.
coin_intended: Amount, external_coin_intended: Amount,
/// The minimum amount of SRI to add as liquidity. /// The minimum amount of SRI to add as liquidity.
sri_minimum: Amount, sri_minimum: Amount,
/// The minimum amount of the coin to add as liquidity. /// The minimum amount of the coin to add as liquidity.
coin_minimum: Amount, external_coin_minimum: Amount,
}, },
/// Transfer these liquidity tokens to the specified address. /// Transfer these liquidity tokens to the specified address.
transfer_liquidity { transfer_liquidity {
@@ -40,17 +45,17 @@ pub enum Call {
/// The minimum amount of SRI to receive. /// The minimum amount of SRI to receive.
sri_minimum: Amount, sri_minimum: Amount,
/// The minimum amount of the coin to receive. /// The minimum amount of the coin to receive.
coin_minimum: Amount, external_coin_minimum: Amount,
}, },
/// Swap an exact amount of coins. /// Swap an exact amount of coins.
swap_exact { swap {
/// The coins to swap. /// The coins to swap.
coins_to_swap: Balance, coins_to_swap: Balance,
/// The minimum balance to receive. /// The minimum balance to receive.
minimum_to_receive: Balance, minimum_to_receive: Balance,
}, },
/// Swap for an exact amount of coins. /// Swap for an exact amount of coins.
swap_for_exact { swap_for {
/// The coins to receive. /// The coins to receive.
coins_to_receive: Balance, coins_to_receive: Balance,
/// The maximum amount to swap. /// The maximum amount to swap.
@@ -64,8 +69,8 @@ impl Call {
Call::add_liquidity { .. } | Call::add_liquidity { .. } |
Call::transfer_liquidity { .. } | Call::transfer_liquidity { .. } |
Call::remove_liquidity { .. } | Call::remove_liquidity { .. } |
Call::swap_exact { .. } | Call::swap { .. } |
Call::swap_for_exact { .. } => true, Call::swap_for { .. } => true,
} }
} }
} }
@@ -74,41 +79,43 @@ impl Call {
#[derive(Clone, PartialEq, Eq, Debug, BorshSerialize, BorshDeserialize)] #[derive(Clone, PartialEq, Eq, Debug, BorshSerialize, BorshDeserialize)]
pub enum Event { pub enum Event {
/// Liquidity was added to a pool. /// Liquidity was added to a pool.
LiquidityAdded { LiquidityAddition {
/// The account which added the liquidity. /// The account which received the minted liquidity tokens.
origin: SeraiAddress,
/// The account which received the liquidity tokens.
recipient: SeraiAddress, recipient: SeraiAddress,
/// The pool liquidity was added to. /// The liquidity tokens which were minted.
pool: ExternalCoin, liquidity_tokens: ExternalBalance,
/// The amount of liquidity tokens which were minted.
liquidity_tokens_minted: Amount,
/// The amount of the coin which was added to the pool's liquidity.
coin_amount: Amount,
/// The amount of SRI which was added to the pool's liquidity. /// The amount of SRI which was added to the pool's liquidity.
sri_amount: Amount, sri_amount: Amount,
/// The amount of the coin which was added to the pool's liquidity.
external_coin_amount: Amount,
},
/// The specified liquidity tokens were transferred.
LiquidityTransfer {
/// The address transferred from.
from: SeraiAddress,
/// The address transferred to.
to: SeraiAddress,
/// The liquidity tokens transferred.
liquidity_tokens: ExternalBalance,
}, },
/// Liquidity was removed from a pool. /// Liquidity was removed from a pool.
LiquidityRemoved { LiquidityRemoval {
/// The account which removed the liquidity. /// The account which removed the liquidity.
origin: SeraiAddress, from: SeraiAddress,
/// The pool liquidity was removed from. /// The liquidity tokens which were burnt.
pool: ExternalCoin, liquidity_tokens: ExternalBalance,
/// The mount of liquidity tokens which were burnt.
liquidity_tokens_burnt: Amount,
/// The amount of the coin which was removed from the pool's liquidity.
coin_amount: Amount,
/// The amount of SRI which was removed from the pool's liquidity. /// The amount of SRI which was removed from the pool's liquidity.
sri_amount: Amount, sri_amount: Amount,
/// The amount of the coin which was removed from the pool's liquidity.
external_coin_amount: Amount,
}, },
/// A swap through the liquidity pools occurred. /// A swap through the liquidity pools occurred.
Swap { Swap {
/// The account which made the swap. /// The account which made the swap.
origin: SeraiAddress, from: SeraiAddress,
/// The recipient for the output of the swap.
recipient: SeraiAddress,
/// The deltas incurred by the pools. /// The deltas incurred by the pools.
/// ///
/// For a swap of sriABC to sriDEF, this would be /// For a swap of sriABC to sriDEF, this would be

View File

@@ -1,23 +1,38 @@
use borsh::{BorshSerialize, BorshDeserialize}; use borsh::{BorshSerialize, BorshDeserialize};
use serai_primitives::{ use serai_primitives::{
crypto::Signature, address::SeraiAddress, balance::ExternalBalance, genesis::GenesisValues, crypto::Signature, address::SeraiAddress, coin::ExternalCoin, balance::ExternalBalance,
genesis_liquidity::GenesisValues,
}; };
/// The address used for to hold genesis liquidity for a pool.
pub fn address(coin: ExternalCoin) -> SeraiAddress {
SeraiAddress::system(borsh::to_vec(&(b"GenesisLiquidity", coin)).unwrap())
}
/// A call to the genesis liquidity. /// A call to the genesis liquidity.
#[derive(Clone, PartialEq, Eq, Debug, BorshSerialize, BorshDeserialize)] #[derive(Clone, PartialEq, Eq, Debug, BorshSerialize, BorshDeserialize)]
pub enum Call { pub enum Call {
/// Oraclize the value of non-Bitcoin external coins relative to Bitcoin. /// Oraclize the values of the coins available on genesis, relative to BTC.
///
/// This will trigger the addition of the liquidity into the pools and their initialization.
oraclize_values { oraclize_values {
/// The values of the non-Bitcoin external coins. /// The values of the non-Bitcoin external coins.
values: GenesisValues, values: GenesisValues,
/// The signature by the genesis validators for these values. /// The signature by the genesis validators for these values.
signature: Signature, signature: Signature,
}, },
/// Remove liquidity. /// Transfer genesis liquidity.
remove_liquidity { transfer_genesis_liquidity {
/// The account to transfer the liquidity to.
to: SeraiAddress,
/// The genesis liquidity to transfer.
genesis_liquidity: ExternalBalance,
},
/// Remove genesis liquidity.
remove_genesis_liquidity {
/// The genesis liquidity to remove. /// The genesis liquidity to remove.
balance: ExternalBalance, genesis_liquidity: ExternalBalance,
}, },
} }
@@ -25,7 +40,7 @@ impl Call {
pub(crate) fn is_signed(&self) -> bool { pub(crate) fn is_signed(&self) -> bool {
match self { match self {
Call::oraclize_values { .. } => false, Call::oraclize_values { .. } => false,
Call::remove_liquidity { .. } => true, Call::transfer_genesis_liquidity { .. } | Call::remove_genesis_liquidity { .. } => true,
} }
} }
} }
@@ -38,13 +53,22 @@ pub enum Event {
/// The recipient of the genesis liquidity. /// The recipient of the genesis liquidity.
recipient: SeraiAddress, recipient: SeraiAddress,
/// The coins added as genesis liquidity. /// The coins added as genesis liquidity.
balance: ExternalBalance, genesis_liquidity: ExternalBalance,
},
/// Genesis liquidity added.
GenesisLiquidityTransferred {
/// The address transferred from.
from: SeraiAddress,
/// The address transferred to.
to: SeraiAddress,
/// The genesis liquidity transferred.
genesis_liquidity: ExternalBalance,
}, },
/// Genesis liquidity removed. /// Genesis liquidity removed.
GenesisLiquidityRemoved { GenesisLiquidityRemoved {
/// The account which removed the genesis liquidity. /// The account which removed the genesis liquidity.
origin: SeraiAddress, from: SeraiAddress,
/// The amount of genesis liquidity removed. /// The amount of genesis liquidity removed.
balance: ExternalBalance, genesis_liquidity: ExternalBalance,
}, },
} }

View File

@@ -279,12 +279,13 @@ mod substrate {
/// The implicit context to verify transactions with. /// The implicit context to verify transactions with.
fn implicit_context() -> ImplicitContext; fn implicit_context() -> ImplicitContext;
/// The size of the current block.
fn current_block_size(&self) -> usize;
/// If a block is present in the blockchain. /// If a block is present in the blockchain.
fn block_is_present_in_blockchain(&self, hash: &BlockHash) -> bool; fn block_is_present_in_blockchain(&self, hash: &BlockHash) -> bool;
/// The time embedded into the current block. /// The time embedded into the current block.
/// fn current_time(&self) -> u64;
/// Returns `None` if the time has yet to be set.
fn current_time(&self) -> Option<u64>;
/// Get the next nonce for an account. /// Get the next nonce for an account.
fn next_nonce(&self, signer: &SeraiAddress) -> u32; fn next_nonce(&self, signer: &SeraiAddress) -> u32;
/// If the signer can pay the SRI fee. /// If the signer can pay the SRI fee.
@@ -295,7 +296,7 @@ mod substrate {
) -> Result<(), TransactionValidityError>; ) -> Result<(), TransactionValidityError>;
/// Begin execution of a transaction. /// Begin execution of a transaction.
fn start_transaction(&self); fn start_transaction(&self, len: usize);
/// Consume the next nonce for an account. /// Consume the next nonce for an account.
/// ///
/// This MUST NOT be called if the next nonce is `u32::MAX`. The caller MAY panic in that case. /// This MUST NOT be called if the next nonce is `u32::MAX`. The caller MAY panic in that case.
@@ -390,9 +391,14 @@ mod substrate {
impl<Context: TransactionContext> TransactionWithContext<Context> { impl<Context: TransactionContext> TransactionWithContext<Context> {
fn validate_except_fee<V: ValidateUnsigned<Call = Context::RuntimeCall>>( fn validate_except_fee<V: ValidateUnsigned<Call = Context::RuntimeCall>>(
&self, &self,
len: usize,
source: TransactionSource, source: TransactionSource,
mempool_priority_if_signed: u64, mempool_priority_if_signed: u64,
) -> TransactionValidity { ) -> TransactionValidity {
if self.1.current_block_size().saturating_add(len) > crate::Block::SIZE_LIMIT {
Err(TransactionValidityError::Invalid(InvalidTransaction::ExhaustsResources))?;
}
match &self.0 { match &self.0 {
Transaction::Unsigned { call } => { Transaction::Unsigned { call } => {
let ValidTransaction { priority: _, requires, provides, longevity: _, propagate: _ } = let ValidTransaction { priority: _, requires, provides, longevity: _, propagate: _ } =
@@ -417,13 +423,8 @@ mod substrate {
Err(TransactionValidityError::Unknown(UnknownTransaction::CannotLookup))?; Err(TransactionValidityError::Unknown(UnknownTransaction::CannotLookup))?;
} }
if let Some(include_by) = *include_by { if let Some(include_by) = *include_by {
if let Some(current_time) = self.1.current_time() { if self.1.current_time() >= u64::from(include_by) {
if current_time >= u64::from(include_by) { // Since this transaction has a time bound which has passed, error
// Since this transaction has a time bound which has passed, error
Err(TransactionValidityError::Invalid(InvalidTransaction::Stale))?;
}
} else {
// Since this transaction has a time bound, yet we don't know the time, error
Err(TransactionValidityError::Invalid(InvalidTransaction::Stale))?; Err(TransactionValidityError::Invalid(InvalidTransaction::Stale))?;
} }
} }
@@ -471,7 +472,7 @@ mod substrate {
&self, &self,
source: TransactionSource, source: TransactionSource,
info: &DispatchInfo, info: &DispatchInfo,
_len: usize, len: usize,
) -> TransactionValidity { ) -> TransactionValidity {
let mempool_priority_if_signed = match &self.0 { let mempool_priority_if_signed = match &self.0 {
Transaction::Unsigned { .. } => { Transaction::Unsigned { .. } => {
@@ -493,19 +494,19 @@ mod substrate {
} }
} }
}; };
self.validate_except_fee::<V>(source, mempool_priority_if_signed) self.validate_except_fee::<V>(len, source, mempool_priority_if_signed)
} }
fn apply<V: ValidateUnsigned<Call = Context::RuntimeCall>>( fn apply<V: ValidateUnsigned<Call = Context::RuntimeCall>>(
self, self,
_info: &DispatchInfo, _info: &DispatchInfo,
_len: usize, len: usize,
) -> sp_runtime::ApplyExtrinsicResultWithInfo<PostDispatchInfo> { ) -> sp_runtime::ApplyExtrinsicResultWithInfo<PostDispatchInfo> {
// We use 0 for the mempool priority, as this is no longer in the mempool so it's irrelevant // We use 0 for the mempool priority, as this is no longer in the mempool so it's irrelevant
self.validate_except_fee::<V>(TransactionSource::InBlock, 0)?; self.validate_except_fee::<V>(len, TransactionSource::InBlock, 0)?;
// Start the transaction // Start the transaction
self.1.start_transaction(); self.1.start_transaction(len);
let transaction_hash = self.0.hash(); let transaction_hash = self.0.hash();

View File

@@ -3,7 +3,7 @@ use alloc::vec::Vec;
use borsh::{BorshSerialize, BorshDeserialize}; use borsh::{BorshSerialize, BorshDeserialize};
use serai_primitives::{ use serai_primitives::{
crypto::{SignedEmbeddedEllipticCurveKeys, KeyPair, Signature}, crypto::{EmbeddedEllipticCurveKeys, SignedEmbeddedEllipticCurveKeys, KeyPair, Signature},
address::SeraiAddress, address::SeraiAddress,
balance::Amount, balance::Amount,
network_id::*, network_id::*,
@@ -125,8 +125,8 @@ pub enum Event {
SetEmbeddedEllipticCurveKeys { SetEmbeddedEllipticCurveKeys {
/// The validator which set their keys. /// The validator which set their keys.
validator: SeraiAddress, validator: SeraiAddress,
/// The network which they set embedded elliptic curve keys for. /// The embedded elliptic curve keys they set.
network: ExternalNetworkId, keys: EmbeddedEllipticCurveKeys,
}, },
/// A validator's allocation to a network has increased. /// A validator's allocation to a network has increased.
Allocation { Allocation {

View File

@@ -23,4 +23,6 @@ serai-primitives = { path = "../../primitives", version = "0.1", default-feature
ciphersuite = { path = "../../../crypto/ciphersuite", default-features = false, features = ["std"] } ciphersuite = { path = "../../../crypto/ciphersuite", default-features = false, features = ["std"] }
dalek-ff-group = { path = "../../../crypto/dalek-ff-group", default-features = false, features = ["std"] } dalek-ff-group = { path = "../../../crypto/dalek-ff-group", default-features = false, features = ["std"] }
monero-address = { git = "https://github.com/monero-oxide/monero-oxide", rev = "030c60974f0f0306849c1795bca854a3bbb757b4", version = "0.1.0", default-features = false, features = ["std"] }
monero-ed25519 = { git = "https://github.com/monero-oxide/monero-oxide", rev = "af0368e16395dbdda47cd53e54ccdb78a80b87bf", version = "0.1.0", default-features = false, features = ["std"] }
monero-address = { git = "https://github.com/monero-oxide/monero-oxide", rev = "af0368e16395dbdda47cd53e54ccdb78a80b87bf", version = "0.1.0", default-features = false, features = ["std"] }

View File

@@ -4,39 +4,43 @@
use core::{str::FromStr, fmt}; use core::{str::FromStr, fmt};
use dalek_ff_group::{EdwardsPoint, Ed25519}; use ciphersuite::{group::GroupEncoding, GroupIo};
use ciphersuite::GroupIo; use dalek_ff_group::Ed25519;
use monero_ed25519::{CompressedPoint, Point};
use monero_address::{Network, AddressType as MoneroAddressType, MoneroAddress}; use monero_address::{Network, AddressType as MoneroAddressType, MoneroAddress};
use serai_primitives::address::ExternalAddress; use serai_primitives::address::ExternalAddress;
#[allow(non_snake_case)]
fn read_G(reader: &mut impl borsh::io::Read) -> borsh::io::Result<Point> {
// We use `Ed25519::read_G` for the strong canonicalization requirements before using
//` monero-ed25519` for the actual values
CompressedPoint::from(Ed25519::read_G(reader)?.to_bytes()).decompress().ok_or_else(|| {
borsh::io::Error::other(
"canonically-encoded torsion-free point was rejected by `monero-ed25519`",
)
})
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)] #[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum AddressType { enum AddressType {
Legacy, Legacy,
Subaddress, Subaddress,
Featured(u8),
} }
/// A representation of a Monero address. /// A representation of a Monero address.
#[derive(Clone, Copy, PartialEq, Eq, Debug)] #[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct Address { pub struct Address {
kind: AddressType, kind: AddressType,
spend: EdwardsPoint, spend: Point,
view: EdwardsPoint, view: Point,
} }
fn byte_for_kind(kind: AddressType) -> u8 { fn byte_for_kind(kind: AddressType) -> u8 {
// We use the second and third highest bits for the type
// This leaves the top bit open for interpretation as a VarInt later
match kind { match kind {
AddressType::Legacy => 0, AddressType::Legacy => 0,
AddressType::Subaddress => 1 << 5, AddressType::Subaddress => 1,
AddressType::Featured(flags) => {
// The flags only take up the low three bits
debug_assert!(flags <= 0b111);
(2 << 5) | flags
}
} }
} }
@@ -52,18 +56,13 @@ impl borsh::BorshDeserialize for Address {
let mut kind_byte = [0xff]; let mut kind_byte = [0xff];
reader.read_exact(&mut kind_byte)?; reader.read_exact(&mut kind_byte)?;
let kind_byte = kind_byte[0]; let kind_byte = kind_byte[0];
let kind = match kind_byte >> 5 { let kind = match kind_byte {
0 => AddressType::Legacy, 0 => AddressType::Legacy,
1 => AddressType::Subaddress, 1 => AddressType::Subaddress,
2 => AddressType::Featured(kind_byte & 0b111),
_ => Err(borsh::io::Error::other("unrecognized type"))?, _ => Err(borsh::io::Error::other("unrecognized type"))?,
}; };
// Check this wasn't malleated let spend = read_G(reader)?;
if byte_for_kind(kind) != kind_byte { let view = read_G(reader)?;
Err(borsh::io::Error::other("malleated type byte"))?;
}
let spend = Ed25519::read_G(reader)?;
let view = Ed25519::read_G(reader)?;
Ok(Self { kind, spend, view }) Ok(Self { kind, spend, view })
} }
} }
@@ -75,20 +74,13 @@ impl TryFrom<MoneroAddress> for Address {
let view = address.view().compress().to_bytes(); let view = address.view().compress().to_bytes();
let kind = match address.kind() { let kind = match address.kind() {
MoneroAddressType::Legacy => AddressType::Legacy, MoneroAddressType::Legacy => AddressType::Legacy,
MoneroAddressType::LegacyIntegrated(_) => Err(())?,
MoneroAddressType::Subaddress => AddressType::Subaddress, MoneroAddressType::Subaddress => AddressType::Subaddress,
MoneroAddressType::Featured { subaddress, payment_id, guaranteed } => { MoneroAddressType::LegacyIntegrated(_) | MoneroAddressType::Featured { .. } => Err(())?,
if payment_id.is_some() {
Err(())?
}
// This maintains the same bit layout as featured addresses use
AddressType::Featured(u8::from(*subaddress) + (u8::from(*guaranteed) << 2))
}
}; };
Ok(Address { Ok(Address {
kind, kind,
spend: Ed25519::read_G(&mut spend.as_slice()).map_err(|_| ())?, spend: read_G(&mut spend.as_slice()).map_err(|_| ())?,
view: Ed25519::read_G(&mut view.as_slice()).map_err(|_| ())?, view: read_G(&mut view.as_slice()).map_err(|_| ())?,
}) })
} }
} }
@@ -98,16 +90,8 @@ impl From<Address> for MoneroAddress {
let kind = match address.kind { let kind = match address.kind {
AddressType::Legacy => MoneroAddressType::Legacy, AddressType::Legacy => MoneroAddressType::Legacy,
AddressType::Subaddress => MoneroAddressType::Subaddress, AddressType::Subaddress => MoneroAddressType::Subaddress,
AddressType::Featured(features) => {
debug_assert!(features <= 0b111);
let subaddress = (features & 1) != 0;
let integrated = (features & (1 << 1)) != 0;
debug_assert!(!integrated);
let guaranteed = (features & (1 << 2)) != 0;
MoneroAddressType::Featured { subaddress, payment_id: None, guaranteed }
}
}; };
MoneroAddress::new(Network::Mainnet, kind, address.spend.0, address.view.0) MoneroAddress::new(Network::Mainnet, kind, address.spend, address.view)
} }
} }

View File

@@ -28,11 +28,9 @@ borsh = { version = "1", default-features = false, features = ["std"] }
bitvec = { version = "1", default-features = false, features = ["alloc", "std"] } bitvec = { version = "1", default-features = false, features = ["alloc", "std"] }
serai-abi = { path = "../../abi", version = "0.1", default-features = false, features = ["std"] } serai-abi = { path = "../../abi", version = "0.1", default-features = false, features = ["std"] }
async-lock = "3"
[dev-dependencies] [dev-dependencies]
blake2 = { version = "0.11.0-rc.3", default-features = false } blake2 = { version = "0.11.0-rc.3", default-features = false }
sp-core = { git = "https://github.com/serai-dex/patch-polkadot-sdk", rev = "e01101b68c5b0f588dd4cdee48f801a2c1f75b84" } sp-core = { git = "https://github.com/serai-dex/patch-polkadot-sdk" }
tokio = { version = "1", default-features = false, features = ["rt", "macros"] } tokio = { version = "1", default-features = false, features = ["rt", "macros"] }
dockertest = "0.5" dockertest = "0.5"

View File

@@ -1,76 +1,37 @@
pub use serai_abi::coins::Event; pub use serai_abi::coins::Event;
use crate::{RpcError, TemporalSerai}; use crate::{RpcError, Events};
/// A `TemporalSerai` scoped to the coins module. /// An `Events` scoped to the coins module.
#[derive(Clone)] #[derive(Clone)]
pub struct Coins<'a>(pub(super) &'a TemporalSerai<'a>); pub struct Coins(pub(super) Events);
impl<'a> Coins<'a> { impl Coins {
/// The events from the coins module. /// The events from the coins module.
pub async fn events(&self) -> Result<Vec<Event>, RpcError> { pub fn events(&self) -> impl Iterator<Item = &Event> {
Ok( self.0.events().flatten().filter_map(|event| match event {
self serai_abi::Event::Coins(event) => Some(event),
.0 _ => None,
.events_borrowed() })
.await?
.as_ref()
.expect("`TemporalSerai::events` returned None")
.iter()
.flat_map(IntoIterator::into_iter)
.filter_map(|event| match event {
serai_abi::Event::Coins(event) => Some(event.clone()),
_ => None,
})
.collect(),
)
} }
/// The `Mint` events from the coins module. /// The `Mint` events from the coins module.
pub async fn mint_events(&self) -> Result<Vec<Event>, RpcError> { pub fn mint_events(&self) -> impl Iterator<Item = &Event> {
Ok( self.events().filter(|event| matches!(event, Event::Mint { .. }))
self
.events()
.await?
.into_iter()
.filter(|event| matches!(event, Event::Mint { .. }))
.collect(),
)
} }
/// The `Transfer` events from the coins module. /// The `Transfer` events from the coins module.
pub async fn transfer_events(&self) -> Result<Vec<Event>, RpcError> { pub fn transfer_events(&self) -> impl Iterator<Item = &Event> {
Ok( self.events().filter(|event| matches!(event, Event::Transfer { .. }))
self
.events()
.await?
.into_iter()
.filter(|event| matches!(event, Event::Transfer { .. }))
.collect(),
)
} }
/// The `Burn` events from the coins module. /// The `Burn` events from the coins module.
pub async fn burn_events(&self) -> Result<Vec<Event>, RpcError> { pub fn burn_events(&self) -> impl Iterator<Item = &Event> {
Ok( self.events().filter(|event| matches!(event, Event::Burn { .. }))
self
.events()
.await?
.into_iter()
.filter(|event| matches!(event, Event::Burn { .. }))
.collect(),
)
} }
/// The `BurnWithInstruction` events from the coins module. /// The `BurnWithInstruction` events from the coins module.
pub async fn burn_with_instruction_events(&self) -> Result<Vec<Event>, RpcError> { pub fn burn_with_instruction_events(&self) -> impl Iterator<Item = &Event> {
Ok( self.events().filter(|event| matches!(event, Event::BurnWithInstruction { .. }))
self
.events()
.await?
.into_iter()
.filter(|event| matches!(event, Event::BurnWithInstruction { .. }))
.collect(),
)
} }
} }

View File

@@ -4,42 +4,24 @@ pub use serai_abi::{
UnsignedCall, Transaction, UnsignedCall, Transaction,
}; };
use crate::{RpcError, TemporalSerai}; use crate::{RpcError, Events};
/// A `TemporalSerai` scoped to the in instructions module. /// An `Events` scoped to the in instructions module.
#[derive(Clone)] #[derive(Clone)]
pub struct InInstructions<'serai>(pub(super) &'serai TemporalSerai<'serai>); pub struct InInstructions(pub(super) Events);
impl<'serai> InInstructions<'serai> { impl InInstructions {
/// The events from the in instructions module. /// The events from the in instructions module.
pub async fn events(&self) -> Result<Vec<Event>, RpcError> { fn events(&self) -> impl Iterator<Item = &Event> {
Ok( self.0.events().flatten().filter_map(|event| match event {
self serai_abi::Event::InInstructions(event) => Some(event),
.0 _ => None,
.events_borrowed() })
.await?
.as_ref()
.expect("`TemporalSerai::events` returned None")
.iter()
.flat_map(IntoIterator::into_iter)
.filter_map(|event| match event {
serai_abi::Event::InInstructions(event) => Some(event.clone()),
_ => None,
})
.collect(),
)
} }
/// The `Batch` events from the in instructions module. /// The `Batch` events from the in instructions module.
pub async fn batch_events(&self) -> Result<Vec<Event>, RpcError> { pub fn batch_events(&self) -> impl Iterator<Item = &Event> {
Ok( self.events().filter(|event| matches!(event, Event::Batch { .. }))
self
.events()
.await?
.into_iter()
.filter(|event| matches!(event, Event::Batch { .. }))
.collect(),
)
} }
/// Create a transaction to execute a batch. /// Create a transaction to execute a batch.

View File

@@ -17,8 +17,6 @@ use abi::{
Transaction, Block, Event, Transaction, Block, Event,
}; };
use async_lock::RwLock;
mod coins; mod coins;
pub use coins::Coins; pub use coins::Coins;
@@ -55,16 +53,18 @@ pub struct Serai {
client: TokioClient, client: TokioClient,
} }
/// An RPC client to a Serai node, scoped to a specific block. /// The events from a specific block.
///
/// Upon any request being made for the events emitted by this block, the entire list of events
/// from this block will be cached within this. This allows future calls for events to be done
/// cheaply.
#[derive(Clone)] #[derive(Clone)]
pub struct TemporalSerai<'serai> { pub struct Events {
// These are cached within an `Arc` for cheap `Clone`s
events: Arc<Vec<Vec<Event>>>,
}
/// An RPC client to a Serai node, scoped to a specific block.
#[derive(Clone)]
pub struct State<'serai> {
serai: &'serai Serai, serai: &'serai Serai,
block: BlockHash, block: BlockHash,
events: Arc<RwLock<Option<Vec<Vec<Event>>>>>,
} }
impl Serai { impl Serai {
@@ -175,28 +175,41 @@ impl Serai {
.await .await
} }
/// Scope this RPC client to the state as of a specific block. /// Fetch the events of a specific block.
/// pub async fn events(&self, block: BlockHash) -> Result<Events, RpcError> {
/// This will yield an error if the block chosen isn't finalized. This ensures, given an honest Ok(Events {
/// node, that this scope will be available for the lifetime of this object. events: Arc::new(
pub async fn as_of(&self, block: BlockHash) -> Result<TemporalSerai<'_>, RpcError> { self
if !self.finalized(block).await? { .call::<Vec<Vec<String>>>("blockchain/events", &format!(r#"{{ "block": "{block}" }}"#))
Err(RpcError::NotFinalized)?; .await?
} .into_iter()
Ok(TemporalSerai { serai: self, block, events: Arc::new(RwLock::new(None)) }) .map(|events_per_tx| {
events_per_tx
.into_iter()
.map(|event| {
Event::deserialize(
&mut hex::decode(&event)
.map_err(|_| {
RpcError::InvalidNode("node returned non-hex-encoded event".to_string())
})?
.as_slice(),
)
.map_err(|_| RpcError::InvalidNode("node returned invalid event".to_string()))
})
.collect::<Result<Vec<_>, _>>()
})
.collect::<Result<Vec<_>, _>>()?,
),
})
} }
/// Scope this RPC client to the state as of the latest finalized block. /// Scope this RPC client to the state as of the latest finalized block.
pub async fn as_of_latest_finalized_block(&self) -> Result<TemporalSerai<'_>, RpcError> { pub async fn state(&self) -> Result<State<'_>, RpcError> {
let block = self let block = self
.block_by_number(self.latest_finalized_block_number().await?) .block_by_number(self.latest_finalized_block_number().await?)
.await? .await?
.ok_or_else(|| RpcError::InvalidNode("couldn't fetch latest finalized block".to_string()))?; .ok_or_else(|| RpcError::InvalidNode("couldn't fetch latest finalized block".to_string()))?;
Ok(TemporalSerai { Ok(State { serai: self, block: block.header.hash() })
serai: self,
block: block.header.hash(),
events: Arc::new(RwLock::new(None)),
})
} }
/// Return the P2P addresses for the validators of the specified network. /// Return the P2P addresses for the validators of the specified network.
@@ -208,14 +221,38 @@ impl Serai {
ExternalNetworkId::Bitcoin => r#"["bitcoin"]"#, ExternalNetworkId::Bitcoin => r#"["bitcoin"]"#,
ExternalNetworkId::Ethereum => r#"["ethereum"]"#, ExternalNetworkId::Ethereum => r#"["ethereum"]"#,
ExternalNetworkId::Monero => r#"["monero"]"#, ExternalNetworkId::Monero => r#"["monero"]"#,
_ => Err(RpcError::InternalError("unrecognized external network ID".to_string()))?,
}, },
) )
.await .await
} }
} }
impl<'serai> TemporalSerai<'serai> { impl Events {
/// The events within this container.
///
/// This will yield the events for each transaction within the block, including the implicit
/// transactions at the start and end of each block, within an outer container.
pub fn events(&self) -> impl Iterator<Item: IntoIterator<Item = &Event>> {
self.events.iter()
}
/// Scope to the coins module.
pub fn coins(&self) -> Coins {
Coins(self.clone())
}
/// Scope to the validator sets module.
pub fn validator_sets(&self) -> ValidatorSets {
ValidatorSets(self.clone())
}
/// Scope to the in instructions module.
pub fn in_instructions(&self) -> InInstructions {
InInstructions(self.clone())
}
}
impl<'serai> State<'serai> {
async fn call<ResponseValue: Default + JsonDeserialize>( async fn call<ResponseValue: Default + JsonDeserialize>(
&self, &self,
method: &str, method: &str,
@@ -223,68 +260,4 @@ impl<'serai> TemporalSerai<'serai> {
) -> Result<ResponseValue, RpcError> { ) -> Result<ResponseValue, RpcError> {
self.serai.call(method, &format!(r#"{{ "block": "{}" {params} }}"#, self.block)).await self.serai.call(method, &format!(r#"{{ "block": "{}" {params} }}"#, self.block)).await
} }
/// Fetch the events for this block.
///
/// The returned `Option` will always be `Some(_)`.
async fn events_borrowed(
&self,
) -> Result<async_lock::RwLockReadGuard<'_, Option<Vec<Vec<Event>>>>, RpcError> {
let mut events = self.events.read().await;
if events.is_none() {
drop(events);
{
let mut events_mut = self.events.write().await;
if events_mut.is_none() {
*events_mut = Some(
self
.call::<Vec<Vec<String>>>("blockchain/events", "")
.await?
.into_iter()
.map(|events_per_tx| {
events_per_tx
.into_iter()
.map(|event| {
Event::deserialize(
&mut hex::decode(&event)
.map_err(|_| {
RpcError::InvalidNode("node returned non-hex-encoded event".to_string())
})?
.as_slice(),
)
.map_err(|_| RpcError::InvalidNode("node returned invalid event".to_string()))
})
.collect::<Result<Vec<_>, _>>()
})
.collect::<Result<Vec<_>, _>>()?,
);
}
}
events = self.events.read().await;
}
Ok(events)
}
/// Fetch the events for this block.
///
/// These will be grouped by the transactions which emitted them, including the inherent
/// transactions at the start and end of every block.
pub async fn events(&self) -> Result<Vec<Vec<Event>>, RpcError> {
Ok(self.events_borrowed().await?.clone().expect("`TemporalSerai::events` returned None"))
}
/// Scope to the coins module.
pub fn coins(&self) -> Coins<'_> {
Coins(self)
}
/// Scope to the validator sets module.
pub fn validator_sets(&self) -> ValidatorSets<'_> {
ValidatorSets(self)
}
/// Scope to the in instructions module.
pub fn in_instructions(&self) -> InInstructions<'_> {
InInstructions(self)
}
} }

View File

@@ -14,203 +14,68 @@ pub use serai_abi::{
UnsignedCall, Transaction, UnsignedCall, Transaction,
}; };
use crate::{RpcError, TemporalSerai}; use crate::{RpcError, Events, State};
fn rpc_network(network: impl Into<NetworkId>) -> Result<&'static str, RpcError> { fn rpc_network(network: impl Into<NetworkId>) -> &'static str {
Ok(match network.into() { match network.into() {
NetworkId::Serai => r#""serai""#, NetworkId::Serai => r#""serai""#,
NetworkId::External(ExternalNetworkId::Bitcoin) => r#""bitcoin""#, NetworkId::External(ExternalNetworkId::Bitcoin) => r#""bitcoin""#,
NetworkId::External(ExternalNetworkId::Ethereum) => r#""ethereum""#, NetworkId::External(ExternalNetworkId::Ethereum) => r#""ethereum""#,
NetworkId::External(ExternalNetworkId::Monero) => r#""monero""#, NetworkId::External(ExternalNetworkId::Monero) => r#""monero""#,
_ => Err(RpcError::InternalError("unrecognized network ID".to_string()))?, }
})
} }
/// A `TemporalSerai` scoped to the validator sets module. /// An `Events` scoped to the validator sets module.
#[derive(Clone)] #[derive(Clone)]
pub struct ValidatorSets<'serai>(pub(super) &'serai TemporalSerai<'serai>); pub struct ValidatorSets(pub(super) Events);
impl<'serai> ValidatorSets<'serai> { impl ValidatorSets {
/// The events from the validator sets module. /// The events from the validator sets module.
pub async fn events(&self) -> Result<Vec<Event>, RpcError> { pub fn events(&self) -> impl Iterator<Item = &Event> {
Ok( self.0.events().flatten().filter_map(|event| match event {
self serai_abi::Event::ValidatorSets(event) => Some(event),
.0 _ => None,
.events_borrowed() })
.await?
.as_ref()
.expect("`TemporalSerai::events` returned None")
.iter()
.flat_map(IntoIterator::into_iter)
.filter_map(|event| match event {
serai_abi::Event::ValidatorSets(event) => Some(event.clone()),
_ => None,
})
.collect(),
)
} }
/// The `SetDecided` events from the validator sets module. /// The `SetDecided` events from the validator sets module.
pub async fn set_decided_events(&self) -> Result<Vec<Event>, RpcError> { pub fn set_decided_events(&self) -> impl Iterator<Item = &Event> {
Ok( self.events().filter(|event| matches!(event, Event::SetDecided { .. }))
self
.events()
.await?
.into_iter()
.filter(|event| matches!(event, Event::SetDecided { .. }))
.collect(),
)
} }
/// The `SetKeys` events from the validator sets module. /// The `SetKeys` events from the validator sets module.
pub async fn set_keys_events(&self) -> Result<Vec<Event>, RpcError> { pub fn set_keys_events(&self) -> impl Iterator<Item = &Event> {
Ok( self.events().filter(|event| matches!(event, Event::SetKeys { .. }))
self
.events()
.await?
.into_iter()
.filter(|event| matches!(event, Event::SetKeys { .. }))
.collect(),
)
} }
/// The `AcceptedHandover` events from the validator sets module. /// The `AcceptedHandover` events from the validator sets module.
pub async fn accepted_handover_events(&self) -> Result<Vec<Event>, RpcError> { pub fn accepted_handover_events(&self) -> impl Iterator<Item = &Event> {
Ok( self.events().filter(|event| matches!(event, Event::AcceptedHandover { .. }))
self
.events()
.await?
.into_iter()
.filter(|event| matches!(event, Event::AcceptedHandover { .. }))
.collect(),
)
} }
/// The `SlashReport` events from the validator sets module. /// The `SlashReport` events from the validator sets module.
pub async fn slash_report_events(&self) -> Result<Vec<Event>, RpcError> { pub fn slash_report_events(&self) -> impl Iterator<Item = &Event> {
Ok( self.events().filter(|event| matches!(event, Event::SlashReport { .. }))
self
.events()
.await?
.into_iter()
.filter(|event| matches!(event, Event::SlashReport { .. }))
.collect(),
)
} }
/// The current session for the specified network. /// The `SetEmbeddedEllipticCurveKeys` events from the validator sets module.
pub async fn current_session(&self, network: NetworkId) -> Result<Option<Session>, RpcError> { pub fn set_embedded_elliptic_curve_keys_events(&self) -> impl Iterator<Item = &Event> {
Ok( self.events().filter(|event| matches!(event, Event::SetEmbeddedEllipticCurveKeys { .. }))
self
.0
.call::<Option<_>>(
"validator-sets/current_session",
&format!(r#", "network": {} "#, rpc_network(network)?),
)
.await?
.map(Session),
)
} }
/// The stake for the current validators for the specified network. /// The `Allocation` events from the validator sets module.
pub async fn current_stake(&self, network: NetworkId) -> Result<Option<Amount>, RpcError> { pub fn allocation_events(&self) -> impl Iterator<Item = &Event> {
Ok( self.events().filter(|event| matches!(event, Event::Allocation { .. }))
self
.0
.call::<Option<_>>(
"validator-sets/current_stake",
&format!(r#", "network": {} "#, rpc_network(network)?),
)
.await?
.map(Amount),
)
} }
/// The keys for the specified validator set. /// The `Deallocation` events from the validator sets module.
pub async fn keys(&self, set: ExternalValidatorSet) -> Result<Option<KeyPair>, RpcError> { pub fn deallocation_events(&self) -> impl Iterator<Item = &Event> {
let Some(key_pair) = self self.events().filter(|event| matches!(event, Event::Deallocation { .. }))
.0
.call::<Option<String>>(
"validator-sets/keys",
&format!(r#", "network": {}, "session": {} "#, rpc_network(set.network)?, set.session.0),
)
.await?
else {
return Ok(None);
};
KeyPair::deserialize(
&mut hex::decode(key_pair)
.map_err(|_| RpcError::InvalidNode("validator set's keys weren't valid hex".to_string()))?
.as_slice(),
)
.map(Some)
.map_err(|_| RpcError::InvalidNode("validator set's keys weren't a valid key pair".to_string()))
} }
/// The current validators for the specified network. /// The `DelayedDeallocationClaimed` events from the validator sets module.
pub async fn current_validators( pub fn delayed_deallocation_claimed_events(&self) -> impl Iterator<Item = &Event> {
&self, self.events().filter(|event| matches!(event, Event::DelayedDeallocationClaimed { .. }))
network: NetworkId,
) -> Result<Option<Vec<SeraiAddress>>, RpcError> {
self
.0
.call::<Option<Vec<String>>>(
"validator-sets/current_validators",
&format!(r#", "network": {} "#, rpc_network(network)?),
)
.await?
.map(|validators| {
validators
.into_iter()
.map(|addr| {
SeraiAddress::from_str(&addr)
.map_err(|_| RpcError::InvalidNode("validator's address was invalid".to_string()))
})
.collect()
})
.transpose()
}
/// If the prior validators for this network is still expected to publish a slash report.
pub async fn pending_slash_report(&self, network: ExternalNetworkId) -> Result<bool, RpcError> {
self
.0
.call(
"validator-sets/pending_slash_report",
&format!(r#", "network": {} "#, rpc_network(network)?),
)
.await
}
/// The key on an embedded elliptic curve for the specified validator.
pub async fn embedded_elliptic_curve_keys(
&self,
validator: SeraiAddress,
network: ExternalNetworkId,
) -> Result<Option<EmbeddedEllipticCurveKeys>, RpcError> {
let Some(keys) = self
.0
.call::<Option<String>>(
"validator-sets/embedded_elliptic_curve_keys",
&format!(r#", "validator": {validator}, "network": {} "#, rpc_network(network)?),
)
.await?
else {
return Ok(None);
};
EmbeddedEllipticCurveKeys::deserialize(
&mut hex::decode(keys)
.map_err(|_| {
RpcError::InvalidNode(
"validator's embedded elliptic curve keys weren't valid hex".to_string(),
)
})?
.as_slice(),
)
.map(Some)
.map_err(|_| {
RpcError::InvalidNode("validator's embedded elliptic curve keys weren't valid".to_string())
})
} }
/// Create a transaction to set a validator set's keys. /// Create a transaction to set a validator set's keys.
@@ -247,3 +112,114 @@ impl<'serai> ValidatorSets<'serai> {
} }
} }
} }
impl<'serai> State<'serai> {
/// The current session for the specified network.
pub async fn current_session(&self, network: NetworkId) -> Result<Option<Session>, RpcError> {
Ok(
self
.call::<Option<_>>(
"validator-sets/current_session",
&format!(r#", "network": {} "#, rpc_network(network)),
)
.await?
.map(Session),
)
}
/// The stake for the current validators for the specified network.
pub async fn current_stake(&self, network: NetworkId) -> Result<Option<Amount>, RpcError> {
Ok(
self
.call::<Option<_>>(
"validator-sets/current_stake",
&format!(r#", "network": {} "#, rpc_network(network)),
)
.await?
.map(Amount),
)
}
/// The keys for the specified validator set.
pub async fn keys(&self, set: ExternalValidatorSet) -> Result<Option<KeyPair>, RpcError> {
let Some(key_pair) = self
.call::<Option<String>>(
"validator-sets/keys",
&format!(r#", "network": {}, "session": {} "#, rpc_network(set.network), set.session.0),
)
.await?
else {
return Ok(None);
};
KeyPair::deserialize(
&mut hex::decode(key_pair)
.map_err(|_| RpcError::InvalidNode("validator set's keys weren't valid hex".to_string()))?
.as_slice(),
)
.map(Some)
.map_err(|_| RpcError::InvalidNode("validator set's keys weren't a valid key pair".to_string()))
}
/// The current validators for the specified network.
pub async fn current_validators(
&self,
network: NetworkId,
) -> Result<Option<Vec<SeraiAddress>>, RpcError> {
self
.call::<Option<Vec<String>>>(
"validator-sets/current_validators",
&format!(r#", "network": {} "#, rpc_network(network)),
)
.await?
.map(|validators| {
validators
.into_iter()
.map(|addr| {
SeraiAddress::from_str(&addr)
.map_err(|_| RpcError::InvalidNode("validator's address was invalid".to_string()))
})
.collect()
})
.transpose()
}
/// If the prior validators for this network is still expected to publish a slash report.
pub async fn pending_slash_report(&self, network: ExternalNetworkId) -> Result<bool, RpcError> {
self
.call(
"validator-sets/pending_slash_report",
&format!(r#", "network": {} "#, rpc_network(network)),
)
.await
}
/// The key on an embedded elliptic curve for the specified validator.
pub async fn embedded_elliptic_curve_keys(
&self,
validator: SeraiAddress,
network: ExternalNetworkId,
) -> Result<Option<EmbeddedEllipticCurveKeys>, RpcError> {
let Some(keys) = self
.call::<Option<String>>(
"validator-sets/embedded_elliptic_curve_keys",
&format!(r#", "validator": "{validator}", "network": {} "#, rpc_network(network)),
)
.await?
else {
return Ok(None);
};
EmbeddedEllipticCurveKeys::deserialize(
&mut hex::decode(keys)
.map_err(|_| {
RpcError::InvalidNode(
"validator's embedded elliptic curve keys weren't valid hex".to_string(),
)
})?
.as_slice(),
)
.map(Some)
.map_err(|_| {
RpcError::InvalidNode("validator's embedded elliptic curve keys weren't valid".to_string())
})
}
}

View File

@@ -133,7 +133,11 @@ async fn blockchain() {
.chain(block.transactions.iter().map(serai_abi::Transaction::hash)) .chain(block.transactions.iter().map(serai_abi::Transaction::hash))
.chain(core::iter::once(end_transaction)); .chain(core::iter::once(end_transaction));
let events = serai.as_of(block.header.hash()).await.unwrap().events().await.unwrap(); let events = serai.events(block.header.hash()).await.unwrap();
let events = events
.events()
.map(|iter| iter.into_iter().cloned().collect::<Vec<_>>())
.collect::<Vec<_>>();
assert_eq!(events.len(), 2 + block.transactions.len()); assert_eq!(events.len(), 2 + block.transactions.len());
let mut transaction_leaves = vec![]; let mut transaction_leaves = vec![];

View File

@@ -3,6 +3,7 @@ use serai_abi::{
address::SeraiAddress, address::SeraiAddress,
network_id::{ExternalNetworkId, NetworkId}, network_id::{ExternalNetworkId, NetworkId},
balance::Amount, balance::Amount,
crypto::EmbeddedEllipticCurveKeys,
validator_sets::{Session, ExternalValidatorSet, ValidatorSet, KeyShares}, validator_sets::{Session, ExternalValidatorSet, ValidatorSet, KeyShares},
}, },
validator_sets::Event, validator_sets::Event,
@@ -44,67 +45,94 @@ async fn validator_sets() {
panic!("finalized block remained the genesis block for over five minutes"); panic!("finalized block remained the genesis block for over five minutes");
}; };
// The genesis block should have the expected events
{ {
use sp_core::{Pair as _, sr25519::Pair}; use sp_core::{Pair as _, sr25519::Pair};
let genesis_validators = vec![( let genesis_validators = vec![(
SeraiAddress::from(Pair::from_string("//Alice", None).unwrap().public()), SeraiAddress::from(Pair::from_string("//Alice", None).unwrap().public()),
KeyShares(1), KeyShares(1),
)]; )];
// The genesis block should have the expected events
{ {
let mut events = serai let events = serai
.as_of(serai.block_by_number(0).await.unwrap().unwrap().header.hash()) .events(serai.block_by_number(0).await.unwrap().unwrap().header.hash())
.await .await
.unwrap() .unwrap()
.validator_sets() .validator_sets()
.set_decided_events() .set_embedded_elliptic_curve_keys_events()
.await .cloned()
.unwrap(); .collect::<Vec<_>>();
events.sort_by_key(|event| borsh::to_vec(event).unwrap()); assert_eq!(events.len(), ExternalNetworkId::all().collect::<Vec<_>>().len());
let mut expected = vec![
Event::SetDecided { let state = serai.state().await.unwrap();
set: ValidatorSet { network: NetworkId::Serai, session: Session(0) }, for (event, network) in events.into_iter().zip(ExternalNetworkId::all()) {
validators: genesis_validators.clone(), assert_eq!(
}, event,
Event::SetDecided { Event::SetEmbeddedEllipticCurveKeys {
set: ValidatorSet { network: NetworkId::Serai, session: Session(1) }, validator: genesis_validators[0].0,
validators: genesis_validators.clone(), keys: state
}, .embedded_elliptic_curve_keys(genesis_validators[0].0, network)
Event::SetDecided { .await
set: ValidatorSet { .unwrap()
network: NetworkId::External(ExternalNetworkId::Bitcoin), .unwrap(),
session: Session(0), }
);
}
}
{
assert_eq!(
serai
.events(serai.block_by_number(0).await.unwrap().unwrap().header.hash())
.await
.unwrap()
.validator_sets()
.set_decided_events()
.cloned()
.collect::<Vec<_>>(),
vec![
Event::SetDecided {
set: ValidatorSet { network: NetworkId::Serai, session: Session(0) },
validators: genesis_validators.clone(),
}, },
validators: genesis_validators.clone(), Event::SetDecided {
}, set: ValidatorSet {
Event::SetDecided { network: NetworkId::External(ExternalNetworkId::Bitcoin),
set: ValidatorSet { session: Session(0)
network: NetworkId::External(ExternalNetworkId::Ethereum), },
session: Session(0), validators: genesis_validators.clone(),
}, },
validators: genesis_validators.clone(), Event::SetDecided {
}, set: ValidatorSet {
Event::SetDecided { network: NetworkId::External(ExternalNetworkId::Ethereum),
set: ValidatorSet { session: Session(0)
network: NetworkId::External(ExternalNetworkId::Monero), },
session: Session(0), validators: genesis_validators.clone(),
}, },
validators: genesis_validators.clone(), Event::SetDecided {
}, set: ValidatorSet {
]; network: NetworkId::External(ExternalNetworkId::Monero),
expected.sort_by_key(|event| borsh::to_vec(event).unwrap()); session: Session(0)
assert_eq!(events, expected); },
validators: genesis_validators.clone(),
},
Event::SetDecided {
set: ValidatorSet { network: NetworkId::Serai, session: Session(1) },
validators: genesis_validators.clone(),
},
]
);
} }
assert_eq!( assert_eq!(
serai serai
.as_of(serai.block_by_number(0).await.unwrap().unwrap().header.hash()) .events(serai.block_by_number(0).await.unwrap().unwrap().header.hash())
.await .await
.unwrap() .unwrap()
.validator_sets() .validator_sets()
.accepted_handover_events() .accepted_handover_events()
.await .cloned()
.unwrap(), .collect::<Vec<_>>(),
vec![Event::AcceptedHandover { vec![Event::AcceptedHandover {
set: ValidatorSet { network: NetworkId::Serai, session: Session(0) } set: ValidatorSet { network: NetworkId::Serai, session: Session(0) }
}] }]
@@ -115,34 +143,30 @@ async fn validator_sets() {
{ {
assert_eq!( assert_eq!(
serai serai
.as_of(serai.block_by_number(1).await.unwrap().unwrap().header.hash()) .events(serai.block_by_number(1).await.unwrap().unwrap().header.hash())
.await .await
.unwrap() .unwrap()
.validator_sets() .validator_sets()
.set_decided_events() .set_decided_events()
.await .cloned()
.unwrap(), .collect::<Vec<_>>(),
vec![], vec![],
); );
assert_eq!( assert_eq!(
serai serai
.as_of(serai.block_by_number(1).await.unwrap().unwrap().header.hash()) .events(serai.block_by_number(1).await.unwrap().unwrap().header.hash())
.await .await
.unwrap() .unwrap()
.validator_sets() .validator_sets()
.accepted_handover_events() .accepted_handover_events()
.await .cloned()
.unwrap(), .collect::<Vec<_>>(),
vec![], vec![],
); );
} }
{ {
let serai = serai let serai = serai.state().await.unwrap();
.as_of(serai.block_by_number(0).await.unwrap().unwrap().header.hash())
.await
.unwrap();
let serai = serai.validator_sets();
for network in NetworkId::all() { for network in NetworkId::all() {
match network { match network {
NetworkId::Serai => { NetworkId::Serai => {

View File

@@ -4,7 +4,7 @@ version = "0.1.0"
description = "Coins pallet for Serai" description = "Coins pallet for Serai"
license = "AGPL-3.0-only" license = "AGPL-3.0-only"
repository = "https://github.com/serai-dex/serai/tree/develop/substrate/coins" repository = "https://github.com/serai-dex/serai/tree/develop/substrate/coins"
authors = ["Akil Demir <akildemir72@gmail.com>"] authors = ["Luke Parker <lukeparker5132@gmail.com>"]
edition = "2021" edition = "2021"
rust-version = "1.85" rust-version = "1.85"
@@ -18,10 +18,10 @@ workspace = true
[dependencies] [dependencies]
scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] } scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] }
sp-core = { git = "https://github.com/serai-dex/patch-polkadot-sdk", rev = "e01101b68c5b0f588dd4cdee48f801a2c1f75b84", default-features = false } sp-core = { git = "https://github.com/serai-dex/patch-polkadot-sdk", default-features = false }
frame-system = { git = "https://github.com/serai-dex/patch-polkadot-sdk", rev = "e01101b68c5b0f588dd4cdee48f801a2c1f75b84", default-features = false } frame-system = { git = "https://github.com/serai-dex/patch-polkadot-sdk", default-features = false }
frame-support = { git = "https://github.com/serai-dex/patch-polkadot-sdk", rev = "e01101b68c5b0f588dd4cdee48f801a2c1f75b84", default-features = false } frame-support = { git = "https://github.com/serai-dex/patch-polkadot-sdk", default-features = false }
serai-abi = { path = "../abi", default-features = false, features = ["substrate"] } serai-abi = { path = "../abi", default-features = false, features = ["substrate"] }
serai-core-pallet = { path = "../core", default-features = false } serai-core-pallet = { path = "../core", default-features = false }
@@ -29,9 +29,9 @@ serai-core-pallet = { path = "../core", default-features = false }
[dev-dependencies] [dev-dependencies]
borsh = { version = "1", default-features = false, features = ["std", "derive", "de_strict_order"] } borsh = { version = "1", default-features = false, features = ["std", "derive", "de_strict_order"] }
sp-io = { git = "https://github.com/serai-dex/patch-polkadot-sdk", rev = "e01101b68c5b0f588dd4cdee48f801a2c1f75b84", default-features = false, features = ["std"] } sp-io = { git = "https://github.com/serai-dex/patch-polkadot-sdk", default-features = false, features = ["std"] }
pallet-timestamp = { git = "https://github.com/serai-dex/patch-polkadot-sdk", rev = "e01101b68c5b0f588dd4cdee48f801a2c1f75b84", default-features = false, features = ["std"] } pallet-timestamp = { git = "https://github.com/serai-dex/patch-polkadot-sdk", default-features = false, features = ["std"] }
[features] [features]
std = [ std = [

View File

@@ -227,6 +227,13 @@ mod pallet {
Self::emit_event(Event::Transfer { from: from.into(), to: to.into(), coins }); Self::emit_event(Event::Transfer { from: from.into(), to: to.into(), coins });
Ok(()) Ok(())
} }
/// Burn `coins` from `from`.
pub fn burn_fn(from: Public, coins: Balance) -> Result<(), Error<T, I>> {
Self::burn_internal(from, coins)?;
Self::emit_event(Event::Burn { from: from.into(), coins });
Ok(())
}
} }
#[pallet::call] #[pallet::call]
@@ -245,8 +252,7 @@ mod pallet {
#[pallet::weight((0, DispatchClass::Normal))] // TODO #[pallet::weight((0, DispatchClass::Normal))] // TODO
pub fn burn(origin: OriginFor<T>, coins: Balance) -> DispatchResult { pub fn burn(origin: OriginFor<T>, coins: Balance) -> DispatchResult {
let from = ensure_signed(origin)?; let from = ensure_signed(origin)?;
Self::burn_internal(from, coins)?; Self::burn_fn(from, coins)?;
Self::emit_event(Event::Burn { from: from.into(), coins });
Ok(()) Ok(())
} }

View File

@@ -1,4 +1,4 @@
//! Test environment for Coins pallet. //! Test environment for the Coins pallet.
use borsh::BorshDeserialize; use borsh::BorshDeserialize;
@@ -21,6 +21,8 @@ impl frame_system::Config for Test {
type AccountId = sp_core::sr25519::Public; type AccountId = sp_core::sr25519::Public;
type Lookup = frame_support::sp_runtime::traits::IdentityLookup<Self::AccountId>; type Lookup = frame_support::sp_runtime::traits::IdentityLookup<Self::AccountId>;
type Block = frame_system::mocking::MockBlock<Test>; type Block = frame_system::mocking::MockBlock<Test>;
type BlockLength = serai_core_pallet::Limits;
type BlockWeights = serai_core_pallet::Limits;
} }
#[derive_impl(pallet_timestamp::config_preludes::TestDefaultConfig)] #[derive_impl(pallet_timestamp::config_preludes::TestDefaultConfig)]

View File

@@ -10,7 +10,7 @@ pub type CoinsEvent = serai_abi::coins::Event;
#[test] #[test]
fn mint() { fn mint() {
new_test_ext().execute_with(|| { new_test_ext().execute_with(|| {
Core::start_transaction(); Core::start_transaction(0);
// minting u64::MAX should work // minting u64::MAX should work
let coin = Coin::Serai; let coin = Coin::Serai;
@@ -51,7 +51,7 @@ fn mint() {
#[test] #[test]
fn burn_with_instruction() { fn burn_with_instruction() {
new_test_ext().execute_with(|| { new_test_ext().execute_with(|| {
Core::start_transaction(); Core::start_transaction(0);
// mint some coin // mint some coin
let coin = Coin::External(ExternalCoin::Bitcoin); let coin = Coin::External(ExternalCoin::Bitcoin);
@@ -106,7 +106,7 @@ fn burn_with_instruction() {
#[test] #[test]
fn transfer() { fn transfer() {
new_test_ext().execute_with(|| { new_test_ext().execute_with(|| {
Core::start_transaction(); Core::start_transaction(0);
// mint some coin // mint some coin
let coin = Coin::External(ExternalCoin::Bitcoin); let coin = Coin::External(ExternalCoin::Bitcoin);

View File

@@ -20,12 +20,12 @@ borsh = { version = "1", default-features = false, features = ["derive", "de_str
scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] } scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] }
sp-core = { git = "https://github.com/serai-dex/patch-polkadot-sdk", rev = "e01101b68c5b0f588dd4cdee48f801a2c1f75b84", default-features = false } sp-core = { git = "https://github.com/serai-dex/patch-polkadot-sdk", default-features = false }
frame-system = { git = "https://github.com/serai-dex/patch-polkadot-sdk", rev = "e01101b68c5b0f588dd4cdee48f801a2c1f75b84", default-features = false } frame-system = { git = "https://github.com/serai-dex/patch-polkadot-sdk", default-features = false }
frame-support = { git = "https://github.com/serai-dex/patch-polkadot-sdk", rev = "e01101b68c5b0f588dd4cdee48f801a2c1f75b84", default-features = false } frame-support = { git = "https://github.com/serai-dex/patch-polkadot-sdk", default-features = false }
pallet-timestamp = { git = "https://github.com/serai-dex/patch-polkadot-sdk", rev = "e01101b68c5b0f588dd4cdee48f801a2c1f75b84", default-features = false } pallet-timestamp = { git = "https://github.com/serai-dex/patch-polkadot-sdk", default-features = false }
serai-abi = { path = "../abi", default-features = false, features = ["substrate"] } serai-abi = { path = "../abi", default-features = false, features = ["substrate"] }

View File

@@ -8,6 +8,9 @@ extern crate alloc;
use frame_support::traits::{PreInherents, PostTransactions}; use frame_support::traits::{PreInherents, PostTransactions};
mod limits;
pub use limits::Limits;
mod iumt; mod iumt;
pub use iumt::*; pub use iumt::*;
@@ -83,7 +86,8 @@ pub mod pallet {
#[pallet::config] #[pallet::config]
pub trait Config: pub trait Config:
frame_system::Config<Hash: Into<[u8; 32]>> + pallet_timestamp::Config<Moment = u64> frame_system::Config<Hash: Into<[u8; 32]>, BlockLength = Limits, BlockWeights = Limits>
+ pallet_timestamp::Config<Moment = u64>
{ {
} }
@@ -120,7 +124,7 @@ pub mod pallet {
BlockTransactionsCommitmentMerkle::<T>::new_expecting_none(); BlockTransactionsCommitmentMerkle::<T>::new_expecting_none();
BlockEventsCommitmentMerkle::<T>::new_expecting_none(); BlockEventsCommitmentMerkle::<T>::new_expecting_none();
Self::start_transaction(); Self::start_transaction(0);
<_>::build(config); <_>::build(config);
Self::end_transaction([0; 32]); Self::end_transaction([0; 32]);
@@ -130,7 +134,15 @@ pub mod pallet {
/// The code to run when beginning execution of a transaction. /// The code to run when beginning execution of a transaction.
/// ///
/// The caller MUST ensure two transactions aren't simultaneously started. /// The caller MUST ensure two transactions aren't simultaneously started.
pub fn start_transaction() { pub fn start_transaction(len: usize) {
{
let existing_len = frame_system::AllExtrinsicsLen::<T>::get().unwrap_or(0);
let new_len = existing_len.saturating_add(u32::try_from(len).unwrap_or(u32::MAX));
// We panic here as this should've been caught earlier during validation
assert!(new_len <= u32::try_from(serai_abi::Block::SIZE_LIMIT).unwrap());
frame_system::AllExtrinsicsLen::<T>::set(Some(new_len));
}
TransactionEventsMerkle::<T>::new_expecting_none(); TransactionEventsMerkle::<T>::new_expecting_none();
Self::deposit_event(Event::BeginTransaction); Self::deposit_event(Event::BeginTransaction);
} }
@@ -192,7 +204,21 @@ impl<T: Config> PreInherents for StartOfBlock<T> {
BlockTransactionsCommitmentMerkle::<T>::new_expecting_none(); BlockTransactionsCommitmentMerkle::<T>::new_expecting_none();
BlockEventsCommitmentMerkle::<T>::new_expecting_none(); BlockEventsCommitmentMerkle::<T>::new_expecting_none();
Pallet::<T>::start_transaction(); /*
We assign the implicit transaction with the block the length of the block itself: its
header's length and the length of the length-prefix for the list of transactions.
The length-prefix will be a little-endian `u32`, as `Block` will be borsh-serialized
(https://borsh.io).
The length of each actual transaction is expected to be accurate as the SCALE implementation
defers to the `borsh` serialization.
*/
assert!(
frame_system::AllExtrinsicsLen::<T>::get().is_none(),
"AllExtrinsicsLen wasn't killed at the end of the last block"
);
Pallet::<T>::start_transaction(serai_abi::Header::SIZE + 4);
// Handle the `SeraiPreExecutionDigest` // Handle the `SeraiPreExecutionDigest`
/* /*
@@ -220,7 +246,7 @@ impl<T: Config> PreInherents for StartOfBlock<T> {
pub struct EndOfBlock<T: Config>(PhantomData<T>); pub struct EndOfBlock<T: Config>(PhantomData<T>);
impl<T: Config> PostTransactions for EndOfBlock<T> { impl<T: Config> PostTransactions for EndOfBlock<T> {
fn post_transactions() { fn post_transactions() {
Pallet::<T>::start_transaction(); Pallet::<T>::start_transaction(0);
// Other modules' `PostTransactions` // Other modules' `PostTransactions`
@@ -229,6 +255,8 @@ impl<T: Config> PostTransactions for EndOfBlock<T> {
end_of_block_transaction_hash[.. 16].copy_from_slice(&[0xff; 16]); end_of_block_transaction_hash[.. 16].copy_from_slice(&[0xff; 16]);
Pallet::<T>::end_transaction(end_of_block_transaction_hash); Pallet::<T>::end_transaction(end_of_block_transaction_hash);
frame_system::AllExtrinsicsLen::<T>::kill();
use serai_abi::SeraiExecutionDigest; use serai_abi::SeraiExecutionDigest;
frame_system::Pallet::<T>::deposit_log( frame_system::Pallet::<T>::deposit_log(
frame_support::sp_runtime::generic::DigestItem::Consensus( frame_support::sp_runtime::generic::DigestItem::Consensus(

Some files were not shown because too many files have changed in this diff Show More