Add traits necessary for serai_abi::Transaction to be usable in-runtime

This commit is contained in:
Luke Parker
2025-02-26 05:05:35 -05:00
parent dd5e43760d
commit 88c7ae3e7d
12 changed files with 390 additions and 169 deletions

View File

@@ -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 = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"], optional = true }
scale-info = { version = "2", 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 } 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 } serai-primitives = { path = "../primitives", version = "0.1", default-features = false }
@@ -42,8 +43,10 @@ std = [
"scale?/std", "scale?/std",
"scale-info?/std", "scale-info?/std",
"sp-runtime?/std", "sp-runtime?/std",
"frame-support?/std",
"serai-primitives/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"] default = ["std"]

View File

@@ -11,14 +11,13 @@ pub struct HeaderV1 {
/// ///
/// The genesis block has number 0. /// The genesis block has number 0.
pub number: u64, pub number: u64,
/// The block this header builds upon. /// The commitment to the DAG this header builds upon.
pub parent_hash: BlockHash, pub builds_upon: BlockHash,
/// The UNIX time in milliseconds this block was created at. /// The UNIX time in milliseconds this block was created at.
pub unix_time_in_millis: u64, pub unix_time_in_millis: u64,
/// The root of a Merkle tree commiting to the transactions within this block. /// The commitment 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 validators set unique keys
// TODO: Some transactions don't have unique hashes due to assuming vaalidators set unique keys pub transactions_commitment: [u8; 32],
pub transactions_root: [u8; 32],
/// A commitment to the consensus data used to justify adding this block to the blockchain. /// A commitment to the consensus data used to justify adding this block to the blockchain.
pub consensus_commitment: [u8; 32], pub consensus_commitment: [u8; 32],
} }
@@ -37,16 +36,16 @@ impl Header {
Header::V1(HeaderV1 { number, .. }) => *number, Header::V1(HeaderV1 { number, .. }) => *number,
} }
} }
/// Get the hash of the header. /// Get the commitment to the DAG this header builds upon.
pub fn parent_hash(&self) -> BlockHash { pub fn builds_upon(&self) -> BlockHash {
match self { match self {
Header::V1(HeaderV1 { parent_hash, .. }) => *parent_hash, Header::V1(HeaderV1 { builds_upon, .. }) => *builds_upon,
} }
} }
/// Get the hash of the header. /// The commitment to the transactions within this block.
pub fn transactions_root(&self) -> [u8; 32] { pub fn transactions_commitment(&self) -> [u8; 32] {
match self { match self {
Header::V1(HeaderV1 { transactions_root, .. }) => *transactions_root, Header::V1(HeaderV1 { transactions_commitment, .. }) => *transactions_commitment,
} }
} }
/// Get the hash of the header. /// Get the hash of the header.
@@ -69,16 +68,34 @@ pub struct Block {
#[cfg(feature = "substrate")] #[cfg(feature = "substrate")]
mod substrate { mod substrate {
use core::fmt::Debug;
use scale::{Encode, Decode}; use scale::{Encode, Decode};
use scale_info::TypeInfo; use scale_info::TypeInfo;
use sp_core::H256; use sp_core::H256;
use sp_runtime::{ use sp_runtime::{
generic::Digest, generic::{DigestItem, Digest},
traits::{Header as HeaderTrait, HeaderProvider, Block as BlockTrait}, traits::{Header as HeaderTrait, HeaderProvider, Block as BlockTrait},
}; };
use super::*; 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. /// The consensus data for a V1 header.
/// ///
@@ -86,6 +103,12 @@ mod substrate {
/// solely considered used for consensus now. /// solely considered used for consensus now.
#[derive(Clone, PartialEq, Eq, Debug, Encode, Decode, TypeInfo, sp_runtime::Serialize)] #[derive(Clone, PartialEq, Eq, Debug, Encode, Decode, TypeInfo, sp_runtime::Serialize)]
pub struct ConsensusV1 { 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. /// The state root.
state_root: H256, state_root: H256,
/// The consensus digests. /// The consensus digests.
@@ -96,8 +119,6 @@ mod substrate {
#[derive(Clone, PartialEq, Eq, Debug, Encode, Decode, TypeInfo, sp_runtime::Serialize)] #[derive(Clone, PartialEq, Eq, Debug, Encode, Decode, TypeInfo, sp_runtime::Serialize)]
pub struct SubstrateHeaderV1 { pub struct SubstrateHeaderV1 {
number: u64, number: u64,
parent_hash: H256,
transactions_root: H256,
consensus: ConsensusV1, consensus: ConsensusV1,
} }
@@ -110,45 +131,48 @@ mod substrate {
impl From<&SubstrateHeader> for Header { impl From<&SubstrateHeader> for Header {
fn from(header: &SubstrateHeader) -> Self { fn from(header: &SubstrateHeader) -> Self {
use sp_consensus_babe::SlotDuration;
use sc_consensus_babe::CompatibleDigestItem;
match header { match header {
SubstrateHeader::V1(header) => Header::V1(HeaderV1 { 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, number: header.number,
parent_hash: BlockHash(header.parent_hash.0), builds_upon: digest
unix_time_in_millis: header .as_ref()
.consensus .map(|digest| digest.builds_upon)
.digest .unwrap_or(BlockHash::from([0; 32])),
.logs() unix_time_in_millis: digest
.iter() .as_ref()
.find_map(|digest_item| { .map(|digest| digest.unix_time_in_millis)
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), .unwrap_or(0),
transactions_root: header.transactions_root.0, transactions_commitment: digest
.as_ref()
.map(|digest| digest.transactions_commitment)
.unwrap_or([0; 32]),
consensus_commitment: sp_core::blake2_256(&header.consensus.encode()), consensus_commitment: sp_core::blake2_256(&header.consensus.encode()),
}), })
}
} }
} }
} }
/// A block, as needed by Substrate. /// A block, as needed by Substrate.
#[derive(Clone, PartialEq, Eq, Debug, Encode, Decode, sp_runtime::Serialize)] #[derive(Clone, Debug, PartialEq, Eq, Encode, Decode, sp_runtime::Serialize)]
pub struct SubstrateBlock { #[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<Call>,
> {
header: SubstrateHeader, header: SubstrateHeader,
#[serde(skip)] // This makes this unsafe to deserialize, but we don't impl `Deserialize` #[serde(skip)] // This makes this unsafe to deserialize, but we don't impl `Deserialize`
transactions: Vec<Transaction>, transactions: Vec<Transaction<RuntimeCall>>,
} }
impl HeaderTrait for SubstrateHeader { impl HeaderTrait for SubstrateHeader {
@@ -165,9 +189,12 @@ mod substrate {
) -> Self { ) -> Self {
SubstrateHeader::V1(SubstrateHeaderV1 { SubstrateHeader::V1(SubstrateHeaderV1 {
number, number,
consensus: ConsensusV1 {
parent_hash, parent_hash,
transactions_root: extrinsics_root, transactions_root: extrinsics_root,
consensus: ConsensusV1 { state_root, digest }, state_root,
digest,
},
}) })
} }
@@ -186,13 +213,13 @@ mod substrate {
fn extrinsics_root(&self) -> &Self::Hash { fn extrinsics_root(&self) -> &Self::Hash {
match self { 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) { fn set_extrinsics_root(&mut self, extrinsics_root: Self::Hash) {
match self { match self {
SubstrateHeader::V1(SubstrateHeaderV1 { transactions_root, .. }) => { SubstrateHeader::V1(SubstrateHeaderV1 { consensus, .. }) => {
*transactions_root = extrinsics_root; consensus.transactions_root = extrinsics_root;
} }
} }
} }
@@ -212,13 +239,13 @@ mod substrate {
fn parent_hash(&self) -> &Self::Hash { fn parent_hash(&self) -> &Self::Hash {
match self { 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) { fn set_parent_hash(&mut self, parent_hash: Self::Hash) {
match self { match self {
SubstrateHeader::V1(SubstrateHeaderV1 { parent_hash: existing, .. }) => { SubstrateHeader::V1(SubstrateHeaderV1 { consensus, .. }) => {
*existing = parent_hash; consensus.parent_hash = parent_hash;
} }
} }
} }
@@ -239,12 +266,16 @@ mod substrate {
} }
} }
impl HeaderProvider for SubstrateBlock { impl<RuntimeCall: 'static + Send + Sync + Clone + PartialEq + Eq + Debug + From<Call>>
HeaderProvider for SubstrateBlock<RuntimeCall>
{
type HeaderT = SubstrateHeader; type HeaderT = SubstrateHeader;
} }
impl BlockTrait for SubstrateBlock { impl<RuntimeCall: 'static + Send + Sync + Clone + PartialEq + Eq + Debug + From<Call>> BlockTrait
type Extrinsic = Transaction; for SubstrateBlock<RuntimeCall>
{
type Extrinsic = Transaction<RuntimeCall>;
type Header = SubstrateHeader; type Header = SubstrateHeader;
type Hash = H256; type Hash = H256;
fn header(&self) -> &Self::Header { fn header(&self) -> &Self::Header {

View File

@@ -3,23 +3,27 @@ use alloc::{vec, vec::Vec};
use borsh::{io, BorshSerialize, BorshDeserialize}; 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 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`. /// An error regarding `SignedCalls`.
#[derive(Clone, PartialEq, Eq, Debug)] #[derive(Clone, PartialEq, Eq, Debug)]
pub enum SignedCallsError { pub enum SignedCallsError {
/// No calls were included. /// No calls were included.
NoCalls, NoCalls,
/// Too many calls were included.
TooManyCalls,
/// An unsigned call was included. /// An unsigned call was included.
IncludedUnsignedCall, IncludedUnsignedCall,
} }
/// A `Vec` of signed calls. /// A `Vec` of signed calls.
#[derive(Clone, PartialEq, Eq, Debug)] #[derive(Clone, PartialEq, Eq, Debug)]
pub struct SignedCalls(Vec<Call>); pub struct SignedCalls(BoundedVec<Call, ConstU32<{ MAX_CALLS }>>);
impl TryFrom<Vec<Call>> for SignedCalls { impl TryFrom<Vec<Call>> for SignedCalls {
type Error = SignedCallsError; type Error = SignedCallsError;
fn try_from(calls: Vec<Call>) -> Result<Self, Self::Error> { fn try_from(calls: Vec<Call>) -> Result<Self, Self::Error> {
@@ -31,7 +35,7 @@ impl TryFrom<Vec<Call>> for SignedCalls {
Err(SignedCallsError::IncludedUnsignedCall)?; Err(SignedCallsError::IncludedUnsignedCall)?;
} }
} }
Ok(SignedCalls(calls)) calls.try_into().map_err(|_| SignedCallsError::TooManyCalls).map(SignedCalls)
} }
} }
@@ -58,10 +62,10 @@ impl TryFrom<Call> for UnsignedCall {
/// Part of the context used to sign with, from the protocol. /// Part of the context used to sign with, from the protocol.
#[derive(Clone, PartialEq, Eq, Debug, BorshSerialize, BorshDeserialize)] #[derive(Clone, PartialEq, Eq, Debug, BorshSerialize, BorshDeserialize)]
pub struct ImplicitContext { pub struct ImplicitContext {
/// The ID of the the protocol.
pub protocol_id: [u8; 32],
/// The genesis hash of the blockchain. /// 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. /// 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. /// The historic block this transaction builds upon.
/// ///
/// This transaction can not be included in a blockchain which does not include this block. /// 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 /// This transaction can not be included in a block whose time is equal or greater to this value.
/// value. pub include_by: Option<NonZero<u64>>,
pub expires_at: Option<NonZero<u64>>,
/// The signer. /// The signer.
pub signer: SeraiAddress, pub signer: SeraiAddress,
@@ -84,10 +87,10 @@ pub struct ExplicitContext {
/// The signer's nonce. /// The signer's nonce.
pub nonce: u32, 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. /// This fee is paid regardless of the success of any of the calls.
pub fee: u64, pub fee: Amount,
} }
/// A signature, with context. /// A signature, with context.
@@ -106,8 +109,8 @@ pub struct Transaction<RuntimeCall: 'static + From<Call> = Call> {
/// ///
/// These calls are executed atomically. Either all successfully execute or none do. The /// These calls are executed atomically. Either all successfully execute or none do. The
/// transaction's fee is paid regardless. /// transaction's fee is paid regardless.
// TODO: Bound // TODO: if this is unsigned, we only allow a single call. Should we serialize that as 0?
calls: Vec<Call>, calls: BoundedVec<Call, ConstU32<{ MAX_CALLS }>>,
/// The calls, as defined by Substrate. /// The calls, as defined by Substrate.
runtime_calls: Vec<RuntimeCall>, runtime_calls: Vec<RuntimeCall>,
/// The signature, if present. /// The signature, if present.
@@ -129,7 +132,8 @@ impl<RuntimeCall: 'static + From<Call>> BorshSerialize for Transaction<RuntimeCa
impl<RuntimeCall: 'static + From<Call>> BorshDeserialize for Transaction<RuntimeCall> { impl<RuntimeCall: 'static + From<Call>> BorshDeserialize for Transaction<RuntimeCall> {
fn deserialize_reader<R: io::Read>(reader: &mut R) -> io::Result<Self> { fn deserialize_reader<R: io::Read>(reader: &mut R) -> io::Result<Self> {
// Read the calls // Read the calls
let calls = Vec::<Call>::deserialize_reader(reader)?; let calls =
serai_primitives::sp_borsh::borsh_deserialize_bounded_vec::<_, Call, MAX_CALLS>(reader)?;
// Populate the runtime calls // Populate the runtime calls
let mut runtime_calls = Vec::with_capacity(calls.len()); let mut runtime_calls = Vec::with_capacity(calls.len());
for call in calls.iter().cloned() { for call in calls.iter().cloned() {
@@ -160,25 +164,35 @@ impl<RuntimeCall: 'static + From<Call>> BorshDeserialize for Transaction<Runtime
} }
impl<RuntimeCall: 'static + From<Call>> Transaction<RuntimeCall> { impl<RuntimeCall: 'static + From<Call>> Transaction<RuntimeCall> {
/// 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<u8> {
let mut message = Vec::with_capacity(
(calls.len() * 64) +
core::mem::size_of::<ImplicitContext>() +
core::mem::size_of::<ExplicitContext>(),
);
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. /// The message to sign to produce a signature.
pub fn signature_message( pub fn signature_message(
calls: &SignedCalls, calls: &SignedCalls,
implicit_context: &ImplicitContext, implicit_context: &ImplicitContext,
explicit_context: &ExplicitContext, explicit_context: &ExplicitContext,
) -> Vec<u8> { ) -> Vec<u8> {
let mut message = Vec::with_capacity( Self::signature_message_unchecked(&calls.0, implicit_context, explicit_context)
(calls.0.len() * 64) +
core::mem::size_of::<ImplicitContext>() +
core::mem::size_of::<ExplicitContext>(),
);
calls.0.serialize(&mut message).unwrap();
implicit_context.serialize(&mut message).unwrap();
explicit_context.serialize(&mut message).unwrap();
message
} }
/// A transaction with signed calls. /// A transaction with signed calls.
pub fn is_signed( pub fn signed(
calls: SignedCalls, calls: SignedCalls,
explicit_context: ExplicitContext, explicit_context: ExplicitContext,
signature: Signature, signature: Signature,
@@ -199,121 +213,290 @@ impl<RuntimeCall: 'static + From<Call>> Transaction<RuntimeCall> {
pub fn unsigned(call: UnsignedCall) -> Self { pub fn unsigned(call: UnsignedCall) -> Self {
let call = call.0; let call = call.0;
Self { 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()], runtime_calls: vec![call.into()],
contextualized_signature: None, contextualized_signature: None,
} }
} }
/// If the transaction is signed.
pub fn is_signed(&self) -> bool {
self.calls[0].is_signed()
}
} }
#[cfg(feature = "substrate")] #[cfg(feature = "substrate")]
mod 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::*; use super::*;
impl scale::Encode for Transaction { impl<RuntimeCall: 'static + From<Call>> Encode for Transaction<RuntimeCall> {
fn encode(&self) -> Vec<u8> { fn encode(&self) -> Vec<u8> {
borsh::to_vec(self).unwrap() borsh::to_vec(self).unwrap()
} }
} }
impl scale::Decode for Transaction { impl<RuntimeCall: 'static + From<Call>> Decode for Transaction<RuntimeCall> {
fn decode<I: scale::Input>(input: &mut I) -> Result<Self, scale::Error> { fn decode<I: scale::Input>(input: &mut I) -> Result<Self, scale::Error> {
struct ScaleRead<'a, I: scale::Input>(&'a mut I); struct ScaleRead<'a, I: scale::Input>(&'a mut I, Option<scale::Error>);
impl<I: scale::Input> borsh::io::Read for ScaleRead<'_, I> { impl<I: scale::Input> borsh::io::Read for ScaleRead<'_, I> {
fn read(&mut self, buf: &mut [u8]) -> borsh::io::Result<usize> { fn read(&mut self, buf: &mut [u8]) -> borsh::io::Result<usize> {
let remaining_len = self let remaining_len = self.0.remaining_len().map_err(|err| {
.0 self.1 = Some(err);
.remaining_len() borsh::io::Error::new(borsh::io::ErrorKind::Other, "")
.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 // 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)); let to_read = buf.len().min(remaining_len.unwrap_or(1));
self self.0.read(&mut buf[.. to_read]).map_err(|err| {
.0 self.1 = Some(err);
.read(&mut buf[.. to_read]) borsh::io::Error::new(borsh::io::ErrorKind::Other, "")
.map_err(|err| borsh::io::Error::new(borsh::io::ErrorKind::Other, err))?; })?;
Ok(to_read) 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<RuntimeCall: 'static + From<Call>> sp_runtime::traits::ExtrinsicLike }
for Transaction<RuntimeCall>
/// The context which transactions are executed in.
pub trait TransactionContext<RuntimeCall: 'static + From<Call>>:
'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<u64>;
/// 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<Call>,
Context: TransactionContext<RuntimeCall>,
>(Transaction<RuntimeCall>, #[codec(skip)] PhantomData<Context>);
impl<RuntimeCall: 'static + From<Call>> ExtrinsicLike for Transaction<RuntimeCall> {
fn is_signed(&self) -> Option<bool> { fn is_signed(&self) -> Option<bool> {
Some(self.calls[0].is_signed()) Some(Transaction::is_signed(self))
} }
fn is_bare(&self) -> bool { fn is_bare(&self) -> bool {
!self.calls[0].is_signed() !Transaction::is_signed(self)
}
}
/*
impl<
Call: 'static + TransactionMember + From<Call> + TryInto<Call>,
> sp_runtime::traits::Extrinsic for Transaction<Call>
{
type Call = Call;
type SignaturePayload = (SeraiAddress, Signature, Extra);
fn is_signed(&self) -> Option<bool> {
Some(self.signature.is_some())
}
fn new(call: Call, signature: Option<Self::SignaturePayload>) -> Option<Self> {
Some(Self { call: call.clone().try_into().ok()?, mapped_call: call, signature })
} }
} }
impl< impl<
Call: 'static + TransactionMember + From<crate::Call> + TryInto<crate::Call>, RuntimeCall: 'static + From<Call> + GetDispatchInfo,
> frame_support::traits::ExtrinsicCall for Transaction<Call, Extra> Context: TransactionContext<RuntimeCall>,
> GetDispatchInfo for TransactionWithContext<RuntimeCall, Context>
{ {
fn call(&self) -> &Call { fn get_dispatch_info(&self) -> DispatchInfo {
&self.mapped_call 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< impl<RuntimeCall: 'static + From<Call>, Context: TransactionContext<RuntimeCall>>
Call: 'static + TransactionMember + From<crate::Call>, Checkable<Context> for Transaction<RuntimeCall>
> sp_runtime::traits::ExtrinsicMetadata for Transaction<Call, Extra>
{ {
type SignedExtensions = Extra; type Checked = TransactionWithContext<RuntimeCall, Context>;
const VERSION: u8 = 0; fn check(self, context: &Context) -> Result<Self::Checked, TransactionValidityError> {
} if let Some(ContextualizedSignature { explicit_context, signature }) =
&self.contextualized_signature
impl<
Call: 'static + TransactionMember + From<crate::Call> + GetDispatchInfo,
> GetDispatchInfo for Transaction<Call, Extra>
{ {
fn get_dispatch_info(&self) -> frame_support::dispatch::DispatchInfo { let ExplicitContext { historic_block, include_by, signer, nonce, fee } = &explicit_context;
self.mapped_call.get_dispatch_info() 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))?;
} }
} }
match context.next_nonce(signer).cmp(nonce) {
impl< core::cmp::Ordering::Less => {
Call: 'static + TransactionMember + From<crate::Call>, Err(TransactionValidityError::Invalid(InvalidTransaction::Stale))?
> sp_runtime::traits::BlindCheckable for Transaction<Call, Extra> }
{ core::cmp::Ordering::Equal => {}
type Checked = sp_runtime::generic::CheckedExtrinsic<Public, Call, Extra>; core::cmp::Ordering::Greater => {
Err(TransactionValidityError::Invalid(InvalidTransaction::Future))?
fn check( }
self, }
) -> Result<Self::Checked, sp_runtime::transaction_validity::TransactionValidityError> { if !sp_core::sr25519::Signature::from(*signature).verify(
Ok(match self.signature { Transaction::<RuntimeCall>::signature_message_unchecked(
Some((signer, signature, extra)) => { &self.calls,
if !signature.verify( Context::implicit_context(),
(&self.call, &extra, extra.additional_signed()?).encode().as_slice(), explicit_context,
&signer.into(), )
.as_slice(),
&sp_core::sr25519::Public::from(*signer),
) { ) {
Err(sp_runtime::transaction_validity::InvalidTransaction::BadProof)? Err(sp_runtime::transaction_validity::InvalidTransaction::BadProof)?;
}
context.pay_fee(signer, *fee)?;
} }
sp_runtime::generic::CheckedExtrinsic { Ok(TransactionWithContext(self, PhantomData))
signed: Some((signer.into(), extra)), }
function: self.mapped_call,
#[cfg(feature = "try-runtime")]
fn unchecked_into_checked_i_know_what_i_am_doing(
self,
c: &Context,
) -> Result<Self::Checked, TransactionValidityError> {
// 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)
} }
} }
None => sp_runtime::generic::CheckedExtrinsic { signed: None, function: self.mapped_call },
impl<
RuntimeCall: 'static
+ Send
+ Sync
+ From<Call>
+ Dispatchable<
RuntimeOrigin: From<Option<SeraiAddress>>,
Info = DispatchInfo,
PostInfo = PostDispatchInfo,
>,
Context: TransactionContext<RuntimeCall>,
> Applyable for TransactionWithContext<RuntimeCall, Context>
{
type Call = RuntimeCall;
fn validate<V: ValidateUnsigned<Call = RuntimeCall>>(
&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<V: ValidateUnsigned<Call = RuntimeCall>>(
mut self,
_info: &DispatchInfo,
_len: usize,
) -> sp_runtime::ApplyExtrinsicResultWithInfo<PostDispatchInfo> {
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::*;

View File

@@ -20,6 +20,7 @@ pub enum Call {
/// The keys being set. /// The keys being set.
key_pair: KeyPair, key_pair: KeyPair,
/// The participants in the validator set who signed off on these keys. /// The participants in the validator set who signed off on these keys.
// TODO: Bound
#[borsh( #[borsh(
serialize_with = "serai_primitives::sp_borsh::borsh_serialize_bitvec", serialize_with = "serai_primitives::sp_borsh::borsh_serialize_bitvec",
deserialize_with = "serai_primitives::sp_borsh::borsh_deserialize_bitvec" deserialize_with = "serai_primitives::sp_borsh::borsh_deserialize_bitvec"

View File

@@ -64,8 +64,8 @@ dockertest = "0.5"
serai-docker-tests = { path = "../../tests/docker" } serai-docker-tests = { path = "../../tests/docker" }
[features] [features]
serai = ["thiserror/std", "serde", "serde_json", "serai-abi/serde", "multiaddr", "sp-core", "sp-runtime", "frame-system", "simple-request"] serai = ["thiserror/std", "serde", "serde_json", "multiaddr", "sp-core", "sp-runtime", "frame-system", "simple-request"]
borsh = ["serai-abi/borsh"] borsh = []
networks = [] networks = []
bitcoin = ["networks", "dep:bitcoin"] bitcoin = ["networks", "dep:bitcoin"]

View File

@@ -1,4 +1,4 @@
use sp_core::bounded_vec::BoundedVec; use sp_core::bounded::BoundedVec;
use serai_abi::primitives::{Amount, Coin, ExternalCoin, SeraiAddress}; use serai_abi::primitives::{Amount, Coin, ExternalCoin, SeraiAddress};
use crate::{SeraiError, TemporalSerai}; use crate::{SeraiError, TemporalSerai};

View File

@@ -6,7 +6,7 @@ use rand_core::OsRng;
use sp_core::{ use sp_core::{
ConstU32, ConstU32,
bounded_vec::BoundedVec, bounded::BoundedVec,
sr25519::{Pair, Signature}, sr25519::{Pair, Signature},
Pair as PairTrait, Pair as PairTrait,
}; };

View File

@@ -1,6 +1,6 @@
use rand_core::{RngCore, OsRng}; 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; use serai_abi::in_instructions::primitives::DexCall;

View File

@@ -22,7 +22,7 @@
use super::*; use super::*;
use frame_benchmarking::{benchmarks, whitelisted_caller}; 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 frame_system::RawOrigin as SystemOrigin;
use sp_runtime::traits::StaticLookup; use sp_runtime::traits::StaticLookup;

View File

@@ -20,6 +20,7 @@ zeroize = { version = "^1.5", features = ["derive"] }
borsh = { version = "1", default-features = false, features = ["derive", "de_strict_order"] } borsh = { version = "1", default-features = false, features = ["derive", "de_strict_order"] }
bitvec = { version = "1", default-features = false, features = ["alloc"] } 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 } 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"] } ciphersuite = { path = "../../crypto/ciphersuite", default-features = false, features = ["alloc", "ristretto"] }

View File

@@ -23,6 +23,8 @@ const HUMAN_READABLE_PART: bech32::Hrp = bech32::Hrp::parse_unchecked("sri");
/// The address for an account on Serai. /// The address for an account on Serai.
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Zeroize, BorshSerialize, BorshDeserialize)] #[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]); pub struct SeraiAddress(pub [u8; 32]);
impl SeraiAddress { impl SeraiAddress {

View File

@@ -2,8 +2,6 @@ use borsh::{io::*, BorshSerialize, BorshDeserialize};
use sp_core::{ConstU32, bounded::BoundedVec}; use sp_core::{ConstU32, bounded::BoundedVec};
// TODO: Don't serialize this as a Vec<u8>. Shorten the length-prefix, technically encoding as an
// enum.
pub fn borsh_serialize_bitvec<W: Write>( pub fn borsh_serialize_bitvec<W: Write>(
bitvec: &bitvec::vec::BitVec<u8, bitvec::order::Lsb0>, bitvec: &bitvec::vec::BitVec<u8, bitvec::order::Lsb0>,
writer: &mut W, writer: &mut W,
@@ -21,6 +19,8 @@ pub fn borsh_deserialize_bitvec<R: Read>(
type SerializeBoundedVecAs<T> = alloc::vec::Vec<T>; type SerializeBoundedVecAs<T> = alloc::vec::Vec<T>;
// TODO: Don't serialize this as a Vec<u8>. Shorten the length-prefix, technically encoding as an
// enum.
pub fn borsh_serialize_bounded_vec<W: Write, T: BorshSerialize, const B: u32>( pub fn borsh_serialize_bounded_vec<W: Write, T: BorshSerialize, const B: u32>(
bounded: &BoundedVec<T, ConstU32<B>>, bounded: &BoundedVec<T, ConstU32<B>>,
writer: &mut W, writer: &mut W,