diff --git a/substrate/abi/Cargo.toml b/substrate/abi/Cargo.toml index cfd5a203..4cbfb672 100644 --- a/substrate/abi/Cargo.toml +++ b/substrate/abi/Cargo.toml @@ -12,76 +12,38 @@ rust-version = "1.85" all-features = true rustdoc-args = ["--cfg", "docsrs"] +[package.metadata.cargo-machete] +ignored = ["serde"] + [lints] workspace = true [dependencies] -bitvec = { version = "1", default-features = false, features = ["alloc", "serde"] } - -scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive", "bit-vec"] } -scale-info = { version = "2", default-features = false, features = ["derive", "bit-vec"] } - -borsh = { version = "1", default-features = false, features = ["derive", "de_strict_order"], optional = true } -serde = { version = "1", default-features = false, features = ["derive", "alloc"], optional = true } +borsh = { version = "1", default-features = false, features = ["derive", "de_strict_order"] } +bitvec = { version = "1", default-features = false, features = ["alloc"] } sp-core = { git = "https://github.com/serai-dex/polkadot-sdk", branch = "serai-next", default-features = false } -sp-runtime = { git = "https://github.com/serai-dex/polkadot-sdk", branch = "serai-next", default-features = false } -sp-consensus-babe = { git = "https://github.com/serai-dex/polkadot-sdk", branch = "serai-next", default-features = false } -sp-consensus-grandpa = { git = "https://github.com/serai-dex/polkadot-sdk", branch = "serai-next", default-features = false } - -frame-support = { git = "https://github.com/serai-dex/polkadot-sdk", branch = "serai-next", default-features = false } +serde = { version = "1", 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 } +sp-runtime = { git = "https://github.com/serai-dex/polkadot-sdk", branch = "serai-next", default-features = false, features = ["serde"], optional = true } serai-primitives = { path = "../primitives", version = "0.1", default-features = false } -serai-coins-primitives = { path = "../coins/primitives", version = "0.1", default-features = false } -serai-validator-sets-primitives = { path = "../validator-sets/primitives", version = "0.1", default-features = false } -serai-genesis-liquidity-primitives = { path = "../genesis-liquidity/primitives", version = "0.1", default-features = false } -serai-emissions-primitives = { path = "../emissions/primitives", version = "0.1", default-features = false } -serai-in-instructions-primitives = { path = "../in-instructions/primitives", version = "0.1", default-features = false } -serai-signals-primitives = { path = "../signals/primitives", version = "0.1", default-features = false } [features] std = [ + "borsh/std", + "bitvec/std", - - "scale/std", - "scale-info/std", - - "borsh?/std", - "serde?/std", - "sp-core/std", - "sp-runtime/std", - "sp-consensus-babe/std", - "sp-consensus-grandpa/std", - - "frame-support/std", + "serde?/std", + "scale?/std", + "scale-info?/std", + "sp-runtime?/std", "serai-primitives/std", - "serai-coins-primitives/std", - "serai-validator-sets-primitives/std", - "serai-genesis-liquidity-primitives/std", - "serai-emissions-primitives/std", - "serai-in-instructions-primitives/std", - "serai-signals-primitives/std", -] -borsh = [ - "dep:borsh", - "serai-primitives/borsh", - "serai-coins-primitives/borsh", - "serai-validator-sets-primitives/borsh", - "serai-genesis-liquidity-primitives/borsh", - "serai-in-instructions-primitives/borsh", - "serai-signals-primitives/borsh", -] -serde = [ - "dep:serde", - "serai-primitives/serde", - "serai-coins-primitives/serde", - "serai-validator-sets-primitives/serde", - "serai-genesis-liquidity-primitives/serde", - "serai-in-instructions-primitives/serde", - "serai-signals-primitives/serde", ] +substrate = ["serde", "scale", "scale-info", "sp-runtime"] default = ["std"] diff --git a/substrate/abi/README.md b/substrate/abi/README.md new file mode 100644 index 00000000..b4168def --- /dev/null +++ b/substrate/abi/README.md @@ -0,0 +1,4 @@ +# serai-abi + +Serai's ABI, inclusive to the transaction, event, and block types. MIT-licensed to ensure usability +in a variety of contexts. diff --git a/substrate/abi/src/babe.rs b/substrate/abi/src/babe.rs deleted file mode 100644 index 9bba63d9..00000000 --- a/substrate/abi/src/babe.rs +++ /dev/null @@ -1,17 +0,0 @@ -use sp_consensus_babe::EquivocationProof; - -use serai_primitives::{Header, SeraiAddress}; - -#[derive(Clone, PartialEq, Eq, Debug, scale::Encode, scale::Decode, scale_info::TypeInfo)] -pub struct ReportEquivocation { - pub equivocation_proof: alloc::boxed::Box>, - pub key_owner_proof: SeraiAddress, -} - -// We could define a Babe Config here and use the literal pallet_babe::Call -// The disadvantage to this would be the complexity and presence of junk fields such as `__Ignore` -#[derive(Clone, PartialEq, Eq, Debug, scale::Encode, scale::Decode, scale_info::TypeInfo)] -pub enum Call { - report_equivocation(ReportEquivocation), - report_equivocation_unsigned(ReportEquivocation), -} diff --git a/substrate/abi/src/block.rs b/substrate/abi/src/block.rs new file mode 100644 index 00000000..2fe103a9 --- /dev/null +++ b/substrate/abi/src/block.rs @@ -0,0 +1,250 @@ +use alloc::vec::Vec; + +use borsh::{BorshSerialize, BorshDeserialize}; + +use crate::{primitives::BlockHash, Transaction}; + +/// A V1 header for a block. +#[derive(Clone, Copy, PartialEq, Eq, Debug, BorshSerialize, BorshDeserialize)] +pub struct HeaderV1 { + /// The index of this block on the blockchain. + /// + /// The genesis block has number 0. + pub number: u64, + /// The block this header builds upon. + pub parent_hash: BlockHash, + /// The root of a Merkle tree commiting to the transactions within this block. + // TODO: Review the format of this defined by Substrate + pub transactions_root: [u8; 32], + /// A commitment to the consensus data used to justify adding this block to the blockchain. + pub consensus_commitment: [u8; 32], +} + +/// A header for a block. +#[derive(Clone, Copy, PartialEq, Eq, Debug, BorshSerialize, BorshDeserialize)] +pub enum Header { + /// A version 1 header. + V1(HeaderV1), +} + +impl Header { + /// Get the hash of the header. + pub fn number(&self) -> u64 { + match self { + Header::V1(HeaderV1 { number, .. }) => *number, + } + } + /// Get the hash of the header. + pub fn parent_hash(&self) -> BlockHash { + match self { + Header::V1(HeaderV1 { parent_hash, .. }) => *parent_hash, + } + } + /// Get the hash of the header. + pub fn transactions_root(&self) -> [u8; 32] { + match self { + Header::V1(HeaderV1 { transactions_root, .. }) => *transactions_root, + } + } + /// Get the hash of the header. + pub fn hash(&self) -> BlockHash { + BlockHash(sp_core::blake2_256(&borsh::to_vec(self).unwrap())) + } +} + +/// A block. +/// +/// This does not guarantee consistency. The header's `transactions_root` may not match the +/// contained transactions. +#[derive(Clone, PartialEq, Eq, Debug, BorshSerialize, BorshDeserialize)] +pub struct Block { + /// The block's header. + pub header: Header, + /// The block's transactions. + pub transactions: Vec, +} + +#[cfg(feature = "substrate")] +mod substrate { + use scale::{Encode, Decode}; + use scale_info::TypeInfo; + + use sp_core::H256; + use sp_runtime::{ + generic::Digest, + traits::{Header as HeaderTrait, HeaderProvider, Block as BlockTrait}, + }; + + use super::*; + + /// The consensus data for a V1 header. + /// + /// This is not considered part of the protocol proper and may be pruned in the future. It's + /// solely considered used for consensus now. + #[derive(Clone, PartialEq, Eq, Debug, Encode, Decode, TypeInfo, sp_runtime::Serialize)] + pub struct ConsensusV1 { + /// The state root. + state_root: H256, + /// The consensus digests. + digest: Digest, + } + + /// A V1 header for a block, as needed by 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, + } + + /// A header for a block, as needed by Substrate. + #[derive(Clone, PartialEq, Eq, Debug, Encode, Decode, TypeInfo, sp_runtime::Serialize)] + pub enum SubstrateHeader { + /// A version 1 header. + V1(SubstrateHeaderV1), + } + + impl From<&SubstrateHeader> for Header { + fn from(header: &SubstrateHeader) -> Header { + match header { + SubstrateHeader::V1(header) => Header::V1(HeaderV1 { + number: header.number, + parent_hash: BlockHash(header.parent_hash.0), + transactions_root: header.transactions_root.0, + 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 { + header: SubstrateHeader, + #[serde(skip)] // This makes this unsafe to deserialize, but we don't impl `Deserialize` + transactions: Vec, + } + + impl HeaderTrait for SubstrateHeader { + type Number = u64; + type Hash = H256; + type Hashing = sp_runtime::traits::BlakeTwo256; + + fn new( + number: Self::Number, + extrinsics_root: Self::Hash, + state_root: Self::Hash, + parent_hash: Self::Hash, + digest: Digest, + ) -> Self { + SubstrateHeader::V1(SubstrateHeaderV1 { + number, + parent_hash, + transactions_root: extrinsics_root, + consensus: ConsensusV1 { state_root, digest }, + }) + } + + fn number(&self) -> &Self::Number { + match self { + SubstrateHeader::V1(SubstrateHeaderV1 { number, .. }) => number, + } + } + fn set_number(&mut self, number: Self::Number) { + match self { + SubstrateHeader::V1(SubstrateHeaderV1 { number: existing, .. }) => { + *existing = number; + } + } + } + + fn extrinsics_root(&self) -> &Self::Hash { + match self { + SubstrateHeader::V1(SubstrateHeaderV1 { transactions_root, .. }) => transactions_root, + } + } + fn set_extrinsics_root(&mut self, extrinsics_root: Self::Hash) { + match self { + SubstrateHeader::V1(SubstrateHeaderV1 { transactions_root, .. }) => { + *transactions_root = extrinsics_root; + } + } + } + + fn state_root(&self) -> &Self::Hash { + match self { + SubstrateHeader::V1(SubstrateHeaderV1 { consensus, .. }) => &consensus.state_root, + } + } + fn set_state_root(&mut self, state_root: Self::Hash) { + match self { + SubstrateHeader::V1(SubstrateHeaderV1 { consensus, .. }) => { + consensus.state_root = state_root; + } + } + } + + fn parent_hash(&self) -> &Self::Hash { + match self { + SubstrateHeader::V1(SubstrateHeaderV1 { parent_hash, .. }) => parent_hash, + } + } + fn set_parent_hash(&mut self, parent_hash: Self::Hash) { + match self { + SubstrateHeader::V1(SubstrateHeaderV1 { parent_hash: existing, .. }) => { + *existing = parent_hash; + } + } + } + + fn digest(&self) -> &Digest { + match self { + SubstrateHeader::V1(SubstrateHeaderV1 { consensus, .. }) => &consensus.digest, + } + } + fn digest_mut(&mut self) -> &mut Digest { + match self { + SubstrateHeader::V1(SubstrateHeaderV1 { consensus, .. }) => &mut consensus.digest, + } + } + + fn hash(&self) -> H256 { + H256::from(Header::from(self).hash().0) + } + } + + impl HeaderProvider for SubstrateBlock { + type HeaderT = SubstrateHeader; + } + + impl BlockTrait for SubstrateBlock { + type Extrinsic = Transaction; + type Header = SubstrateHeader; + type Hash = H256; + fn header(&self) -> &Self::Header { + &self.header + } + fn extrinsics(&self) -> &[Self::Extrinsic] { + &self.transactions + } + fn deconstruct(self) -> (Self::Header, Vec) { + (self.header, self.transactions) + } + fn new(header: Self::Header, transactions: Vec) -> Self { + Self { header, transactions } + } + fn encode_from(header: &Self::Header, transactions: &[Self::Extrinsic]) -> Vec { + let header = header.encode(); + let transactions = transactions.encode(); + let mut block = header; + block.extend(transactions); + block + } + fn hash(&self) -> Self::Hash { + self.header.hash() + } + } +} +#[cfg(feature = "substrate")] +pub use substrate::*; diff --git a/substrate/abi/src/coins.rs b/substrate/abi/src/coins.rs index 9466db0f..56266aae 100644 --- a/substrate/abi/src/coins.rs +++ b/substrate/abi/src/coins.rs @@ -1,25 +1,70 @@ -use serai_primitives::{Balance, SeraiAddress}; +use borsh::{BorshSerialize, BorshDeserialize}; -pub use serai_coins_primitives as primitives; -use primitives::OutInstructionWithBalance; +use serai_primitives::{ + address::SeraiAddress, balance::Balance, instructions::OutInstructionWithBalance, +}; -#[derive(Clone, PartialEq, Eq, Debug, scale::Encode, scale::Decode, scale_info::TypeInfo)] -#[cfg_attr(feature = "borsh", derive(borsh::BorshSerialize, borsh::BorshDeserialize))] -#[cfg_attr(feature = "serde", derive(serde::Serialize))] -#[cfg_attr(all(feature = "std", feature = "serde"), derive(serde::Deserialize))] +/// A call to coins. +#[derive(Clone, PartialEq, Eq, Debug, BorshSerialize, BorshDeserialize)] pub enum Call { - transfer { to: SeraiAddress, balance: Balance }, - burn { balance: Balance }, - burn_with_instruction { instruction: OutInstructionWithBalance }, + /// Transfer these coins to the specified address. + transfer { + /// The address to transfer to. + to: SeraiAddress, + /// The coins to transfer. + coins: Balance, + }, + /// Burn these coins. + burn { + /// The coins to burn. + coins: Balance, + }, + /// Burn these coins with an `OutInstruction` specified. + burn_with_instruction { + /// The `OutInstruction`, with the coins to burn. + instruction: OutInstructionWithBalance, + }, } -#[derive(Clone, PartialEq, Eq, Debug, scale::Encode, scale::Decode, scale_info::TypeInfo)] -#[cfg_attr(feature = "borsh", derive(borsh::BorshSerialize, borsh::BorshDeserialize))] -#[cfg_attr(feature = "serde", derive(serde::Serialize))] -#[cfg_attr(all(feature = "std", feature = "serde"), derive(serde::Deserialize))] -pub enum Event { - Mint { to: SeraiAddress, balance: Balance }, - Burn { from: SeraiAddress, balance: Balance }, - BurnWithInstruction { from: SeraiAddress, instruction: OutInstructionWithBalance }, - Transfer { from: SeraiAddress, to: SeraiAddress, balance: Balance }, +impl Call { + pub(crate) fn is_signed(&self) -> bool { + match self { + Call::transfer { .. } | Call::burn { .. } | Call::burn_with_instruction { .. } => true, + } + } +} + +/// An event from the system. +#[derive(Clone, PartialEq, Eq, Debug, BorshSerialize, BorshDeserialize)] +pub enum Event { + /// The specified coins were minted. + Mint { + /// The address minted to. + to: SeraiAddress, + /// The coins minted. + coins: Balance, + }, + /// The specified coins were burnt. + Burn { + /// The address burnt from. + from: SeraiAddress, + /// The coins burnt. + coins: Balance, + }, + /// The specified coins were burnt with an `OutInstruction` specified. + BurnWithInstruction { + /// The address burnt from. + from: SeraiAddress, + /// The `OutInstruction` specified, and the coins burnt. + instruction: OutInstructionWithBalance, + }, + /// The specified coins were transferred. + Transfer { + /// The address transferred from. + from: SeraiAddress, + /// The address transferred to. + to: SeraiAddress, + /// The coins transferred. + coins: Balance, + }, } diff --git a/substrate/abi/src/dex.rs b/substrate/abi/src/dex.rs index d85dace3..23adf1ee 100644 --- a/substrate/abi/src/dex.rs +++ b/substrate/abi/src/dex.rs @@ -1,75 +1,121 @@ -use sp_runtime::BoundedVec; +use alloc::vec::Vec; -use serai_primitives::*; +use borsh::{BorshSerialize, BorshDeserialize}; -type PoolId = ExternalCoin; -type MaxSwapPathLength = sp_core::ConstU32<3>; +use serai_primitives::{ + address::SeraiAddress, + coin::ExternalCoin, + balance::{Amount, ExternalBalance, Balance}, +}; -#[derive(Clone, PartialEq, Eq, Debug, scale::Encode, scale::Decode, scale_info::TypeInfo)] -#[cfg_attr(feature = "serde", derive(serde::Serialize))] -#[cfg_attr(all(feature = "std", feature = "serde"), derive(serde::Deserialize))] +/// A call to the DEX. +#[derive(Clone, PartialEq, Eq, Debug, BorshSerialize, BorshDeserialize)] pub enum Call { + /// Add liquidity. add_liquidity { + /// The coin to add liquidity for. coin: ExternalCoin, - coin_desired: SubstrateAmount, - sri_desired: SubstrateAmount, - coin_min: SubstrateAmount, - sri_min: SubstrateAmount, - mint_to: SeraiAddress, + /// The intended amount of SRI to add as liquidity. + sri_intended: Amount, + /// The intended amount of the coin to add as liquidity. + coin_intended: Amount, + /// The minimum amount of SRI to add as liquidity. + sri_minimum: Amount, + /// The minimum amount of the coin to add as liquidity. + coin_minimum: Amount, }, + /// Transfer these liquidity tokens to the specified address. + transfer_liquidity { + /// The address to transfer to. + to: SeraiAddress, + /// The liquidity tokens to transfer. + liquidity_tokens: ExternalBalance, + }, + /// Remove liquidity. remove_liquidity { - coin: ExternalCoin, - lp_token_burn: SubstrateAmount, - coin_min_receive: SubstrateAmount, - sri_min_receive: SubstrateAmount, - withdraw_to: SeraiAddress, + /// The liquidity tokens to burn, removing the underlying liquidity from the pool. + /// + /// The `coin` within the balance is the coin to remove liquidity for. + liquidity_tokens: ExternalBalance, + /// The minimum amount of SRI to receive. + sri_minimum: Amount, + /// The minimum amount of the coin to receive. + coin_minimum: Amount, }, - swap_exact_tokens_for_tokens { - path: BoundedVec, - amount_in: SubstrateAmount, - amount_out_min: SubstrateAmount, - send_to: SeraiAddress, + /// Swap an exact amount of coins. + swap_exact { + /// The coins to swap. + coins_to_swap: Balance, + /// The minimum balance to receive. + minimum_to_receive: Balance, }, - swap_tokens_for_exact_tokens { - path: BoundedVec, - amount_out: SubstrateAmount, - amount_in_max: SubstrateAmount, - send_to: SeraiAddress, + /// Swap for an exact amount of coins. + swap_for_exact { + /// The coins to receive. + coins_to_receive: Balance, + /// The maximum amount to swap. + maximum_to_swap: Balance, }, } -#[derive(Clone, PartialEq, Eq, Debug, scale::Encode, scale::Decode, scale_info::TypeInfo)] -#[cfg_attr(feature = "serde", derive(serde::Serialize))] -#[cfg_attr(all(feature = "std", feature = "serde"), derive(serde::Deserialize))] +impl Call { + pub(crate) fn is_signed(&self) -> bool { + match self { + Call::add_liquidity { .. } | + Call::transfer_liquidity { .. } | + Call::remove_liquidity { .. } | + Call::swap_exact { .. } | + Call::swap_for_exact { .. } => true, + } + } +} + +/// An event from the DEX. +#[derive(Clone, PartialEq, Eq, Debug, BorshSerialize, BorshDeserialize)] pub enum Event { - PoolCreated { - pool_id: PoolId, - pool_account: SeraiAddress, - }, - + /// Liquidity was added to a pool. LiquidityAdded { - who: SeraiAddress, - mint_to: SeraiAddress, - pool_id: PoolId, - coin_amount: SubstrateAmount, - sri_amount: SubstrateAmount, - lp_token_minted: SubstrateAmount, + /// The account which added the liquidity. + origin: SeraiAddress, + /// The account which received the liquidity tokens. + recipient: SeraiAddress, + /// The pool liquidity was added to. + pool: ExternalCoin, + /// The amount of liquidity tokens which were minted. + liquidity_tokens_minted: Amount, + /// The amount of the coin which was added to the pool's liquidity. + coin_amount: Amount, + /// The amount of SRI which was added to the pool's liquidity. + sri_amount: Amount, }, + /// Liquidity was removed from a pool. LiquidityRemoved { - who: SeraiAddress, - withdraw_to: SeraiAddress, - pool_id: PoolId, - coin_amount: SubstrateAmount, - sri_amount: SubstrateAmount, - lp_token_burned: SubstrateAmount, + /// The account which removed the liquidity. + origin: SeraiAddress, + /// The pool liquidity was removed from. + pool: ExternalCoin, + /// The mount of liquidity tokens which were burnt. + liquidity_tokens_burnt: Amount, + /// The amount of the coin which was removed from the pool's liquidity. + coin_amount: Amount, + /// The amount of SRI which was removed from the pool's liquidity. + sri_amount: Amount, }, - SwapExecuted { - who: SeraiAddress, - send_to: SeraiAddress, - path: BoundedVec, - amount_in: SubstrateAmount, - amount_out: SubstrateAmount, + /// A swap through the liquidity pools occurred. + Swap { + /// The account which made the swap. + origin: SeraiAddress, + /// The recipient for the output of the swap. + recipient: SeraiAddress, + /// The deltas incurred by the pools. + /// + /// For a swap of sriABC to sriDEF, this would be + /// `[Balance { sriABC, 1 }, Balance { SRI, 2 }, Balance { sriDEF, 3 }]`, where + /// `Balance { sriABC, 1 }` was added to the `sriABC-SRI` pool, `Balance { SRI, 2 }` was + /// removed from the `sriABC-SRI` pool and added to the `sriDEF-SRI` pool, and + /// `Balance { sriDEF, 3 }` was removed from the `sriDEF-SRI` pool. + deltas: Vec, }, } diff --git a/substrate/abi/src/economic_security.rs b/substrate/abi/src/economic_security.rs index 2899e090..f4fcd1ce 100644 --- a/substrate/abi/src/economic_security.rs +++ b/substrate/abi/src/economic_security.rs @@ -1,8 +1,13 @@ -use serai_primitives::ExternalNetworkId; +use borsh::{BorshSerialize, BorshDeserialize}; -#[derive(Clone, PartialEq, Eq, Debug, scale::Encode, scale::Decode, scale_info::TypeInfo)] -#[cfg_attr(feature = "borsh", derive(borsh::BorshSerialize, borsh::BorshDeserialize))] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +use serai_primitives::network_id::ExternalNetworkId; + +/// An event from economic security. +#[derive(Clone, PartialEq, Eq, Debug, BorshSerialize, BorshDeserialize)] pub enum Event { - EconomicSecurityReached { network: ExternalNetworkId }, + /// Economic security was achieved for a network's validator set. + EconomicSecurityAchieved { + /// The network whose validator set achieved economic security. + network: ExternalNetworkId, + }, } diff --git a/substrate/abi/src/emissions.rs b/substrate/abi/src/emissions.rs deleted file mode 100644 index ff948f5f..00000000 --- a/substrate/abi/src/emissions.rs +++ /dev/null @@ -1 +0,0 @@ -pub use serai_emissions_primitives as primitives; diff --git a/substrate/abi/src/genesis_liquidity.rs b/substrate/abi/src/genesis_liquidity.rs index 7660b3ab..fd737d2f 100644 --- a/substrate/abi/src/genesis_liquidity.rs +++ b/substrate/abi/src/genesis_liquidity.rs @@ -1,20 +1,50 @@ -pub use serai_genesis_liquidity_primitives as primitives; +use borsh::{BorshSerialize, BorshDeserialize}; -use serai_primitives::*; -use primitives::*; +use serai_primitives::{ + crypto::Signature, address::SeraiAddress, balance::ExternalBalance, genesis::GenesisValues, +}; -#[derive(Clone, PartialEq, Eq, Debug, scale::Encode, scale::Decode, scale_info::TypeInfo)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +/// A call to the genesis liquidity. +#[derive(Clone, PartialEq, Eq, Debug, BorshSerialize, BorshDeserialize)] pub enum Call { - remove_coin_liquidity { balance: ExternalBalance }, - oraclize_values { values: Values, signature: Signature }, + /// Oraclize the value of non-Bitcoin external coins relative to Bitcoin. + oraclize_values { + /// The values of the non-Bitcoin external coins. + values: GenesisValues, + /// The signature by the genesis validators for these values. + signature: Signature, + }, + /// Remove liquidity. + remove_liquidity { + /// The genesis liquidity to remove. + balance: ExternalBalance, + }, } -#[derive(Clone, PartialEq, Eq, Debug, scale::Encode, scale::Decode, scale_info::TypeInfo)] -#[cfg_attr(feature = "borsh", derive(borsh::BorshSerialize, borsh::BorshDeserialize))] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub enum Event { - GenesisLiquidityAdded { by: SeraiAddress, balance: ExternalBalance }, - GenesisLiquidityRemoved { by: SeraiAddress, balance: ExternalBalance }, - GenesisLiquidityAddedToPool { coin: ExternalBalance, sri: Amount }, +impl Call { + pub(crate) fn is_signed(&self) -> bool { + match self { + Call::oraclize_values { .. } => false, + Call::remove_liquidity { .. } => true, + } + } +} + +/// An event from the genesis liquidity. +#[derive(Clone, PartialEq, Eq, Debug, BorshSerialize, BorshDeserialize)] +pub enum Event { + /// Genesis liquidity added. + GenesisLiquidityAdded { + /// The recipient of the genesis liquidity. + recipient: SeraiAddress, + /// The coins added as genesis liquidity. + balance: ExternalBalance, + }, + /// Genesis liquidity removed. + GenesisLiquidityRemoved { + /// The account which removed the genesis liquidity. + origin: SeraiAddress, + /// The amount of genesis liquidity removed. + balance: ExternalBalance, + }, } diff --git a/substrate/abi/src/grandpa.rs b/substrate/abi/src/grandpa.rs deleted file mode 100644 index 376b0b1d..00000000 --- a/substrate/abi/src/grandpa.rs +++ /dev/null @@ -1,25 +0,0 @@ -use sp_consensus_grandpa::EquivocationProof; - -use serai_primitives::{BlockNumber, SeraiAddress}; - -#[derive(Clone, PartialEq, Eq, Debug, scale::Encode, scale::Decode, scale_info::TypeInfo)] -pub struct ReportEquivocation { - pub equivocation_proof: alloc::boxed::Box>, - pub key_owner_proof: SeraiAddress, -} - -#[derive(Clone, PartialEq, Eq, Debug, scale::Encode, scale::Decode, scale_info::TypeInfo)] -pub enum Call { - report_equivocation(ReportEquivocation), - report_equivocation_unsigned(ReportEquivocation), -} - -#[derive(Clone, PartialEq, Eq, Debug, scale::Encode, scale::Decode, scale_info::TypeInfo)] -#[cfg_attr(feature = "serde", derive(serde::Serialize))] -#[cfg_attr(all(feature = "std", feature = "serde"), derive(serde::Deserialize))] -pub enum Event { - NewAuthorities { authority_set: alloc::vec::Vec<(SeraiAddress, u64)> }, - // TODO: Remove these - Paused, - Resumed, -} diff --git a/substrate/abi/src/in_instructions.rs b/substrate/abi/src/in_instructions.rs index 1f5a64de..a3ced76a 100644 --- a/substrate/abi/src/in_instructions.rs +++ b/substrate/abi/src/in_instructions.rs @@ -1,30 +1,47 @@ -use serai_primitives::*; +use borsh::{BorshSerialize, BorshDeserialize}; -pub use serai_in_instructions_primitives as primitives; -use primitives::SignedBatch; -use serai_validator_sets_primitives::Session; +use serai_primitives::{ + BlockHash, network_id::ExternalNetworkId, validator_sets::Session, instructions::SignedBatch, +}; -#[derive(Clone, PartialEq, Eq, Debug, scale::Encode, scale::Decode, scale_info::TypeInfo)] -#[cfg_attr(feature = "borsh", derive(borsh::BorshSerialize, borsh::BorshDeserialize))] -#[cfg_attr(feature = "serde", derive(serde::Serialize))] -#[cfg_attr(all(feature = "std", feature = "serde"), derive(serde::Deserialize))] +/// A call to `InInstruction`s. +#[derive(Clone, PartialEq, Eq, Debug, BorshSerialize, BorshDeserialize)] pub enum Call { - execute_batch { batch: SignedBatch }, + /// Execute a batch of `InInstruction`s. + execute_batch { + /// The batch to execute. + batch: SignedBatch, + }, } -#[derive(Clone, PartialEq, Eq, Debug, scale::Encode, scale::Decode, scale_info::TypeInfo)] -#[cfg_attr(feature = "serde", derive(serde::Serialize))] -#[cfg_attr(all(feature = "std", feature = "serde"), derive(serde::Deserialize))] +impl Call { + pub(crate) fn is_signed(&self) -> bool { + match self { + Call::execute_batch { .. } => false, + } + } +} + +/// An event from `InInstruction`s. +#[derive(Clone, PartialEq, Eq, Debug, BorshSerialize, BorshDeserialize)] pub enum Event { + /// A batch of `InInstruction`s was executed. Batch { + /// The network for which a batch was executed. network: ExternalNetworkId, + /// The session which published the batch. publishing_session: Session, + /// The ID of the batch. id: u32, + /// The hash of the block on the external network which caused this batch's creation. external_network_block_hash: BlockHash, + /// The hash of the `InInstruction`s within this batch. in_instructions_hash: [u8; 32], + /// The results of each `InInstruction` within the batch. + #[borsh( + serialize_with = "serai_primitives::sp_borsh::borsh_serialize_bitvec", + deserialize_with = "serai_primitives::sp_borsh::borsh_deserialize_bitvec" + )] in_instruction_results: bitvec::vec::BitVec, }, - Halt { - network: ExternalNetworkId, - }, } diff --git a/substrate/abi/src/lib.rs b/substrate/abi/src/lib.rs index 255f3254..b8f43f0b 100644 --- a/substrate/abi/src/lib.rs +++ b/substrate/abi/src/lib.rs @@ -1,94 +1,98 @@ -#![cfg_attr(docsrs, feature(doc_cfg))] #![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![doc = include_str!("../README.md")] +#![deny(missing_docs)] #![cfg_attr(not(feature = "std"), no_std)] #![allow(non_camel_case_types)] extern crate alloc; +use borsh::{BorshSerialize, BorshDeserialize}; + pub use serai_primitives as primitives; +/// Call/Event for the system. pub mod system; -pub mod timestamp; - +/// Call/Event for coins. pub mod coins; -pub mod liquidity_tokens; -pub mod dex; +/// Call/Event for validator sets. pub mod validator_sets; - -pub mod genesis_liquidity; -pub mod emissions; - -pub mod economic_security; - -pub mod in_instructions; - +/// Call/Event for signals. pub mod signals; -pub mod babe; -pub mod grandpa; +/// Call/Event for the DEX. +pub mod dex; -pub mod tx; +/// Call/Event for genesis liquidity. +pub mod genesis_liquidity; +/// Event for economic security. +pub mod economic_security; -#[derive(Clone, PartialEq, Eq, Debug, scale::Encode, scale::Decode, scale_info::TypeInfo)] +/// Call/Event for `InInstruction`s. +pub mod in_instructions; + +mod transaction; +pub use transaction::*; + +mod block; +pub use block::*; + +/// All calls. +#[derive(Clone, PartialEq, Eq, Debug, BorshSerialize, BorshDeserialize)] +#[borsh(use_discriminant = true)] +#[repr(u8)] pub enum Call { - Timestamp(timestamp::Call), - Coins(coins::Call), - LiquidityTokens(liquidity_tokens::Call), - Dex(dex::Call), - ValidatorSets(validator_sets::Call), - GenesisLiquidity(genesis_liquidity::Call), - InInstructions(in_instructions::Call), - Signals(signals::Call), - Babe(babe::Call), - Grandpa(grandpa::Call), + // The call for the system. + // System(system::Call) = 0, + /// The call for coins. + Coins(coins::Call) = 1, + /// The call for validator sets. + ValidatorSets(validator_sets::Call) = 2, + /// The call for signals. + Signals(signals::Call) = 3, + /// The call for the DEX. + Dex(dex::Call) = 4, + /// The call for genesis liquidity. + GenesisLiquidity(genesis_liquidity::Call) = 5, + // The call for economic security. + // EconomicSecurity = 6, + /// The call for `InInstruction`s. + InInstructions(in_instructions::Call) = 7, } -// TODO: Remove this -#[derive(Clone, PartialEq, Eq, Debug, scale::Encode, scale::Decode, scale_info::TypeInfo)] -pub enum TransactionPaymentEvent { - TransactionFeePaid { who: serai_primitives::SeraiAddress, actual_fee: u64, tip: u64 }, +impl Call { + pub(crate) fn is_signed(&self) -> bool { + match self { + Call::Coins(call) => call.is_signed(), + Call::ValidatorSets(call) => call.is_signed(), + Call::Signals(call) => call.is_signed(), + Call::Dex(call) => call.is_signed(), + Call::GenesisLiquidity(call) => call.is_signed(), + Call::InInstructions(call) => call.is_signed(), + } + } } -#[derive(Clone, PartialEq, Eq, Debug, scale::Encode, scale::Decode, scale_info::TypeInfo)] +/// All events. +#[derive(Clone, PartialEq, Eq, Debug, BorshSerialize, BorshDeserialize)] +#[borsh(use_discriminant = true)] +#[repr(u8)] pub enum Event { - System(system::Event), - Timestamp, - TransactionPayment(TransactionPaymentEvent), - Coins(coins::Event), - LiquidityTokens(liquidity_tokens::Event), - Dex(dex::Event), - ValidatorSets(validator_sets::Event), - GenesisLiquidity(genesis_liquidity::Event), - Emissions, - EconomicSecurity(economic_security::Event), - InInstructions(in_instructions::Event), - Signals(signals::Event), - Babe, - Grandpa(grandpa::Event), + /// The event for the system. + System(system::Event) = 0, + /// The event for coins. + Coins(coins::Event) = 1, + /// The event for validator sets. + ValidatorSets(validator_sets::Event) = 2, + /// The event for signals. + Signals(signals::Event) = 3, + /// The event for the DEX. + Dex(dex::Event) = 4, + /// The event for genesis liquidity. + GenesisLiquidity(genesis_liquidity::Event) = 5, + /// The event for economic security. + EconomicSecurity(economic_security::Event) = 6, + /// The event for `InInstruction`s. + InInstructions(in_instructions::Event) = 7, } - -#[derive(Clone, Copy, PartialEq, Eq, Debug, scale::Encode, scale::Decode, scale_info::TypeInfo)] -#[cfg_attr(feature = "serde", derive(serde::Serialize))] -#[cfg_attr(all(feature = "std", feature = "serde"), derive(serde::Deserialize))] -pub struct Extra { - pub era: sp_runtime::generic::Era, - #[codec(compact)] - pub nonce: u32, - #[codec(compact)] - pub tip: u64, -} - -#[derive(Clone, PartialEq, Eq, Debug, scale::Encode, scale::Decode, scale_info::TypeInfo)] -#[cfg_attr(feature = "borsh", derive(borsh::BorshSerialize, borsh::BorshDeserialize))] -#[cfg_attr(feature = "serde", derive(serde::Serialize))] -#[cfg_attr(all(feature = "std", feature = "serde"), derive(serde::Deserialize))] -pub struct SignedPayloadExtra { - pub spec_version: u32, - pub tx_version: u32, - pub genesis: [u8; 32], - pub mortality_checkpoint: [u8; 32], -} - -pub type Transaction = tx::Transaction; diff --git a/substrate/abi/src/liquidity_tokens.rs b/substrate/abi/src/liquidity_tokens.rs deleted file mode 100644 index 6bdc651b..00000000 --- a/substrate/abi/src/liquidity_tokens.rs +++ /dev/null @@ -1,18 +0,0 @@ -use serai_primitives::{Balance, SeraiAddress}; - -#[derive(Clone, PartialEq, Eq, Debug, scale::Encode, scale::Decode, scale_info::TypeInfo)] -#[cfg_attr(feature = "borsh", derive(borsh::BorshSerialize, borsh::BorshDeserialize))] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub enum Call { - burn { balance: Balance }, - transfer { to: SeraiAddress, balance: Balance }, -} - -#[derive(Clone, PartialEq, Eq, Debug, scale::Encode, scale::Decode, scale_info::TypeInfo)] -#[cfg_attr(feature = "borsh", derive(borsh::BorshSerialize, borsh::BorshDeserialize))] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub enum Event { - Mint { to: SeraiAddress, balance: Balance }, - Burn { from: SeraiAddress, balance: Balance }, - Transfer { from: SeraiAddress, to: SeraiAddress, balance: Balance }, -} diff --git a/substrate/abi/src/signals.rs b/substrate/abi/src/signals.rs index 6a77672f..588ca7f4 100644 --- a/substrate/abi/src/signals.rs +++ b/substrate/abi/src/signals.rs @@ -1,59 +1,132 @@ -use serai_primitives::{NetworkId, SeraiAddress}; +use borsh::{BorshSerialize, BorshDeserialize}; -use serai_validator_sets_primitives::ValidatorSet; +use serai_primitives::{ + address::SeraiAddress, network_id::NetworkId, validator_sets::ValidatorSet, signals::Signal, +}; -pub use serai_signals_primitives as primitives; -use primitives::SignalId; - -#[derive(Clone, PartialEq, Eq, Debug, scale::Encode, scale::Decode, scale_info::TypeInfo)] -#[cfg_attr(feature = "borsh", derive(borsh::BorshSerialize, borsh::BorshDeserialize))] -#[cfg_attr(feature = "serde", derive(serde::Serialize))] -#[cfg_attr(all(feature = "std", feature = "serde"), derive(serde::Deserialize))] +/// A call to signals. +#[derive(Clone, PartialEq, Eq, Debug, BorshSerialize, BorshDeserialize)] pub enum Call { - register_retirement_signal { in_favor_of: [u8; 32] }, - revoke_retirement_signal { retirement_signal_id: [u8; 32] }, - favor { signal_id: SignalId, for_network: NetworkId }, - revoke_favor { signal_id: SignalId, for_network: NetworkId }, - stand_against { signal_id: SignalId, for_network: NetworkId }, + /// Register a retirement signal. + register_retirement_signal { + /// The protocol favored over the current protocol. + in_favor_of: [u8; 32], + }, + /// Revoke a retirement signal. + revoke_retirement_signal { + /// The protocol which was favored over the current protocol + was_in_favor_of: [u8; 32], + }, + /// Favor a signal. + favor { + /// The signal to favor. + signal: Signal, + /// The network this validator is expressing favor with. + /// + /// A validator may be an active validator for multiple networks. The validator must specify + /// which network they're expressing favor with in this call. + with_network: NetworkId, + }, + /// Revoke favor for a signal. + revoke_favor { + /// The signal to revoke favor for. + signal: Signal, + /// The network this validator is revoking favor with. + /// + /// A validator may have expressed favor with multiple networks. The validator must specify + /// which network they're revoking favor with in this call. + with_network: NetworkId, + }, + /// Stand against a signal. + /// + /// This has no effects other than emitting an event that this signal is stood against. If the + /// origin has prior expressed favor, they must still call `revoke_favor` for each network they + /// expressed favor with. + stand_against { + /// The signal to stand against. + signal: Signal, + }, } -#[derive(Clone, PartialEq, Eq, Debug, scale::Encode, scale::Decode, scale_info::TypeInfo)] -#[cfg_attr(feature = "borsh", derive(borsh::BorshSerialize, borsh::BorshDeserialize))] -#[cfg_attr(feature = "serde", derive(serde::Serialize))] -#[cfg_attr(all(feature = "std", feature = "serde"), derive(serde::Deserialize))] +impl Call { + pub(crate) fn is_signed(&self) -> bool { + match self { + Call::register_retirement_signal { .. } | + Call::revoke_retirement_signal { .. } | + Call::favor { .. } | + Call::revoke_favor { .. } | + Call::stand_against { .. } => true, + } + } +} + +/// An event from signals. +#[derive(Clone, PartialEq, Eq, Debug, BorshSerialize, BorshDeserialize)] pub enum Event { + /// A retirement signal has been registered. RetirementSignalRegistered { - signal_id: [u8; 32], + /// The retirement signal's ID. + signal: [u8; 32], + /// The protocol retirement is proposed in favor of. in_favor_of: [u8; 32], + /// The address which registered this signal. registrant: SeraiAddress, }, + /// A retirement signal was revoked. RetirementSignalRevoked { - signal_id: [u8; 32], + /// The retirement signal's ID. + signal: [u8; 32], }, + /// A signal was favored. SignalFavored { - signal_id: SignalId, + /// The signal favored. + signal: Signal, + /// The validator the signal was favored by. by: SeraiAddress, - for_network: NetworkId, - }, - SetInFavor { - signal_id: SignalId, - set: ValidatorSet, - }, - RetirementSignalLockedIn { - signal_id: [u8; 32], - }, - SetNoLongerInFavor { - signal_id: SignalId, - set: ValidatorSet, + /// The network with which the signal was favored. + with_network: NetworkId, }, + /// Favor for a signal was revoked. FavorRevoked { - signal_id: SignalId, + /// The signal whose favor was revoked. + signal: Signal, + /// The validator who revoked their favor for the signal. by: SeraiAddress, - for_network: NetworkId, + /// The network with which favor for the signal was revoked. + with_network: NetworkId, }, + /// A supermajority of a validator set now favor a signal. + SetInFavor { + /// The signal which now has a supermajority of a validator set favoring it. + signal: Signal, + /// The validator set which is now considered to favor the signal. + set: ValidatorSet, + }, + /// A validator set is no longer considered to favor a signal. + SetNoLongerInFavor { + /// The signal which no longer has the validator set considered in favor of it. + signal: Signal, + /// The validator set which is no longer considered to be in favor of the signal. + set: ValidatorSet, + }, + /// A retirement signal has been locked in. + RetirementSignalLockedIn { + /// The signal which has been locked in. + signal: [u8; 32], + }, + /// A validator set's ability to publish batches was halted. + /// + /// This also halts set rotation in effect, as handovers are via new sets starting to publish + /// batches. + SetHalted { + /// The signal which has been locked in. + signal: [u8; 32], + }, + /// An account has stood against a signal. AgainstSignal { - signal_id: SignalId, + /// The signal stood against. + signal: Signal, + /// The account which stood against the signal. who: SeraiAddress, - for_network: NetworkId, }, } diff --git a/substrate/abi/src/system.rs b/substrate/abi/src/system.rs index d025e767..6023e51c 100644 --- a/substrate/abi/src/system.rs +++ b/substrate/abi/src/system.rs @@ -1,13 +1,11 @@ -use frame_support::dispatch::{DispatchInfo, DispatchError}; +use borsh::{BorshSerialize, BorshDeserialize}; -use serai_primitives::SeraiAddress; - -#[derive(Clone, PartialEq, Eq, Debug, scale::Encode, scale::Decode, scale_info::TypeInfo)] +/// An event from the system. +#[derive(Clone, PartialEq, Eq, Debug, BorshSerialize, BorshDeserialize)] pub enum Event { - ExtrinsicSuccess { dispatch_info: DispatchInfo }, - ExtrinsicFailed { dispatch_error: DispatchError, dispatch_info: DispatchInfo }, - CodeUpdated, - NewAccount { account: SeraiAddress }, - KilledAccount { account: SeraiAddress }, - Remarked { sender: SeraiAddress, hash: [u8; 32] }, + /// The transaction successfully executed. + TransactionSuccess, + /// The transaction failed to execute. + // TODO: Add an error to this + TransactionFailed, } diff --git a/substrate/abi/src/timestamp.rs b/substrate/abi/src/timestamp.rs deleted file mode 100644 index af763928..00000000 --- a/substrate/abi/src/timestamp.rs +++ /dev/null @@ -1,9 +0,0 @@ -#[derive(Clone, PartialEq, Eq, Debug, scale::Encode, scale::Decode, scale_info::TypeInfo)] -#[cfg_attr(feature = "serde", derive(serde::Serialize))] -#[cfg_attr(all(feature = "std", feature = "serde"), derive(serde::Deserialize))] -pub enum Call { - set { - #[codec(compact)] - now: u64, - }, -} diff --git a/substrate/abi/src/transaction.rs b/substrate/abi/src/transaction.rs new file mode 100644 index 00000000..72456a01 --- /dev/null +++ b/substrate/abi/src/transaction.rs @@ -0,0 +1,319 @@ +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 }, + }) + } + } + */ +} diff --git a/substrate/abi/src/tx.rs b/substrate/abi/src/tx.rs deleted file mode 100644 index b1fccf09..00000000 --- a/substrate/abi/src/tx.rs +++ /dev/null @@ -1,186 +0,0 @@ -use scale::Encode; - -use sp_core::sr25519::{Public, Signature}; -use sp_runtime::traits::Verify; - -use serai_primitives::SeraiAddress; - -use frame_support::dispatch::GetDispatchInfo; - -pub trait TransactionMember: - Clone + PartialEq + Eq + core::fmt::Debug + scale::Encode + scale::Decode + scale_info::TypeInfo -{ -} -impl< - T: Clone - + PartialEq - + Eq - + core::fmt::Debug - + scale::Encode - + scale::Decode - + scale_info::TypeInfo, - > TransactionMember for T -{ -} - -type TransactionEncodeAs<'a, Extra> = - (&'a crate::Call, &'a Option<(SeraiAddress, Signature, Extra)>); -type TransactionDecodeAs = (crate::Call, Option<(SeraiAddress, Signature, Extra)>); - -// We use our own Transaction struct, over UncheckedExtrinsic, for more control, a bit more -// simplicity, and in order to be immune to https://github.com/paritytech/polkadot-sdk/issues/2947 -#[allow(private_bounds)] -#[derive(Clone, PartialEq, Eq, Debug)] -pub struct Transaction< - Call: 'static + TransactionMember + From, - Extra: 'static + TransactionMember, -> { - call: crate::Call, - mapped_call: Call, - signature: Option<(SeraiAddress, Signature, Extra)>, -} - -impl, Extra: 'static + TransactionMember> - Transaction -{ - pub fn new(call: crate::Call, signature: Option<(SeraiAddress, Signature, Extra)>) -> Self { - Self { call: call.clone(), mapped_call: call.into(), signature } - } - - pub fn call(&self) -> &crate::Call { - &self.call - } -} - -impl, Extra: 'static + TransactionMember> - scale::Encode for Transaction -{ - fn using_encoded R>(&self, f: F) -> R { - let tx: TransactionEncodeAs = (&self.call, &self.signature); - tx.using_encoded(f) - } -} -impl, Extra: 'static + TransactionMember> - scale::Decode for Transaction -{ - fn decode(input: &mut I) -> Result { - let (call, signature) = TransactionDecodeAs::decode(input)?; - let mapped_call = Call::from(call.clone()); - Ok(Self { call, mapped_call, signature }) - } -} -impl, Extra: 'static + TransactionMember> - scale_info::TypeInfo for Transaction -{ - type Identity = TransactionDecodeAs; - - // Define the type info as the info of the type equivalent to what we encode as - fn type_info() -> scale_info::Type { - TransactionDecodeAs::::type_info() - } -} - -#[cfg(feature = "serde")] -mod _serde { - use scale::Encode; - use serde::ser::*; - use super::*; - impl, Extra: 'static + TransactionMember> - Serialize for Transaction - { - fn serialize(&self, serializer: S) -> Result { - let encoded = self.encode(); - serializer.serialize_bytes(&encoded) - } - } - - #[cfg(feature = "std")] - use serde::de::*; - #[cfg(feature = "std")] - impl< - 'a, - Call: 'static + TransactionMember + From, - Extra: 'static + TransactionMember, - > Deserialize<'a> for Transaction - { - fn deserialize>(de: D) -> Result { - let bytes = sp_core::bytes::deserialize(de)?; - ::decode(&mut &bytes[..]) - .map_err(|e| serde::de::Error::custom(format!("invalid transaction: {e}"))) - } - } -} - -impl< - Call: 'static + TransactionMember + From + TryInto, - Extra: 'static + TransactionMember, - > 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, - Extra: 'static + TransactionMember, - > frame_support::traits::ExtrinsicCall for Transaction -{ - fn call(&self) -> &Call { - &self.mapped_call - } -} - -impl< - Call: 'static + TransactionMember + From, - Extra: 'static + TransactionMember + sp_runtime::traits::SignedExtension, - > sp_runtime::traits::ExtrinsicMetadata for Transaction -{ - type SignedExtensions = Extra; - - const VERSION: u8 = 0; -} - -impl< - Call: 'static + TransactionMember + From + GetDispatchInfo, - Extra: 'static + TransactionMember, - > GetDispatchInfo for Transaction -{ - fn get_dispatch_info(&self) -> frame_support::dispatch::DispatchInfo { - self.mapped_call.get_dispatch_info() - } -} - -impl< - Call: 'static + TransactionMember + From, - Extra: 'static + TransactionMember + sp_runtime::traits::SignedExtension, - > 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 }, - }) - } -} diff --git a/substrate/abi/src/validator_sets.rs b/substrate/abi/src/validator_sets.rs index b47a7a68..f2a77b55 100644 --- a/substrate/abi/src/validator_sets.rs +++ b/substrate/abi/src/validator_sets.rs @@ -1,79 +1,144 @@ +use borsh::{BorshSerialize, BorshDeserialize}; + use sp_core::{ConstU32, bounded::BoundedVec}; -pub use serai_validator_sets_primitives as primitives; +use serai_primitives::{ + crypto::{ExternalKey, KeyPair, Signature}, + address::SeraiAddress, + balance::Amount, + network_id::*, + validator_sets::*, +}; -use serai_primitives::*; -use serai_validator_sets_primitives::*; - -#[derive(Clone, PartialEq, Eq, Debug, scale::Encode, scale::Decode, scale_info::TypeInfo)] -#[cfg_attr(feature = "serde", derive(serde::Serialize))] -#[cfg_attr(all(feature = "std", feature = "serde"), derive(serde::Deserialize))] +/// A call to the validator sets. +#[derive(Clone, PartialEq, Eq, Debug, BorshSerialize, BorshDeserialize)] pub enum Call { + /// Set the keys for a validator set. set_keys { + /// The network whose latest validator set is setting their keys. network: ExternalNetworkId, + /// The keys being set. key_pair: KeyPair, + /// The participants in the validator set who signed off on these keys. + #[borsh( + serialize_with = "serai_primitives::sp_borsh::borsh_serialize_bitvec", + deserialize_with = "serai_primitives::sp_borsh::borsh_deserialize_bitvec" + )] signature_participants: bitvec::vec::BitVec, + /// The signature confirming these keys are valid. signature: Signature, }, - set_embedded_elliptic_curve_key { - embedded_elliptic_curve: EmbeddedEllipticCurve, - key: BoundedVec>, - }, + /// Report a validator set's slashes onto Serai. report_slashes { + /// The network whose retiring validator set is setting their keys. network: ExternalNetworkId, + /// The slashes they're reporting. slashes: SlashReport, + /// The signature confirming the validity of this slash report. signature: Signature, }, + /// Set a validator's keys on embedded elliptic curves for a specific network. + set_embedded_elliptic_curve_keys { + /// The network the origin is setting their embedded elliptic curve keys for. + network: ExternalNetworkId, + /// The keys on the embedded elliptic curves. + /// + /// This may be a single key if the external network uses the same embedded elliptic curve as + /// used for the key to oraclize onto Serai. + #[borsh( + serialize_with = "serai_primitives::sp_borsh::borsh_serialize_bounded_vec", + deserialize_with = "serai_primitives::sp_borsh::borsh_deserialize_bounded_vec" + )] + keys: BoundedVec>, + }, + /// Allocate stake to a network. allocate { + /// The network to allocate stake to. network: NetworkId, + /// The amount of stake to allocate. amount: Amount, }, + /// Deallocate stake from a network. + /// + /// This deallocation may be immediate or may be delayed depending on if the origin is an + /// active, or even recent, validator. If delayed, it will have to be claimed at a later time. deallocate { + /// The network to deallocate stake from. network: NetworkId, + /// The amount of stake to deallocate. amount: Amount, }, + /// Claim a now-unlocked deallocation. claim_deallocation { - network: NetworkId, - session: Session, + /// The validator set which claiming the deallocation was delayed until. + deallocation: ValidatorSet, }, } -#[derive(Clone, PartialEq, Eq, Debug, scale::Encode, scale::Decode, scale_info::TypeInfo)] -#[cfg_attr(feature = "borsh", derive(borsh::BorshSerialize, borsh::BorshDeserialize))] -#[cfg_attr(feature = "serde", derive(serde::Serialize))] -#[cfg_attr(all(feature = "std", feature = "serde"), derive(serde::Deserialize))] +impl Call { + pub(crate) fn is_signed(&self) -> bool { + match self { + Call::set_keys { .. } | Call::report_slashes { .. } => false, + Call::set_embedded_elliptic_curve_keys { .. } | + Call::allocate { .. } | + Call::deallocate { .. } | + Call::claim_deallocation { .. } => true, + } + } +} + +/// An event from the validator sets. +#[derive(Clone, PartialEq, Eq, Debug, BorshSerialize, BorshDeserialize)] pub enum Event { + /// A new validator set was declared. NewSet { + /// The set declared. set: ValidatorSet, }, - ParticipantRemoved { - set: ValidatorSet, - removed: SeraiAddress, - }, - KeyGen { + /// A validator set has set their keys. + SetKeys { + /// The set which set their keys. set: ExternalValidatorSet, + /// The keys sets. key_pair: KeyPair, }, + /// A validator set has accepted responsibility from the prior validator set. AcceptedHandover { + /// The set which accepted responsibility from the prior set. set: ValidatorSet, }, + /// A validator set has retired. SetRetired { + /// The set retired. set: ValidatorSet, }, - AllocationIncreased { + /// A validator's allocation to a network has increased. + Allocation { + /// The validator who increased their allocation. validator: SeraiAddress, + /// The network the stake was allocated to. network: NetworkId, + /// The amount of stake allocated. amount: Amount, }, - AllocationDecreased { + /// A validator's allocation to a network has decreased. + Deallocation { + /// The validator who decreased their allocation. validator: SeraiAddress, + /// The network the stake was deallocated from. network: NetworkId, + /// The amount of stake deallocated. amount: Amount, + /// The session which claiming the deallocation was delayed until. delayed_until: Option, }, + /// A validator's deallocation from a network has been claimed. + /// + /// This is only emited for deallocations which were delayed and has to be explicitly claimed. DeallocationClaimed { + /// The validator who claimed their deallocation. validator: SeraiAddress, - network: NetworkId, - session: Session, + /// The validator set the deallocation was delayed until. + deallocation: ValidatorSet, }, } diff --git a/substrate/coins/primitives/src/lib.rs b/substrate/coins/primitives/src/lib.rs index 97a48c8d..8b137891 100644 --- a/substrate/coins/primitives/src/lib.rs +++ b/substrate/coins/primitives/src/lib.rs @@ -1,54 +1 @@ -#![cfg_attr(docsrs, feature(doc_cfg))] -#![cfg_attr(docsrs, feature(doc_auto_cfg))] -#![cfg_attr(not(feature = "std"), no_std)] -#[cfg(feature = "std")] -use zeroize::Zeroize; - -#[cfg(feature = "borsh")] -use borsh::{BorshSerialize, BorshDeserialize}; -#[cfg(feature = "serde")] -use serde::{Serialize, Deserialize}; - -use scale::{Encode, Decode, MaxEncodedLen}; -use scale_info::TypeInfo; - -use serai_primitives::{ExternalBalance, SeraiAddress, ExternalAddress, system_address}; - -pub const FEE_ACCOUNT: SeraiAddress = system_address(b"Coins-fees"); - -// TODO: Replace entirely with just Address -#[derive(Clone, PartialEq, Eq, Debug, Encode, Decode, MaxEncodedLen, TypeInfo)] -#[cfg_attr(feature = "std", derive(Zeroize))] -#[cfg_attr(feature = "borsh", derive(BorshSerialize, BorshDeserialize))] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub struct OutInstruction { - pub address: ExternalAddress, -} - -#[derive(Clone, PartialEq, Eq, Debug, Encode, Decode, MaxEncodedLen, TypeInfo)] -#[cfg_attr(feature = "std", derive(Zeroize))] -#[cfg_attr(feature = "borsh", derive(BorshSerialize, BorshDeserialize))] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub struct OutInstructionWithBalance { - pub instruction: OutInstruction, - pub balance: ExternalBalance, -} - -#[derive(Clone, PartialEq, Eq, Debug, Encode, Decode, MaxEncodedLen, TypeInfo)] -#[cfg_attr(feature = "std", derive(Zeroize))] -#[cfg_attr(feature = "borsh", derive(BorshSerialize, BorshDeserialize))] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub enum Destination { - Native(SeraiAddress), - External(OutInstruction), -} - -#[test] -fn address() { - use sp_runtime::traits::TrailingZeroInput; - assert_eq!( - FEE_ACCOUNT, - SeraiAddress::decode(&mut TrailingZeroInput::new(b"Coins-fees")).unwrap() - ); -} diff --git a/substrate/genesis-liquidity/primitives/src/lib.rs b/substrate/genesis-liquidity/primitives/src/lib.rs index 4e4c277c..8b137891 100644 --- a/substrate/genesis-liquidity/primitives/src/lib.rs +++ b/substrate/genesis-liquidity/primitives/src/lib.rs @@ -1,54 +1 @@ -#![cfg_attr(docsrs, feature(doc_cfg))] -#![cfg_attr(docsrs, feature(doc_auto_cfg))] -#![cfg_attr(not(feature = "std"), no_std)] -#[cfg(feature = "std")] -use zeroize::Zeroize; - -#[cfg(feature = "borsh")] -use borsh::{BorshSerialize, BorshDeserialize}; -#[cfg(feature = "serde")] -use serde::{Serialize, Deserialize}; - -use sp_std::vec::Vec; - -use scale::{Encode, Decode, MaxEncodedLen}; -use scale_info::TypeInfo; - -use serai_primitives::*; -use validator_sets_primitives::ValidatorSet; - -pub const INITIAL_GENESIS_LP_SHARES: u64 = 10_000; - -// This is the account to hold and manage the genesis liquidity. -pub const GENESIS_LIQUIDITY_ACCOUNT: SeraiAddress = system_address(b"GenesisLiquidity-account"); - -#[derive(Clone, Copy, PartialEq, Eq, Debug, Encode, Decode, MaxEncodedLen, TypeInfo)] -#[cfg_attr(feature = "std", derive(Zeroize))] -#[cfg_attr(feature = "borsh", derive(BorshSerialize, BorshDeserialize))] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub struct Values { - pub monero: u64, - pub ether: u64, - pub dai: u64, -} - -#[derive(Clone, Copy, PartialEq, Eq, Debug, Encode, Decode, MaxEncodedLen, TypeInfo)] -#[cfg_attr(feature = "std", derive(Zeroize))] -#[cfg_attr(feature = "borsh", derive(BorshSerialize, BorshDeserialize))] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub struct LiquidityAmount { - pub shares: u64, - pub coins: u64, -} - -impl LiquidityAmount { - pub fn zero() -> Self { - LiquidityAmount { shares: 0, coins: 0 } - } -} - -/// The message for the oraclize_values signature. -pub fn oraclize_values_message(set: &ValidatorSet, values: &Values) -> Vec { - (b"GenesisLiquidity-oraclize_values", set, values).encode() -} diff --git a/substrate/in-instructions/primitives/src/lib.rs b/substrate/in-instructions/primitives/src/lib.rs index 5c74bf55..8b137891 100644 --- a/substrate/in-instructions/primitives/src/lib.rs +++ b/substrate/in-instructions/primitives/src/lib.rs @@ -1,141 +1 @@ -#![cfg_attr(docsrs, feature(doc_cfg))] -#![cfg_attr(docsrs, feature(doc_auto_cfg))] -#![cfg_attr(not(feature = "std"), no_std)] -#[cfg(feature = "std")] -use zeroize::Zeroize; - -#[cfg(feature = "borsh")] -use borsh::{BorshSerialize, BorshDeserialize}; -#[cfg(feature = "serde")] -use serde::{Serialize, Deserialize}; - -use scale::{Encode, Decode, MaxEncodedLen}; -use scale_info::TypeInfo; - -use sp_application_crypto::sr25519::Signature; - -#[cfg(not(feature = "std"))] -use sp_std::vec::Vec; -use sp_runtime::RuntimeDebug; - -#[rustfmt::skip] -use serai_primitives::{BlockHash, ExternalNetworkId, NetworkId, ExternalBalance, Balance, SeraiAddress, ExternalAddress, system_address}; - -mod shorthand; -pub use shorthand::*; - -pub const MAX_BATCH_SIZE: usize = 25_000; // ~25kb - -// This is the account which will be the origin for any dispatched `InInstruction`s. -pub const IN_INSTRUCTION_EXECUTOR: SeraiAddress = system_address(b"InInstructions-executor"); - -#[derive(Clone, PartialEq, Eq, Debug, Encode, Decode, MaxEncodedLen, TypeInfo)] -#[cfg_attr(feature = "std", derive(Zeroize))] -#[cfg_attr(feature = "borsh", derive(BorshSerialize, BorshDeserialize))] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub enum OutAddress { - Serai(SeraiAddress), - External(ExternalAddress), -} - -impl OutAddress { - pub fn is_native(&self) -> bool { - matches!(self, Self::Serai(_)) - } - - pub fn as_native(self) -> Option { - match self { - Self::Serai(addr) => Some(addr), - _ => None, - } - } - - pub fn as_external(self) -> Option { - match self { - Self::External(addr) => Some(addr), - Self::Serai(_) => None, - } - } -} - -#[derive(Clone, PartialEq, Eq, Debug, Encode, Decode, MaxEncodedLen, TypeInfo)] -#[cfg_attr(feature = "std", derive(Zeroize))] -#[cfg_attr(feature = "borsh", derive(BorshSerialize, BorshDeserialize))] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub enum DexCall { - // address to send the lp tokens to - // TODO: Update this per documentation/Shorthand - SwapAndAddLiquidity(SeraiAddress), - // minimum out balance and out address - Swap(Balance, OutAddress), -} - -#[derive(Clone, PartialEq, Eq, Debug, Encode, Decode, MaxEncodedLen, TypeInfo)] -#[cfg_attr(feature = "std", derive(Zeroize))] -#[cfg_attr(feature = "borsh", derive(BorshSerialize, BorshDeserialize))] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub enum InInstruction { - Transfer(SeraiAddress), - Dex(DexCall), - GenesisLiquidity(SeraiAddress), - SwapToStakedSRI(SeraiAddress, NetworkId), -} - -#[derive(Clone, PartialEq, Eq, Encode, Decode, MaxEncodedLen, TypeInfo, RuntimeDebug)] -#[cfg_attr(feature = "std", derive(Zeroize))] -#[cfg_attr(feature = "borsh", derive(BorshSerialize, BorshDeserialize))] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub struct RefundableInInstruction { - pub origin: Option, - pub instruction: InInstruction, -} - -#[derive(Clone, PartialEq, Eq, Debug, Encode, Decode, MaxEncodedLen, TypeInfo)] -#[cfg_attr(feature = "std", derive(Zeroize))] -#[cfg_attr(feature = "borsh", derive(BorshSerialize, BorshDeserialize))] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub struct InInstructionWithBalance { - pub instruction: InInstruction, - pub balance: ExternalBalance, -} - -#[derive(Clone, PartialEq, Eq, Encode, Decode, TypeInfo, RuntimeDebug)] -#[cfg_attr(feature = "std", derive(Zeroize))] -#[cfg_attr(feature = "borsh", derive(BorshSerialize, BorshDeserialize))] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub struct Batch { - pub network: ExternalNetworkId, - pub id: u32, - pub external_network_block_hash: BlockHash, - pub instructions: Vec, -} - -#[derive(Clone, PartialEq, Eq, Encode, Decode, TypeInfo, RuntimeDebug)] -#[cfg_attr(feature = "borsh", derive(BorshSerialize, BorshDeserialize))] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub struct SignedBatch { - pub batch: Batch, - #[cfg_attr( - feature = "borsh", - borsh( - serialize_with = "serai_primitives::borsh_serialize_signature", - deserialize_with = "serai_primitives::borsh_deserialize_signature" - ) - )] - pub signature: Signature, -} - -#[cfg(feature = "std")] -impl Zeroize for SignedBatch { - fn zeroize(&mut self) { - self.batch.zeroize(); - self.signature.as_mut().zeroize(); - } -} - -// TODO: Make this an associated method? -/// The message for the batch signature. -pub fn batch_message(batch: &Batch) -> Vec { - [b"InInstructions-batch".as_ref(), &batch.encode()].concat() -} diff --git a/substrate/in-instructions/primitives/src/shorthand.rs b/substrate/in-instructions/primitives/src/shorthand.rs deleted file mode 100644 index 6f29f6f3..00000000 --- a/substrate/in-instructions/primitives/src/shorthand.rs +++ /dev/null @@ -1,56 +0,0 @@ -#[cfg(feature = "std")] -use zeroize::Zeroize; - -#[cfg(feature = "borsh")] -use borsh::{BorshSerialize, BorshDeserialize}; -#[cfg(feature = "serde")] -use serde::{Serialize, Deserialize}; - -use scale::{Encode, Decode, MaxEncodedLen}; -use scale_info::TypeInfo; - -use serai_primitives::{Amount, ExternalAddress, ExternalCoin, SeraiAddress}; - -use coins_primitives::OutInstruction; - -use crate::RefundableInInstruction; -#[cfg(feature = "std")] -use crate::InInstruction; - -#[derive(Clone, PartialEq, Eq, Debug, Encode, Decode, MaxEncodedLen, TypeInfo)] -#[cfg_attr(feature = "std", derive(Zeroize))] -#[cfg_attr(feature = "borsh", derive(BorshSerialize, BorshDeserialize))] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub enum Shorthand { - Raw(RefundableInInstruction), - Swap { - origin: Option, - coin: ExternalCoin, - minimum: Amount, - out: OutInstruction, - }, - SwapAndAddLiquidity { - origin: Option, - minimum: Amount, - gas: Amount, - address: SeraiAddress, - }, -} - -impl Shorthand { - #[cfg(feature = "std")] - pub fn transfer(origin: Option, address: SeraiAddress) -> Self { - Self::Raw(RefundableInInstruction { origin, instruction: InInstruction::Transfer(address) }) - } -} - -impl TryFrom for RefundableInInstruction { - type Error = &'static str; - fn try_from(shorthand: Shorthand) -> Result { - Ok(match shorthand { - Shorthand::Raw(instruction) => instruction, - Shorthand::Swap { .. } => todo!(), - Shorthand::SwapAndAddLiquidity { .. } => todo!(), - }) - } -} diff --git a/substrate/primitives/Cargo.toml b/substrate/primitives/Cargo.toml index cd8c0844..98a81493 100644 --- a/substrate/primitives/Cargo.toml +++ b/substrate/primitives/Cargo.toml @@ -16,29 +16,19 @@ rustdoc-args = ["--cfg", "docsrs"] workspace = true [dependencies] -zeroize = { version = "^1.5", features = ["derive"], optional = true } +zeroize = { version = "^1.5", features = ["derive"] } +borsh = { version = "1", default-features = false, features = ["derive", "de_strict_order"] } -ciphersuite = { path = "../../crypto/ciphersuite", default-features = false, optional = true } - -scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] } -scale-info = { version = "2", default-features = false, features = ["derive"] } - -borsh = { version = "1", default-features = false, features = ["derive", "de_strict_order"], optional = true } -serde = { version = "1", default-features = false, features = ["derive", "alloc"], optional = true } - -sp-application-crypto = { git = "https://github.com/serai-dex/polkadot-sdk", branch = "serai-next", default-features = false } +bitvec = { version = "1", default-features = false, features = ["alloc"] } sp-core = { git = "https://github.com/serai-dex/polkadot-sdk", branch = "serai-next", default-features = false } -sp-runtime = { git = "https://github.com/serai-dex/polkadot-sdk", branch = "serai-next", default-features = false } -sp-io = { git = "https://github.com/serai-dex/polkadot-sdk", branch = "serai-next", default-features = false } -sp-std = { git = "https://github.com/serai-dex/polkadot-sdk", branch = "serai-next", default-features = false } -frame-support = { git = "https://github.com/serai-dex/polkadot-sdk", branch = "serai-next", default-features = false } +ciphersuite = { path = "../../crypto/ciphersuite", default-features = false, features = ["alloc", "ristretto"] } +dkg = { path = "../../crypto/dkg", default-features = false } -[dev-dependencies] -rand_core = { version = "0.6", default-features = false, features = ["getrandom"] } +bech32 = { version = "0.11", default-features = false } [features] -std = ["zeroize", "ciphersuite/std", "scale/std", "borsh?/std", "serde?/std", "scale-info/std", "sp-core/std", "sp-runtime/std", "sp-std/std", "frame-support/std"] -borsh = ["dep:borsh"] -serde = ["dep:serde"] +std = ["zeroize/std", "borsh/std", "ciphersuite/std", "dkg/std", "sp-core/std", "bech32/std"] default = ["std"] +borsh = [] # TODO +serde = [] # TODO diff --git a/substrate/primitives/README.md b/substrate/primitives/README.md new file mode 100644 index 00000000..1ca4e6e1 --- /dev/null +++ b/substrate/primitives/README.md @@ -0,0 +1,4 @@ +# Serai Primitives + +`serai-primitives` represents foundational data-types used within Serai's +Substrate blockchain. diff --git a/substrate/primitives/src/account.rs b/substrate/primitives/src/account.rs deleted file mode 100644 index 9680adc5..00000000 --- a/substrate/primitives/src/account.rs +++ /dev/null @@ -1,144 +0,0 @@ -#[cfg(feature = "std")] -use zeroize::Zeroize; - -#[cfg(feature = "borsh")] -use borsh::{BorshSerialize, BorshDeserialize}; -#[cfg(feature = "serde")] -use serde::{Serialize, Deserialize}; - -use scale::{Encode, Decode, MaxEncodedLen}; -use scale_info::TypeInfo; - -use sp_core::sr25519::Public; -pub use sp_core::sr25519::Signature; -#[cfg(feature = "std")] -use sp_core::{Pair as PairTrait, sr25519::Pair}; - -use sp_runtime::traits::{LookupError, Lookup, StaticLookup}; - -pub type PublicKey = Public; - -#[cfg(feature = "borsh")] -pub fn borsh_serialize_public( - public: &Public, - writer: &mut W, -) -> Result<(), borsh::io::Error> { - borsh::BorshSerialize::serialize(&public.0, writer) -} - -#[cfg(feature = "borsh")] -pub fn borsh_deserialize_public( - reader: &mut R, -) -> Result { - let public: [u8; 32] = borsh::BorshDeserialize::deserialize_reader(reader)?; - Ok(Public(public)) -} - -#[cfg(feature = "borsh")] -pub fn borsh_serialize_signature( - signature: &Signature, - writer: &mut W, -) -> Result<(), borsh::io::Error> { - borsh::BorshSerialize::serialize(&signature.0, writer) -} - -#[cfg(feature = "borsh")] -pub fn borsh_deserialize_signature( - reader: &mut R, -) -> Result { - let signature: [u8; 64] = borsh::BorshDeserialize::deserialize_reader(reader)?; - Ok(Signature(signature)) -} - -// TODO: Remove this for solely Public? -#[derive( - Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Encode, Decode, MaxEncodedLen, TypeInfo, -)] -#[cfg_attr(feature = "std", derive(Zeroize))] -#[cfg_attr(feature = "borsh", derive(BorshSerialize, BorshDeserialize))] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub struct SeraiAddress(pub [u8; 32]); -impl SeraiAddress { - pub fn new(key: [u8; 32]) -> SeraiAddress { - SeraiAddress(key) - } -} - -impl From<[u8; 32]> for SeraiAddress { - fn from(key: [u8; 32]) -> SeraiAddress { - SeraiAddress(key) - } -} - -impl From for SeraiAddress { - fn from(key: PublicKey) -> SeraiAddress { - SeraiAddress(key.0) - } -} - -impl From for PublicKey { - fn from(address: SeraiAddress) -> PublicKey { - PublicKey::from_raw(address.0) - } -} - -#[cfg(feature = "std")] -impl std::fmt::Display for SeraiAddress { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - // TODO: Bech32 - write!(f, "{:?}", self.0) - } -} - -/// Create a Substraate key pair by a name. -/// -/// This should never be considered to have a secure private key. It has effectively no entropy. -#[cfg(feature = "std")] -pub fn insecure_pair_from_name(name: &str) -> Pair { - Pair::from_string(&format!("//{name}"), None).unwrap() -} - -/// Create a private key for an arbitrary ciphersuite by a name. -/// -/// This key should never be considered a secure private key. It has effectively no entropy. -#[cfg(feature = "std")] -pub fn insecure_arbitrary_key_from_name(name: &str) -> C::F { - C::hash_to_F(name.as_bytes()) -} - -pub struct AccountLookup; -impl Lookup for AccountLookup { - type Source = SeraiAddress; - type Target = PublicKey; - fn lookup(&self, source: SeraiAddress) -> Result { - Ok(PublicKey::from_raw(source.0)) - } -} -impl StaticLookup for AccountLookup { - type Source = SeraiAddress; - type Target = PublicKey; - fn lookup(source: SeraiAddress) -> Result { - Ok(source.into()) - } - fn unlookup(source: PublicKey) -> SeraiAddress { - source.into() - } -} - -pub const fn system_address(pallet: &'static [u8]) -> SeraiAddress { - let mut address = [0; 32]; - let mut set = false; - // Implement a while loop since we can't use a for loop - let mut i = 0; - while i < pallet.len() { - address[i] = pallet[i]; - if address[i] != 0 { - set = true; - } - i += 1; - } - // Make sure this address isn't the identity point - // Doesn't do address != [0; 32] since that's not const - assert!(set, "address is the identity point"); - SeraiAddress(address) -} diff --git a/substrate/primitives/src/address.rs b/substrate/primitives/src/address.rs new file mode 100644 index 00000000..5b850125 --- /dev/null +++ b/substrate/primitives/src/address.rs @@ -0,0 +1,143 @@ +use alloc::vec::Vec; + +use zeroize::Zeroize; + +use borsh::{BorshSerialize, BorshDeserialize}; + +use sp_core::{sr25519::Public, ConstU32, bounded::BoundedVec}; + +/* + We only use a single HRP across all networks. This follows Serai's general practice. Addresses + for external networks are represented in binary, without network information. to minimize + bandwidth and reduce potential for malleability. + + This is continued here not solely to be a continuance, yet also with appreciation for the + simplicity. This does make it easier for users to make the mistake of using a testnet address + where they intended to use a mainnet address (and vice-versa). Since public keys are usable on + any network, this should have limited impact and accordingly not be the end of the world. + + There's also precedent for this due to Ethereum (though they do have a somewhat-adopted checksum + scheme to encode the network regardless). +*/ +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)] +pub struct SeraiAddress(pub [u8; 32]); + +impl SeraiAddress { + /// Generate an address for use by the system. + /// + /// The returned addresses MAY be valid points. This assumes its infeasible to find the discrete + /// logarithm for a point whose representation has a known Blake2b-256 preimage. + // The alternative would be to massage this until its not a valid point, which isn't worth the + // computational expense as this should be a hard problem for outputs which happen to be points. + pub fn system(label: &[u8]) -> Self { + Self(sp_core::blake2_256(label)) + } +} + +impl From for SeraiAddress { + fn from(key: Public) -> Self { + // The encoding of a Ristretto point is the encoding of its address + Self(key.0) + } +} +/* + A `SeraiAddress` may not be a valid public key. The `sr25519::Public` is not a checked public + key, solely bytes alleged to be a public key, which any `SeraiAddress` converted into a `Public` + is also alleged to be. +*/ +impl From for Public { + fn from(address: SeraiAddress) -> Self { + Self::from_raw(address.0) + } +} + +// We use Bech32m to encode addresses +impl core::fmt::Display for SeraiAddress { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match bech32::encode_to_fmt::(f, HUMAN_READABLE_PART, &self.0) { + Ok(()) => Ok(()), + Err(bech32::EncodeError::TooLong(_)) => { + unreachable!("32 bytes exceeded bech32 length limit?") + } + Err(bech32::EncodeError::Fmt(e)) => Err(e), + // bech32::EncodeError is non-exhaustive + Err(_) => Err(core::fmt::Error), + } + } +} + +/// An error from decoding an address. +pub enum DecodeError { + /// The Bech32m encoding was invalid. + InvalidBech32m, + /// The Bech32m Human-Readable Part was distinct. + DistinctHrp, + /// The encoded data's length was wrong. + InvalidLength, +} + +impl core::str::FromStr for SeraiAddress { + type Err = DecodeError; + fn from_str(s: &str) -> Result { + // We drop bech32's error to remain opaque to the implementation + let decoded = bech32::primitives::decode::CheckedHrpstring::new::(s) + .map_err(|_| DecodeError::InvalidBech32m)?; + if decoded.hrp() != HUMAN_READABLE_PART { + Err(DecodeError::DistinctHrp)?; + } + + let mut res = Self([0; 32]); + let mut iter = decoded.byte_iter(); + for i in 0 .. 32 { + let Some(byte) = iter.next() else { Err(DecodeError::InvalidLength)? }; + res.0[i] = byte; + } + if iter.next().is_some() { + Err(DecodeError::InvalidLength)?; + } + Ok(res) + } +} + +/// An address for an external network. +#[derive(Clone, PartialEq, Eq, Debug, borsh::BorshSerialize, borsh::BorshDeserialize)] +pub struct ExternalAddress( + #[borsh( + serialize_with = "crate::borsh_serialize_bounded_vec", + deserialize_with = "crate::borsh_deserialize_bounded_vec" + )] + BoundedVec>, +); + +impl ExternalAddress { + /// The maximum length for an `ExternalAddress`. + pub const MAX_LEN: u32 = 512; +} + +/// An error when converting from a `Vec`. +pub enum FromVecError { + /// The source `Vec` was too long to be converted. + TooLong, +} + +impl TryFrom> for ExternalAddress { + type Error = FromVecError; + fn try_from(vec: Vec) -> Result { + vec.try_into().map(ExternalAddress).map_err(|_| FromVecError::TooLong) + } +} + +impl From for Vec { + fn from(ext: ExternalAddress) -> Vec { + ext.0.into_inner() + } +} + +impl zeroize::Zeroize for ExternalAddress { + fn zeroize(&mut self) { + self.0.as_mut().zeroize(); + } +} diff --git a/substrate/primitives/src/amount.rs b/substrate/primitives/src/amount.rs deleted file mode 100644 index c82fe34d..00000000 --- a/substrate/primitives/src/amount.rs +++ /dev/null @@ -1,54 +0,0 @@ -use core::{ - ops::{Add, Sub, Mul}, - fmt::Debug, -}; - -#[cfg(feature = "std")] -use zeroize::Zeroize; - -#[cfg(feature = "borsh")] -use borsh::{BorshSerialize, BorshDeserialize}; -#[cfg(feature = "serde")] -use serde::{Serialize, Deserialize}; - -use scale::{Encode, Decode, MaxEncodedLen}; -use scale_info::TypeInfo; - -/// The type used for amounts within Substrate. -// Distinct from Amount due to Substrate's requirements on this type. -// While Amount could have all the necessary traits implemented, not only are they many, it'd make -// Amount a large type with a variety of misc functions. -// The current type's minimalism sets clear bounds on usage. -pub type SubstrateAmount = u64; -/// The type used for amounts. -#[derive( - Clone, Copy, PartialEq, Eq, PartialOrd, Debug, Encode, Decode, MaxEncodedLen, TypeInfo, -)] -#[cfg_attr(feature = "std", derive(Zeroize))] -#[cfg_attr(feature = "borsh", derive(BorshSerialize, BorshDeserialize))] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub struct Amount(pub SubstrateAmount); - -// TODO: these impl shouldn't panic and return error to be dealt with. -// Otherwise we might have a panic that stops the network. -impl Add for Amount { - type Output = Amount; - fn add(self, other: Amount) -> Amount { - // Explicitly use checked_add so even if range checks are disabled, this is still checked - Amount(self.0.checked_add(other.0).unwrap()) - } -} - -impl Sub for Amount { - type Output = Amount; - fn sub(self, other: Amount) -> Amount { - Amount(self.0.checked_sub(other.0).unwrap()) - } -} - -impl Mul for Amount { - type Output = Amount; - fn mul(self, other: Amount) -> Amount { - Amount(self.0.checked_mul(other.0).unwrap()) - } -} diff --git a/substrate/primitives/src/balance.rs b/substrate/primitives/src/balance.rs index 6743e6fa..d2d6578e 100644 --- a/substrate/primitives/src/balance.rs +++ b/substrate/primitives/src/balance.rs @@ -1,38 +1,111 @@ use core::ops::{Add, Sub, Mul}; - -#[cfg(feature = "std")] use zeroize::Zeroize; -#[cfg(feature = "borsh")] use borsh::{BorshSerialize, BorshDeserialize}; -#[cfg(feature = "serde")] -use serde::{Serialize, Deserialize}; -use scale::{Encode, Decode, MaxEncodedLen}; -use scale_info::TypeInfo; +use crate::coin::{ExternalCoin, Coin}; -use crate::{Amount, Coin, ExternalCoin}; +/// The type internally used to represent amounts. +// https://github.com/rust-lang/rust/issues/8995 +pub type AmountRepr = u64; -/// The type used for balances (a Coin and Balance). -#[derive(Clone, Copy, PartialEq, Eq, Debug, Encode, Decode, MaxEncodedLen, TypeInfo)] -#[cfg_attr(feature = "std", derive(Zeroize))] -#[cfg_attr(feature = "borsh", derive(BorshSerialize, BorshDeserialize))] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub struct Balance { - pub coin: Coin, +/// A wrapper used to represent amounts. +#[derive( + Clone, + Copy, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, + Debug, + Zeroize, + BorshSerialize, + BorshDeserialize, +)] +pub struct Amount(pub AmountRepr); + +impl Add for Amount { + type Output = Option; + fn add(self, other: Amount) -> Option { + self.0.checked_add(other.0).map(Amount) + } +} + +impl Sub for Amount { + type Output = Option; + fn sub(self, other: Amount) -> Option { + self.0.checked_sub(other.0).map(Amount) + } +} + +impl Mul for Amount { + type Output = Option; + fn mul(self, other: Amount) -> Option { + self.0.checked_mul(other.0).map(Amount) + } +} + +/// An ExternalCoin and an Amount, forming a balance for an external coin. +#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize, BorshSerialize, BorshDeserialize)] +pub struct ExternalBalance { + /// The coin this is a balance for. + pub coin: ExternalCoin, + /// The amount of this balance. pub amount: Amount, } -/// The type used for balances (a Coin and Balance). -#[derive(Clone, Copy, PartialEq, Eq, Debug, Encode, Decode, MaxEncodedLen, TypeInfo)] -#[cfg_attr(feature = "std", derive(Zeroize))] -#[cfg_attr(feature = "borsh", derive(BorshSerialize, BorshDeserialize))] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub struct ExternalBalance { - pub coin: ExternalCoin, +impl Add for ExternalBalance { + type Output = Option; + fn add(self, other: Amount) -> Option { + (self.amount + other).map(|amount| ExternalBalance { coin: self.coin, amount }) + } +} + +impl Sub for ExternalBalance { + type Output = Option; + fn sub(self, other: Amount) -> Option { + (self.amount - other).map(|amount| ExternalBalance { coin: self.coin, amount }) + } +} + +impl Mul for ExternalBalance { + type Output = Option; + fn mul(self, other: Amount) -> Option { + (self.amount * other).map(|amount| ExternalBalance { coin: self.coin, amount }) + } +} + +/// A Coin and an Amount, forming a balance for a coin. +#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize, BorshSerialize, BorshDeserialize)] +pub struct Balance { + /// The coin this is a balance for. + pub coin: Coin, + /// The amount of this balance. pub amount: Amount, } +impl Add for Balance { + type Output = Option; + fn add(self, other: Amount) -> Option { + (self.amount + other).map(|amount| Balance { coin: self.coin, amount }) + } +} + +impl Sub for Balance { + type Output = Option; + fn sub(self, other: Amount) -> Option { + (self.amount - other).map(|amount| Balance { coin: self.coin, amount }) + } +} + +impl Mul for Balance { + type Output = Option; + fn mul(self, other: Amount) -> Option { + (self.amount * other).map(|amount| Balance { coin: self.coin, amount }) + } +} + impl From for Balance { fn from(balance: ExternalBalance) -> Self { Balance { coin: balance.coin.into(), amount: balance.amount } @@ -49,25 +122,3 @@ impl TryFrom for ExternalBalance { } } } - -// TODO: these impl either should be removed or return errors in case of overflows -impl Add for Balance { - type Output = Balance; - fn add(self, other: Amount) -> Balance { - Balance { coin: self.coin, amount: self.amount + other } - } -} - -impl Sub for Balance { - type Output = Balance; - fn sub(self, other: Amount) -> Balance { - Balance { coin: self.coin, amount: self.amount - other } - } -} - -impl Mul for Balance { - type Output = Balance; - fn mul(self, other: Amount) -> Balance { - Balance { coin: self.coin, amount: self.amount * other } - } -} diff --git a/substrate/primitives/src/block.rs b/substrate/primitives/src/block.rs deleted file mode 100644 index db80a20c..00000000 --- a/substrate/primitives/src/block.rs +++ /dev/null @@ -1,55 +0,0 @@ -#[cfg(feature = "std")] -use zeroize::Zeroize; - -#[cfg(feature = "borsh")] -use borsh::{BorshSerialize, BorshDeserialize}; -#[cfg(feature = "serde")] -use serde::{Serialize, Deserialize}; - -use scale::{Encode, Decode, MaxEncodedLen}; -use scale_info::TypeInfo; - -use sp_core::H256; - -/// The type used to identify block numbers. -#[derive( - Clone, Copy, Default, PartialEq, Eq, Hash, Debug, Encode, Decode, MaxEncodedLen, TypeInfo, -)] -#[cfg_attr(feature = "std", derive(Zeroize))] -#[cfg_attr(feature = "borsh", derive(BorshSerialize, BorshDeserialize))] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub struct BlockNumber(pub u64); -impl From for BlockNumber { - fn from(number: u64) -> BlockNumber { - BlockNumber(number) - } -} - -/// The type used to identify block hashes. -// This may not be universally compatible -// If a block exists with a hash which isn't 32-bytes, it can be hashed into a value with 32-bytes -// This would require the processor to maintain a mapping of 32-byte IDs to actual hashes, which -// would be fine -#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Encode, Decode, MaxEncodedLen, TypeInfo)] -#[cfg_attr(feature = "std", derive(Zeroize))] -#[cfg_attr(feature = "borsh", derive(BorshSerialize, BorshDeserialize))] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub struct BlockHash(pub [u8; 32]); - -impl AsRef<[u8]> for BlockHash { - fn as_ref(&self) -> &[u8] { - self.0.as_ref() - } -} - -impl From<[u8; 32]> for BlockHash { - fn from(hash: [u8; 32]) -> BlockHash { - BlockHash(hash) - } -} - -impl From for BlockHash { - fn from(hash: H256) -> BlockHash { - BlockHash(hash.into()) - } -} diff --git a/substrate/primitives/src/coin.rs b/substrate/primitives/src/coin.rs new file mode 100644 index 00000000..93ddaa76 --- /dev/null +++ b/substrate/primitives/src/coin.rs @@ -0,0 +1,118 @@ +use zeroize::Zeroize; + +use borsh::{io, BorshSerialize, BorshDeserialize}; + +use crate::network_id::{ExternalNetworkId, NetworkId}; + +/// The type used to identify coins native to external networks. +/// +/// This type serializes to a subset of `Coin`. +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Zeroize, BorshSerialize, BorshDeserialize)] +#[borsh(use_discriminant = true)] +#[non_exhaustive] +pub enum ExternalCoin { + /// Bitcoin, from the Bitcoin network. + Bitcoin = 1, + /// Ether, from the Ethereum network. + Ether = 2, + /// Dai Stablecoin, from the Ethereum network. + Dai = 3, + /// Monero, from the Monero network. + Monero = 4, +} + +impl ExternalCoin { + /// All external coins. + pub fn all() -> impl Iterator { + [ExternalCoin::Bitcoin, ExternalCoin::Ether, ExternalCoin::Dai, ExternalCoin::Monero] + .into_iter() + } +} + +/// The type used to identify coins. +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Zeroize)] +pub enum Coin { + /// The Serai coin. + Serai, + /// An external coin. + External(ExternalCoin), +} + +impl BorshSerialize for Coin { + fn serialize(&self, writer: &mut W) -> io::Result<()> { + match self { + Self::Serai => writer.write_all(&[0]), + Self::External(external) => external.serialize(writer), + } + } +} + +impl BorshDeserialize for Coin { + fn deserialize_reader(reader: &mut R) -> io::Result { + let mut kind = [0xff]; + reader.read_exact(&mut kind)?; + match kind[0] { + 0 => Ok(Self::Serai), + _ => ExternalCoin::deserialize_reader(&mut kind.as_slice()).map(Into::into), + } + } +} + +impl Coin { + /// All coins. + pub fn all() -> impl Iterator { + core::iter::once(Coin::Serai).chain(ExternalCoin::all().map(Into::into)) + } +} + +impl From for Coin { + fn from(coin: ExternalCoin) -> Self { + Coin::External(coin) + } +} + +impl TryFrom for ExternalCoin { + type Error = (); + + fn try_from(coin: Coin) -> Result { + match coin { + Coin::Serai => Err(())?, + Coin::External(ext) => Ok(ext), + } + } +} + +impl ExternalCoin { + /// The external network this coin is native to. + pub fn network(&self) -> ExternalNetworkId { + match self { + ExternalCoin::Bitcoin => ExternalNetworkId::Bitcoin, + ExternalCoin::Ether | ExternalCoin::Dai => ExternalNetworkId::Ethereum, + ExternalCoin::Monero => ExternalNetworkId::Monero, + } + } + + /// The decimals used for a single human unit of this coin. + /// + /// This may be less than the decimals used for a single human unit of this coin *by defined + /// convention*. If so, that means Serai is *truncating* the decimals. A coin which is defined + /// as having 8 decimals, while Serai claims it has 4 decimals, will have `0.00019999` + /// interpreted as `0.0001` (in human units, in atomic units, 19999 will be interpreted as 1). + pub fn decimals(&self) -> u32 { + match self { + // Ether and DAI have 18 decimals, yet we only track 8 in order to fit them within u64s + ExternalCoin::Bitcoin | ExternalCoin::Ether | ExternalCoin::Dai => 8, + ExternalCoin::Monero => 12, + } + } +} + +impl Coin { + /// The network this coin is native to. + pub fn network(&self) -> NetworkId { + match self { + Coin::Serai => NetworkId::Serai, + Coin::External(c) => c.network().into(), + } + } +} diff --git a/substrate/primitives/src/constants.rs b/substrate/primitives/src/constants.rs index a3d4b6f9..3239090c 100644 --- a/substrate/primitives/src/constants.rs +++ b/substrate/primitives/src/constants.rs @@ -1,39 +1,13 @@ -use crate::BlockNumber; +use core::time::Duration; -// 1 MB -pub const BLOCK_SIZE: u32 = 1024 * 1024; -// 6 seconds -// TODO: Use Duration -pub const TARGET_BLOCK_TIME: u64 = 6; +/// The target block time. +pub const TARGET_BLOCK_TIME: Duration = Duration::from_secs(6); -/// Measured in blocks. -pub const MINUTES: BlockNumber = 60 / TARGET_BLOCK_TIME; -pub const HOURS: BlockNumber = 60 * MINUTES; -pub const DAYS: BlockNumber = 24 * HOURS; -pub const WEEKS: BlockNumber = 7 * DAYS; -// Defines a month as 30 days, which is slightly inaccurate -pub const MONTHS: BlockNumber = 30 * DAYS; -// Defines a year as 12 inaccurate months, which is 360 days literally (~1.5% off) -pub const YEARS: BlockNumber = 12 * MONTHS; +/// The intended duration for a session. +// 1 week +pub const SESSION_LENGTH: Duration = Duration::from_secs(7 * 24 * 60 * 60); -/// 6 months of blocks -pub const GENESIS_SRI_TRICKLE_FEED: u64 = 6 * MONTHS; - -// 100 Million SRI -pub const GENESIS_SRI: u64 = 100_000_000 * 10_u64.pow(8); - -/// This needs to be long enough for arbitrage to occur and make holding any fake price up -/// sufficiently unrealistic. -#[allow(clippy::cast_possible_truncation)] -pub const ARBITRAGE_TIME: u16 = (2 * HOURS) as u16; - -/// Since we use the median price, double the window length. -/// -/// We additionally +1 so there is a true median. -pub const MEDIAN_PRICE_WINDOW_LENGTH: u16 = (2 * ARBITRAGE_TIME) + 1; - -/// Amount of blocks per epoch in the fast-epoch feature that is used in tests. -pub const FAST_EPOCH_DURATION: u64 = 2 * MINUTES; - -/// Amount of blocks for the initial period era of the emissions under the fast-epoch feature. -pub const FAST_EPOCH_INITIAL_PERIOD: u64 = 2 * FAST_EPOCH_DURATION; +/// The maximum amount of key shares per set. +pub const MAX_KEY_SHARES_PER_SET: u16 = 150; +/// The maximum amount of key shares per set, as an u32. +pub const MAX_KEY_SHARES_PER_SET_U32: u32 = MAX_KEY_SHARES_PER_SET as u32; diff --git a/substrate/primitives/src/crypto.rs b/substrate/primitives/src/crypto.rs new file mode 100644 index 00000000..12398aa8 --- /dev/null +++ b/substrate/primitives/src/crypto.rs @@ -0,0 +1,66 @@ +use zeroize::Zeroize; +use borsh::{BorshSerialize, BorshDeserialize}; + +use sp_core::{ConstU32, bounded::BoundedVec}; + +/// A Ristretto public key. +#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize, BorshSerialize, BorshDeserialize)] +pub struct Public(pub [u8; 32]); +impl From for Public { + fn from(public: sp_core::sr25519::Public) -> Self { + Self(public.0) + } +} +impl From for sp_core::sr25519::Public { + fn from(public: Public) -> Self { + Self::from_raw(public.0) + } +} + +/// A sr25519 signature. +#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize, BorshSerialize, BorshDeserialize)] +pub struct Signature(pub [u8; 64]); +impl From for Signature { + fn from(signature: sp_core::sr25519::Signature) -> Self { + Self(signature.0) + } +} +impl From for sp_core::sr25519::Signature { + fn from(signature: Signature) -> Self { + Self::from_raw(signature.0) + } +} + +/// A key for an external network. +#[derive(Clone, PartialEq, Eq, Debug, BorshSerialize, BorshDeserialize)] +pub struct ExternalKey( + #[borsh( + serialize_with = "crate::borsh_serialize_bounded_vec", + deserialize_with = "crate::borsh_deserialize_bounded_vec" + )] + pub BoundedVec>, +); + +impl Zeroize for ExternalKey { + fn zeroize(&mut self) { + self.0.as_mut().zeroize(); + } +} + +impl ExternalKey { + /// The maximum length for am external key. + /* + This support keys up to 96 bytes (such as BLS12-381 G2, which is the largest elliptic-curve + group element we might reasonably use as a key). This can always be increased if we need to + adopt a different cryptosystem (one where verification keys are multiple group elements, or + where group elements do exceed 96 bytes, such as RSA). + */ + pub const MAX_LEN: u32 = 96; +} + +/// The key pair for a validator set. +/// +/// This is their Ristretto key, used for publishing data onto Serai, and their key on the external +/// network. +#[derive(Clone, PartialEq, Eq, Debug, Zeroize, BorshSerialize, BorshDeserialize)] +pub struct KeyPair(pub Public, pub ExternalKey); diff --git a/substrate/primitives/src/dex.rs b/substrate/primitives/src/dex.rs deleted file mode 100644 index 4dd9de42..00000000 --- a/substrate/primitives/src/dex.rs +++ /dev/null @@ -1,19 +0,0 @@ -#[cfg(feature = "borsh")] -use borsh::{BorshSerialize, BorshDeserialize}; -#[cfg(feature = "serde")] -use serde::{Serialize, Deserialize}; - -use scale::{Encode, Decode, MaxEncodedLen}; - -use crate::{Coin, SubstrateAmount}; - -#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Encode, Decode, MaxEncodedLen)] -#[cfg_attr(feature = "borsh", derive(BorshSerialize, BorshDeserialize))] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub struct QuotePriceParams { - pub coin1: Coin, - pub coin2: Coin, - pub amount: SubstrateAmount, - pub include_fee: bool, - pub exact_in: bool, -} diff --git a/substrate/primitives/src/genesis.rs b/substrate/primitives/src/genesis.rs new file mode 100644 index 00000000..4ff4e3da --- /dev/null +++ b/substrate/primitives/src/genesis.rs @@ -0,0 +1,24 @@ +use alloc::vec::Vec; + +use zeroize::Zeroize; +use borsh::{BorshSerialize, BorshDeserialize}; + +use crate::balance::Amount; + +/// The value of non-Bitcoin externals coins present at genesis, relative to Bitcoin. +#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize, BorshSerialize, BorshDeserialize)] +pub struct GenesisValues { + /// The value of Ether, relative to Bitcoin. + pub ether: Amount, + /// The value of DAI, relative to Bitcoin. + pub dai: Amount, + /// The value of Monero, relative to Bitcoin. + pub monero: Amount, +} + +impl GenesisValues { + /// The message for the oraclize_values signature. + pub fn oraclize_values_message(&self) -> Vec { + borsh::to_vec(&(b"GenesisLiquidity-oraclize_values", self)).unwrap() + } +} diff --git a/substrate/primitives/src/instructions/in/batch.rs b/substrate/primitives/src/instructions/in/batch.rs new file mode 100644 index 00000000..52659824 --- /dev/null +++ b/substrate/primitives/src/instructions/in/batch.rs @@ -0,0 +1,171 @@ +use alloc::{vec, vec::Vec}; + +use zeroize::Zeroize; + +use borsh::{io, BorshSerialize, BorshDeserialize}; + +use crate::{ + BlockHash, crypto::Signature, network_id::ExternalNetworkId, + instructions::InInstructionWithBalance, +}; + +/* + `Batch`s have a size limit we enforce upon deserialization. + + The naive solution would be to deserialize, then serialize, and check the serialized length is + less than the maximum. This performs a redundant allocation and is computationally non-trivial. + + The next solution would be to wrap the deserialization with a `Cursor` so one can check the + amount read, yet `Cursor` isn't available under no-std. + + We solve this by manually implementing a `Cursor`-equivalent (for our purposes) which let us + check the total amount read is `<=` the maximum size. + + The issue is we need every call to `BorshDeserialize::deserialize_reader` to use our custom + reader, which requires manually implementing it, which means we can't use the derive macro and + can't ensure it follows the borsh specification. We solve this by generating two identical + structs, one internal with a derived `BorshDeserialize::deserialize_reader`, one public with a + manually implemented `BorshDeserialize::deserialize_reader` wrapping the internal struct's. This + lets us ensure the correct reader is used and error if the size limit is hit, while still using + a derived `BorshDeserialize` which will definitively be compliant. +*/ +macro_rules! batch_struct { + (#[$derive: meta] $pub: vis $name: ident) => { + /// A batch of `InInstruction`s to publish onto Serai. + #[allow(clippy::needless_pub_self)] + #[$derive] + $pub struct $name { + /// The size this will be once encoded. + #[allow(dead_code)] // This is unused for the `BatchDeserialize` instance + #[borsh(skip)] + encoded_size: usize, + + /// The network this batch of instructions is coming from. + network: ExternalNetworkId, + /// The ID of this `Batch`. + id: u32, + /// The hash of the external network's block which produced this `Batch`. + external_network_block_hash: BlockHash, + /// The instructions to execute. + instructions: Vec, + } + } +} + +batch_struct!(#[derive(BorshDeserialize)] pub(self) BatchDeserialize); +batch_struct!(#[derive(Clone, PartialEq, Eq, Debug, Zeroize, BorshSerialize)] pub Batch); + +impl BorshDeserialize for Batch { + fn deserialize_reader(reader: &mut R) -> io::Result { + // A custom reader which enforces the `Batch`'s max size limit + struct SizeLimitReader<'a, R: io::Read> { + reader: &'a mut R, + read: usize, + } + impl io::Read for SizeLimitReader<'_, R> { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + let read = self.reader.read(buf)?; + self.read = self.read.saturating_add(read); + if self.read > Batch::MAX_SIZE { + Err(io::Error::new(io::ErrorKind::Other, "Batch size exceeded maximum"))?; + } + Ok(read) + } + } + + let mut size_limit_reader = SizeLimitReader { reader, read: 0 }; + let BatchDeserialize { + encoded_size: _, + network, + id, + external_network_block_hash, + instructions, + } = <_>::deserialize_reader(&mut size_limit_reader)?; + Ok(Batch { + encoded_size: size_limit_reader.read, + network, + id, + external_network_block_hash, + instructions, + }) + } +} + +/// An error incurred while pushing an instruction onto a `Batch`. +pub enum PushInstructionError { + /// The Batch's max size was exceeded. + MaxSizeExceeded, +} + +impl Batch { + /// The maximum size of a valid `Batch`'s encoding. + const MAX_SIZE: usize = 32_768; + + /// Create a new Batch. + pub fn new(network: ExternalNetworkId, id: u32, external_network_block_hash: BlockHash) -> Self { + let mut batch = + Batch { encoded_size: 0, network, id, external_network_block_hash, instructions: vec![] }; + batch.encoded_size = borsh::to_vec(&batch).unwrap().len(); + batch + } + + /// Push an `InInstructionWithBalance` onto this `Batch`. + pub fn push_instruction( + &mut self, + instruction: InInstructionWithBalance, + ) -> Result<(), PushInstructionError> { + if (self.encoded_size.saturating_add(borsh::to_vec(&instruction).unwrap().len())) > + Self::MAX_SIZE + { + Err(PushInstructionError::MaxSizeExceeded)?; + } + self.instructions.push(instruction); + Ok(()) + } + + /// The message to sign when publishing this Batch. + pub fn publish_batch_message(&self) -> Vec { + const DST: &[u8] = b"InInstructions-publish_batch"; + // We don't estimate the size of this Batch, we just reserve a small constant capacity + let mut buf = Vec::with_capacity(1024); + (DST, self).serialize(&mut buf).unwrap(); + buf + } + + /// The network this batch of instructions is coming from. + pub fn network(&self) -> ExternalNetworkId { + self.network + } + + /// The ID of this `Batch`. + pub fn id(&self) -> u32 { + self.id + } + + /// The hash of the external network's block which produced this `Batch`. + pub fn external_network_block_hash(&self) -> BlockHash { + self.external_network_block_hash + } + + /// The instructions within this `Batch`. + pub fn instructions(&self) -> &[InInstructionWithBalance] { + &self.instructions + } +} + +/// A signed batch. +#[derive(Clone, PartialEq, Eq, Debug, BorshSerialize, BorshDeserialize)] +pub struct SignedBatch { + /// The signed batch. + pub batch: Batch, + /// The signature. + pub signature: Signature, +} + +#[cfg(feature = "std")] +impl Zeroize for SignedBatch { + fn zeroize(&mut self) { + self.batch.zeroize(); + self.signature.0.as_mut().zeroize(); + } +} diff --git a/substrate/primitives/src/instructions/in/mod.rs b/substrate/primitives/src/instructions/in/mod.rs new file mode 100644 index 00000000..6e5801f0 --- /dev/null +++ b/substrate/primitives/src/instructions/in/mod.rs @@ -0,0 +1,86 @@ +use zeroize::Zeroize; +use borsh::{BorshSerialize, BorshDeserialize}; + +use crate::{ + address::{SeraiAddress, ExternalAddress}, + balance::{Amount, ExternalBalance, Balance}, + instructions::OutInstruction, +}; + +mod batch; +pub use batch::*; + +/// The destination for coins. +#[derive(Clone, PartialEq, Eq, Debug, Zeroize, BorshSerialize, BorshDeserialize)] +pub enum Destination { + /// The Serai address to transfer the coins to. + Serai(SeraiAddress), + /// Burn the coins with the included `OutInstruction`. + Burn(OutInstruction), +} + +/// An instruction on how to handle coins in. +#[derive(Clone, PartialEq, Eq, Debug, Zeroize, BorshSerialize, BorshDeserialize)] +pub enum InInstruction { + /// Add the coins as genesis liquidity. + GenesisLiquidity(SeraiAddress), + /// Use the coins to swap to staked SRI, pre-economic security. + SwapToStakedSri { + /// The validator to allocate the stake to. + validator: SeraiAddress, + /// The minimum amount of staked SRI to swap to. + minimum: Amount, + }, + /// Transfer the coins to a Serai address, swapping some for SRI. + TransferWithSwap { + /// The Serai address to transfer the coins to, after swapping some. + to: SeraiAddress, + /// The maximum amount of coins to swap for the intended amount of SRI. + maximum_swap: Amount, + /// The SRI amount to swap some of the coins for. + sri: Amount, + }, + /// Transfer the coins to a Serai address. + Transfer { + /// The Serai address to transfer the coins to. + to: SeraiAddress, + }, + /// Swap part of the coins to SRI and add the coins as liquidity. + SwapAndAddLiquidity { + /// The owner to-be of the added liquidity. + owner: SeraiAddress, + /// The amount of SRI to add within the liquidity position. + sri: Amount, + /// The minimum amount of the coin to add as liquidity. + minimum_coin: Amount, + /// The amount of SRI to swap to and send to the owner to-be to pay for transactions on Serai. + sri_for_fees: Amount, + }, + /// Swap the coins. + Swap { + /// The minimum balance to receive. + minimum_out: Balance, + /// The destination to transfer the balance to. + /// + /// If `Destination::Burn`, the balance out will be burnt with the included `OutInstruction`. + destination: Destination, + }, +} + +/// An instruction on how to handle coins in with the address to return the coins to on error. +#[derive(Clone, PartialEq, Eq, Debug, Zeroize, BorshSerialize, BorshDeserialize)] +pub struct RefundableInInstruction { + /// The instruction on how to handle coins in. + pub instruction: InInstruction, + /// The address to return the coins to on error. + pub return_address: Option, +} + +/// An instruction on how to handle coins in with the balance to use for the coins in. +#[derive(Clone, PartialEq, Eq, Debug, Zeroize, BorshSerialize, BorshDeserialize)] +pub struct InInstructionWithBalance { + /// The instruction on how to handle coins in. + pub instruction: OutInstruction, + /// The coins in. + pub balance: ExternalBalance, +} diff --git a/substrate/primitives/src/instructions/mod.rs b/substrate/primitives/src/instructions/mod.rs new file mode 100644 index 00000000..73ad54e1 --- /dev/null +++ b/substrate/primitives/src/instructions/mod.rs @@ -0,0 +1,5 @@ +mod r#in; +pub use r#in::{InInstruction, InInstructionWithBalance, PushInstructionError, Batch, SignedBatch}; + +mod out; +pub use out::{OutInstruction, OutInstructionWithBalance}; diff --git a/substrate/primitives/src/instructions/out.rs b/substrate/primitives/src/instructions/out.rs new file mode 100644 index 00000000..0dc04c62 --- /dev/null +++ b/substrate/primitives/src/instructions/out.rs @@ -0,0 +1,21 @@ +use zeroize::Zeroize; + +use borsh::{BorshSerialize, BorshDeserialize}; + +use crate::{address::ExternalAddress, balance::ExternalBalance}; + +/// An instruction on how to transfer coins out. +#[derive(Clone, PartialEq, Eq, Debug, Zeroize, BorshSerialize, BorshDeserialize)] +pub enum OutInstruction { + /// Transfer to the specified address. + Transfer(ExternalAddress), +} + +/// An instruction on how to transfer coins out with the balance to use for the transfer out. +#[derive(Clone, PartialEq, Eq, Debug, Zeroize, BorshSerialize, BorshDeserialize)] +pub struct OutInstructionWithBalance { + /// The instruction on how to transfer coins out. + pub instruction: OutInstruction, + /// The balance to use for the transfer out. + pub balance: ExternalBalance, +} diff --git a/substrate/primitives/src/lib.rs b/substrate/primitives/src/lib.rs index 0092a77b..27a5f906 100644 --- a/substrate/primitives/src/lib.rs +++ b/substrate/primitives/src/lib.rs @@ -1,168 +1,79 @@ -#![cfg_attr(docsrs, feature(doc_cfg))] #![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![doc = include_str!("../README.md")] +#![deny(missing_docs)] #![cfg_attr(not(feature = "std"), no_std)] -#[cfg(feature = "std")] +extern crate alloc; + use zeroize::Zeroize; +use ::borsh::{BorshSerialize, BorshDeserialize}; -#[cfg(feature = "borsh")] -use borsh::{BorshSerialize, BorshDeserialize}; -#[cfg(feature = "serde")] -use serde::{Serialize, Deserialize}; +/// Wrappers to implement Borsh on non-Borsh-implementing types. +#[doc(hidden)] +pub mod sp_borsh; +pub(crate) use sp_borsh::*; -use scale::{Encode, Decode, MaxEncodedLen}; -use scale_info::TypeInfo; +/// Constants within the Serai protocol. +pub mod constants; -#[cfg(test)] -use sp_io::TestExternalities; +/// Cryptographic types. +pub mod crypto; -#[cfg(test)] -use frame_support::{pallet_prelude::*, Identity, traits::StorageInstance}; +/// Address types. +pub mod address; -use sp_core::{ConstU32, bounded::BoundedVec}; -pub use sp_application_crypto as crypto; +/// Types for identifying coins. +pub mod coin; -mod amount; -pub use amount::*; +/// The `Amount`, `ExternalBalance`, and `Balance` types. +pub mod balance; -mod block; -pub use block::*; +/// Types for genesis. +pub mod genesis; -mod networks; -pub use networks::*; +/// Types for identifying networks and their properties. +pub mod network_id; -mod balance; -pub use balance::*; +/// Types for identifying and working with validator sets. +pub mod validator_sets; -mod account; -pub use account::*; +/// Types for signaling. +pub mod signals; -mod constants; -pub use constants::*; +/// Instruction types. +pub mod instructions; -mod dex; -#[allow(unused_imports)] -pub use dex::*; - -pub type BlockNumber = u64; -pub type Header = sp_runtime::generic::Header; - -#[cfg(feature = "borsh")] -pub fn borsh_serialize_bounded_vec( - bounded: &BoundedVec>, - writer: &mut W, -) -> Result<(), borsh::io::Error> { - borsh::BorshSerialize::serialize(bounded.as_slice(), writer) -} - -#[cfg(feature = "borsh")] -pub fn borsh_deserialize_bounded_vec( - reader: &mut R, -) -> Result>, borsh::io::Error> { - let vec: Vec = borsh::BorshDeserialize::deserialize_reader(reader)?; - vec.try_into().map_err(|_| borsh::io::Error::other("bound exceeded")) -} - -pub const MAX_ADDRESS_LEN: u32 = 512; - -#[derive(Clone, PartialEq, Eq, Debug, Encode, Decode, MaxEncodedLen, TypeInfo)] -#[cfg_attr(feature = "borsh", derive(BorshSerialize, BorshDeserialize))] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub struct ExternalAddress( - #[cfg_attr( - feature = "borsh", - borsh( - serialize_with = "borsh_serialize_bounded_vec", - deserialize_with = "borsh_deserialize_bounded_vec" - ) - )] - BoundedVec>, -); -#[cfg(feature = "std")] -impl Zeroize for ExternalAddress { - fn zeroize(&mut self) { - self.0.as_mut().zeroize() +/// The type used to identify block numbers. +/// +/// A block's number is its zero-indexed position on the list of blocks which form a blockchain. +/// For non-linear structures, this would presumably be the zero-indexed position within some +/// topological order. +#[derive( + Clone, Copy, Default, PartialEq, Eq, Hash, Debug, Zeroize, BorshSerialize, BorshDeserialize, +)] +pub struct BlockNumber(pub u64); +impl From for BlockNumber { + fn from(number: u64) -> BlockNumber { + BlockNumber(number) } } -impl ExternalAddress { - #[cfg(feature = "std")] - pub fn new(address: Vec) -> Result { - Ok(ExternalAddress(address.try_into().map_err(|_| "address length exceeds {MAX_ADDRESS_LEN}")?)) - } - - #[cfg(feature = "std")] - pub fn consume(self) -> Vec { - self.0.into_inner() +/// The type used to identify block hashes. +/* + Across all networks, block hashes may not be 32 bytes. There may be a network which targets 256 + bits of security and accordingly has a 64-byte block hash. Serai only targets a 128-bit security + level so this is fine for our use-case. If we do ever see a 64-byte block hash, we can simply + hash it into a 32-byte hash or truncate it. +*/ +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Zeroize, BorshSerialize, BorshDeserialize)] +pub struct BlockHash(pub [u8; 32]); +impl From<[u8; 32]> for BlockHash { + fn from(hash: [u8; 32]) -> BlockHash { + BlockHash(hash) } } - -impl AsRef<[u8]> for ExternalAddress { - fn as_ref(&self) -> &[u8] { - self.0.as_ref() +impl From for BlockHash { + fn from(hash: sp_core::H256) -> BlockHash { + BlockHash(hash.into()) } } - -/// Lexicographically reverses a given byte array. -pub fn reverse_lexicographic_order(bytes: [u8; N]) -> [u8; N] { - let mut res = [0u8; N]; - for (i, byte) in bytes.iter().enumerate() { - res[i] = !*byte; - } - res -} - -#[test] -fn test_reverse_lexicographic_order() { - TestExternalities::default().execute_with(|| { - use rand_core::{RngCore, OsRng}; - - struct Storage; - impl StorageInstance for Storage { - fn pallet_prefix() -> &'static str { - "LexicographicOrder" - } - - const STORAGE_PREFIX: &'static str = "storage"; - } - type Map = StorageMap; - - struct StorageReverse; - impl StorageInstance for StorageReverse { - fn pallet_prefix() -> &'static str { - "LexicographicOrder" - } - - const STORAGE_PREFIX: &'static str = "storagereverse"; - } - type MapReverse = StorageMap; - - // populate the maps - let mut amounts = vec![]; - for _ in 0 .. 100 { - amounts.push(OsRng.next_u64()); - } - - let mut amounts_sorted = amounts.clone(); - amounts_sorted.sort(); - for a in amounts { - Map::set(a.to_be_bytes(), Some(())); - MapReverse::set(reverse_lexicographic_order(a.to_be_bytes()), Some(())); - } - - // retrive back and check whether they are sorted as expected - let total_size = amounts_sorted.len(); - let mut map_iter = Map::iter_keys(); - let mut reverse_map_iter = MapReverse::iter_keys(); - for i in 0 .. amounts_sorted.len() { - let first = map_iter.next().unwrap(); - let second = reverse_map_iter.next().unwrap(); - - assert_eq!(u64::from_be_bytes(first), amounts_sorted[i]); - assert_eq!( - u64::from_be_bytes(reverse_lexicographic_order(second)), - amounts_sorted[total_size - (i + 1)] - ); - } - }); -} diff --git a/substrate/primitives/src/network_id.rs b/substrate/primitives/src/network_id.rs new file mode 100644 index 00000000..2491ee49 --- /dev/null +++ b/substrate/primitives/src/network_id.rs @@ -0,0 +1,124 @@ +use zeroize::Zeroize; + +use borsh::{io, BorshSerialize, BorshDeserialize}; + +use crate::coin::{ExternalCoin, Coin}; + +/// Identifier for an embedded elliptic curve. +#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize, BorshSerialize, BorshDeserialize)] +pub enum EmbeddedEllipticCurve { + /// The Embedwards25519 curve, defined over (embedded into) Ed25519's/Ristretto's scalar field. + Embedwards25519, + /// The secq256k1 curve, forming a cycle with secp256k1. + Secq256k1, +} + +/// The type used to identify external networks. +/// +/// This type serializes to a subset of `NetworkId`. +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Zeroize, BorshSerialize, BorshDeserialize)] +#[borsh(use_discriminant = true)] +#[non_exhaustive] +pub enum ExternalNetworkId { + /// The Bitcoin network. + Bitcoin = 1, + /// The Ethereum network. + Ethereum = 2, + /// The Monero network. + Monero = 3, +} + +impl ExternalNetworkId { + /// All external networks. + pub fn all() -> impl Iterator { + [ExternalNetworkId::Bitcoin, ExternalNetworkId::Ethereum, ExternalNetworkId::Monero].into_iter() + } +} + +impl ExternalNetworkId { + /// The embedded elliptic curves actively used for this network. + /// + /// This is guaranteed to return `[Embedwards25519]` or + /// `[Embedwards25519, *network specific curve*]`. + pub fn embedded_elliptic_curves(&self) -> &'static [EmbeddedEllipticCurve] { + match self { + // We need to generate a Ristretto key for oraclizing and a Secp256k1 key for the network + Self::Bitcoin | Self::Ethereum => { + &[EmbeddedEllipticCurve::Embedwards25519, EmbeddedEllipticCurve::Secq256k1] + } + // Since the oraclizing key curve is the same as the network's curve, we only need it + Self::Monero => &[EmbeddedEllipticCurve::Embedwards25519], + } + } + + /// The coins native to this network. + pub fn coins(&self) -> &'static [ExternalCoin] { + match self { + Self::Bitcoin => &[ExternalCoin::Bitcoin], + Self::Ethereum => &[ExternalCoin::Ether, ExternalCoin::Dai], + Self::Monero => &[ExternalCoin::Monero], + } + } +} + +/// The type used to identify networks. +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Zeroize)] +pub enum NetworkId { + /// The Serai network. + Serai, + /// An external network. + External(ExternalNetworkId), +} + +impl BorshSerialize for NetworkId { + fn serialize(&self, writer: &mut W) -> io::Result<()> { + match self { + Self::Serai => writer.write_all(&[0]), + Self::External(external) => external.serialize(writer), + } + } +} + +impl BorshDeserialize for NetworkId { + fn deserialize_reader(reader: &mut R) -> io::Result { + let mut kind = [0xff]; + reader.read_exact(&mut kind)?; + match kind[0] { + 0 => Ok(Self::Serai), + _ => ExternalNetworkId::deserialize_reader(&mut kind.as_slice()).map(Into::into), + } + } +} + +impl NetworkId { + /// All networks. + pub fn all() -> impl Iterator { + core::iter::once(NetworkId::Serai).chain(ExternalNetworkId::all().map(Into::into)) + } + + /// The coins native to this network. + pub fn coins(self) -> impl Iterator { + let (coins, external_coins): (&[Coin], &[ExternalCoin]) = match self { + NetworkId::Serai => (&[Coin::Serai], &[]), + NetworkId::External(ext) => (&[], ext.coins()), + }; + coins.iter().copied().chain(external_coins.iter().copied().map(Into::into)) + } +} + +impl From for NetworkId { + fn from(network: ExternalNetworkId) -> Self { + NetworkId::External(network) + } +} + +impl TryFrom for ExternalNetworkId { + type Error = (); + + fn try_from(network: NetworkId) -> Result { + match network { + NetworkId::Serai => Err(())?, + NetworkId::External(ext) => Ok(ext), + } + } +} diff --git a/substrate/primitives/src/networks.rs b/substrate/primitives/src/networks.rs deleted file mode 100644 index ace34127..00000000 --- a/substrate/primitives/src/networks.rs +++ /dev/null @@ -1,479 +0,0 @@ -#[cfg(feature = "std")] -use zeroize::Zeroize; - -use scale::{Decode, Encode, EncodeLike, MaxEncodedLen}; -use scale_info::TypeInfo; - -#[cfg(feature = "borsh")] -use borsh::{BorshSerialize, BorshDeserialize}; -#[cfg(feature = "serde")] -use serde::{Serialize, Deserialize}; - -use sp_core::{ConstU32, bounded::BoundedVec}; -use sp_std::{vec, vec::Vec}; - -#[cfg(feature = "borsh")] -use crate::{borsh_serialize_bounded_vec, borsh_deserialize_bounded_vec}; - -/// Identifier for an embedded elliptic curve. -#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Encode, Decode, MaxEncodedLen, TypeInfo)] -#[cfg_attr(feature = "std", derive(Zeroize))] -#[cfg_attr(feature = "borsh", derive(BorshSerialize, BorshDeserialize))] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub enum EmbeddedEllipticCurve { - Embedwards25519, - Secq256k1, -} - -/// The type used to identify external networks. -#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, TypeInfo)] -#[cfg_attr(feature = "std", derive(Zeroize))] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub enum ExternalNetworkId { - Bitcoin, - Ethereum, - Monero, -} - -impl Encode for ExternalNetworkId { - fn encode(&self) -> Vec { - match self { - ExternalNetworkId::Bitcoin => vec![1], - ExternalNetworkId::Ethereum => vec![2], - ExternalNetworkId::Monero => vec![3], - } - } -} - -impl Decode for ExternalNetworkId { - fn decode(input: &mut I) -> Result { - let kind = input.read_byte()?; - match kind { - 1 => Ok(Self::Bitcoin), - 2 => Ok(Self::Ethereum), - 3 => Ok(Self::Monero), - _ => Err(scale::Error::from("invalid format")), - } - } -} - -impl MaxEncodedLen for ExternalNetworkId { - fn max_encoded_len() -> usize { - 1 - } -} - -impl EncodeLike for ExternalNetworkId {} - -#[cfg(feature = "borsh")] -impl BorshSerialize for ExternalNetworkId { - fn serialize(&self, writer: &mut W) -> std::io::Result<()> { - writer.write_all(&self.encode()) - } -} - -#[cfg(feature = "borsh")] -impl BorshDeserialize for ExternalNetworkId { - fn deserialize_reader(reader: &mut R) -> std::io::Result { - let mut kind = [0; 1]; - reader.read_exact(&mut kind)?; - ExternalNetworkId::decode(&mut kind.as_slice()) - .map_err(|_| std::io::Error::other("invalid format")) - } -} - -/// The type used to identify networks. -#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, PartialOrd, Ord, TypeInfo)] -#[cfg_attr(feature = "std", derive(Zeroize))] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub enum NetworkId { - Serai, - External(ExternalNetworkId), -} - -impl Encode for NetworkId { - fn encode(&self) -> Vec { - match self { - NetworkId::Serai => vec![0], - NetworkId::External(network) => network.encode(), - } - } -} - -impl Decode for NetworkId { - fn decode(input: &mut I) -> Result { - let kind = input.read_byte()?; - match kind { - 0 => Ok(Self::Serai), - _ => Ok(ExternalNetworkId::decode(&mut [kind].as_slice())?.into()), - } - } -} - -impl MaxEncodedLen for NetworkId { - fn max_encoded_len() -> usize { - 1 - } -} - -impl EncodeLike for NetworkId {} - -#[cfg(feature = "borsh")] -impl BorshSerialize for NetworkId { - fn serialize(&self, writer: &mut W) -> std::io::Result<()> { - writer.write_all(&self.encode()) - } -} - -#[cfg(feature = "borsh")] -impl BorshDeserialize for NetworkId { - fn deserialize_reader(reader: &mut R) -> std::io::Result { - let mut kind = [0; 1]; - reader.read_exact(&mut kind)?; - NetworkId::decode(&mut kind.as_slice()).map_err(|_| std::io::Error::other("invalid format")) - } -} - -impl ExternalNetworkId { - /// The embedded elliptic curve actively used for this network. - /// - /// This is guaranteed to return `[]`, `[Embedwards25519]`, or - /// `[Embedwards25519, *network specific curve*]`. - pub fn embedded_elliptic_curves(&self) -> &'static [EmbeddedEllipticCurve] { - match self { - // We need to generate a Ristretto key for oraclizing and a Secp256k1 key for the network - Self::Bitcoin | Self::Ethereum => { - &[EmbeddedEllipticCurve::Embedwards25519, EmbeddedEllipticCurve::Secq256k1] - } - // Since the oraclizing key curve is the same as the network's curve, we only need it - Self::Monero => &[EmbeddedEllipticCurve::Embedwards25519], - } - } - - pub fn coins(&self) -> Vec { - match self { - Self::Bitcoin => vec![ExternalCoin::Bitcoin], - Self::Ethereum => vec![ExternalCoin::Ether, ExternalCoin::Dai], - Self::Monero => vec![ExternalCoin::Monero], - } - } -} - -impl NetworkId { - /// The embedded elliptic curve actively used for this network. - /// - /// This is guaranteed to return `[]`, `[Embedwards25519]`, or - /// `[Embedwards25519, *network specific curve*]`. - pub fn embedded_elliptic_curves(&self) -> &'static [EmbeddedEllipticCurve] { - match self { - Self::Serai => &[], - Self::External(network) => network.embedded_elliptic_curves(), - } - } - - pub fn coins(&self) -> Vec { - match self { - Self::Serai => vec![Coin::Serai], - Self::External(network) => { - network.coins().into_iter().map(core::convert::Into::into).collect() - } - } - } -} - -impl From for NetworkId { - fn from(network: ExternalNetworkId) -> Self { - NetworkId::External(network) - } -} - -impl TryFrom for ExternalNetworkId { - type Error = (); - - fn try_from(network: NetworkId) -> Result { - match network { - NetworkId::Serai => Err(())?, - NetworkId::External(n) => Ok(n), - } - } -} - -pub const EXTERNAL_NETWORKS: [ExternalNetworkId; 3] = - [ExternalNetworkId::Bitcoin, ExternalNetworkId::Ethereum, ExternalNetworkId::Monero]; - -pub const NETWORKS: [NetworkId; 4] = [ - NetworkId::Serai, - NetworkId::External(ExternalNetworkId::Bitcoin), - NetworkId::External(ExternalNetworkId::Ethereum), - NetworkId::External(ExternalNetworkId::Monero), -]; - -pub const EXTERNAL_COINS: [ExternalCoin; 4] = - [ExternalCoin::Bitcoin, ExternalCoin::Ether, ExternalCoin::Dai, ExternalCoin::Monero]; - -pub const COINS: [Coin; 5] = [ - Coin::Serai, - Coin::External(ExternalCoin::Bitcoin), - Coin::External(ExternalCoin::Ether), - Coin::External(ExternalCoin::Dai), - Coin::External(ExternalCoin::Monero), -]; - -/// The type used to identify external coins. -#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, TypeInfo)] -#[cfg_attr(feature = "std", derive(Zeroize))] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub enum ExternalCoin { - Bitcoin, - Ether, - Dai, - Monero, -} - -impl Encode for ExternalCoin { - fn encode(&self) -> Vec { - match self { - ExternalCoin::Bitcoin => vec![4], - ExternalCoin::Ether => vec![5], - ExternalCoin::Dai => vec![6], - ExternalCoin::Monero => vec![7], - } - } -} - -impl Decode for ExternalCoin { - fn decode(input: &mut I) -> Result { - let kind = input.read_byte()?; - match kind { - 4 => Ok(Self::Bitcoin), - 5 => Ok(Self::Ether), - 6 => Ok(Self::Dai), - 7 => Ok(Self::Monero), - _ => Err(scale::Error::from("invalid format")), - } - } -} -impl MaxEncodedLen for ExternalCoin { - fn max_encoded_len() -> usize { - 1 - } -} - -impl EncodeLike for ExternalCoin {} - -#[cfg(feature = "borsh")] -impl BorshSerialize for ExternalCoin { - fn serialize(&self, writer: &mut W) -> std::io::Result<()> { - writer.write_all(&self.encode()) - } -} - -#[cfg(feature = "borsh")] -impl BorshDeserialize for ExternalCoin { - fn deserialize_reader(reader: &mut R) -> std::io::Result { - let mut kind = [0; 1]; - reader.read_exact(&mut kind)?; - ExternalCoin::decode(&mut kind.as_slice()).map_err(|_| std::io::Error::other("invalid format")) - } -} - -/// The type used to identify coins. -#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, TypeInfo)] -#[cfg_attr(feature = "std", derive(Zeroize))] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub enum Coin { - Serai, - External(ExternalCoin), -} - -impl Encode for Coin { - fn encode(&self) -> Vec { - match self { - Coin::Serai => vec![0], - Coin::External(ec) => ec.encode(), - } - } -} - -impl Decode for Coin { - fn decode(input: &mut I) -> Result { - let kind = input.read_byte()?; - match kind { - 0 => Ok(Self::Serai), - _ => Ok(ExternalCoin::decode(&mut [kind].as_slice())?.into()), - } - } -} - -impl MaxEncodedLen for Coin { - fn max_encoded_len() -> usize { - 1 - } -} - -impl EncodeLike for Coin {} - -#[cfg(feature = "borsh")] -impl BorshSerialize for Coin { - fn serialize(&self, writer: &mut W) -> std::io::Result<()> { - writer.write_all(&self.encode()) - } -} - -#[cfg(feature = "borsh")] -impl BorshDeserialize for Coin { - fn deserialize_reader(reader: &mut R) -> std::io::Result { - let mut kind = [0; 1]; - reader.read_exact(&mut kind)?; - Coin::decode(&mut kind.as_slice()).map_err(|_| std::io::Error::other("invalid format")) - } -} - -impl From for Coin { - fn from(coin: ExternalCoin) -> Self { - Coin::External(coin) - } -} - -impl TryFrom for ExternalCoin { - type Error = (); - - fn try_from(coin: Coin) -> Result { - match coin { - Coin::Serai => Err(())?, - Coin::External(c) => Ok(c), - } - } -} - -impl ExternalCoin { - pub fn network(&self) -> ExternalNetworkId { - match self { - ExternalCoin::Bitcoin => ExternalNetworkId::Bitcoin, - ExternalCoin::Ether | ExternalCoin::Dai => ExternalNetworkId::Ethereum, - ExternalCoin::Monero => ExternalNetworkId::Monero, - } - } - - pub fn name(&self) -> &'static str { - match self { - ExternalCoin::Bitcoin => "Bitcoin", - ExternalCoin::Ether => "Ether", - ExternalCoin::Dai => "Dai Stablecoin", - ExternalCoin::Monero => "Monero", - } - } - - pub fn symbol(&self) -> &'static str { - match self { - ExternalCoin::Bitcoin => "BTC", - ExternalCoin::Ether => "ETH", - ExternalCoin::Dai => "DAI", - ExternalCoin::Monero => "XMR", - } - } - - pub fn decimals(&self) -> u32 { - match self { - // Ether and DAI have 18 decimals, yet we only track 8 in order to fit them within u64s - ExternalCoin::Bitcoin | ExternalCoin::Ether | ExternalCoin::Dai => 8, - ExternalCoin::Monero => 12, - } - } -} - -impl Coin { - pub fn native() -> Coin { - Coin::Serai - } - - pub fn network(&self) -> NetworkId { - match self { - Coin::Serai => NetworkId::Serai, - Coin::External(c) => c.network().into(), - } - } - - pub fn name(&self) -> &'static str { - match self { - Coin::Serai => "Serai", - Coin::External(c) => c.name(), - } - } - - pub fn symbol(&self) -> &'static str { - match self { - Coin::Serai => "SRI", - Coin::External(c) => c.symbol(), - } - } - - pub fn decimals(&self) -> u32 { - match self { - Coin::Serai => 8, - Coin::External(c) => c.decimals(), - } - } - - pub fn is_native(&self) -> bool { - matches!(self, Coin::Serai) - } -} - -// Max of 8 coins per network -// Since Serai isn't interested in listing tokens, as on-chain DEXs will almost certainly have -// more liquidity, the only reason we'd have so many coins from a network is if there's no DEX -// on-chain -// There's probably no chain with so many *worthwhile* coins and no on-chain DEX -// This could probably be just 4, yet 8 is a hedge for the unforeseen -// If necessary, this can be increased with a fork -pub const MAX_COINS_PER_NETWORK: u32 = 8; - -/// Network definition. -#[derive(Clone, PartialEq, Eq, Debug)] -#[cfg_attr(feature = "borsh", derive(BorshSerialize, BorshDeserialize))] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub struct Network { - #[cfg_attr( - feature = "borsh", - borsh( - serialize_with = "borsh_serialize_bounded_vec", - deserialize_with = "borsh_deserialize_bounded_vec" - ) - )] - coins: BoundedVec>, -} - -#[cfg(feature = "std")] -impl Zeroize for Network { - fn zeroize(&mut self) { - for coin in self.coins.as_mut() { - coin.zeroize(); - } - self.coins.truncate(0); - } -} - -impl Network { - #[cfg(feature = "std")] - pub fn new(coins: Vec) -> Result { - if coins.is_empty() { - Err("no coins provided")?; - } - - let network = coins[0].network(); - for coin in coins.iter().skip(1) { - if coin.network() != network { - Err("coins have different networks")?; - } - } - - Ok(Network { - coins: coins.try_into().map_err(|_| "coins length exceeds {MAX_COINS_PER_NETWORK}")?, - }) - } - - pub fn coins(&self) -> &[Coin] { - &self.coins - } -} diff --git a/substrate/primitives/src/signals.rs b/substrate/primitives/src/signals.rs new file mode 100644 index 00000000..1ccb8dd0 --- /dev/null +++ b/substrate/primitives/src/signals.rs @@ -0,0 +1,16 @@ +use zeroize::Zeroize; +use borsh::{BorshSerialize, BorshDeserialize}; + +use crate::network_id::ExternalNetworkId; + +/// A signal. +#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize, BorshSerialize, BorshDeserialize)] +pub enum Signal { + /// A signal to retire the current protocol. + Retire { + /// The protocol to retire in favor of. + in_favor_of: [u8; 32], + }, + /// A signal to halt an external network. + Halt(ExternalNetworkId), +} diff --git a/substrate/primitives/src/sp_borsh.rs b/substrate/primitives/src/sp_borsh.rs new file mode 100644 index 00000000..2f4dce42 --- /dev/null +++ b/substrate/primitives/src/sp_borsh.rs @@ -0,0 +1,37 @@ +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, +) -> Result<()> { + let vec: &[u8] = bitvec.as_raw_slice(); + BorshSerialize::serialize(vec, writer) +} + +pub fn borsh_deserialize_bitvec( + reader: &mut R, +) -> Result> { + let bitvec: alloc::vec::Vec = BorshDeserialize::deserialize_reader(reader)?; + Ok(bitvec::vec::BitVec::from_vec(bitvec)) +} + +type SerializeBoundedVecAs = alloc::vec::Vec; + +pub fn borsh_serialize_bounded_vec( + bounded: &BoundedVec>, + writer: &mut W, +) -> Result<()> { + let vec: &SerializeBoundedVecAs = bounded.as_ref(); + BorshSerialize::serialize(vec, writer) +} + +pub fn borsh_deserialize_bounded_vec( + reader: &mut R, +) -> Result>> { + let vec: SerializeBoundedVecAs = BorshDeserialize::deserialize_reader(reader)?; + vec.try_into().map_err(|_| Error::new(ErrorKind::Other, "bound exceeded")) +} diff --git a/substrate/primitives/src/validator_sets/mod.rs b/substrate/primitives/src/validator_sets/mod.rs new file mode 100644 index 00000000..b4322ad9 --- /dev/null +++ b/substrate/primitives/src/validator_sets/mod.rs @@ -0,0 +1,76 @@ +use alloc::vec::Vec; + +use zeroize::Zeroize; +use borsh::{BorshSerialize, BorshDeserialize}; + +use ciphersuite::{group::GroupEncoding, Ciphersuite, Ristretto}; + +use crate::{ + crypto::{Public, KeyPair}, + network_id::{ExternalNetworkId, NetworkId}, +}; + +mod slashes; +pub use slashes::*; + +/// The type used to identify a specific session of validators. +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Zeroize, BorshSerialize, BorshDeserialize)] +pub struct Session(pub u32); + +/// The type used to identify a specific set of validators for an external network. +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Zeroize, BorshSerialize, BorshDeserialize)] +pub struct ExternalValidatorSet { + /// The network this set of validators are for. + pub network: ExternalNetworkId, + /// Which session this set of validators is occuring during. + pub session: Session, +} + +/// The type used to identify a specific set of validators. +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Zeroize, BorshSerialize, BorshDeserialize)] +pub struct ValidatorSet { + /// The network this set of validators are for. + pub network: NetworkId, + /// Which session this set of validators is occuring during. + pub session: Session, +} + +impl From for ValidatorSet { + fn from(set: ExternalValidatorSet) -> Self { + ValidatorSet { network: set.network.into(), session: set.session } + } +} + +impl TryFrom for ExternalValidatorSet { + type Error = (); + + fn try_from(set: ValidatorSet) -> Result { + set.network.try_into().map(|network| ExternalValidatorSet { network, session: set.session }) + } +} + +impl ExternalValidatorSet { + /// The MuSig context for this validator set. + pub fn musig_context(&self) -> Vec { + borsh::to_vec(&(b"ValidatorSets-musig_key".as_ref(), self)).unwrap() + } + + /// The MuSig public key for a validator set. + /// + /// This function panics on invalid input, per the definition of `dkg::musig::musig_key`. + pub fn musig_key(&self, set_keys: &[Public]) -> Public { + let mut keys = Vec::new(); + for key in set_keys { + keys.push( + ::read_G::<&[u8]>(&mut key.0.as_ref()) + .expect("invalid participant"), + ); + } + Public(dkg::musig::musig_key::(&self.musig_context(), &keys).unwrap().to_bytes()) + } + + /// The message for the `set_keys` signature. + pub fn set_keys_message(&self, key_pair: &KeyPair) -> Vec { + borsh::to_vec(&(b"ValidatorSets-set_keys", self, key_pair)).unwrap() + } +} diff --git a/substrate/validator-sets/primitives/src/slash_points.rs b/substrate/primitives/src/validator_sets/slashes.rs similarity index 85% rename from substrate/validator-sets/primitives/src/slash_points.rs rename to substrate/primitives/src/validator_sets/slashes.rs index 0cc72b2f..acc4a68d 100644 --- a/substrate/validator-sets/primitives/src/slash_points.rs +++ b/substrate/primitives/src/validator_sets/slashes.rs @@ -1,35 +1,25 @@ use core::{num::NonZero, time::Duration}; +use alloc::vec::Vec; -#[cfg(feature = "std")] use zeroize::Zeroize; -use scale::{Encode, Decode, MaxEncodedLen}; -use scale_info::TypeInfo; - -#[cfg(feature = "borsh")] use borsh::{BorshSerialize, BorshDeserialize}; -#[cfg(feature = "serde")] -use serde::{Serialize, Deserialize}; use sp_core::{ConstU32, bounded::BoundedVec}; -#[cfg(not(feature = "std"))] -use sp_std::vec::Vec; -use serai_primitives::{TARGET_BLOCK_TIME, Amount}; - -use crate::{SESSION_LENGTH, MAX_KEY_SHARES_PER_SET_U32}; +use crate::{ + constants::{TARGET_BLOCK_TIME, SESSION_LENGTH, MAX_KEY_SHARES_PER_SET_U32}, + balance::Amount, +}; /// Each slash point is equivalent to the downtime implied by missing a block proposal. -// Takes a NonZero so that the result is never 0. +// Takes a NonZero so that the result is never 0, making this safe to divide by. fn downtime_per_slash_point(validators: NonZero) -> Duration { - Duration::from_secs(TARGET_BLOCK_TIME) * u32::from(u16::from(validators)) + TARGET_BLOCK_TIME * u32::from(u16::from(validators)) } /// A slash for a validator. -#[derive(Clone, Copy, PartialEq, Eq, Debug, Encode, Decode, MaxEncodedLen, TypeInfo)] -#[cfg_attr(feature = "std", derive(Zeroize))] -#[cfg_attr(feature = "borsh", derive(BorshSerialize, BorshDeserialize))] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize, BorshSerialize, BorshDeserialize)] pub enum Slash { /// The slash points accumulated by this validator. /// @@ -46,7 +36,7 @@ pub enum Slash { impl Slash { /// Calculate the penalty which should be applied to the validator. /// - /// Does not panic, even due to overflows, if `allocated_stake + session_rewards <= u64::MAX`. + /// Does not panic, even when compiled with checked arithmetic. pub fn penalty( self, validators: NonZero, @@ -206,31 +196,34 @@ impl Slash { } } -#[derive(Clone, PartialEq, Eq, Debug, Encode, Decode, TypeInfo, MaxEncodedLen)] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub struct SlashReport(pub BoundedVec>); +/// A report of all slashes incurred for a `ValidatorSet`. +#[derive(Clone, PartialEq, Eq, Debug, BorshSerialize, BorshDeserialize)] +pub struct SlashReport( + #[borsh( + serialize_with = "crate::borsh_serialize_bounded_vec", + deserialize_with = "crate::borsh_deserialize_bounded_vec" + )] + pub BoundedVec>, +); -#[cfg(feature = "borsh")] -impl BorshSerialize for SlashReport { - fn serialize(&self, writer: &mut W) -> borsh::io::Result<()> { - BorshSerialize::serialize(self.0.as_slice(), writer) - } -} -#[cfg(feature = "borsh")] -impl BorshDeserialize for SlashReport { - fn deserialize_reader(reader: &mut R) -> borsh::io::Result { - let slashes = Vec::::deserialize_reader(reader)?; - slashes - .try_into() - .map(Self) - .map_err(|_| borsh::io::Error::other("length of slash report exceeds max validators")) - } +/// An error when converting from a `Vec`. +pub enum FromVecError { + /// The source `Vec` was too long to be converted. + TooLong, } impl TryFrom> for SlashReport { - type Error = &'static str; - fn try_from(slashes: Vec) -> Result { - slashes.try_into().map(Self).map_err(|_| "length of slash report exceeds max validators") + type Error = FromVecError; + fn try_from(slashes: Vec) -> Result { + slashes.try_into().map(Self).map_err(|_| FromVecError::TooLong) + } +} + +impl zeroize::Zeroize for SlashReport { + fn zeroize(&mut self) { + for slash in self.0.as_mut() { + slash.zeroize(); + } } } @@ -238,13 +231,18 @@ impl SlashReport { /// The message to sign when publishing this SlashReport. // This is assumed binding to the ValidatorSet via the key signed with pub fn report_slashes_message(&self) -> Vec { - (b"ValidatorSets-report_slashes", &self.0).encode() + const DST: &[u8] = b"ValidatorSets-report_slashes"; + let mut buf = Vec::with_capacity( + DST.len() + core::mem::size_of::() + (self.0.len() * core::mem::size_of::()), + ); + (DST, self).serialize(&mut buf).unwrap(); + buf } } #[test] fn test_penalty() { - for validators in [1, 50, 100, crate::MAX_KEY_SHARES_PER_SET] { + for validators in [1, 50, 100, crate::constants::MAX_KEY_SHARES_PER_SET] { let validators = NonZero::new(validators).unwrap(); // 12 hours of slash points should only decrease the rewards proportionately let twelve_hours_of_slash_points = @@ -316,11 +314,13 @@ fn test_penalty() { #[test] fn no_overflow() { + // Test with u16::MAX for validators, maximizing the downtime each slash point represents Slash::Points(u32::MAX).penalty( NonZero::new(u16::MAX).unwrap(), Amount(u64::MAX), Amount(u64::MAX), ); + // Test with 1 for validators, in case validators is inversely correlated Slash::Points(u32::MAX).penalty(NonZero::new(1).unwrap(), Amount(u64::MAX), Amount(u64::MAX)); } diff --git a/substrate/runtime/Cargo.toml b/substrate/runtime/Cargo.toml index f806281d..0009c802 100644 --- a/substrate/runtime/Cargo.toml +++ b/substrate/runtime/Cargo.toml @@ -48,7 +48,7 @@ frame-executive = { git = "https://github.com/serai-dex/polkadot-sdk", branch = frame-benchmarking = { git = "https://github.com/serai-dex/polkadot-sdk", branch = "serai-next", default-features = false, optional = true } serai-primitives = { path = "../primitives", default-features = false } -serai-abi = { path = "../abi", default-features = false, features = ["serde"] } +serai-abi = { path = "../abi", default-features = false } pallet-timestamp = { git = "https://github.com/serai-dex/polkadot-sdk", branch = "serai-next", default-features = false } pallet-authorship = { git = "https://github.com/serai-dex/polkadot-sdk", branch = "serai-next", default-features = false } @@ -107,7 +107,6 @@ std = [ "serai-primitives/std", "serai-abi/std", - "serai-abi/serde", "pallet-timestamp/std", "pallet-authorship/std", diff --git a/substrate/signals/primitives/src/lib.rs b/substrate/signals/primitives/src/lib.rs index c7f0565a..8b137891 100644 --- a/substrate/signals/primitives/src/lib.rs +++ b/substrate/signals/primitives/src/lib.rs @@ -1,17 +1 @@ -#![cfg_attr(docsrs, feature(doc_cfg))] -#![cfg_attr(docsrs, feature(doc_auto_cfg))] -#![cfg_attr(not(feature = "std"), no_std)] -use scale::{Encode, Decode, MaxEncodedLen}; -use scale_info::TypeInfo; - -use serai_primitives::ExternalNetworkId; - -#[derive(Clone, Copy, PartialEq, Eq, Debug, Encode, Decode, MaxEncodedLen, TypeInfo)] -#[cfg_attr(feature = "std", derive(zeroize::Zeroize))] -#[cfg_attr(feature = "borsh", derive(borsh::BorshSerialize, borsh::BorshDeserialize))] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub enum SignalId { - Retirement([u8; 32]), - Halt(ExternalNetworkId), -} diff --git a/substrate/validator-sets/primitives/src/lib.rs b/substrate/validator-sets/primitives/src/lib.rs index 9b252607..8b137891 100644 --- a/substrate/validator-sets/primitives/src/lib.rs +++ b/substrate/validator-sets/primitives/src/lib.rs @@ -1,177 +1 @@ -#![cfg_attr(not(feature = "std"), no_std)] -use core::time::Duration; - -#[cfg(feature = "std")] -use zeroize::Zeroize; - -use dalek_ff_group::Ristretto; -use ciphersuite::{group::GroupEncoding, Ciphersuite}; - -use scale::{Encode, Decode, MaxEncodedLen}; -use scale_info::TypeInfo; - -#[cfg(feature = "borsh")] -use borsh::{BorshSerialize, BorshDeserialize}; -#[cfg(feature = "serde")] -use serde::{Serialize, Deserialize}; - -use sp_core::{ConstU32, bounded::BoundedVec, sr25519::Public}; -#[cfg(not(feature = "std"))] -use sp_std::vec::Vec; - -use serai_primitives::{ExternalNetworkId, NetworkId}; - -mod slash_points; -pub use slash_points::*; - -/// The expected duration for a session. -// 1 week -pub const SESSION_LENGTH: Duration = Duration::from_secs(7 * 24 * 60 * 60); - -/// The maximum length for a key. -// Support keys up to 96 bytes (BLS12-381 G2). -pub const MAX_KEY_LEN: u32 = 96; - -/// The maximum amount of key shares per set. -pub const MAX_KEY_SHARES_PER_SET: u16 = 150; -pub const MAX_KEY_SHARES_PER_SET_U32: u32 = MAX_KEY_SHARES_PER_SET as u32; - -/// The type used to identify a specific session of validators. -#[derive( - Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Encode, Decode, TypeInfo, MaxEncodedLen, -)] -#[cfg_attr(feature = "std", derive(Zeroize))] -#[cfg_attr(feature = "borsh", derive(BorshSerialize, BorshDeserialize))] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub struct Session(pub u32); - -/// The type used to identify a specific validator set during a specific session. -#[derive( - Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Encode, Decode, TypeInfo, MaxEncodedLen, -)] -#[cfg_attr(feature = "std", derive(Zeroize))] -#[cfg_attr(feature = "borsh", derive(BorshSerialize, BorshDeserialize))] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub struct ValidatorSet { - pub session: Session, - pub network: NetworkId, -} - -/// The type used to identify a specific validator set during a specific session. -#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Encode, Decode, TypeInfo, MaxEncodedLen)] -#[cfg_attr(feature = "std", derive(Zeroize))] -#[cfg_attr(feature = "borsh", derive(BorshSerialize, BorshDeserialize))] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub struct ExternalValidatorSet { - pub session: Session, - pub network: ExternalNetworkId, -} - -impl From for ValidatorSet { - fn from(set: ExternalValidatorSet) -> Self { - ValidatorSet { session: set.session, network: set.network.into() } - } -} - -impl TryFrom for ExternalValidatorSet { - type Error = (); - - fn try_from(set: ValidatorSet) -> Result { - match set.network { - NetworkId::Serai => Err(())?, - NetworkId::External(network) => Ok(ExternalValidatorSet { session: set.session, network }), - } - } -} - -/// The type representing a Key from an external network. -pub type ExternalKey = BoundedVec>; - -/// The key pair for a validator set. -/// -/// This is their Ristretto key, used for publishing data onto Serai, and their key on the external -/// network. -#[derive(Clone, PartialEq, Eq, Debug, Encode, Decode, TypeInfo, MaxEncodedLen)] -#[cfg_attr(feature = "borsh", derive(BorshSerialize, BorshDeserialize))] -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -pub struct KeyPair( - #[cfg_attr( - feature = "borsh", - borsh( - serialize_with = "serai_primitives::borsh_serialize_public", - deserialize_with = "serai_primitives::borsh_deserialize_public" - ) - )] - pub Public, - #[cfg_attr( - feature = "borsh", - borsh( - serialize_with = "serai_primitives::borsh_serialize_bounded_vec", - deserialize_with = "serai_primitives::borsh_deserialize_bounded_vec" - ) - )] - pub ExternalKey, -); -#[cfg(feature = "std")] -impl Zeroize for KeyPair { - fn zeroize(&mut self) { - self.0 .0.zeroize(); - self.1.as_mut().zeroize(); - } -} - -/// The MuSig context for a validator set. -pub fn musig_context(set: ValidatorSet) -> [u8; 32] { - let mut context = [0; 32]; - const DST: &[u8] = b"ValidatorSets-musig_key"; - context[.. DST.len()].copy_from_slice(DST); - let set = set.encode(); - context[DST.len() .. (DST.len() + set.len())].copy_from_slice(&set); - context -} - -/// The MuSig public key for a validator set. -/// -/// This function panics on invalid input, per the definition of `dkg::musig::musig_key`. -pub fn musig_key(set: ValidatorSet, set_keys: &[Public]) -> Public { - let mut keys = Vec::new(); - for key in set_keys { - keys.push( - ::read_G::<&[u8]>(&mut key.0.as_ref()) - .expect("invalid participant"), - ); - } - Public(dkg_musig::musig_key_vartime::(musig_context(set), &keys).unwrap().to_bytes()) -} - -/// The message for the `set_keys` signature. -pub fn set_keys_message(set: &ExternalValidatorSet, key_pair: &KeyPair) -> Vec { - (b"ValidatorSets-set_keys", set, key_pair).encode() -} - -/// For a set of validators whose key shares may exceed the maximum, reduce until they equal the -/// maximum. -/// -/// Reduction occurs by reducing each validator in a reverse round-robin. -pub fn amortize_excess_key_shares(validators: &mut [(Public, u64)]) { - let total_key_shares = validators.iter().map(|(_, shares)| shares).sum::(); - for i in 0 .. usize::try_from(total_key_shares.saturating_sub(u64::from(MAX_KEY_SHARES_PER_SET))) - .unwrap() - { - validators[validators.len() - ((i % validators.len()) + 1)].1 -= 1; - } -} - -/// Returns the post-amortization key shares for the top validator. -/// -/// Panics when `validators == 0`. -pub fn post_amortization_key_shares_for_top_validator( - validators: usize, - top: u64, - key_shares: u64, -) -> u64 { - top - - (key_shares.saturating_sub(MAX_KEY_SHARES_PER_SET.into()) / - u64::try_from(validators).unwrap()) -}