8 Commits

Author SHA1 Message Date
Luke Parker
ee9b9778b5 Patch lazy_static to std::sync::LazyLock
This does remove the no-`std` variant of `lazy_static`, but that was unused and
`std` was not additively implemented (making it poor to work with).

This primarily tidies our `deny.toml` with one less `git` dependency.
2025-12-11 03:50:29 -05:00
Luke Parker
5a3cf1f2be Dispatch InInstruction as expected 2025-12-11 03:45:17 -05:00
Luke Parker
2fbe925c4d Expand the stack size CI with macOS, oksh, and osh
Fixes the installation of `gash`.

Attempted `posh` on macOS and `mrsh`, leading to
https://github.com/serai-dex/serai/issues/703 and
https://github.com/serai-dex/serai/issues/704 respectively. Attempted `gosh`
leading to https://github.com/u-root/u-root/issues/3474. Attempted `nsh` but
hit https://github.com/nuta/nsh/issues/49 (while `nsh` appears no longer under
development, meaning that's unlikely to be fixed).

A future improvement would be to provide `elf.h` on macOS, enabling using
`chelf` and restoring it as the source of truth.
2025-12-10 02:04:16 -05:00
Luke Parker
6aad496d86 find -name > find -iname 2025-12-10 01:56:38 -05:00
Luke Parker
d3464cfcb3 Extend Monero action with support for macOS 2025-12-09 23:07:29 -05:00
Luke Parker
8dbea8452d Intentionally attempt to produce finalizations every single block
While aggressive, the existing value (given the documentation for it) is far
too large to be reasonable here.
2025-12-09 21:59:20 -05:00
Luke Parker
f94b7ca50e Add verification of SignedBatch to the in-instructions pallet 2025-12-09 21:58:18 -05:00
Luke Parker
5e39f9bc1e Add gash as a shell tested with
Its notable as shell used within Guix.
2025-12-09 20:26:23 -05:00
17 changed files with 484 additions and 79 deletions

View File

@@ -19,25 +19,32 @@ runs:
- name: Download the Monero Daemon
if: steps.cache-monerod.outputs.cache-hit != 'true'
# Calculates OS/ARCH to demonstrate it, yet then locks to linux-x64 due
# to the contained folder not following the same naming scheme and
# requiring further expansion not worth doing right now
shell: bash
run: |
RUNNER_OS=${{ runner.os }}
RUNNER_ARCH=${{ runner.arch }}
OS=${{ runner.os }}
ARCH=${{ runner.arch }}
RUNNER_OS=${RUNNER_OS,,}
RUNNER_ARCH=${RUNNER_ARCH,,}
OS=$(echo "$OS" | tr "[:upper:]" "[:lower:]")
ARCH=$(echo "$ARCH" | tr "[:upper:]" "[:lower:]")
RUNNER_OS=linux
RUNNER_ARCH=x64
if [ "$OS" = "windows" ]; then
OS=win
echo "Windows is unsupported at this time"
exit 1
fi
if [ "$OS" = "macos" ]; then
OS=mac
fi
if [ "$ARCH" = "arm64" ]; then
ARCH=armv8
fi
FILE=monero-$RUNNER_OS-$RUNNER_ARCH-${{ inputs.version }}.tar.bz2
FILE=monero-$OS-$ARCH-${{ inputs.version }}.tar.bz2
wget https://downloads.getmonero.org/cli/$FILE
tar -xvf $FILE
rm $FILE
sudo mv monero-x86_64-linux-gnu-${{ inputs.version }}/monerod /usr/bin/monerod
sudo mv $(find . -name monerod) /usr/bin/monerod
sudo chmod 777 /usr/bin/monerod
sudo chmod +x /usr/bin/monerod

View File

@@ -12,7 +12,7 @@ jobs:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # 6.0.0
- name: Install cargo deny
run: cargo +1.91.1 install cargo-deny --version =0.18.8
run: cargo +1.91.1 install cargo-deny --version =0.18.9
- name: Run cargo deny
run: cargo deny -L error --all-features check --hide-inclusion-graph

View File

