//! Additional order authorization without changing legacy signed-order bytes. use crate::*; use frame_support::{ BoundedBTreeMap, dispatch::DispatchResult, ensure, traits::{Get, OnKilledAccount}, transactional, }; use sp_runtime::traits::Zero; pub const ORDER_ID_DOMAIN: &[u8] = b"bittensor/ms/v2/order-id"; /// Bound account-approved entries independently of legacy batching. pub type MaxAuthorizedOrders = ConstU32<8>; /// At most eight live approvals per existing system account. Terminal and expired /// entries are reclaimed on the next approval; the nonce watermark stays bounded. pub type MaxPendingOrders = ConstU32<8>; #[derive( Clone, Debug, PartialEq, Eq, Encode, Decode, DecodeWithMemTracking, TypeInfo, MaxEncodedLen, )] pub struct OrderApproval { pub id: H256, pub commitment: [u8; 64], pub expiry: u64, pub status: Option, } #[derive( Clone, Debug, Default, PartialEq, Eq, Encode, Decode, DecodeWithMemTracking, TypeInfo, MaxEncodedLen, )] pub struct AccountOrderApprovals { pub highest_nonce: Option, pub orders: BoundedBTreeMap, } impl OnKilledAccount for Pallet { fn on_killed_account(who: &AccountId32) { // One bounded value, with no per-order tombstones left behind. In // particular, this hook must never keep an account alive. ApprovedOrders::::remove(who); } } #[derive( Clone, Debug, PartialEq, Eq, Encode, Decode, DecodeWithMemTracking, TypeInfo, MaxEncodedLen, )] pub enum OrderAuthorization { #[codec(index = 0)] AccountApproved, } #[derive( Clone, Debug, PartialEq, Eq, Encode, Decode, DecodeWithMemTracking, TypeInfo, MaxEncodedLen, )] pub struct AuthorizedOrder { pub nonce: u64, pub order: VersionedOrder, pub authorization: OrderAuthorization, pub partial_fill: Option, } /// Local order proof encoding. The first three variants are byte-for-byte /// identical to MultiSignature. This never changes the outer extrinsic signature. #[derive( Clone, Debug, PartialEq, Eq, Encode, Decode, DecodeWithMemTracking, TypeInfo, MaxEncodedLen, )] pub enum OrderSignature { #[codec(index = 0)] Ed25519(sp_core::ed25519::Signature), #[codec(index = 1)] Sr25519(sp_core::sr25519::Signature), #[codec(index = 2)] Ecdsa(sp_core::ecdsa::Signature), #[codec(index = 3)] AccountApproved, } impl OrderSignature { pub fn is_authenticated(&self) -> bool { matches!(self, Self::AccountApproved) } pub fn verify(&self, message: &[u8], signer: &AccountId32) -> bool { use sp_runtime::{MultiSignature, traits::Verify}; let signature = match self { Self::Ed25519(signature) => MultiSignature::Ed25519(*signature), Self::Sr25519(signature) => MultiSignature::Sr25519(*signature), Self::Ecdsa(signature) => MultiSignature::Ecdsa(*signature), Self::AccountApproved => return false, }; signature.verify(message, signer) } } impl From for OrderSignature { fn from(signature: sp_runtime::MultiSignature) -> Self { match signature { sp_runtime::MultiSignature::Ed25519(signature) => Self::Ed25519(signature), sp_runtime::MultiSignature::Sr25519(signature) => Self::Sr25519(signature), sp_runtime::MultiSignature::Ecdsa(signature) => Self::Ecdsa(signature), } } } impl TryFrom for SignedOrder { type Error = &'static str; fn try_from(signed: AuthorizedOrder) -> Result { Ok(Self { order: signed.order.with_nonce(signed.nonce)?, signature: match signed.authorization { OrderAuthorization::AccountApproved => OrderSignature::AccountApproved, }, partial_fill: signed.partial_fill, }) } } impl Pallet { pub(crate) fn authenticated_order( signed: &SignedOrder, ) -> Result, sp_runtime::DispatchError> { let Some((nonce, order)) = signed.order.authorized_terms() else { ensure!( !signed.signature.is_authenticated(), Error::::InvalidAuthorizationMode ); return Ok(None); }; let authorization = match &signed.signature { OrderSignature::AccountApproved => OrderAuthorization::AccountApproved, _ => return Err(Error::::InvalidAuthorizationMode.into()), }; Ok(Some(AuthorizedOrder { nonce, order, authorization, partial_fill: signed.partial_fill, })) } pub(crate) fn check_authorized_batch_bound( orders: &[SignedOrder], ) -> DispatchResult { // Bound attempted account approvals, including malformed combinations, // before any signature verification or mutable order processing. let count = orders .iter() .filter(|signed| signed.order.is_authorized() || signed.signature.is_authenticated()) .count(); ensure!( count <= >::get() as usize, Error::::OrderBatchTooLarge ); Ok(()) } pub fn execute_orders_weight( orders: &[SignedOrder], ) -> frame_support::weights::Weight { use crate::weights::WeightInfo; let count = orders .iter() .filter(|signed| signed.order.is_authorized() || signed.signature.is_authenticated()) .count() as u32; if count == 0 { T::WeightInfo::execute_orders(orders.len() as u32) } else { // The two-dimensional reference benchmark covers total entries 8..max // and authenticated entries 1..8. Small batches charge its minimum. let n = (orders.len() as u32).max(8); T::WeightInfo::execute_authenticated_orders(n, count) } } pub fn execute_batched_orders_weight( orders: &[SignedOrder], ) -> frame_support::weights::Weight { use crate::weights::WeightInfo; let count = orders .iter() .filter(|signed| signed.order.is_authorized() || signed.signature.is_authenticated()) .count() as u32; if count == 0 { T::WeightInfo::execute_batched_orders(orders.len() as u32) } else { let n = (orders.len() as u32).max(8); T::WeightInfo::execute_authenticated_batched_orders(n, count) } } #[transactional] pub(crate) fn cancel_authenticated_order( who: AccountId32, nonce: u64, order: VersionedOrder, ) -> DispatchResult { let order_id = Self::derive_authorized_order_id(nonce, &order); ApprovedOrders::::try_mutate_exists(&who, |state| -> DispatchResult { // Cancelling an order that was never approved must not create state. let Some(state) = state else { return Ok(()) }; let Some(approval) = state.orders.get_mut(&nonce) else { return Ok(()); }; ensure!(approval.id == order_id, Error::::OrderNonceUsed); ensure!( !matches!( approval.status, Some(OrderStatus::Fulfilled | OrderStatus::Cancelled) ), Error::::OrderAlreadyProcessed ); approval.status = Some(OrderStatus::Cancelled); Ok(()) })?; Self::deposit_event(Event::OrderCancelled { order_id, signer: who, }); Ok(()) } pub fn derive_authorized_order_id(nonce: u64, order: &VersionedOrder) -> H256 { let mut bytes = ORDER_ID_DOMAIN.to_vec(); ( frame_system::Pallet::::block_hash( frame_system::pallet_prelude::BlockNumberFor::::zero(), ), order.signer(), nonce, order, ) .encode_to(&mut bytes); H256(sp_core::hashing::blake2_256(&bytes)) } /// Full-strength owner approval; H256 remains only the shared bookkeeping ID. pub fn order_approval_commitment(nonce: u64, order: &VersionedOrder) -> [u8; 64] { let terms = ( frame_system::Pallet::::block_hash( frame_system::pallet_prelude::BlockNumberFor::::zero(), ), order.signer(), nonce, order, ) .encode(); subtensor_hashed::mldsa::shake256_64(&[b"bittensor/ms/v2/order-approval", &terms]) } pub(crate) fn approve_order( owner: &AccountId32, nonce: u64, order: &VersionedOrder, ) -> Result { use frame_support::traits::UnixTime; // Derived/proxy origins can exist without a System account. Do not give // them orphaned storage that can never trigger OnKilledAccount. let account = frame_system::Account::::get(owner); ensure!( account.providers > 0 || account.sufficients > 0, Error::::OrderAccountMissing ); let id = Self::derive_authorized_order_id(nonce, order); let expiry = order.view().expiry; let now = T::TimeProvider::now().as_millis() as u64; ensure!(now <= expiry, Error::::OrderExpired); ApprovedOrders::::try_mutate(owner, |state| -> DispatchResult { let state = state.get_or_insert_with(AccountOrderApprovals::default); if let Some(existing) = state.orders.get_mut(&nonce) { ensure!(existing.id == id, Error::::OrderNonceUsed); ensure!( !matches!( existing.status, Some(OrderStatus::Fulfilled | OrderStatus::Cancelled) ), Error::::OrderAlreadyProcessed ); existing.commitment = Self::order_approval_commitment(nonce, order); return Ok(()); } ensure!( state.highest_nonce.is_none_or(|highest| nonce > highest), Error::::OrderNonceUsed ); state.orders.retain(|_, approval| { now <= approval.expiry && !matches!( approval.status, Some(OrderStatus::Fulfilled | OrderStatus::Cancelled) ) }); state .orders .try_insert( nonce, OrderApproval { id, commitment: Self::order_approval_commitment(nonce, order), expiry, status: None, }, ) .map_err(|_| Error::::TooManyApprovedOrders)?; state.highest_nonce = Some(nonce); Ok(()) }) .map(|()| id) } pub fn approved_order(owner: &AccountId32, nonce: u64) -> Option { ApprovedOrders::::get(owner)?.orders.get(&nonce).cloned() } pub(crate) fn order_status( owner: &AccountId32, nonce: Option, id: H256, ) -> Option { if let Some(nonce) = nonce { Self::approved_order(owner, nonce) .filter(|approval| approval.id == id)? .status } else { Orders::::get(id) } } pub(crate) fn set_order_status( owner: &AccountId32, nonce: Option, id: H256, status: OrderStatus, ) { if let Some(nonce) = nonce { ApprovedOrders::::mutate_exists(owner, |state| { // Asset movement may reap the owner during execution. Never // recreate its approvals or fall back to permanent Orders state. if let Some(approval) = state .as_mut() .and_then(|state| state.orders.get_mut(&nonce)) && approval.id == id { approval.status = Some(status); } }); } else { Orders::::insert(id, status); } } pub(crate) fn validate_authorized_order( signed: &AuthorizedOrder, order: &OrderView, id: H256, now_ms: u64, price: substrate_fixed::types::U64F64, relayer: &AccountId32, ) -> Result<(u64, Option), sp_runtime::DispatchError> { use frame_support::traits::{Contains, Get}; ensure!(!order.netuid.is_root(), Error::::RootNetUidNotAllowed); ensure!( T::AuthenticatedOrderSignerFilter::contains(&order.signer), Error::::OrderSignerFrozen ); ensure!( order.chain_id == T::ChainId::get(), Error::::ChainIdMismatch ); let approval = Self::approved_order(&order.signer, signed.nonce) .ok_or(Error::::OrderNotApproved)?; ensure!(approval.id == id, Error::::OrderNonceUsed); let (amount, provider) = Self::validate_order_execution(order, signed.partial_fill, id, now_ms, price, relayer)?; match &signed.authorization { OrderAuthorization::AccountApproved => { ensure!( approval.commitment == Self::order_approval_commitment(signed.nonce, &signed.order), Error::::OrderNotApproved ); } } Ok((amount, provider)) } #[transactional] pub(crate) fn try_execute_authorized_order( signed: AuthorizedOrder, id: H256, relayer: &AccountId32, ) -> DispatchResult { use frame_support::traits::UnixTime; let mut order = signed.order.view(); order.authorization_nonce = Some(signed.nonce); let now_ms = T::TimeProvider::now().as_millis() as u64; let price = T::SwapInterface::current_alpha_price(order.netuid); let (amount, provider) = Self::validate_authorized_order(&signed, &order, id, now_ms, price, relayer)?; Self::execute_validated_order(&order, signed.partial_fill, id, amount, provider)?; Ok(()) } #[transactional] pub(crate) fn do_execute_authenticated_batch( netuid: NetUid, orders: BoundedVec, T::MaxOrdersPerBatch>, relayer: &AccountId32, ) -> DispatchResult { use alloc::collections::BTreeSet; use frame_support::traits::{Get, UnixTime}; ensure!(!netuid.is_root(), Error::::RootNetUidNotAllowed); ensure!( orders.len() <= T::MaxOrdersPerBatch::get() as usize, Error::::OrderBatchTooLarge ); let now_ms = T::TimeProvider::now().as_millis() as u64; let price = T::SwapInterface::current_alpha_price(netuid); let mut buys: BoundedVec, T::MaxOrdersPerBatch> = BoundedVec::new(); let mut sells: BoundedVec, T::MaxOrdersPerBatch> = BoundedVec::new(); let mut seen_ids = BTreeSet::new(); let mut seen_providers = BTreeSet::new(); for signed in orders { let order = signed.order.view(); let id = Self::derive_order_id(&signed.order); ensure!(seen_ids.insert(id), Error::::DuplicateOrderInBatch); ensure!(order.netuid == netuid, Error::::OrderNetUidMismatch); let (amount, provider) = Self::is_order_valid(&signed, id, now_ms, price, relayer)?; if let Some(provider) = provider { ensure!(seen_providers.insert(provider), Error::::NoLinkedOutput); } let entry = Self::classify_order(&order, signed.partial_fill, id, amount, provider); let side = if entry.side.is_buy() { &mut buys } else { &mut sells }; side.try_push(entry) .map_err(|_| Error::::OrderBatchTooLarge)?; } Self::execute_classified_batch(netuid, price, buys, sells)?; Ok(()) } }