use core::num::NonZero; use alloc::{vec, vec::Vec}; use borsh::{io, BorshSerialize, BorshDeserialize}; use serai_primitives::{address::SeraiAddress, crypto::Signature}; use crate::Call; // use frame_support::dispatch::GetDispatchInfo; /// An error regarding `SignedCalls`. #[derive(Clone, PartialEq, Eq, Debug)] pub enum SignedCallsError { /// No calls were included. NoCalls, /// An unsigned call was included. IncludedUnsignedCall, } /// A `Vec` of signed calls. #[derive(Clone, PartialEq, Eq, Debug)] pub struct SignedCalls(Vec); impl TryFrom> for SignedCalls { type Error = SignedCallsError; fn try_from(calls: Vec) -> Result { if calls.is_empty() { Err(SignedCallsError::NoCalls)?; } for call in &calls { if !call.is_signed() { Err(SignedCallsError::IncludedUnsignedCall)?; } } Ok(SignedCalls(calls)) } } /// An error regarding `UnsignedCall`. #[derive(Clone, PartialEq, Eq, Debug)] pub enum UnsignedCallError { /// A signed call was specified. SignedCall, } /// An unsigned call. #[derive(Clone, PartialEq, Eq, Debug)] pub struct UnsignedCall(Call); impl TryFrom for UnsignedCall { type Error = UnsignedCallError; fn try_from(call: Call) -> Result { if call.is_signed() { Err(UnsignedCallError::SignedCall)?; } Ok(UnsignedCall(call)) } } /// 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], } /// Part of the context used to sign with, specified within the transaction itself. #[derive(Clone, PartialEq, Eq, Debug, BorshSerialize, BorshDeserialize)] 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], /// The block this transaction expires at. /// /// This transaction can not be included in a block whose number is equal or greater to this /// value. pub expires_at: Option>, /// The signer. pub signer: SeraiAddress, /// The signer's nonce. pub nonce: u32, /// The fee paid to the network for inclusion. /// /// This fee is paid regardless of the success of any of the calls. pub fee: u64, } /// A signature, with context. #[derive(Clone, PartialEq, Eq, Debug, BorshSerialize, BorshDeserialize)] pub struct ContextualizedSignature { /// The explicit context. explicit_context: ExplicitContext, /// The signature. signature: Signature, } /// The Serai transaction type. #[derive(Clone, PartialEq, Eq, Debug)] pub struct Transaction = Call> { /// The calls, as defined in Serai's ABI. /// /// These calls are executed atomically. Either all successfully execute or none do. The /// transaction's fee is paid regardless. // TODO: Bound calls: Vec, /// The calls, as defined by Substrate. runtime_calls: Vec, /// The signature, if present. contextualized_signature: Option, } impl> BorshSerialize for Transaction { fn serialize(&self, writer: &mut W) -> io::Result<()> { // Write the calls self.calls.serialize(writer)?; // Write the signature, if present. Presence is deterministic to the calls if let Some(contextualized_signature) = self.contextualized_signature.as_ref() { contextualized_signature.serialize(writer)?; } Ok(()) } } impl> BorshDeserialize for Transaction { fn deserialize_reader(reader: &mut R) -> io::Result { // Read the calls let calls = Vec::::deserialize_reader(reader)?; // Populate the runtime calls let mut runtime_calls = Vec::with_capacity(calls.len()); for call in calls.iter().cloned() { runtime_calls.push(RuntimeCall::from(call)); } // Determine if this is signed or unsigned let mut signed = None; for call in &calls { let call_is_signed = call.is_signed(); if signed.is_none() { signed = Some(call_is_signed) }; if signed != Some(call_is_signed) { Err(io::Error::new(io::ErrorKind::Other, "calls were a mixture of signed and unsigned"))?; } } let Some(signed) = signed else { Err(io::Error::new(io::ErrorKind::Other, "transaction had no calls"))? }; // Read the signature, if these calls are signed let contextualized_signature = if signed { Some(::deserialize_reader(reader)?) } else { None }; Ok(Transaction { calls, runtime_calls, contextualized_signature }) } } impl> Transaction { /// 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 } /// A transaction with signed calls. pub fn is_signed( calls: SignedCalls, explicit_context: ExplicitContext, signature: Signature, ) -> Self { let calls = calls.0; let mut runtime_calls = Vec::with_capacity(calls.len()); for call in calls.iter().cloned() { runtime_calls.push(call.into()); } Self { calls, runtime_calls, contextualized_signature: Some(ContextualizedSignature { explicit_context, signature }), } } /// A transaction with an unsigned call. pub fn unsigned(call: UnsignedCall) -> Self { let call = call.0; Self { calls: vec![call.clone()], runtime_calls: vec![call.into()], contextualized_signature: None, } } } #[cfg(feature = "substrate")] mod substrate { use super::*; impl scale::Encode for Transaction { fn encode(&self) -> Vec { borsh::to_vec(self).unwrap() } } impl scale::Decode for Transaction { fn decode(input: &mut I) -> Result { struct ScaleRead<'a, I: scale::Input>(&'a mut I); 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))?; // 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))?; Ok(to_read) } } Self::deserialize_reader(&mut ScaleRead(input)).map_err(|err| err.downcast().unwrap()) } } impl> sp_runtime::traits::ExtrinsicLike for Transaction { fn is_signed(&self) -> Option { Some(self.calls[0].is_signed()) } 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 }) } } impl< Call: 'static + TransactionMember + From + TryInto, > frame_support::traits::ExtrinsicCall for Transaction { fn call(&self) -> &Call { &self.mapped_call } } impl< Call: 'static + TransactionMember + From, > sp_runtime::traits::ExtrinsicMetadata for Transaction { type SignedExtensions = Extra; 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, } } None => sp_runtime::generic::CheckedExtrinsic { signed: None, function: self.mapped_call }, }) } } */ }