@@ -46,7 +46,7 @@ jobs:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # 6.0.0
- name: Install cargo deny
run: cargo +1.91.1 install cargo-deny --version =0.18.8
run: cargo +1.91.1 install cargo-deny --version =0.18.9
- name: Run cargo deny
run: cargo deny -L error --all-features check --hide-inclusion-graph
@@ -74,7 +74,7 @@ jobs:
cache: false
- name: Run forge fmt
run: FOUNDRY_FMT_SORT_INPUTS=false FOUNDRY_FMT_LINE_LENGTH=100 FOUNDRY_FMT_TAB_WIDTH=2 FOUNDRY_FMT_BRACKET_SPACING=true FOUNDRY_FMT_INT_TYPES=preserve forge fmt --check $(find . -iname "*.sol")
run: FOUNDRY_FMT_SORT_INPUTS=false FOUNDRY_FMT_LINE_LENGTH=100 FOUNDRY_FMT_TAB_WIDTH=2 FOUNDRY_FMT_BRACKET_SPACING=true FOUNDRY_FMT_INT_TYPES=preserve forge fmt --check $(find . -name "*.sol")
machete:
runs-on: ubuntu-latest
@@ -209,6 +209,6 @@ jobs:
- name: shellcheck
run: |
sudo apt install -y shellcheck
find . -iname "*.sh" | while read -r script; do
find . -name "*.sh" | while read -r script; do
shellcheck --enable=all --shell=sh --severity=info $script
done

View File

