4 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
12 changed files with 316 additions and 61 deletions

View File

@@ -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,58 +16,131 @@ 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))
cp monerod monerod-chelf
git clone https://github.com/Gottox/chelf
cd chelf
git checkout b2994186cea7b7d61a588fd06c1cc1ae75bcc21a
make
./chelf -s "$STACK" ../monerod-chelf
cd ..
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
sudo apt update -y
sudo apt install -y ksh bash dash zsh busybox mksh posh gash 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 gash yash brush; do
# 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
git checkout b2994186cea7b7d61a588fd06c1cc1ae75bcc21a
make
./chelf -s "$STACK" ../monerod-chelf
cd ..
fi
# 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

4
Cargo.lock generated
View File

@@ -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"
@@ -8433,6 +8432,7 @@ name = "serai-in-instructions-pallet"
version = "0.1.0"
dependencies = [
"bitvec",
"borsh",
"frame-support",
"frame-system",
"parity-scale-codec",

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,6 +21,7 @@ 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 }
@@ -40,6 +41,7 @@ serai-genesis-liquidity-pallet = { path = "../genesis-liquidity", default-featur
[features]
std = [
"scale/std",
"borsh/std",
"sp-core/std",
"sp-application-crypto/std",

View File

@@ -7,10 +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};
@@ -59,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]
@@ -67,7 +189,29 @@ 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(())
}
}
@@ -121,6 +265,13 @@ mod pallet {
}
}
// 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) {

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,
},
}