diff --git a/substrate/abi/Cargo.toml b/substrate/abi/Cargo.toml index 4cbfb672..e048aba3 100644 --- a/substrate/abi/Cargo.toml +++ b/substrate/abi/Cargo.toml @@ -28,6 +28,7 @@ serde = { version = "1", default-features = false, features = ["derive"], option scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"], optional = true } scale-info = { version = "2", default-features = false, features = ["derive"], optional = true } sp-runtime = { git = "https://github.com/serai-dex/polkadot-sdk", branch = "serai-next", default-features = false, features = ["serde"], optional = true } +frame-support = { git = "https://github.com/serai-dex/polkadot-sdk", branch = "serai-next", default-features = false, optional = true } serai-primitives = { path = "../primitives", version = "0.1", default-features = false } @@ -42,8 +43,10 @@ std = [ "scale?/std", "scale-info?/std", "sp-runtime?/std", + "frame-support?/std", "serai-primitives/std", ] -substrate = ["serde", "scale", "scale-info", "sp-runtime"] +substrate = ["serde", "scale", "scale-info", "sp-runtime", "frame-support"] +try-runtime = ["sp-runtime/try-runtime"] default = ["std"] diff --git a/substrate/abi/src/block.rs b/substrate/abi/src/block.rs index 04616aed..93a8e4e8 100644 --- a/substrate/abi/src/block.rs +++ b/substrate/abi/src/block.rs @@ -11,14 +11,13 @@ pub struct HeaderV1 { /// /// The genesis block has number 0. pub number: u64, - /// The block this header builds upon. - pub parent_hash: BlockHash, + /// The commitment to the DAG this header builds upon. + pub builds_upon: BlockHash, /// The UNIX time in milliseconds this block was created at. pub unix_time_in_millis: u64, - /// The root of a Merkle tree commiting to the transactions within this block. - // TODO: Review the format of this defined by Substrate. We don't want to commit to the signature - // TODO: Some transactions don't have unique hashes due to assuming vaalidators set unique keys - pub transactions_root: [u8; 32], + /// The commitment to the transactions within this block. + // TODO: Some transactions don't have unique hashes due to assuming validators set unique keys + pub transactions_commitment: [u8; 32], /// A commitment to the consensus data used to justify adding this block to the blockchain. pub consensus_commitment: [u8; 32], } @@ -37,16 +36,16 @@ impl Header { Header::V1(HeaderV1 { number, .. }) => *number, } } - /// Get the hash of the header. - pub fn parent_hash(&self) -> BlockHash { + /// Get the commitment to the DAG this header builds upon. + pub fn builds_upon(&self) -> BlockHash { match self { - Header::V1(HeaderV1 { parent_hash, .. }) => *parent_hash, + Header::V1(HeaderV1 { builds_upon, .. }) => *builds_upon, } } - /// Get the hash of the header. - pub fn transactions_root(&self) -> [u8; 32] { + /// The commitment to the transactions within this block. + pub fn transactions_commitment(&self) -> [u8; 32] { match self { - Header::V1(HeaderV1 { transactions_root, .. }) => *transactions_root, + Header::V1(HeaderV1 { transactions_commitment, .. }) => *transactions_commitment, } } /// Get the hash of the header. @@ -69,16 +68,34 @@ pub struct Block { #[cfg(feature = "substrate")] mod substrate { + use core::fmt::Debug; + use scale::{Encode, Decode}; use scale_info::TypeInfo; use sp_core::H256; use sp_runtime::{ - generic::Digest, + generic::{DigestItem, Digest}, traits::{Header as HeaderTrait, HeaderProvider, Block as BlockTrait}, }; use super::*; + use crate::Call; + + /// The digest for all of the Serai-specific header fields. + #[derive(Clone, Copy, PartialEq, Eq, BorshSerialize, BorshDeserialize)] + pub struct SeraiDigest { + /// The commitment to the DAG this header builds upon. + pub builds_upon: BlockHash, + /// The UNIX time in milliseconds this block was created at. + pub unix_time_in_millis: u64, + /// The commitment to the transactions within this block. + pub transactions_commitment: [u8; 32], + } + + impl SeraiDigest { + const CONSENSUS_ID: [u8; 4] = *b"SRID"; + } /// The consensus data for a V1 header. /// @@ -86,6 +103,12 @@ mod substrate { /// solely considered used for consensus now. #[derive(Clone, PartialEq, Eq, Debug, Encode, Decode, TypeInfo, sp_runtime::Serialize)] pub struct ConsensusV1 { + /// The hash of the immediately preceding block. + parent_hash: H256, + /// The root for the Merkle tree of transactions, as defined by Substrate. + /// + /// The format of this differs from Serai's format for the commitment to the transactions. + transactions_root: H256, /// The state root. state_root: H256, /// The consensus digests. @@ -96,8 +119,6 @@ mod substrate { #[derive(Clone, PartialEq, Eq, Debug, Encode, Decode, TypeInfo, sp_runtime::Serialize)] pub struct SubstrateHeaderV1 { number: u64, - parent_hash: H256, - transactions_root: H256, consensus: ConsensusV1, } @@ -110,45 +131,48 @@ mod substrate { impl From<&SubstrateHeader> for Header { fn from(header: &SubstrateHeader) -> Self { - use sp_consensus_babe::SlotDuration; - use sc_consensus_babe::CompatibleDigestItem; - match header { - SubstrateHeader::V1(header) => Header::V1(HeaderV1 { - number: header.number, - parent_hash: BlockHash(header.parent_hash.0), - unix_time_in_millis: header - .consensus - .digest - .logs() - .iter() - .find_map(|digest_item| { - digest_item.as_babe_pre_digest().map(|pre_digest| { - pre_digest - .slot() - .timestamp(SlotDuration::from_millis( - serai_primitives::constants::TARGET_BLOCK_TIME.as_millis().try_into().unwrap(), - )) - // This returns `None` if the slot is so far in the future, it'd cause an - // overflow. - .unwrap_or(sp_timestamp::Timestamp::new(u64::MAX)) - .as_millis() - }) - }) - .unwrap_or(0), - transactions_root: header.transactions_root.0, - consensus_commitment: sp_core::blake2_256(&header.consensus.encode()), - }), + SubstrateHeader::V1(header) => { + let digest = + header.consensus.digest.logs().iter().find_map(|digest_item| match digest_item { + DigestItem::PreRuntime(consensus, encoded) + if *consensus == SeraiDigest::CONSENSUS_ID => + { + SeraiDigest::deserialize_reader(&mut encoded.as_slice()).ok() + } + _ => None, + }); + Header::V1(HeaderV1 { + number: header.number, + builds_upon: digest + .as_ref() + .map(|digest| digest.builds_upon) + .unwrap_or(BlockHash::from([0; 32])), + unix_time_in_millis: digest + .as_ref() + .map(|digest| digest.unix_time_in_millis) + .unwrap_or(0), + transactions_commitment: digest + .as_ref() + .map(|digest| digest.transactions_commitment) + .unwrap_or([0; 32]), + consensus_commitment: sp_core::blake2_256(&header.consensus.encode()), + }) + } } } } /// A block, as needed by Substrate. - #[derive(Clone, PartialEq, Eq, Debug, Encode, Decode, sp_runtime::Serialize)] - pub struct SubstrateBlock { + #[derive(Clone, Debug, PartialEq, Eq, Encode, Decode, sp_runtime::Serialize)] + #[codec(encode_bound(skip_type_params(RuntimeCall)))] + #[codec(decode_bound(skip_type_params(RuntimeCall)))] + pub struct SubstrateBlock< + RuntimeCall: 'static + Send + Sync + Clone + PartialEq + Eq + Debug + From, + > { header: SubstrateHeader, #[serde(skip)] // This makes this unsafe to deserialize, but we don't impl `Deserialize` - transactions: Vec, + transactions: Vec>, } impl HeaderTrait for SubstrateHeader { @@ -165,9 +189,12 @@ mod substrate { ) -> Self { SubstrateHeader::V1(SubstrateHeaderV1 { number, - parent_hash, - transactions_root: extrinsics_root, - consensus: ConsensusV1 { state_root, digest }, + consensus: ConsensusV1 { + parent_hash, + transactions_root: extrinsics_root, + state_root, + digest, + }, }) } @@ -186,13 +213,13 @@ mod substrate { fn extrinsics_root(&self) -> &Self::Hash { match self { - SubstrateHeader::V1(SubstrateHeaderV1 { transactions_root, .. }) => transactions_root, + SubstrateHeader::V1(SubstrateHeaderV1 { consensus, .. }) => &consensus.transactions_root, } } fn set_extrinsics_root(&mut self, extrinsics_root: Self::Hash) { match self { - SubstrateHeader::V1(SubstrateHeaderV1 { transactions_root, .. }) => { - *transactions_root = extrinsics_root; + SubstrateHeader::V1(SubstrateHeaderV1 { consensus, .. }) => { + consensus.transactions_root = extrinsics_root; } } } @@ -212,13 +239,13 @@ mod substrate { fn parent_hash(&self) -> &Self::Hash { match self { - SubstrateHeader::V1(SubstrateHeaderV1 { parent_hash, .. }) => parent_hash, + SubstrateHeader::V1(SubstrateHeaderV1 { consensus, .. }) => &consensus.parent_hash, } } fn set_parent_hash(&mut self, parent_hash: Self::Hash) { match self { - SubstrateHeader::V1(SubstrateHeaderV1 { parent_hash: existing, .. }) => { - *existing = parent_hash; + SubstrateHeader::V1(SubstrateHeaderV1 { consensus, .. }) => { + consensus.parent_hash = parent_hash; } } } @@ -239,12 +266,16 @@ mod substrate { } } - impl HeaderProvider for SubstrateBlock { + impl> + HeaderProvider for SubstrateBlock + { type HeaderT = SubstrateHeader; } - impl BlockTrait for SubstrateBlock { - type Extrinsic = Transaction; + impl> BlockTrait + for SubstrateBlock + { + type Extrinsic = Transaction; type Header = SubstrateHeader; type Hash = H256; fn header(&self) -> &Self::Header { diff --git a/substrate/abi/src/transaction.rs b/substrate/abi/src/transaction.rs index 72456a01..33e6d2a8 100644 --- a/substrate/abi/src/transaction.rs +++ b/substrate/abi/src/transaction.rs @@ -3,23 +3,27 @@ use alloc::{vec, vec::Vec}; use borsh::{io, BorshSerialize, BorshDeserialize}; -use serai_primitives::{address::SeraiAddress, crypto::Signature}; +use sp_core::{ConstU32, bounded::BoundedVec}; +use serai_primitives::{BlockHash, address::SeraiAddress, balance::Amount, crypto::Signature}; use crate::Call; -// use frame_support::dispatch::GetDispatchInfo; +/// The maximum amount of calls allowed in a transaction. +pub const MAX_CALLS: u32 = 8; /// An error regarding `SignedCalls`. #[derive(Clone, PartialEq, Eq, Debug)] pub enum SignedCallsError { /// No calls were included. NoCalls, + /// Too many calls were included. + TooManyCalls, /// An unsigned call was included. IncludedUnsignedCall, } /// A `Vec` of signed calls. #[derive(Clone, PartialEq, Eq, Debug)] -pub struct SignedCalls(Vec); +pub struct SignedCalls(BoundedVec>); impl TryFrom> for SignedCalls { type Error = SignedCallsError; fn try_from(calls: Vec) -> Result { @@ -31,7 +35,7 @@ impl TryFrom> for SignedCalls { Err(SignedCallsError::IncludedUnsignedCall)?; } } - Ok(SignedCalls(calls)) + calls.try_into().map_err(|_| SignedCallsError::TooManyCalls).map(SignedCalls) } } @@ -58,10 +62,10 @@ impl TryFrom for UnsignedCall { /// Part of the context used to sign with, from the protocol. #[derive(Clone, PartialEq, Eq, Debug, BorshSerialize, BorshDeserialize)] pub struct ImplicitContext { - /// The ID of the the protocol. - pub protocol_id: [u8; 32], /// The genesis hash of the blockchain. - pub genesis: [u8; 32], + pub genesis: BlockHash, + /// The ID of the current protocol. + pub protocol_id: [u8; 32], } /// Part of the context used to sign with, specified within the transaction itself. @@ -70,13 +74,12 @@ pub struct ExplicitContext { /// The historic block this transaction builds upon. /// /// This transaction can not be included in a blockchain which does not include this block. - pub historic_block: [u8; 32], + pub historic_block: BlockHash, - /// The block this transaction expires at. + /// The UNIX time this transaction must be included by (and expires after). /// - /// This transaction can not be included in a block whose number is equal or greater to this - /// value. - pub expires_at: Option>, + /// This transaction can not be included in a block whose time is equal or greater to this value. + pub include_by: Option>, /// The signer. pub signer: SeraiAddress, @@ -84,10 +87,10 @@ pub struct ExplicitContext { /// The signer's nonce. pub nonce: u32, - /// The fee paid to the network for inclusion. + /// The fee, in SRI, paid to the network for inclusion. /// /// This fee is paid regardless of the success of any of the calls. - pub fee: u64, + pub fee: Amount, } /// A signature, with context. @@ -106,8 +109,8 @@ pub struct Transaction = Call> { /// /// These calls are executed atomically. Either all successfully execute or none do. The /// transaction's fee is paid regardless. - // TODO: Bound - calls: Vec, + // TODO: if this is unsigned, we only allow a single call. Should we serialize that as 0? + calls: BoundedVec>, /// The calls, as defined by Substrate. runtime_calls: Vec, /// The signature, if present. @@ -129,7 +132,8 @@ impl> BorshSerialize for Transaction> BorshDeserialize for Transaction { fn deserialize_reader(reader: &mut R) -> io::Result { // Read the calls - let calls = Vec::::deserialize_reader(reader)?; + let calls = + serai_primitives::sp_borsh::borsh_deserialize_bounded_vec::<_, Call, MAX_CALLS>(reader)?; // Populate the runtime calls let mut runtime_calls = Vec::with_capacity(calls.len()); for call in calls.iter().cloned() { @@ -160,25 +164,35 @@ impl> BorshDeserialize for Transaction> Transaction { + /// The message to sign to produce a signature, for calls which may or may not be signed and are + /// unchecked. + fn signature_message_unchecked( + calls: &[Call], + implicit_context: &ImplicitContext, + explicit_context: &ExplicitContext, + ) -> Vec { + let mut message = Vec::with_capacity( + (calls.len() * 64) + + core::mem::size_of::() + + core::mem::size_of::(), + ); + calls.serialize(&mut message).unwrap(); + implicit_context.serialize(&mut message).unwrap(); + explicit_context.serialize(&mut message).unwrap(); + message + } + /// The message to sign to produce a signature. pub fn signature_message( calls: &SignedCalls, implicit_context: &ImplicitContext, explicit_context: &ExplicitContext, ) -> Vec { - let mut message = Vec::with_capacity( - (calls.0.len() * 64) + - core::mem::size_of::() + - core::mem::size_of::(), - ); - calls.0.serialize(&mut message).unwrap(); - implicit_context.serialize(&mut message).unwrap(); - explicit_context.serialize(&mut message).unwrap(); - message + Self::signature_message_unchecked(&calls.0, implicit_context, explicit_context) } /// A transaction with signed calls. - pub fn is_signed( + pub fn signed( calls: SignedCalls, explicit_context: ExplicitContext, signature: Signature, @@ -199,121 +213,290 @@ impl> Transaction { pub fn unsigned(call: UnsignedCall) -> Self { let call = call.0; Self { - calls: vec![call.clone()], + calls: vec![call.clone()] + .try_into() + .expect("couldn't convert a length-1 Vec to a BoundedVec"), runtime_calls: vec![call.into()], contextualized_signature: None, } } + + /// If the transaction is signed. + pub fn is_signed(&self) -> bool { + self.calls[0].is_signed() + } } #[cfg(feature = "substrate")] mod substrate { + use core::{marker::PhantomData, fmt::Debug}; + + use scale::{Encode, Decode}; + use sp_runtime::{ + transaction_validity::*, + traits::{Verify, ExtrinsicLike, Dispatchable, ValidateUnsigned, Checkable, Applyable}, + Weight, + }; + #[rustfmt::skip] + use frame_support::dispatch::{DispatchClass, Pays, DispatchInfo, GetDispatchInfo, PostDispatchInfo}; + use super::*; - impl scale::Encode for Transaction { + impl> Encode for Transaction { fn encode(&self) -> Vec { borsh::to_vec(self).unwrap() } } - impl scale::Decode for Transaction { + impl> Decode for Transaction { fn decode(input: &mut I) -> Result { - struct ScaleRead<'a, I: scale::Input>(&'a mut I); + struct ScaleRead<'a, I: scale::Input>(&'a mut I, Option); impl borsh::io::Read for ScaleRead<'_, I> { fn read(&mut self, buf: &mut [u8]) -> borsh::io::Result { - let remaining_len = self - .0 - .remaining_len() - .map_err(|err| borsh::io::Error::new(borsh::io::ErrorKind::Other, err))?; + let remaining_len = self.0.remaining_len().map_err(|err| { + self.1 = Some(err); + borsh::io::Error::new(borsh::io::ErrorKind::Other, "") + })?; // If we're still calling `read`, we try to read at least one more byte let to_read = buf.len().min(remaining_len.unwrap_or(1)); - self - .0 - .read(&mut buf[.. to_read]) - .map_err(|err| borsh::io::Error::new(borsh::io::ErrorKind::Other, err))?; + self.0.read(&mut buf[.. to_read]).map_err(|err| { + self.1 = Some(err); + borsh::io::Error::new(borsh::io::ErrorKind::Other, "") + })?; Ok(to_read) } } - Self::deserialize_reader(&mut ScaleRead(input)).map_err(|err| err.downcast().unwrap()) + let mut input = ScaleRead(input, None); + match Self::deserialize_reader(&mut input) { + Ok(res) => Ok(res), + Err(_) => Err(input.1.unwrap()), + } } } - impl> sp_runtime::traits::ExtrinsicLike - for Transaction + + /// The context which transactions are executed in. + pub trait TransactionContext>: + 'static + Send + Sync + Clone + PartialEq + Eq + Debug { + /// The base weight for a signed transaction. + const SIGNED_WEIGHT: Weight; + + /// The implicit context to verify transactions with. + fn implicit_context() -> &'static ImplicitContext; + + /// If a block is present in the blockchain. + fn block_is_present_in_blockchain(&self, hash: &BlockHash) -> bool; + /// The time embedded into the current block. + /// + /// Returns `None` if the time has yet to be set. + fn current_time(&self) -> Option; + /// The next nonce for an account. + fn next_nonce(&self, signer: &SeraiAddress) -> u32; + /// Have the transaction pay its SRI fee. + fn pay_fee(&self, signer: &SeraiAddress, fee: Amount) -> Result<(), TransactionValidityError>; + } + + /// A transaction with the context necessary to evaluate it within Substrate. + #[derive(Clone, PartialEq, Eq, Debug, Encode, Decode)] + pub struct TransactionWithContext< + RuntimeCall: 'static + From, + Context: TransactionContext, + >(Transaction, #[codec(skip)] PhantomData); + + impl> ExtrinsicLike for Transaction { fn is_signed(&self) -> Option { - Some(self.calls[0].is_signed()) + Some(Transaction::is_signed(self)) } fn is_bare(&self) -> bool { - !self.calls[0].is_signed() - } - } - /* - impl< - Call: 'static + TransactionMember + From + TryInto, - > sp_runtime::traits::Extrinsic for Transaction - { - type Call = Call; - type SignaturePayload = (SeraiAddress, Signature, Extra); - fn is_signed(&self) -> Option { - Some(self.signature.is_some()) - } - fn new(call: Call, signature: Option) -> Option { - Some(Self { call: call.clone().try_into().ok()?, mapped_call: call, signature }) + !Transaction::is_signed(self) } } impl< - Call: 'static + TransactionMember + From + TryInto, - > frame_support::traits::ExtrinsicCall for Transaction + RuntimeCall: 'static + From + GetDispatchInfo, + Context: TransactionContext, + > GetDispatchInfo for TransactionWithContext { - fn call(&self) -> &Call { - &self.mapped_call + fn get_dispatch_info(&self) -> DispatchInfo { + let (extension_weight, class, pays_fee) = if Transaction::is_signed(&self.0) { + (Context::SIGNED_WEIGHT, DispatchClass::Normal, Pays::Yes) + } else { + (Weight::zero(), DispatchClass::Operational, Pays::No) + }; + DispatchInfo { + call_weight: self + .0 + .calls + .iter() + .cloned() + .map(|call| RuntimeCall::from(call).get_dispatch_info().call_weight) + .fold(Weight::zero(), |accum, item| accum + item), + extension_weight, + class, + pays_fee, + } } } - impl< - Call: 'static + TransactionMember + From, - > sp_runtime::traits::ExtrinsicMetadata for Transaction + impl, Context: TransactionContext> + Checkable for Transaction { - type SignedExtensions = Extra; + type Checked = TransactionWithContext; - const VERSION: u8 = 0; - } - - impl< - Call: 'static + TransactionMember + From + GetDispatchInfo, - > GetDispatchInfo for Transaction - { - fn get_dispatch_info(&self) -> frame_support::dispatch::DispatchInfo { - self.mapped_call.get_dispatch_info() - } - } - - impl< - Call: 'static + TransactionMember + From, - > sp_runtime::traits::BlindCheckable for Transaction - { - type Checked = sp_runtime::generic::CheckedExtrinsic; - - fn check( - self, - ) -> Result { - Ok(match self.signature { - Some((signer, signature, extra)) => { - if !signature.verify( - (&self.call, &extra, extra.additional_signed()?).encode().as_slice(), - &signer.into(), - ) { - Err(sp_runtime::transaction_validity::InvalidTransaction::BadProof)? - } - - sp_runtime::generic::CheckedExtrinsic { - signed: Some((signer.into(), extra)), - function: self.mapped_call, + fn check(self, context: &Context) -> Result { + if let Some(ContextualizedSignature { explicit_context, signature }) = + &self.contextualized_signature + { + let ExplicitContext { historic_block, include_by, signer, nonce, fee } = &explicit_context; + if !context.block_is_present_in_blockchain(historic_block) { + // We don't know if this is a block from a fundamentally distinct blockchain or a + // continuation of this blockchain we have yet to sync (which would be `Future`) + Err(TransactionValidityError::Unknown(UnknownTransaction::CannotLookup))?; + } + if let Some(include_by) = *include_by { + if let Some(current_time) = context.current_time() { + if current_time >= u64::from(include_by) { + // Since this transaction has a time bound which has passed, error + Err(TransactionValidityError::Invalid(InvalidTransaction::Stale))?; + } + } else { + // Since this transaction has a time bound, yet we don't know the time, error + Err(TransactionValidityError::Invalid(InvalidTransaction::Stale))?; } } - None => sp_runtime::generic::CheckedExtrinsic { signed: None, function: self.mapped_call }, - }) + match context.next_nonce(signer).cmp(nonce) { + core::cmp::Ordering::Less => { + Err(TransactionValidityError::Invalid(InvalidTransaction::Stale))? + } + core::cmp::Ordering::Equal => {} + core::cmp::Ordering::Greater => { + Err(TransactionValidityError::Invalid(InvalidTransaction::Future))? + } + } + if !sp_core::sr25519::Signature::from(*signature).verify( + Transaction::::signature_message_unchecked( + &self.calls, + Context::implicit_context(), + explicit_context, + ) + .as_slice(), + &sp_core::sr25519::Public::from(*signer), + ) { + Err(sp_runtime::transaction_validity::InvalidTransaction::BadProof)?; + } + context.pay_fee(signer, *fee)?; + } + + Ok(TransactionWithContext(self, PhantomData)) + } + + #[cfg(feature = "try-runtime")] + fn unchecked_into_checked_i_know_what_i_am_doing( + self, + c: &Context, + ) -> Result { + // This satisfies the API, not necessarily the intent, yet this fn is only intended to be used + // within tests. Accordingly, it's fine to be stricter than necessarily + self.check(c) + } + } + + impl< + RuntimeCall: 'static + + Send + + Sync + + From + + Dispatchable< + RuntimeOrigin: From>, + Info = DispatchInfo, + PostInfo = PostDispatchInfo, + >, + Context: TransactionContext, + > Applyable for TransactionWithContext + { + type Call = RuntimeCall; + + fn validate>( + &self, + source: sp_runtime::transaction_validity::TransactionSource, + info: &DispatchInfo, + _len: usize, + ) -> sp_runtime::transaction_validity::TransactionValidity { + if !self.0.is_signed() { + let ValidTransaction { priority: _, requires, provides, longevity: _, propagate: _ } = + V::validate_unsigned(source, &self.0.runtime_calls[0])?; + Ok(ValidTransaction { + // We should always try to include unsigned transactions prior to signed + priority: u64::MAX, + requires, + provides, + // This is valid until included + longevity: u64::MAX, + // Ensure this is propagated + propagate: true, + }) + } else { + let explicit_context = &self.0.contextualized_signature.as_ref().unwrap().explicit_context; + let requires = if let Some(prior_nonce) = explicit_context.nonce.checked_sub(1) { + vec![borsh::to_vec(&(explicit_context.signer, prior_nonce)).unwrap()] + } else { + vec![] + }; + let provides = + vec![borsh::to_vec(&(explicit_context.signer, explicit_context.nonce)).unwrap()]; + Ok(ValidTransaction { + // Prioritize transactions by their fees + priority: { + let fee = explicit_context.fee.0; + Weight::from_all(fee).checked_div_per_component(&info.call_weight).unwrap_or(0) + }, + requires, + provides, + // This revalidates the transaction every block. This is required due to this being + // denominated in blocks, and our transaction expiration being denominated in seconds. + longevity: 1, + propagate: true, + }) + } + } + + fn apply>( + mut self, + _info: &DispatchInfo, + _len: usize, + ) -> sp_runtime::ApplyExtrinsicResultWithInfo { + if !self.0.is_signed() { + V::pre_dispatch(&self.0.runtime_calls[0])?; + match self.0.runtime_calls.remove(0).dispatch(None.into()) { + Ok(res) => Ok(Ok(res)), + // Unsigned transactions should only be included if valid in all regards + // This isn't actually a "mandatory" but the intent is the same + Err(_err) => Err(TransactionValidityError::Invalid(InvalidTransaction::BadMandatory)), + } + } else { + Ok(frame_support::storage::transactional::with_storage_layer(|| { + for call in self.0.runtime_calls { + match call.dispatch( + Some(self.0.contextualized_signature.as_ref().unwrap().explicit_context.signer) + .into(), + ) { + Ok(_res) => {} + // Because this call errored, don't continue and revert all prior calls + Err(e) => Err(e)?, + } + } + // Since all calls errored, return all + Ok(PostDispatchInfo { + // `None` stands for the worst case, which is what we want + actual_weight: None, + // Signed transactions always pay their fee + // TODO: Do we want to handle this so we can not charge fees on removing genesis + // liquidity? + pays_fee: Pays::Yes, + }) + })) + } } } - */ } +#[cfg(feature = "substrate")] +pub use substrate::*; diff --git a/substrate/abi/src/validator_sets.rs b/substrate/abi/src/validator_sets.rs index f2a77b55..e3b9f84e 100644 --- a/substrate/abi/src/validator_sets.rs +++ b/substrate/abi/src/validator_sets.rs @@ -20,6 +20,7 @@ pub enum Call { /// The keys being set. key_pair: KeyPair, /// The participants in the validator set who signed off on these keys. + // TODO: Bound #[borsh( serialize_with = "serai_primitives::sp_borsh::borsh_serialize_bitvec", deserialize_with = "serai_primitives::sp_borsh::borsh_deserialize_bitvec" diff --git a/substrate/client/Cargo.toml b/substrate/client/Cargo.toml index cc97d3b5..0ca04a47 100644 --- a/substrate/client/Cargo.toml +++ b/substrate/client/Cargo.toml @@ -64,8 +64,8 @@ dockertest = "0.5" serai-docker-tests = { path = "../../tests/docker" } [features] -serai = ["thiserror/std", "serde", "serde_json", "serai-abi/serde", "multiaddr", "sp-core", "sp-runtime", "frame-system", "simple-request"] -borsh = ["serai-abi/borsh"] +serai = ["thiserror/std", "serde", "serde_json", "multiaddr", "sp-core", "sp-runtime", "frame-system", "simple-request"] +borsh = [] networks = [] bitcoin = ["networks", "dep:bitcoin"] diff --git a/substrate/client/src/serai/dex.rs b/substrate/client/src/serai/dex.rs index f820d3de..88ccf3f8 100644 --- a/substrate/client/src/serai/dex.rs +++ b/substrate/client/src/serai/dex.rs @@ -1,4 +1,4 @@ -use sp_core::bounded_vec::BoundedVec; +use sp_core::bounded::BoundedVec; use serai_abi::primitives::{Amount, Coin, ExternalCoin, SeraiAddress}; use crate::{SeraiError, TemporalSerai}; diff --git a/substrate/client/tests/common/validator_sets.rs b/substrate/client/tests/common/validator_sets.rs index 666ea379..4e362c40 100644 --- a/substrate/client/tests/common/validator_sets.rs +++ b/substrate/client/tests/common/validator_sets.rs @@ -6,7 +6,7 @@ use rand_core::OsRng; use sp_core::{ ConstU32, - bounded_vec::BoundedVec, + bounded::BoundedVec, sr25519::{Pair, Signature}, Pair as PairTrait, }; diff --git a/substrate/client/tests/dex.rs b/substrate/client/tests/dex.rs index 93422f5e..e48512dc 100644 --- a/substrate/client/tests/dex.rs +++ b/substrate/client/tests/dex.rs @@ -1,6 +1,6 @@ use rand_core::{RngCore, OsRng}; -use sp_core::{Pair as PairTrait, bounded_vec::BoundedVec}; +use sp_core::{Pair as PairTrait, bounded::BoundedVec}; use serai_abi::in_instructions::primitives::DexCall; diff --git a/substrate/dex/pallet/src/benchmarking.rs b/substrate/dex/pallet/src/benchmarking.rs index 071411d7..866bcf15 100644 --- a/substrate/dex/pallet/src/benchmarking.rs +++ b/substrate/dex/pallet/src/benchmarking.rs @@ -22,7 +22,7 @@ use super::*; use frame_benchmarking::{benchmarks, whitelisted_caller}; -use frame_support::{assert_ok, storage::bounded_vec::BoundedVec}; +use frame_support::{assert_ok, storage::bounded::BoundedVec}; use frame_system::RawOrigin as SystemOrigin; use sp_runtime::traits::StaticLookup; diff --git a/substrate/primitives/Cargo.toml b/substrate/primitives/Cargo.toml index 98a81493..e6d31c52 100644 --- a/substrate/primitives/Cargo.toml +++ b/substrate/primitives/Cargo.toml @@ -20,6 +20,7 @@ zeroize = { version = "^1.5", features = ["derive"] } borsh = { version = "1", default-features = false, features = ["derive", "de_strict_order"] } bitvec = { version = "1", default-features = false, features = ["alloc"] } +scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] } sp-core = { git = "https://github.com/serai-dex/polkadot-sdk", branch = "serai-next", default-features = false } ciphersuite = { path = "../../crypto/ciphersuite", default-features = false, features = ["alloc", "ristretto"] } diff --git a/substrate/primitives/src/address.rs b/substrate/primitives/src/address.rs index 5b850125..81bf44e1 100644 --- a/substrate/primitives/src/address.rs +++ b/substrate/primitives/src/address.rs @@ -23,6 +23,8 @@ const HUMAN_READABLE_PART: bech32::Hrp = bech32::Hrp::parse_unchecked("sri"); /// The address for an account on Serai. #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Zeroize, BorshSerialize, BorshDeserialize)] +#[rustfmt::skip] +#[derive(scale::Encode, scale::Decode)] // This is safe as scale and borsh share an encoding here pub struct SeraiAddress(pub [u8; 32]); impl SeraiAddress { diff --git a/substrate/primitives/src/sp_borsh.rs b/substrate/primitives/src/sp_borsh.rs index 2f4dce42..9e601f65 100644 --- a/substrate/primitives/src/sp_borsh.rs +++ b/substrate/primitives/src/sp_borsh.rs @@ -2,8 +2,6 @@ use borsh::{io::*, BorshSerialize, BorshDeserialize}; use sp_core::{ConstU32, bounded::BoundedVec}; -// TODO: Don't serialize this as a Vec. Shorten the length-prefix, technically encoding as an -// enum. pub fn borsh_serialize_bitvec( bitvec: &bitvec::vec::BitVec, writer: &mut W, @@ -21,6 +19,8 @@ pub fn borsh_deserialize_bitvec( type SerializeBoundedVecAs = alloc::vec::Vec; +// TODO: Don't serialize this as a Vec. Shorten the length-prefix, technically encoding as an +// enum. pub fn borsh_serialize_bounded_vec( bounded: &BoundedVec>, writer: &mut W,