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-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"]

View File

@@ -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<Call>,
> {
header: SubstrateHeader,
#[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 {
@@ -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<RuntimeCall: 'static + Send + Sync + Clone + PartialEq + Eq + Debug + From<Call>>
HeaderProvider for SubstrateBlock<RuntimeCall>
{
type HeaderT = SubstrateHeader;
}
impl BlockTrait for SubstrateBlock {
type Extrinsic = Transaction;
impl<RuntimeCall: 'static + Send + Sync + Clone + PartialEq + Eq + Debug + From<Call>> BlockTrait
for SubstrateBlock<RuntimeCall>
{
type Extrinsic = Transaction<RuntimeCall>;
type Header = SubstrateHeader;
type Hash = H256;
fn header(&self) -> &Self::Header {

View File

@@ -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<Call>);
pub struct SignedCalls(BoundedVec<Call, ConstU32<{ MAX_CALLS }>>);
impl TryFrom<Vec<Call>> for SignedCalls {
type Error = SignedCallsError;
fn try_from(calls: Vec<Call>) -> Result<Self, Self::Error> {
@@ -31,7 +35,7 @@ impl TryFrom<Vec<Call>> for SignedCalls {
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.
#[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<NonZero<u64>>,
/// This transaction can not be included in a block whose time is equal or greater to this value.
pub include_by: Option<NonZero<u64>>,
/// 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<RuntimeCall: 'static + From<Call> = Call> {
///
/// These calls are executed atomically. Either all successfully execute or none do. The
/// transaction's fee is paid regardless.
// TODO: Bound
calls: Vec<Call>,
// TODO: if this is unsigned, we only allow a single call. Should we serialize that as 0?
calls: BoundedVec<Call, ConstU32<{ MAX_CALLS }>>,
/// The calls, as defined by Substrate.
runtime_calls: Vec<RuntimeCall>,
/// 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> {
fn deserialize_reader<R: io::Read>(reader: &mut R) -> io::Result<Self> {
// 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
let mut runtime_calls = Vec::with_capacity(calls.len());
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> {
/// 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.
pub fn signature_message(
calls: &SignedCalls,
implicit_context: &ImplicitContext,
explicit_context: &ExplicitContext,
) -> Vec<u8> {
let mut message = Vec::with_capacity(
(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
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<RuntimeCall: 'static + From<Call>> Transaction<RuntimeCall> {
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<RuntimeCall: 'static + From<Call>> Encode for Transaction<RuntimeCall> {
fn encode(&self) -> Vec<u8> {
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> {
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> {
fn read(&mut self, buf: &mut [u8]) -> borsh::io::Result<usize> {
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<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> {
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<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 })
!Transaction::is_signed(self)
}
}
impl<
Call: 'static + TransactionMember + From<crate::Call> + TryInto<crate::Call>,
> frame_support::traits::ExtrinsicCall for Transaction<Call, Extra>
RuntimeCall: 'static + From<Call> + GetDispatchInfo,
Context: TransactionContext<RuntimeCall>,
> GetDispatchInfo for TransactionWithContext<RuntimeCall, Context>
{
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<crate::Call>,
> sp_runtime::traits::ExtrinsicMetadata for Transaction<Call, Extra>
impl<RuntimeCall: 'static + From<Call>, Context: TransactionContext<RuntimeCall>>
Checkable<Context> for Transaction<RuntimeCall>
{
type SignedExtensions = Extra;
type Checked = TransactionWithContext<RuntimeCall, Context>;
const VERSION: u8 = 0;
}
impl<
Call: 'static + TransactionMember + From<crate::Call> + GetDispatchInfo,
> GetDispatchInfo for Transaction<Call, Extra>
{
fn get_dispatch_info(&self) -> frame_support::dispatch::DispatchInfo {
self.mapped_call.get_dispatch_info()
}
}
impl<
Call: 'static + TransactionMember + From<crate::Call>,
> sp_runtime::traits::BlindCheckable for Transaction<Call, Extra>
{
type Checked = sp_runtime::generic::CheckedExtrinsic<Public, Call, Extra>;
fn check(
self,
) -> Result<Self::Checked, sp_runtime::transaction_validity::TransactionValidityError> {
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<Self::Checked, TransactionValidityError> {
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::<RuntimeCall>::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<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)
}
}
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.
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"

View File

@@ -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"]

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 crate::{SeraiError, TemporalSerai};

View File

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

View File

@@ -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;

View File

@@ -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;

View File

@@ -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"] }

View File

@@ -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 {

View File

@@ -2,8 +2,6 @@ use borsh::{io::*, BorshSerialize, BorshDeserialize};
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>(
bitvec: &bitvec::vec::BitVec<u8, bitvec::order::Lsb0>,
writer: &mut W,
@@ -21,6 +19,8 @@ pub fn borsh_deserialize_bitvec<R: Read>(
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>(
bounded: &BoundedVec<T, ConstU32<B>>,
writer: &mut W,