Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into feat/new-pallets
Browse files Browse the repository at this point in the history
  • Loading branch information
Moliholy committed Dec 11, 2024
2 parents f0153fb + 9fbb666 commit 7348b3c
Show file tree
Hide file tree
Showing 16 changed files with 923 additions and 143 deletions.
24 changes: 0 additions & 24 deletions Cargo.lock

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

1 change: 0 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ testnet-runtime = { path = "runtime/testnet" }
mainnet-runtime = { path = "runtime/mainnet" }
pallet-dmarket = { path = "pallets/dmarket", default-features = false }
pallet-marketplace = { path = "pallets/marketplace", default-features = false }
pallet-migration = { path = "pallets/migration", default-features = false }
pallet-multibatching = { path = "pallets/multibatching", default-features = false }
runtime-common = { path = "runtime/common", default-features = false }
pallet-escrow = { path = "pallets/escrow", default-features = false }
Expand Down
6 changes: 4 additions & 2 deletions pallets/multibatching/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ An alternative to standard batching utilities.

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.
approvals are collected off-chain. See docs for `batch()` and `batch_v2()`
for detailed description.

## Dispatchable functions

- `batch()`: The batching function, allows making multiple calls by
multiple users in a single transaction.
- `batch_v2()`: The batching function, allows making multiple calls by
multiple users in a single transaction.
67 changes: 67 additions & 0 deletions pallets/multibatching/src/benchmarking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,5 +98,72 @@ pub mod benchmarks {
_(RawOrigin::Signed(sender), domain, sender.into(), bias, expires_at, calls, approvals);
}

#[benchmark]
fn batch_v2(c: Linear<1, { T::MaxCalls::get() }>, s: Linear<1, { T::MaxCalls::get() }>) {
let call_count = c as usize;
let signer_count = s as usize;

let domain: [u8; 8] = T::Domain::get();
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_v2 {
domain,
sender: sender.into(),
bias,
expires_at,
calls: calls.clone(),
approvals: BoundedVec::new(),
}
.into();
let pseudo_call_bytes = pseudo_call.encode();
let pseudo_call_bytes = [b"<Bytes>", &pseudo_call_bytes[..], b"</Bytes>"].concat();
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());

#[extrinsic_call]
Pallet::<T>::batch_v2(
RawOrigin::Signed(sender),
domain,
sender.into(),
bias,
expires_at,
calls,
approvals,
);
}

impl_benchmark_test_suite!(Multibatching, crate::mock::new_test_ext(), crate::mock::Test);
}
131 changes: 131 additions & 0 deletions pallets/multibatching/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,137 @@ pub mod pallet {
<T as Config>::WeightInfo::batch(calls_len as u32, approvals.len() as u32);
Ok(Some(base_weight.saturating_add(weight)).into())
}

/// Execute multiple calls from multiple callers in a single batch.
///
/// If one of the calls fails, the whole batch reverts.
///
/// This function works the same as [Pallet::batch], but the bytes signed by
/// approvers must be wrapped in between <Bytes> ... </Bytes>.
/// This is how the rawSign is currently implemented in modern substrate clients.
///
#[pallet::call_index(1)]
#[pallet::weight({
let dispatch_infos = calls.iter().map(|call| call.call.get_dispatch_info()).collect::<Vec<_>>();
let dispatch_weight = dispatch_infos.iter()
.map(|di| di.weight)
.fold(Weight::zero(), |total: Weight, weight: Weight| total.saturating_add(weight))
.saturating_add(<T as Config>::WeightInfo::batch_v2(calls.len() as u32, approvals.len() as u32));
let dispatch_class = {
let all_operational = dispatch_infos.iter()
.map(|di| di.class)
.all(|class| class == DispatchClass::Operational);
if all_operational {
DispatchClass::Operational
} else {
DispatchClass::Normal
}
};
(dispatch_weight, dispatch_class)
})]
pub fn batch_v2(
origin: OriginFor<T>,
domain: [u8; 8],
sender: <T as frame_system::Config>::AccountId,
bias: [u8; 32],
expires_at: <T as pallet_timestamp::Config>::Moment,
calls: BoundedVec<BatchedCall<T>, <T as Config>::MaxCalls>,
approvals: BoundedVec<Approval<T>, <T as Config>::MaxCalls>,
) -> DispatchResultWithPostInfo {
if calls.is_empty() {
return Err(Error::<T>::NoCalls.into());
}
if approvals.is_empty() {
return Err(Error::<T>::NoApprovals.into());
}

if approvals.len() > 1 {
for pair in approvals.windows(2) {
match pair {
[a, b] if a.from < b.from => (),
_ => return Err(Error::<T>::UnsortedApprovals.into()),
};
}
}

// Origin must be `sender`.
match ensure_signed(origin) {
Ok(account_id) if account_id == sender => account_id,
Ok(_) => return Err(Error::<T>::BatchSenderIsNotOrigin.into()),
Err(e) => return Err(e.into()),
};

if pallet_timestamp::Pallet::<T>::get() > expires_at {
return Err(Error::<T>::Expired.into());
}

ensure!(domain == <T as Config>::Domain::get(), Error::<T>::InvalidDomain);

let bytes = Batch {
pallet_index: Self::index() as u8,
call_index: 1,
domain,
sender: sender.clone(),
bias,
expires_at,
calls: calls.clone(),
approvals_zero: 0,
}
.encode();
let bytes = [b"<Bytes>", &bytes[..], b"</Bytes>"].concat();
let hash = <<T as frame_system::Config>::Hashing>::hash(&bytes);

if Applied::<T>::contains_key(hash) {
return Err(Error::<T>::AlreadyApplied.into());
}

Applied::<T>::insert(hash, ());

// Check the signatures.
for (i, approval) in approvals.iter().enumerate() {
let ok = approval
.signature
.verify(bytes.as_ref(), &approval.from.clone().into_account());
if !ok {
return Err(Error::<T>::InvalidSignature(i as u16).into());
}
}

let mut weight = Weight::zero();

let calls_len = calls.len();

// Apply calls.
for (i, payload) in calls.into_iter().enumerate() {
let ok = approvals.binary_search_by_key(&&payload.from, |a| &a.from).is_ok();
if !ok {
return Err(Error::<T>::InvalidCallOrigin(i as u16).into());
}

let info = payload.call.get_dispatch_info();
let origin = <<T as frame_system::Config>::RuntimeOrigin>::from(
frame_system::RawOrigin::Signed(payload.from.into_account()),
);
let result = payload.call.dispatch(origin);
weight = weight.saturating_add(extract_actual_weight(&result, &info));
result.map_err(|mut err| {
// Take the weight of this function itself into account.
let base_weight = <T as Config>::WeightInfo::batch(
i.saturating_add(1) as u32,
approvals.len() as u32,
);
// Return the actual used weight + base_weight of this call.
err.post_info = Some(base_weight + weight).into();
err
})?;
}

Self::deposit_event(Event::BatchApplied { hash });

let base_weight =
<T as Config>::WeightInfo::batch(calls_len as u32, approvals.len() as u32);
Ok(Some(base_weight.saturating_add(weight)).into())
}
}
}

Expand Down
Loading

0 comments on commit 7348b3c

Please sign in to comment.