diff --git a/pallets/multibatching/README.md b/pallets/multibatching/README.md index 166aaa9..18ac810 100644 --- a/pallets/multibatching/README.md +++ b/pallets/multibatching/README.md @@ -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. diff --git a/pallets/multibatching/src/benchmarking.rs b/pallets/multibatching/src/benchmarking.rs index 8cffda5..5abb469 100644 --- a/pallets/multibatching/src/benchmarking.rs +++ b/pallets/multibatching/src/benchmarking.rs @@ -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::::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:: { from: signer.clone().into(), call }) + .expect("Benchmark config must match runtime config for BoundedVec size"); + } + + let pseudo_call: ::RuntimeCall = Call::::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"", &pseudo_call_bytes[..], b""].concat(); + let hash = keccak_256(&pseudo_call_bytes); + + let mut approvals = BoundedVec::new(); + for (public, _signer, account) in &signers { + approvals + .try_push(Approval:: { + 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::::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); } diff --git a/pallets/multibatching/src/lib.rs b/pallets/multibatching/src/lib.rs index a37c372..a7a247a 100644 --- a/pallets/multibatching/src/lib.rs +++ b/pallets/multibatching/src/lib.rs @@ -314,6 +314,137 @@ pub mod pallet { ::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 ... . + /// 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::>(); + let dispatch_weight = dispatch_infos.iter() + .map(|di| di.weight) + .fold(Weight::zero(), |total: Weight, weight: Weight| total.saturating_add(weight)) + .saturating_add(::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, + domain: [u8; 8], + sender: ::AccountId, + bias: [u8; 32], + expires_at: ::Moment, + calls: BoundedVec, ::MaxCalls>, + approvals: BoundedVec, ::MaxCalls>, + ) -> DispatchResultWithPostInfo { + if calls.is_empty() { + return Err(Error::::NoCalls.into()); + } + if approvals.is_empty() { + return Err(Error::::NoApprovals.into()); + } + + if approvals.len() > 1 { + for pair in approvals.windows(2) { + match pair { + [a, b] if a.from < b.from => (), + _ => return Err(Error::::UnsortedApprovals.into()), + }; + } + } + + // Origin must be `sender`. + match ensure_signed(origin) { + Ok(account_id) if account_id == sender => account_id, + Ok(_) => return Err(Error::::BatchSenderIsNotOrigin.into()), + Err(e) => return Err(e.into()), + }; + + if pallet_timestamp::Pallet::::get() > expires_at { + return Err(Error::::Expired.into()); + } + + ensure!(domain == ::Domain::get(), Error::::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[..], b""].concat(); + let hash = <::Hashing>::hash(&bytes); + + if Applied::::contains_key(hash) { + return Err(Error::::AlreadyApplied.into()); + } + + Applied::::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::::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::::InvalidCallOrigin(i as u16).into()); + } + + let info = payload.call.get_dispatch_info(); + let origin = <::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 = ::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 = + ::WeightInfo::batch(calls_len as u32, approvals.len() as u32); + Ok(Some(base_weight.saturating_add(weight)).into()) + } } } diff --git a/pallets/multibatching/src/tests.rs b/pallets/multibatching/src/tests.rs index 46062fe..e0cf17f 100644 --- a/pallets/multibatching/src/tests.rs +++ b/pallets/multibatching/src/tests.rs @@ -628,4 +628,626 @@ mod multibatching_test { ); }) } + + #[test] + fn multibatching_batch_v2_should_work() { + new_test_ext().execute_with(|| { + let call_count = 10; + let signer_count = 10; + + let domain: [u8; 8] = *b"MYTH_NET"; + let bias = [0u8; 32]; + let expires_at = Timestamp::get().saturating_add( + ::Moment::from(1_000_000_000_u64), + ); + + let sender = account(0); + + let mut signers = + Vec::<(EthereumPair, EthereumSigner, AccountId20)>::with_capacity(signer_count); + for _ in 0..signer_count { + let pair: EthereumPair = EthereumPair::generate().0; + let signer: EthereumSigner = pair.public().into(); + let account = signer.clone().into_account(); + signers.push((pair, signer, account)); + } + + let mut calls = BoundedVec::new(); + let iter = (0..call_count).into_iter().zip(signers.iter().cycle()); + for (_, (_, signer, _)) in iter { + let call = frame_system::Call::remark { remark: vec![] }.into(); + calls + .try_push(BatchedCall:: { from: signer.clone().into(), call }) + .ok() + .expect("Mock config must match runtime config for BoundedVec size"); + } + + let pseudo_call: ::RuntimeCall = Call::::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"", &pseudo_call_bytes[..], b""].concat(); + let hash = keccak_256(&pseudo_call_bytes); + + //eprintln!("test bytes: {}", hex::encode(&pseudo_call_bytes)); + //eprintln!("test hash: {}", hex::encode(hash.into())); + + let mut approvals = BoundedVec::new(); + for (pair, _, account) in &signers { + approvals + .try_push(Approval:: { + from: EthereumSigner::from(account.0).into(), + signature: EthereumSignature::from(pair.sign_prehashed(&hash.into())) + .into(), + }) + .ok() + .expect("Benchmark config must match runtime config for BoundedVec size"); + eprintln!("test from: {:?}", &approvals.last().unwrap().from); + eprintln!("test sig: {:?}", &approvals.last().unwrap().signature); + } + approvals.sort_by_key(|a| a.from.clone()); + + assert_ok!(Multibatching::batch_v2( + RuntimeOrigin::signed(sender.clone()), + domain, + sender.clone().into(), + bias, + expires_at, + calls, + approvals, + )); + }) + } + + #[test] + fn multibatching_batch_v2_fails_with_wrong_hashing() { + new_test_ext().execute_with(|| { + let call_count = 10; + let signer_count = 10; + + let domain: [u8; 8] = *b"MYTH_NET"; + let bias = [0u8; 32]; + let expires_at = + Timestamp::get() + ::Moment::from(100_000_u64); + + let sender = account(0); + + let mut signers = + Vec::<(EthereumPair, EthereumSigner, AccountId20)>::with_capacity(signer_count); + for _ in 0..signer_count { + let pair: EthereumPair = EthereumPair::generate().0; + let signer: EthereumSigner = pair.public().into(); + let account = signer.clone().into_account(); + signers.push((pair, signer, account)); + } + + let mut calls = BoundedVec::new(); + let iter = (0..call_count).into_iter().zip(signers.iter().cycle()); + for (_, (_, signer, _)) in iter { + let call = frame_system::Call::remark { remark: vec![] }.into(); + calls + .try_push(BatchedCall:: { from: signer.clone().into(), call }) + .ok() + .expect("Mock config must match runtime config for BoundedVec size"); + } + + let pseudo_call: ::RuntimeCall = Call::::batch { + 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"", &pseudo_call_bytes[..], b""].concat(); + let hash = blake2_256(&pseudo_call_bytes); + + //eprintln!("test bytes: {}", hex::encode(&pseudo_call_bytes)); + //eprintln!("test hash: {}", hex::encode(hash.into())); + + let mut approvals = BoundedVec::new(); + for (pair, _, account) in &signers { + approvals + .try_push(Approval:: { + from: EthereumSigner::from(account.0).into(), + signature: EthereumSignature::from(pair.sign_prehashed(&hash.into())) + .into(), + }) + .ok() + .expect("Benchmark config must match runtime config for BoundedVec size"); + eprintln!("test from: {:?}", &approvals.last().unwrap().from); + eprintln!("test sig: {:?}", &approvals.last().unwrap().signature); + } + approvals.sort_by_key(|a| a.from.clone()); + + assert_noop!( + Multibatching::batch_v2( + RuntimeOrigin::signed(sender.clone()), + domain, + sender.clone().into(), + bias, + expires_at, + calls, + approvals, + ), + Error::::InvalidSignature(0) + ); + }) + } + + #[test] + fn multibatching_batch_v2_fails_with_no_signatures() { + new_test_ext().execute_with(|| { + let call_count = 10; + let signer_count = 10; + + let domain: [u8; 8] = *b"MYTH_NET"; + let bias = [0u8; 32]; + let expires_at = + Timestamp::get() + ::Moment::from(100_000_u64); + + let sender = account(0); + + let mut signers = + Vec::<(EthereumPair, EthereumSigner, AccountId20)>::with_capacity(signer_count); + for _ in 0..signer_count { + let pair: EthereumPair = EthereumPair::generate().0; + let signer: EthereumSigner = pair.public().into(); + let account = signer.clone().into_account(); + signers.push((pair, signer, account)); + } + + let mut calls = BoundedVec::new(); + let iter = (0..call_count).into_iter().zip(signers.iter().cycle()); + for (_, (_, signer, _)) in iter { + let call = frame_system::Call::remark { remark: vec![] }.into(); + calls + .try_push(BatchedCall:: { from: signer.clone().into(), call }) + .ok() + .expect("Mock config must match runtime config for BoundedVec size"); + } + + let pseudo_call: ::RuntimeCall = Call::::batch { + 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"", &pseudo_call_bytes[..], b""].concat(); + let _hash = keccak_256(&pseudo_call_bytes); + + //eprintln!("test bytes: {}", hex::encode(&pseudo_call_bytes)); + //eprintln!("test hash: {}", hex::encode(hash.into())); + + let approvals = BoundedVec::new(); + + assert_noop!( + Multibatching::batch_v2( + RuntimeOrigin::signed(sender.clone()), + domain, + sender.clone().into(), + bias, + expires_at, + calls, + approvals, + ), + Error::::NoApprovals + ); + }) + } + + #[test] + fn multibatching_batch_v2_fails_if_already_applied() { + new_test_ext().execute_with(|| { + let call_count = 10; + let signer_count = 10; + + let domain: [u8; 8] = *b"MYTH_NET"; + let bias = [0u8; 32]; + let expires_at = + Timestamp::get() + ::Moment::from(100_000_u64); + + let sender = account(0); + + let mut signers = + Vec::<(EthereumPair, EthereumSigner, AccountId20)>::with_capacity(signer_count); + for _ in 0..signer_count { + let pair: EthereumPair = EthereumPair::generate().0; + let signer: EthereumSigner = pair.public().into(); + let account = signer.clone().into_account(); + signers.push((pair, signer, account)); + } + + let mut calls = BoundedVec::new(); + let iter = (0..call_count).into_iter().zip(signers.iter().cycle()); + for (_, (_, signer, _)) in iter { + let call = frame_system::Call::remark { remark: vec![] }.into(); + calls + .try_push(BatchedCall:: { from: signer.clone().into(), call }) + .ok() + .expect("Mock config must match runtime config for BoundedVec size"); + } + + let pseudo_call: ::RuntimeCall = Call::::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"", &pseudo_call_bytes[..], b""].concat(); + let hash = keccak_256(&pseudo_call_bytes); + + //eprintln!("test bytes: {}", hex::encode(&pseudo_call_bytes)); + //eprintln!("test hash: {}", hex::encode(hash.into())); + + let mut approvals = BoundedVec::new(); + for (pair, _, account) in &signers { + approvals + .try_push(Approval:: { + from: EthereumSigner::from(account.0).into(), + signature: EthereumSignature::from(pair.sign_prehashed(&hash.into())) + .into(), + }) + .ok() + .expect("Benchmark config must match runtime config for BoundedVec size"); + eprintln!("test from: {:?}", &approvals.last().unwrap().from); + eprintln!("test sig: {:?}", &approvals.last().unwrap().signature); + } + approvals.sort_by_key(|a| a.from.clone()); + + assert_ok!(Multibatching::batch_v2( + RuntimeOrigin::signed(sender.clone()), + domain, + sender.clone().into(), + bias, + expires_at, + calls.clone(), + approvals.clone(), + )); + assert_noop!( + Multibatching::batch_v2( + RuntimeOrigin::signed(sender.clone()), + domain, + sender.clone().into(), + bias, + expires_at, + calls, + approvals, + ), + Error::::AlreadyApplied + ); + }) + } + + #[test] + fn multibatching_batch_v2_should_fail_if_toplevel_signer_is_not_origin() { + new_test_ext().execute_with(|| { + let call_count = 10; + let signer_count = 10; + + let domain: [u8; 8] = *b"MYTH_NET"; + let bias = [0u8; 32]; + let expires_at = + Timestamp::get() + ::Moment::from(100_000_u64); + + let sender = account(0); + + let mut signers = + Vec::<(EthereumPair, EthereumSigner, AccountId20)>::with_capacity(signer_count); + for _ in 0..signer_count { + let pair: EthereumPair = EthereumPair::generate().0; + let signer: EthereumSigner = pair.public().into(); + let account = signer.clone().into_account(); + signers.push((pair, signer, account)); + } + + let mut calls = BoundedVec::new(); + let iter = (0..call_count).into_iter().zip(signers.iter().cycle()); + for (_, (_, signer, _)) in iter { + let call = frame_system::Call::remark { remark: vec![] }.into(); + calls + .try_push(BatchedCall:: { from: signer.clone().into(), call }) + .ok() + .expect("Mock config must match runtime config for BoundedVec size"); + } + + let wrong_sender = account(1); + let pseudo_call: ::RuntimeCall = Call::::batch { + domain, + sender: wrong_sender.into(), + bias, + expires_at, + calls: calls.clone(), + approvals: BoundedVec::new(), + } + .into(); + let pseudo_call_bytes = pseudo_call.encode(); + let pseudo_call_bytes = [b"", &pseudo_call_bytes[..], b""].concat(); + let hash = keccak_256(&pseudo_call_bytes); + + //eprintln!("test bytes: {}", hex::encode(&pseudo_call_bytes)); + //eprintln!("test hash: {}", hex::encode(hash.into())); + + let mut approvals = BoundedVec::new(); + for (pair, _, account) in &signers { + approvals + .try_push(Approval:: { + from: EthereumSigner::from(account.0).into(), + signature: EthereumSignature::from(pair.sign_prehashed(&hash.into())) + .into(), + }) + .ok() + .expect("Benchmark config must match runtime config for BoundedVec size"); + eprintln!("test from: {:?}", &approvals.last().unwrap().from); + eprintln!("test sig: {:?}", &approvals.last().unwrap().signature); + } + approvals.sort_by_key(|a| a.from.clone()); + + assert_noop!( + Multibatching::batch_v2( + RuntimeOrigin::signed(sender.clone()), + domain, + wrong_sender.clone().into(), + bias, + expires_at, + calls, + approvals, + ), + Error::::BatchSenderIsNotOrigin + ); + }) + } + + #[test] + fn multibatching_batch_v2_should_fail_if_domain_is_invalid() { + new_test_ext().execute_with(|| { + let call_count = 10; + let signer_count = 10; + + let domain: [u8; 8] = *b"wrongdom"; + let bias = [0u8; 32]; + let expires_at = + Timestamp::get() + ::Moment::from(100_000_u64); + + let sender = account(0); + + let mut signers = + Vec::<(EthereumPair, EthereumSigner, AccountId20)>::with_capacity(signer_count); + for _ in 0..signer_count { + let pair: EthereumPair = EthereumPair::generate().0; + let signer: EthereumSigner = pair.public().into(); + let account = signer.clone().into_account(); + signers.push((pair, signer, account)); + } + + let mut calls = BoundedVec::new(); + let iter = (0..call_count).into_iter().zip(signers.iter().cycle()); + for (_, (_, signer, _)) in iter { + let call = frame_system::Call::remark { remark: vec![] }.into(); + calls + .try_push(BatchedCall:: { from: signer.clone().into(), call }) + .ok() + .expect("Mock config must match runtime config for BoundedVec size"); + } + + let pseudo_call: ::RuntimeCall = Call::::batch { + 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"", &pseudo_call_bytes[..], b""].concat(); + let hash = keccak_256(&pseudo_call_bytes); + + //eprintln!("test bytes: {}", hex::encode(&pseudo_call_bytes)); + //eprintln!("test hash: {}", hex::encode(hash.into())); + + let mut approvals = BoundedVec::new(); + for (pair, _, account) in &signers { + approvals + .try_push(Approval:: { + from: EthereumSigner::from(account.0).into(), + signature: EthereumSignature::from(pair.sign_prehashed(&hash.into())) + .into(), + }) + .ok() + .expect("Benchmark config must match runtime config for BoundedVec size"); + eprintln!("test from: {:?}", &approvals.last().unwrap().from); + eprintln!("test sig: {:?}", &approvals.last().unwrap().signature); + } + approvals.sort_by_key(|a| a.from.clone()); + + assert_noop!( + Multibatching::batch_v2( + RuntimeOrigin::signed(sender.clone()), + domain, + sender.clone().into(), + bias, + expires_at, + calls, + approvals, + ), + Error::::InvalidDomain + ); + }) + } + + #[test] + fn multibatching_batch_v2_should_fail_if_batch_not_signed_by_any_caller() { + new_test_ext().execute_with(|| { + let call_count = 10; + let signer_count = 10; + + let domain: [u8; 8] = *b"MYTH_NET"; + let bias = [0u8; 32]; + let expires_at = + Timestamp::get() + ::Moment::from(100_000_u64); + + let sender = account(0); + + let mut signers = + Vec::<(EthereumPair, EthereumSigner, AccountId20)>::with_capacity(signer_count); + for _ in 0..signer_count { + let pair: EthereumPair = EthereumPair::generate().0; + let signer: EthereumSigner = pair.public().into(); + let account = signer.clone().into_account(); + signers.push((pair, signer, account)); + } + + let mut calls = BoundedVec::new(); + let iter = (0..call_count).into_iter().zip(signers.iter().cycle()); + for (_, (_, signer, _)) in iter { + let call = frame_system::Call::remark { remark: vec![] }.into(); + calls + .try_push(BatchedCall:: { from: signer.clone().into(), call }) + .ok() + .expect("Mock config must match runtime config for BoundedVec size"); + } + + let pseudo_call: ::RuntimeCall = Call::::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"", &pseudo_call_bytes[..], b""].concat(); + let hash = keccak_256(&pseudo_call_bytes); + + //eprintln!("test bytes: {}", hex::encode(&pseudo_call_bytes)); + //eprintln!("test hash: {}", hex::encode(hash.into())); + + let mut approvals = BoundedVec::new(); + for (pair, _, account) in &signers { + approvals + .try_push(Approval:: { + from: EthereumSigner::from(account.0).into(), + signature: EthereumSignature::from(pair.sign_prehashed(&hash.into())) + .into(), + }) + .ok() + .expect("Benchmark config must match runtime config for BoundedVec size"); + eprintln!("test from: {:?}", &approvals.last().unwrap().from); + eprintln!("test sig: {:?}", &approvals.last().unwrap().signature); + } + approvals.remove(0); + approvals.sort_by_key(|a| a.from.clone()); + + assert_noop!( + Multibatching::batch_v2( + RuntimeOrigin::signed(sender.clone()), + domain, + sender.clone().into(), + bias, + expires_at, + calls, + approvals, + ), + Error::::InvalidCallOrigin(0) + ); + }) + } + + #[test] + fn multibatching_batch_v2_should_fail_if_caller_signature_incorrect() { + new_test_ext().execute_with(|| { + let call_count = 10; + let signer_count = 10; + + let domain: [u8; 8] = *b"MYTH_NET"; + let bias = [0u8; 32]; + let expires_at = + Timestamp::get() + ::Moment::from(100_000_u64); + + let sender = account(0); + + let mut signers = + Vec::<(EthereumPair, EthereumSigner, AccountId20)>::with_capacity(signer_count); + for _ in 0..signer_count { + let pair: EthereumPair = EthereumPair::generate().0; + let signer: EthereumSigner = pair.public().into(); + let account = signer.clone().into_account(); + signers.push((pair, signer, account)); + } + + let mut calls = BoundedVec::new(); + let iter = (0..call_count).into_iter().zip(signers.iter().cycle()); + for (_, (_, signer, _)) in iter { + let call = frame_system::Call::remark { remark: vec![] }.into(); + calls + .try_push(BatchedCall:: { from: signer.clone().into(), call }) + .ok() + .expect("Mock config must match runtime config for BoundedVec size"); + } + + let pseudo_call: ::RuntimeCall = Call::::batch { + 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"", &pseudo_call_bytes[..], b""].concat(); + let hash = keccak_256(&pseudo_call_bytes); + + //eprintln!("test bytes: {}", hex::encode(&pseudo_call_bytes)); + //eprintln!("test hash: {}", hex::encode(hash.into())); + + let mut approvals = BoundedVec::new(); + for (pair, _, account) in &signers { + approvals + .try_push(Approval:: { + from: EthereumSigner::from(account.0).into(), + signature: EthereumSignature::from(pair.sign_prehashed(&hash.into())) + .into(), + }) + .ok() + .expect("Benchmark config must match runtime config for BoundedVec size"); + eprintln!("test from: {:?}", &approvals.last().unwrap().from); + eprintln!("test sig: {:?}", &approvals.last().unwrap().signature); + } + // sign by wrong signer + approvals.sort_by_key(|a| a.from.clone()); + approvals[0].signature = signers[1].0.sign_prehashed(&hash.into()).into(); + + assert_noop!( + Multibatching::batch_v2( + RuntimeOrigin::signed(sender.clone()), + domain, + sender.clone().into(), + bias, + expires_at, + calls, + approvals, + ), + Error::::InvalidSignature(0) + ); + }) + } } diff --git a/pallets/multibatching/src/weights.rs b/pallets/multibatching/src/weights.rs index e0cef11..f7f65ab 100644 --- a/pallets/multibatching/src/weights.rs +++ b/pallets/multibatching/src/weights.rs @@ -39,6 +39,7 @@ use core::marker::PhantomData; /// Weight functions needed for `pallet_multibatching`. pub trait WeightInfo { fn batch(c: u32, s: u32, ) -> Weight; + fn batch_v2(c: u32, s: u32, ) -> Weight; } /// Weights for `pallet_multibatching` using the Substrate node and recommended hardware. @@ -65,6 +66,28 @@ impl WeightInfo for SubstrateWeight { .saturating_add(T::DbWeight::get().reads(3_u64)) .saturating_add(T::DbWeight::get().writes(1_u64)) } + + /// Storage: `Timestamp::Now` (r:1 w:0) + /// Proof: `Timestamp::Now` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Multibatching::Domain` (r:1 w:0) + /// Proof: `Multibatching::Domain` (`max_values`: Some(1), `max_size`: Some(32), added: 527, mode: `MaxEncodedLen`) + /// Storage: `Multibatching::Applied` (r:1 w:1) + /// Proof: `Multibatching::Applied` (`max_values`: None, `max_size`: Some(32), added: 2507, mode: `MaxEncodedLen`) + /// The range of component `c` is `[1, 1000]`. + /// The range of component `s` is `[1, 10]`. + fn batch_v2(c: u32, s: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `67` + // Estimated: `3497` + // Minimum execution time: 271_000_000 picoseconds. + Weight::from_parts(285_000_000, 3497) + // Standard Error: 1_615 + .saturating_add(Weight::from_parts(1_687_188, 0).saturating_mul(c.into())) + // Standard Error: 160_912 + .saturating_add(Weight::from_parts(27_710_194, 0).saturating_mul(s.into())) + .saturating_add(T::DbWeight::get().reads(3_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } } // For backwards compatibility and tests. @@ -90,4 +113,26 @@ impl WeightInfo for () { .saturating_add(RocksDbWeight::get().reads(3_u64)) .saturating_add(RocksDbWeight::get().writes(1_u64)) } + + /// Storage: `Timestamp::Now` (r:1 w:0) + /// Proof: `Timestamp::Now` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Multibatching::Domain` (r:1 w:0) + /// Proof: `Multibatching::Domain` (`max_values`: Some(1), `max_size`: Some(32), added: 527, mode: `MaxEncodedLen`) + /// Storage: `Multibatching::Applied` (r:1 w:1) + /// Proof: `Multibatching::Applied` (`max_values`: None, `max_size`: Some(32), added: 2507, mode: `MaxEncodedLen`) + /// The range of component `c` is `[1, 1000]`. + /// The range of component `s` is `[1, 10]`. + fn batch_v2(c: u32, s: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `67` + // Estimated: `3497` + // Minimum execution time: 271_000_000 picoseconds. + Weight::from_parts(285_000_000, 3497) + // Standard Error: 1_615 + .saturating_add(Weight::from_parts(1_687_188, 0).saturating_mul(c.into())) + // Standard Error: 160_912 + .saturating_add(Weight::from_parts(27_710_194, 0).saturating_mul(s.into())) + .saturating_add(RocksDbWeight::get().reads(3_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } } diff --git a/runtime/mainnet/src/lib.rs b/runtime/mainnet/src/lib.rs index ef81e41..b448bab 100644 --- a/runtime/mainnet/src/lib.rs +++ b/runtime/mainnet/src/lib.rs @@ -226,7 +226,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { spec_name: create_runtime_str!("mythos"), impl_name: create_runtime_str!("mythos"), authoring_version: 1, - spec_version: 1010, + spec_version: 1011, impl_version: 0, apis: RUNTIME_API_VERSIONS, transaction_version: 1, diff --git a/runtime/mainnet/src/weights/pallet_multibatching.rs b/runtime/mainnet/src/weights/pallet_multibatching.rs index e9cbeaf..e3115d9 100644 --- a/runtime/mainnet/src/weights/pallet_multibatching.rs +++ b/runtime/mainnet/src/weights/pallet_multibatching.rs @@ -57,4 +57,25 @@ impl pallet_multibatching::WeightInfo for WeightInfo .saturating_add(T::DbWeight::get().reads(2)) .saturating_add(T::DbWeight::get().writes(1)) } + + /// Storage: `Timestamp::Now` (r:1 w:0) + /// Proof: `Timestamp::Now` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Multibatching::Applied` (r:1 w:1) + /// Proof: `Multibatching::Applied` (`max_values`: None, `max_size`: Some(32), added: 2507, mode: `MaxEncodedLen`) + /// The range of component `c` is `[1, 128]`. + /// The range of component `s` is `[1, 128]`. + fn batch_v2(c: u32, s: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `12` + // Estimated: `3497` + // Minimum execution time: 488_143_000 picoseconds. + Weight::from_parts(497_863_000, 0) + .saturating_add(Weight::from_parts(0, 3497)) + // Standard Error: 200_979 + .saturating_add(Weight::from_parts(5_989_676, 0).saturating_mul(c.into())) + // Standard Error: 200_979 + .saturating_add(Weight::from_parts(57_523_530, 0).saturating_mul(s.into())) + .saturating_add(T::DbWeight::get().reads(2)) + .saturating_add(T::DbWeight::get().writes(1)) + } } diff --git a/runtime/testnet/src/lib.rs b/runtime/testnet/src/lib.rs index a4c6259..3cc18e0 100644 --- a/runtime/testnet/src/lib.rs +++ b/runtime/testnet/src/lib.rs @@ -244,7 +244,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { spec_name: create_runtime_str!("muse"), impl_name: create_runtime_str!("muse"), authoring_version: 1, - spec_version: 1019, + spec_version: 1020, impl_version: 0, apis: RUNTIME_API_VERSIONS, transaction_version: 1, diff --git a/runtime/testnet/src/weights/pallet_multibatching.rs b/runtime/testnet/src/weights/pallet_multibatching.rs index b2d2a00..995539c 100644 --- a/runtime/testnet/src/weights/pallet_multibatching.rs +++ b/runtime/testnet/src/weights/pallet_multibatching.rs @@ -57,4 +57,25 @@ impl pallet_multibatching::WeightInfo for WeightInfo .saturating_add(T::DbWeight::get().reads(2)) .saturating_add(T::DbWeight::get().writes(1)) } + + /// Storage: `Timestamp::Now` (r:1 w:0) + /// Proof: `Timestamp::Now` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Multibatching::Applied` (r:1 w:1) + /// Proof: `Multibatching::Applied` (`max_values`: None, `max_size`: Some(32), added: 2507, mode: `MaxEncodedLen`) + /// The range of component `c` is `[1, 128]`. + /// The range of component `s` is `[1, 128]`. + fn batch_v2(c: u32, s: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `12` + // Estimated: `3497` + // Minimum execution time: 507_113_000 picoseconds. + Weight::from_parts(511_413_000, 0) + .saturating_add(Weight::from_parts(0, 3497)) + // Standard Error: 207_182 + .saturating_add(Weight::from_parts(6_199_404, 0).saturating_mul(c.into())) + // Standard Error: 207_182 + .saturating_add(Weight::from_parts(57_476_835, 0).saturating_mul(s.into())) + .saturating_add(T::DbWeight::get().reads(2)) + .saturating_add(T::DbWeight::get().writes(1)) + } }