@@ -16,22 +16,99 @@ jobs:
stack_size:
strategy:
matrix:
os: [ubuntu-latest, ubuntu-24.04, ubuntu-22.04]
os: [ubuntu-latest, ubuntu-24.04, ubuntu-22.04, macos-15-intel, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # 6.0.0
- name: Download Monero
uses: ./.github/actions/monero
- name: Install Go
uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # 6.1.0
with:
go-version: stable
- name: Monero Daemon Cache
id: cache-monerod
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # 4.2.4
with:
path: monerod
key: stack-size-monerod
- name: Download the Monero Daemon
if: steps.cache-monerod.outputs.cache-hit != 'true'
run: |
# We explicitly download the Linux binary as this script executes over an ELF binary
wget https://downloads.getmonero.org/cli/monero-linux-x64-v0.18.4.4.tar.bz2
tar -xvf monero-linux-x64-v0.18.4.4.tar.bz2
mv $(find . -name monerod) .
- name: Verify expected behavior
shell: bash
run: |
cp /usr/bin/monerod monerod
STACK=$((8 * 1024 * 1024))
OS=${{ runner.os }}
if [ "$OS" = "Linux" ]; then
sudo apt update -y
sudo apt install -y ksh bash dash zsh busybox posh mksh yash
sudo ln -s "$(which busybox)" /usr/bin/ash
sudo ln -s "$(which busybox)" /usr/bin/hush
wget http://ftp.us.debian.org/debian/pool/main/g/gash/gash_0.3.1-1_amd64.deb
sudo apt install ./gash_0.3.1-1_amd64.deb
SHELLS="sh ksh bash dash zsh ash hush posh mksh lksh gash yash"
fi
if [ "$OS" = "macOS" ]; then
brew install binutils # `readelf`
# `binutils` is not placed within the path, so find its
# `readelf` bin and manually move it into our path
HOMEBREW_ROOT_PATH=/opt/homebrew # Apple Silicon
if [ $(uname -m) = "x86_64" ]; then HOMEBREW_ROOT_PATH=/usr/local; fi # Intel
sudo cp $(find "$HOMEBREW_ROOT_PATH" -name readelf) /usr/local/bin/
# macOS has the benefit of packaging `oksh`, `osh`, and having distinct core tools
# TODO: `posh` is packaged but doesn't work: https://github.com/serai-dex/serai/issues/703
brew install ksh93 bash dash-shell zsh mksh oksh yash oils-for-unix
SHELLS="sh ksh bash dash zsh mksh oksh yash osh"
# macOS also has the benefit of packaging (via MacPorts) `mrsh`,
# which explicitly attempts to be be exactly POSIX, without any extensions.
# We first have to install MacPorts, the easiest method being via source.
curl -O https://distfiles.macports.org/MacPorts/MacPorts-2.11.6.tar.bz2
tar xf MacPorts-2.11.6.tar.bz2
cd MacPorts-2.11.6
./configure
make
sudo make install
cd ..
PATH=$PATH:/opt/local/bin
sudo port -v selfupdate
# Now, we install `mrsh`
# TODO: https://github.com/serai-dex/serai/issues/704
# sudo port install mrsh
# SHELLS="$SHELLS mrsh"
fi
# Install shells available via `cargo`
cargo install brush-shell
SHELLS="$SHELLS brush"
# We would also test with `nsh` here if not for https://github.com/nuta/nsh/issues/49
# cargo install nsh
# SHELLS="$SHELLS nsh"
# Install shells available via `go`
# TODO: https://github.com/u-root/u-root/issues/3474
# GOBIN=/usr/local/bin go install github.com/u-root/u-root/cmds/core/gosh@latest
# SHELLS="$SHELLS gosh"
# Patch with `muslstack`
cp monerod monerod-muslstack
GOBIN=$(pwd) go install github.com/yaegashi/muslstack@d19cc5866abce3ca59dfc1666df7cc97097d0933
./muslstack -s "$STACK" ./monerod-muslstack
# Patch with `chelf`, which only works on a Linux host (due to requiring `elf.h`)
# TODO: Install the header on macOS so `chelf` may be used as the source of truth
if [ "$OS" = "Linux" ]; then
cp monerod monerod-chelf
git clone https://github.com/Gottox/chelf
cd chelf
@@ -39,35 +116,31 @@ jobs:
make
./chelf -s "$STACK" ../monerod-chelf
cd ..
fi
cp monerod monerod-muslstack
GOBIN=$(pwd) go install github.com/yaegashi/muslstack@d19cc5866abce3ca59dfc1666df7cc97097d0933
./muslstack -s "$STACK" ./monerod-muslstack
sudo apt update -y
sudo apt install -y ksh bash dash zsh busybox mksh posh yash
sudo ln -s "$(which busybox)" /usr/bin/ash
sudo ln -s "$(which busybox)" /usr/bin/hush
cargo install brush-shell
for shell in sh ksh bash dash zsh ash hush mksh lksh posh yash brush; do
# Run our script with all installed shells
for shell in $SHELLS; do
echo "Executing \`$shell\`"
cp monerod monerod-idss-$shell
ln -s "$(which $shell)" sh
./sh ./orchestration/increase_default_stack_size.sh monerod-idss-$shell
rm ./sh
done
# Verify they all had the same result
sha256() {
sha256sum "$1" | cut -d' ' -f1
}
CHELF=$(sha256 monerod-chelf)
find . -iname "monerod-*" | while read -r bin; do
CHELF=$(sha256 monerod-muslstack)
find . -name "monerod-*" | while read -r bin; do
BIN=$(sha256 "$bin")
if [ ! "$CHELF" = "$BIN" ]; then
echo "Different artifact between monerod-chelf ($CHELF) and $bin ($BIN)"
echo "Different artifact between \`monerod-muslstack\` ($CHELF) and \`$bin\` ($BIN)"
exit 1
fi
done
# Verify the integrity of the result
read_stack() {
STACK_INFO=$(readelf "$1" -l | grep STACK -A1)
MEMSZ=$(printf "%s\n" "$STACK_INFO" | tail -n1 | sed -E s/^[[:space:]]*//g | cut -f2 -d' ')
@@ -79,14 +152,14 @@ jobs:
exit 2
fi
UPDATED_STACK=$(read_stack monerod-chelf)
UPDATED_STACK=$(read_stack monerod-muslstack)
if [ "$UPDATED_STACK" -ne "$STACK" ]; then
echo "Updated \`PT_GNU_STACK\` ($UPDATED_STACK) wasn't 8 MB ($STACK)"
exit 3
fi
# Only one byte should be different due to the bit pattern of 8 MB
BYTES_DIFFERENT=$(cmp -l monerod monerod-chelf | wc -l || true)
BYTES_DIFFERENT=$(cmp -l monerod monerod-muslstack | wc -l || true)
if [ "$BYTES_DIFFERENT" -ne 1 ]; then
echo "More than one byte was different between the two binaries"
exit 4

14
Cargo.lock generated
View File

@@ -92,9 +92,9 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]]
name = "alloy-chains"
version = "0.2.21"
version = "0.2.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b9ebac8ff9c2f07667e1803dc777304337e160ce5153335beb45e8ec0751808"
checksum = "35d744058a9daa51a8cf22a3009607498fcf82d3cf4c5444dd8056cdf651f471"
dependencies = [
"alloy-primitives",
"num_enum",
@@ -4148,8 +4148,7 @@ dependencies = [
[[package]]
name = "lazy_static"
version = "1.5.0"
source = "git+https://github.com/rust-lang-nursery/lazy-static.rs?rev=5735630d46572f1e5377c8f2ba0f79d18f53b10c#5735630d46572f1e5377c8f2ba0f79d18f53b10c"
version = "1.99.0"
[[package]]
name = "leb128fmt"
@@ -4321,9 +4320,9 @@ dependencies = [
[[package]]
name = "libp2p-identity"
version = "0.2.12"
version = "0.2.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3104e13b51e4711ff5738caa1fb54467c8604c2e94d607e27745bcf709068774"
checksum = "f0c7892c221730ba55f7196e98b0b8ba5e04b4155651736036628e9f73ed6fc3"
dependencies = [
"bs58",
"ed25519-dalek",
@@ -8433,6 +8432,7 @@ name = "serai-in-instructions-pallet"
version = "0.1.0"
dependencies = [
"bitvec",
"borsh",
"frame-support",
"frame-system",
"parity-scale-codec",
@@ -8441,7 +8441,9 @@ dependencies = [
"serai-core-pallet",
"serai-dex-pallet",
"serai-genesis-liquidity-pallet",
"serai-signals-pallet",
"serai-validator-sets-pallet",
"sp-application-crypto",
"sp-core",
]

View File

@@ -200,8 +200,9 @@ dalek-ff-group = { path = "patches/dalek-ff-group" }
minimal-ed448 = { path = "crypto/ed448" }
modular-frost = { path = "crypto/frost" }
# Patch due to `std` now including the required functionality
is_terminal_polyfill = { path = "./patches/is_terminal_polyfill" }
# Patches due to `std` now including the required functionality
is_terminal_polyfill = { path = "patches/is_terminal_polyfill" }
lazy_static = { path = "patches/lazy_static" }
# This has a non-deprecated `std` alternative since Rust's 2024 edition
home = { path = "patches/home" }
@@ -209,9 +210,6 @@ home = { path = "patches/home" }
darling = { path = "patches/darling" }
thiserror = { path = "patches/thiserror" }
# https://github.com/rust-lang-nursery/lazy-static.rs/issues/201
lazy_static = { git = "https://github.com/rust-lang-nursery/lazy-static.rs", rev = "5735630d46572f1e5377c8f2ba0f79d18f53b10c" }
# directories-next was created because directories was unmaintained
# directories-next is now unmaintained while directories is maintained
# The directories author pulls in ridiculously pointless crates and prefers

View File

@@ -147,7 +147,6 @@ unknown-registry = "deny"
unknown-git = "deny"
allow-registry = ["https://github.com/rust-lang/crates.io-index"]
allow-git = [
"https://github.com/rust-lang-nursery/lazy-static.rs",
"https://github.com/kayabaNerve/elliptic-curves",
"https://github.com/monero-oxide/monero-oxide",
"https://github.com/serai-dex/patch-polkadot-sdk",

View File

@@ -19,7 +19,7 @@ RUN wget -4 https://bitcoincore.org/bin/bitcoin-core-${BITCOIN_VERSION}/SHA256SU
# Verify all sigs and check for a valid signature from laanwj -- 71A3
RUN git clone https://github.com/bitcoin-core/guix.sigs && \
cd guix.sigs/builder-keys && \
find . -iname '*.gpg' -exec gpg --import {} \; && \
find . -name '*.gpg' -exec gpg --import {} \; && \
gpg --verify --status-fd 1 --verify ../../SHA256SUMS.asc ../../SHA256SUMS | grep "^\[GNUPG:\] VALIDSIG.*71A3B16735405025D447E8F274810B012346C9A6"
RUN grep bitcoin-${BITCOIN_VERSION}-$(uname -m)-linux-gnu.tar.gz SHA256SUMS | sha256sum -c

View File

@@ -0,0 +1,16 @@
[package]
name = "lazy_static"
version = "1.99.0"
description = "`lazy_static` which patches to `std::sync::LazyLock`"
license = "MIT"
repository = "https://github.com/serai-dex/serai/tree/develop/patches/lazy_static"
authors = ["Luke Parker <lukeparker5132@gmail.com>"]
keywords = []
edition = "2021"
rust-version = "1.80"
[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
[workspace]

View File

@@ -0,0 +1,14 @@
#[macro_export]
macro_rules! lazy_static {
($($(#[$attr: meta])* $vis: vis static ref $name: ident: $type: ty = $value: expr;)*) => {
$(
$(#[$attr])*
$vis static $name: std::sync::LazyLock<$type> = std::sync::LazyLock::new(|| $value);
)*
}
}
/// Explicitly initialize a static declared with [`lazy_static`].
pub fn initialize<T, F: Fn() -> T>(lazy: &std::sync::LazyLock<T, F>) {
std::sync::LazyLock::force(lazy);
}

View File

@@ -1,9 +1,15 @@
use borsh::{BorshSerialize, BorshDeserialize};
use serai_primitives::{
BlockHash, network_id::ExternalNetworkId, validator_sets::Session, instructions::SignedBatch,
BlockHash, network_id::ExternalNetworkId, validator_sets::Session, address::SeraiAddress,
instructions::SignedBatch,
};
/// The address used for executing `InInstruction`s.
pub fn address() -> SeraiAddress {
SeraiAddress::system(borsh::to_vec(b"InInstructions").unwrap())
}
/// A call to `InInstruction`s.
#[derive(Clone, PartialEq, Eq, Debug, BorshSerialize, BorshDeserialize)]
pub enum Call {

View File

@@ -21,8 +21,10 @@ workspace = true
[dependencies]
scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] }
borsh = { version = "1", default-features = false }
sp-core = { git = "https://github.com/serai-dex/patch-polkadot-sdk", default-features = false }
sp-application-crypto = { git = "https://github.com/serai-dex/patch-polkadot-sdk", 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", default-features = false }
@@ -32,14 +34,17 @@ serai-abi = { path = "../abi", default-features = false, features = ["substrate"
serai-core-pallet = { path = "../core", default-features = false }
serai-coins-pallet = { path = "../coins", default-features = false }
serai-validator-sets-pallet = { path = "../validator-sets", default-features = false }
serai-signals-pallet = { path = "../signals", default-features = false }
serai-dex-pallet = { path = "../dex", default-features = false }
serai-genesis-liquidity-pallet = { path = "../genesis-liquidity", default-features = false }
[features]
std = [
"scale/std",
"borsh/std",
"sp-core/std",
"sp-application-crypto/std",
"frame-system/std",
"frame-support/std",
@@ -49,6 +54,7 @@ std = [
"serai-core-pallet/std",
"serai-coins-pallet/std",
"serai-validator-sets-pallet/std",
"serai-signals-pallet/std",
"serai-dex-pallet/std",
"serai-genesis-liquidity-pallet/std",
]
@@ -61,6 +67,7 @@ try-runtime = [
"serai-core-pallet/try-runtime",
"serai-coins-pallet/try-runtime",
"serai-validator-sets-pallet/try-runtime",
"serai-signals-pallet/try-runtime",
"serai-dex-pallet/try-runtime",
"serai-genesis-liquidity-pallet/try-runtime",
]
@@ -72,6 +79,7 @@ runtime-benchmarks = [
"serai-core-pallet/runtime-benchmarks",
"serai-coins-pallet/runtime-benchmarks",
"serai-validator-sets-pallet/runtime-benchmarks",
"serai-signals-pallet/runtime-benchmarks",
"serai-dex-pallet/runtime-benchmarks",
"serai-genesis-liquidity-pallet/runtime-benchmarks",
]

View File

@@ -7,8 +7,11 @@ extern crate alloc;
#[expect(clippy::cast_possible_truncation)]
#[frame_support::pallet]
mod pallet {
use sp_core::sr25519::Public;
use sp_application_crypto::RuntimePublic;
use frame_support::{pallet_prelude::*, dispatch::RawOrigin};
use frame_system::pallet_prelude::*;
use frame_support::pallet_prelude::*;
use serai_abi::{primitives::prelude::*, in_instructions::Event};
@@ -26,6 +29,7 @@ mod pallet {
+ serai_core_pallet::Config
+ serai_coins_pallet::Config<serai_coins_pallet::CoinsInstance>
+ serai_validator_sets_pallet::Config
+ serai_signals_pallet::Config
+ serai_coins_pallet::Config<serai_coins_pallet::LiquidityTokensInstance>
+ serai_dex_pallet::Config
+ serai_genesis_liquidity_pallet::Config
@@ -36,6 +40,18 @@ mod pallet {
#[pallet::error]
pub enum Error<T> {}
/// The block the last batch was published during.
///
/// This is used to restrict publication of batches to once-per-block, limiting the impact of a
/// compromised publisher from bloating the Serai blockchain with spam.
#[pallet::storage]
type BlockOfLastBatch<T: Config> =
StorageMap<_, Identity, ExternalNetworkId, BlockNumberFor<T>, OptionQuery>;
/// The ID of the last batch which was published.
#[pallet::storage]
type LastBatch<T: Config> = StorageMap<_, Identity, ExternalNetworkId, u32, OptionQuery>;
/// The Pallet struct.
#[pallet::pallet]
pub struct Pallet<T>(_);
@@ -44,6 +60,127 @@ mod pallet {
fn emit_event(event: Event) {
Core::<T>::emit_event(event)
}
/// Execute an `InInstructionWithBalance`.
///
/// We execute this within a database layer to ensure it's atomic, executing entirely or not at
/// all.
#[frame_support::transactional]
fn execute(instruction: InInstructionWithBalance) -> DispatchResult {
let InInstructionWithBalance { instruction, balance: external_balance } = instruction;
let balance = <Balance as From<ExternalBalance>>::from(external_balance);
// Mint the balance to ourself
let address = serai_abi::in_instructions::address();
Coins::<T>::mint(address.into(), balance)?;
match instruction {
InInstruction::GenesisLiquidity(address) => {
serai_genesis_liquidity_pallet::Pallet::<T>::add_liquidity(address, external_balance)?;
}
InInstruction::SwapToStakedSri { validator, minimum } => todo!("TODO"),
InInstruction::TransferWithSwap { to, maximum_to_swap, sri } => {
serai_dex_pallet::Pallet::<T>::swap_for(
RawOrigin::Signed(address.into()).into(),
Balance { coin: Coin::Serai, amount: sri },
Balance { coin: balance.coin, amount: maximum_to_swap },
)?;
Coins::<T>::transfer_fn(
address.into(),
to.into(),
Balance {
coin: balance.coin,
amount: Coins::<T>::balance(Public::from(address), balance.coin),
},
)?;
Coins::<T>::transfer_fn(
address.into(),
to.into(),
Balance {
coin: Coin::Serai,
amount: Coins::<T>::balance(Public::from(address), Coin::Serai),
},
)?;
}
InInstruction::Transfer { to } => {
Coins::<T>::transfer_fn(address.into(), to.into(), balance)?;
}
InInstruction::SwapAndAddLiquidity {
address: destination,
coin,
sri_minimum,
sri_for_fees,
} => {
let external_coin = external_balance.coin;
serai_dex_pallet::Pallet::<T>::swap(
RawOrigin::Signed(address.into()).into(),
Balance {
coin: Coin::External(external_coin),
amount: (balance.amount - coin).ok_or(serai_dex_pallet::Error::<T>::Underflow)?,
},
Balance {
coin: Coin::Serai,
amount: (sri_minimum + sri_for_fees).ok_or(serai_dex_pallet::Error::<T>::Overflow)?,
},
)?;
let sri_intended = (Coins::<T>::balance(Public::from(address), Coin::Serai) -
sri_for_fees)
.expect("swapped to amount sufficient for minimum, fees, but received less than fees?");
let coin_intended = coin;
let coin_minimum = coin;
serai_dex_pallet::Pallet::<T>::add_liquidity(
RawOrigin::Signed(address.into()).into(),
external_coin,
sri_intended,
coin_intended,
sri_minimum,
coin_minimum,
)?;
// Transfer the rest, which will be more than the amount requested for fees, to the
// destination
Coins::<T>::transfer_fn(
address.into(),
destination.into(),
Balance {
coin: Coin::Serai,
amount: Coins::<T>::balance(Public::from(address), Coin::Serai),
},
)?;
}
InInstruction::Swap { address: destination, minimum_to_receive } => {
serai_dex_pallet::Pallet::<T>::swap(
RawOrigin::Signed(address.into()).into(),
balance,
minimum_to_receive,
)?;
let coin = minimum_to_receive.coin;
let received_amount = Coins::<T>::balance(Public::from(address), coin);
let received = Balance { coin, amount: received_amount };
Coins::<T>::transfer_fn(address.into(), destination.into(), received)?;
}
InInstruction::SwapOut { instruction, minimum_to_receive } => {
serai_dex_pallet::Pallet::<T>::swap(
RawOrigin::Signed(address.into()).into(),
balance,
minimum_to_receive.into(),
)?;
let coin = minimum_to_receive.coin;
let received_amount = Coins::<T>::balance(Public::from(address), Coin::from(coin));
let received = ExternalBalance { coin, amount: received_amount };
Coins::<T>::burn_with_instruction(
RawOrigin::Signed(address.into()).into(),
OutInstructionWithBalance { instruction, balance: received },
)?;
}
};
Ok(())
}
}
#[pallet::call]
@@ -52,7 +189,143 @@ mod pallet {
#[pallet::call_index(0)]
#[pallet::weight((0, DispatchClass::Normal))] // TODO
pub fn execute_batch(origin: OriginFor<T>, batch: SignedBatch) -> DispatchResult {
todo!("TODO")
let batch = batch.batch;
let network = batch.network();
let mut in_instruction_results = bitvec::vec::BitVec::new();
for instruction in batch.instructions() {
in_instruction_results.push(Self::execute(instruction.clone()).is_ok());
}
// The publishing session is always the current session
let publishing_session =
serai_validator_sets_pallet::Pallet::<T>::current_session(NetworkId::from(network))
.expect("`SignedBatch` for a network without a session was validated");
Self::emit_event(Event::Batch {
network,
publishing_session,
id: batch.id(),
external_network_block_hash: batch.external_network_block_hash(),
in_instructions_hash: sp_core::blake2_256(&borsh::to_vec(batch.instructions()).unwrap()),
in_instruction_results,
});
Ok(())
}
}
#[pallet::validate_unsigned]
impl<T: Config> ValidateUnsigned for Pallet<T> {
type Call = Call<T>;
fn validate_unsigned(_: TransactionSource, call: &Self::Call) -> TransactionValidity {
let batch = match call {
Call::execute_batch { batch } => batch,
Call::__Ignore(_, _) => Err(InvalidTransaction::Call)?,
};
let network = batch.batch.network();
// Verify the network isn't halted
if serai_signals_pallet::Pallet::<T>::halted(network) {
Err(InvalidTransaction::Custom(1))?;
}
// Verify the `Batch`'s signature
let mut signed_by_latest_decided_session = false;
{
let message = batch.batch.publish_batch_message();
let signed_by_session = |session| {
let Some(key) =
serai_validator_sets_pallet::Pallet::<T>::oraclization_key(ExternalValidatorSet {
network,
session,
})
else {
return false;
};
key.verify(&message, &batch.signature.into())
};
let Some(current_session) =
serai_validator_sets_pallet::Pallet::<T>::current_session(NetworkId::from(network))
else {
Err(InvalidTransaction::BadProof)?
};
if !signed_by_session(current_session) {
let latest_decided_session =
serai_validator_sets_pallet::Pallet::<T>::latest_decided_session(NetworkId::from(
network,
))
.expect("current session yet never one decided?");
if !signed_by_session(latest_decided_session) {
Err(InvalidTransaction::BadProof)?;
}
signed_by_latest_decided_session = true;
}
}
// Verify every coin with the `Batch` corresponds to this network
for instruction in batch.batch.instructions() {
if instruction.balance.coin.network() != network {
Err(InvalidTransaction::Custom(2))?;
}
}
// Verify this is the first `Batch` for this block
let current_block_number = frame_system::Pallet::<T>::block_number();
if BlockOfLastBatch::<T>::get(network) == Some(current_block_number) {
// This transaction is valid in the future, the next block, but not now
Err(InvalidTransaction::Future)?;
}
// Verify this `Batch` descends immediately from the prior `Batch`
let required_last_batch = batch.batch.id().checked_sub(1);
{
let last_batch = LastBatch::<T>::get(network);
if last_batch < required_last_batch {
Err(InvalidTransaction::Future)?;
}
if last_batch > required_last_batch {
Err(InvalidTransaction::Stale)?;
}
}
/*
Set the metadata fields as necessary for further batches to be verified. While this is
mutating the state within the verification function, it's necessary for the verification of
the following transactions.
Additionally, we know these state changes occur as tests verify we can publish `Batch`es
and have the `LastBatch` field be incremented.
*/
BlockOfLastBatch::<T>::set(network, Some(current_block_number));
LastBatch::<T>::set(network, Some(batch.batch.id()));
if signed_by_latest_decided_session {
// Because the latest decided session, which is not the current session, has taken over
// for publishing `Batch`es, it has agreed to become the current session
serai_validator_sets_pallet::Pallet::<T>::accept_handover(network);
}
let mut builder = ValidTransaction::with_tag_prefix("InInstructions");
if let Some(required_last_batch) = required_last_batch {
// TODO: Should this replace the DB mutations present within this verification function?
builder = builder.and_requires((network, required_last_batch));
}
builder
.and_provides((network, batch.batch.id()))
// Set a 10 block longevity, though this should be included in the next block
.longevity(10)
.propagate(true)
.build()
}
/// Explicitly provide a `pre_dispatch` which calls `validate_unsigned`.
///
/// This is reasonably assumed, and the current provided implementation, but not guaranteed by
/// the documentation.
fn pre_dispatch(call: &Self::Call) -> Result<(), TransactionValidityError> {
Self::validate_unsigned(TransactionSource::InBlock, call).map(|_| ())
}
}
}

View File

@@ -433,7 +433,7 @@ pub fn new_full(mut config: Configuration) -> Result<TaskManager, ServiceError>
grandpa::run_grandpa_voter(grandpa::GrandpaParams {
config: grandpa::Config {
gossip_duration: std::time::Duration::from_millis(333),
justification_generation_period: 512,
justification_generation_period: 1,
name: Some(name),
observer_enabled: false,
keystore: if role.is_authority() { Some(keystore) } else { None },

View File

@@ -10,15 +10,6 @@ use crate::{
mod batch;
pub use batch::*;
/// The destination for coins.
#[derive(Clone, PartialEq, Eq, Debug, Zeroize, BorshSerialize, BorshDeserialize)]
pub enum Destination {
/// The Serai address to transfer the coins to.
Serai(SeraiAddress),
/// Burn the coins with the included `OutInstruction`.
Burn(OutInstruction),
}
/// An instruction on how to handle coins in.
#[derive(Clone, PartialEq, Eq, Debug, Zeroize, BorshSerialize, BorshDeserialize)]
pub enum InInstruction {
@@ -33,10 +24,10 @@ pub enum InInstruction {
},
/// Transfer the coins to a Serai address, swapping some for SRI.
TransferWithSwap {
/// The Serai address to transfer the coins to, after swapping some.
/// The Serai address to transfer the coins to.
to: SeraiAddress,
/// The maximum amount of coins to swap for the intended amount of SRI.
maximum_swap: Amount,
maximum_to_swap: Amount,
/// The SRI amount to swap some of the coins for.
sri: Amount,
},
@@ -47,23 +38,28 @@ pub enum InInstruction {
},
/// Swap part of the coins to SRI and add the coins as liquidity.
SwapAndAddLiquidity {
/// The owner to-be of the added liquidity.
owner: SeraiAddress,
/// The amount of SRI to add within the liquidity position.
sri: Amount,
/// The minimum amount of the coin to add as liquidity.
minimum_coin: Amount,
/// The recipient to-be of the added liquidity.
address: SeraiAddress,
/// The amount of the coin to add within the liquidity position.
coin: Amount,
/// The minimum amount of SRI to add as liquidity.
sri_minimum: Amount,
/// The amount of SRI to swap to and send to the owner to-be to pay for transactions on Serai.
sri_for_fees: Amount,
},
/// Swap the coins.
Swap {
/// The minimum balance to receive.
minimum_out: Balance,
/// The destination to transfer the balance to.
///
/// If `Destination::Burn`, the balance out will be burnt with the included `OutInstruction`.
destination: Destination,
/// The destination to received the coins swapped to with.
address: SeraiAddress,
/// The minimum balance to receive from the swap.
minimum_to_receive: Balance,
},
/// Swap the coins, burning them with the included `OutInstruction`.
SwapOut {
/// The instruction to burn the coins swapped to with.
instruction: OutInstruction,
/// The minimum balance to receive from the swap.
minimum_to_receive: ExternalBalance,
},
}

View File

@@ -234,6 +234,11 @@ pub mod pallet {
Ok(())
}
/// Check if an external network was halted.
pub fn halted(network: ExternalNetworkId) -> bool {
Halted::<T>::contains_key(network)
}
}
/// An error from the `signals` pallet.

View File

@@ -345,6 +345,14 @@ mod pallet {
Ok(())
}
/// Have the latest decided session become the current session.
///
/// This is restricted to `ExternalNetworkId` as this process happens internally for
/// `NetworkId::Serai`.
pub fn accept_handover(network: ExternalNetworkId) {
Abstractions::<T>::accept_handover(network.into());
}
/* TODO
pub fn distribute_block_rewards(
network: NetworkId,