Skip to content

Commit

Permalink
Add the multi-batching pallet (#7)
Browse files Browse the repository at this point in the history
* Add multi-batching pallet

* Reject replays, emit events

* Fix missing runtime event assoc type

* Implement the new interface

* Include testing to validate that batch() works

* Remove scaffolding, add more tests

* Benchmarks, cleanup

* Improve benchmarks, add weights

* Add README.md, improve docs

* Implement review fixes, expiry

* Re-generate weights

---------

Co-authored-by: José Molina <[email protected]>
Co-authored-by: Valentin Fernandez <[email protected]>
3 people authored Apr 26, 2024

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
1 parent e85ed1b commit 71e5c8e
Showing 16 changed files with 1,547 additions and 6 deletions.
21 changes: 21 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -42,6 +42,7 @@ hex = { version = "0.4.3", default-features = false }
testnet-runtime = { path = "runtime/testnet" }
mainnet-runtime = { path = "runtime/mainnet" }
pallet-marketplace = { path = "pallets/marketplace", default-features = false }
pallet-multibatching = { path = "pallets/multibatching", default-features = false }
runtime-common = { path = "runtime/common", default-features = false }

# Substrate
9 changes: 9 additions & 0 deletions node/Cargo.toml
Original file line number Diff line number Diff line change
@@ -51,6 +51,7 @@ sp-block-builder = { workspace = true }
sp-blockchain = { workspace = true }
sp-consensus-aura = { workspace = true }
sp-core = { workspace = true }
sp-io = { workspace = true }
sp-keystore = { workspace = true }
sp-offchain = { workspace = true }
sp-runtime = { workspace = true }
@@ -90,8 +91,16 @@ runtime-benchmarks = [
"mainnet-runtime/runtime-benchmarks",
"testnet-runtime/runtime-benchmarks",
"polkadot-cli/runtime-benchmarks",
"cumulus-primitives-core/runtime-benchmarks",
"frame-benchmarking-cli/runtime-benchmarks",
"frame-benchmarking/runtime-benchmarks",
"polkadot-primitives/runtime-benchmarks",
"sc-service/runtime-benchmarks",
"sp-runtime/runtime-benchmarks",
]
try-runtime = [
"polkadot-cli/try-runtime",
"sp-runtime/try-runtime",
"mainnet-runtime/try-runtime",
"testnet-runtime/try-runtime",
]
3 changes: 2 additions & 1 deletion node/src/command.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use std::{net::SocketAddr, path::PathBuf};

use cumulus_client_service::storage_proof_size::HostFunctions as ReclaimHostFunctions;
use cumulus_primitives_core::ParaId;
use frame_benchmarking_cli::{BenchmarkCmd, SUBSTRATE_REFERENCE_HARDWARE};
use log::info;
@@ -289,7 +290,7 @@ pub fn run() -> Result<()> {
match cmd {
BenchmarkCmd::Pallet(cmd) => {
if cfg!(feature = "runtime-benchmarks") {
runner.sync_run(|config| cmd.run::<sp_runtime::traits::HashingFor<Block>, ()>(config))
runner.sync_run(|config| cmd.run::<sp_runtime::traits::HashingFor<Block>, ReclaimHostFunctions>(config))
} else {
Err("Benchmarking wasn't enabled when building the node. \
You can enable it with `--features runtime-benchmarks`."
2 changes: 2 additions & 0 deletions node/src/service.rs
Original file line number Diff line number Diff line change
@@ -45,6 +45,7 @@ pub struct TestnetRuntimeExecutor;

impl NativeExecutionDispatch for TestnetRuntimeExecutor {
type ExtendHostFunctions = (
sp_io::SubstrateHostFunctions,
cumulus_client_service::storage_proof_size::HostFunctions,
frame_benchmarking::benchmarking::HostFunctions,
);
@@ -63,6 +64,7 @@ pub struct MainnetRuntimeExecutor;

impl NativeExecutionDispatch for MainnetRuntimeExecutor {
type ExtendHostFunctions = (
sp_io::SubstrateHostFunctions,
cumulus_client_service::storage_proof_size::HostFunctions,
frame_benchmarking::benchmarking::HostFunctions,
);
55 changes: 55 additions & 0 deletions pallets/multibatching/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
[package]
name = "pallet-multibatching"
version = "1.1.0-dev"
description = "Off-line multisignature atomic batching"
authors = ["Mykola Samardak <[email protected]>"]
edition = "2021"
license = "MIT-0"
publish = false

[package.metadata.docs.rs]
targets = ["x86_64-unknown-linux-gnu"]

[dependencies]
parity-scale-codec = { workspace = true, default-features = false, features = [
"derive",
] }
scale-info = { workspace = true, default-features = false, features = [
"derive",
] }
frame-benchmarking = { workspace = true, default-features = false, optional = true }
frame-support = { workspace = true }
frame-system = { workspace = true }
sp-std = { workspace = true, default-features = false }
sp-core = { workspace = true, default-features = false }
pallet-timestamp = { workspace = true, default-features = false }

# benchmarking dependencies
account = { workspace = true, optional = true }
sp-io = { workspace = true, default-features = false, optional = true }

[dev-dependencies]
sp-runtime = { workspace = true, default-features = false }
account = { workspace = true }
sp-core = { workspace = true, default-features = false }
sp-io = { workspace = true, default-features = false }
sp-keystore = { workspace = true }

[features]
default = ["std"]
std = [
"parity-scale-codec/std",
"frame-benchmarking?/std",
"frame-support/std",
"frame-system/std",
"scale-info/std",
"sp-std/std",
"account?/std",
"sp-io?/std",
]
runtime-benchmarks = [
"frame-benchmarking/runtime-benchmarks",
"account",
"sp-io",
]
try-runtime = ["frame-support/try-runtime"]
18 changes: 18 additions & 0 deletions pallets/multibatching/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Multibatching pallet

An alternative to standard batching utilities.

## Overview

The Multibatching pallet allows for an alternative approach to batching:
calls in a Multibatching batch can be made by multiple users, and their
approvals are collected off-chain. See docs for `batch()` for detailed
description.

## Dispatchable functions

- `batch()`: The batching function, allows making multiple calls by
multiple users in a single transaction.
- `force_set_domain()`: Sets the domain for this specific pallet instance.
Domain is a part of data that has to be signed by each caller in a batch,
and is there to protect the users from replay attacks across networks.
118 changes: 118 additions & 0 deletions pallets/multibatching/src/benchmarking.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
#![cfg(feature = "runtime-benchmarks")]
use super::*;
#[allow(unused_imports)]
use crate::Pallet as Multibatching;
use frame_benchmarking::v2::*;
use frame_support::{dispatch::RawOrigin, BoundedVec};
use sp_core::ecdsa::Public;
use sp_io::{
crypto::{ecdsa_generate, ecdsa_sign_prehashed},
hashing::keccak_256,
};
use sp_std::vec::Vec;

impl<Moment> BenchmarkHelper<Moment> for ()
where
Moment: From<u64>,
{
fn timestamp(value: u64) -> Moment {
value.into()
}
}

fn assert_last_event<T: Config>(generic_event: <T as Config>::RuntimeEvent) {
frame_system::Pallet::<T>::assert_last_event(generic_event.into());
}

#[benchmarks(
where
T::Signer: From<EthereumSigner>,
T::Signature: From<EthereumSignature>,
T::Hash: From<[u8;32]>,
T::Hash: Into<[u8;32]>,
<T as Config>::RuntimeCall: From<Call<T>>,
<T as frame_system::Config>::AccountId: From<AccountId20>,
<T as frame_system::Config>::RuntimeEvent: From<Event<T>>,
<T as frame_system::Config>::RuntimeOrigin: From<frame_system::RawOrigin<AccountId20>>,
)]
pub mod benchmarks {
use super::*;
use account::{AccountId20, EthereumSignature, EthereumSigner};
use frame_support::sp_runtime::traits::IdentifyAccount;
use parity_scale_codec::Encode;

use pallet_timestamp::Pallet as Timestamp;

#[benchmark]
fn batch(c: Linear<1, 1000>, s: Linear<1, 10>) {
let call_count = c as usize;
let signer_count = s as usize;

let domain: [u8; 32] = *b".myth.pallet-multibatching.bench";
let bias = [0u8; 32];
let expires_at = Timestamp::<T>::get() + T::BenchmarkHelper::timestamp(100_000);

let sender: AccountId20 = whitelisted_caller();

let mut signers = Vec::<(Public, EthereumSigner, AccountId20)>::with_capacity(signer_count);
for _ in 0..signer_count {
let public: Public = ecdsa_generate(0.into(), None);
let signer: EthereumSigner = public.into();
let account = signer.clone().into_account();
signers.push((public, signer, account));
}

let mut calls = BoundedVec::new();
let iter = (0..call_count).zip(signers.iter().cycle());
for (_, (_, signer, _)) in iter {
let call = frame_system::Call::remark { remark: Default::default() }.into();
calls
.try_push(BatchedCall::<T> { from: signer.clone().into(), call })
.expect("Benchmark config must match runtime config for BoundedVec size");
}

let pseudo_call: <T as Config>::RuntimeCall = Call::<T>::batch {
domain,
sender: sender.into(),
bias,
expires_at,
calls: calls.clone(),
approvals: BoundedVec::new(),
}
.into();
let pseudo_call_bytes = pseudo_call.encode();
let hash = keccak_256(&pseudo_call_bytes);

let mut approvals = BoundedVec::new();
for (public, _signer, account) in &signers {
approvals
.try_push(Approval::<T> {
from: EthereumSigner::from(account.0).into(),
signature: EthereumSignature::from(
ecdsa_sign_prehashed(0.into(), public, &hash).unwrap(),
)
.into(),
})
.expect("Benchmark config must match runtime config for BoundedVec size");
}
approvals.sort_by_key(|a| a.from.clone());

Pallet::<T>::force_set_domain(RawOrigin::Root.into(), domain)
.expect("force_set_domain must succeed");

#[extrinsic_call]
_(RawOrigin::Signed(sender), domain, sender.into(), bias, expires_at, calls, approvals);
}

#[benchmark]
fn force_set_domain() {
let domain = [0; 32];

#[extrinsic_call]
_(RawOrigin::Root, domain);

assert_last_event::<T>(Event::DomainSet { domain }.into());
}

impl_benchmark_test_suite!(Multibatching, crate::mock::new_test_ext(), crate::mock::Test);
}
Loading

0 comments on commit 71e5c8e

Please sign in to comment.