diff --git a/Cargo.lock b/Cargo.lock index c50f9dbd..ca00eecf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -106,6 +106,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "array-init" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfb6d71005dc22a708c7496eee5c8dc0300ee47355de6256c3b35b12b5fef596" + [[package]] name = "arrayref" version = "0.3.6" @@ -3137,6 +3143,184 @@ dependencies = [ "regex", ] +[[package]] +name = "ink_allocator" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed249de74298ed051ebcf6d3082b8d3dbd19cbc448d9ed3235d8a7b92713049" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "ink_engine" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acb9d32ec27d71fefb3f2b6a26bae82a2c6509d7ad61e8a5107b6291a1b03ecb" +dependencies = [ + "blake2", + "derive_more", + "parity-scale-codec", + "rand 0.8.5", + "secp256k1 0.22.1", + "sha2 0.10.2", + "sha3 0.10.1", +] + +[[package]] +name = "ink_env" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1549f5966167387c89fb3dfcdc59973bfb396cc3a7110d7a31ad5fdea56db0cf" +dependencies = [ + "arrayref", + "blake2", + "cfg-if", + "derive_more", + "ink_allocator", + "ink_engine", + "ink_metadata", + "ink_prelude", + "ink_primitives", + "num-traits", + "parity-scale-codec", + "paste", + "rand 0.8.5", + "rlibc", + "scale-info", + "secp256k1 0.22.1", + "sha2 0.10.2", + "sha3 0.10.1", + "static_assertions", +] + +[[package]] +name = "ink_lang" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e5282f2722ac6dca469e7f223a7b38b2a6d20fbca6b974497e630d5dc8934e9" +dependencies = [ + "derive_more", + "ink_env", + "ink_lang_macro", + "ink_prelude", + "ink_primitives", + "ink_storage", + "parity-scale-codec", +] + +[[package]] +name = "ink_lang_codegen" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb3a5de33b59450adc3f61c5eb05b768067c7ab8af9d00f33e284310598168dc" +dependencies = [ + "blake2", + "derive_more", + "either", + "heck 0.4.0", + "impl-serde", + "ink_lang_ir", + "itertools", + "parity-scale-codec", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "ink_lang_ir" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d4d614462280fa06e15b9ca5725d7c8440dde93c8dae1c6f15422f7756cacb" +dependencies = [ + "blake2", + "either", + "itertools", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "ink_lang_macro" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f85f64141957c5db7cbabbb97a9c16c489e5e9d363e9f147d132a43c71cd29" +dependencies = [ + "ink_lang_codegen", + "ink_lang_ir", + "ink_primitives", + "parity-scale-codec", + "proc-macro2", + "syn", +] + +[[package]] +name = "ink_metadata" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dca6c159a2774f07437c6fd9ea710eb73a6b5e9a031a932bddf08742bf2c081a" +dependencies = [ + "derive_more", + "impl-serde", + "ink_prelude", + "ink_primitives", + "scale-info", + "serde", +] + +[[package]] +name = "ink_prelude" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f7f4dec15e573496c9d2af353e78bde84add391251608f25b5adcf175dc777" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "ink_primitives" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3296dd1c4f4fe12ede7c92d60e6fcb94d46a959ec19c701e4ac588b09e0b4a6" +dependencies = [ + "cfg-if", + "ink_prelude", + "parity-scale-codec", + "scale-info", +] + +[[package]] +name = "ink_storage" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ff9b503995a7b41fe201a7a2643ce22f5a11e0b67db7b685424b6d5fe0ecf0b" +dependencies = [ + "array-init", + "cfg-if", + "derive_more", + "ink_env", + "ink_metadata", + "ink_prelude", + "ink_primitives", + "ink_storage_derive", + "parity-scale-codec", + "scale-info", +] + +[[package]] +name = "ink_storage_derive" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afb68e24e93e8327dda1924868d7ee4dbe01e1ed2b392f28583caa96809b585c" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + [[package]] name = "instant" version = "0.1.12" @@ -4529,6 +4713,19 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" +[[package]] +name = "multisig-serai" +version = "0.1.0" +dependencies = [ + "ink_env", + "ink_lang", + "ink_metadata", + "ink_primitives", + "ink_storage", + "parity-scale-codec", + "scale-info", +] + [[package]] name = "multistream-select" version = "0.11.0" @@ -6092,6 +6289,12 @@ dependencies = [ "digest 0.10.3", ] +[[package]] +name = "rlibc" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc874b127765f014d792f16763a81245ab80500e2ad921ed4ee9e82481ee08fe" + [[package]] name = "rlp" version = "0.5.1" @@ -7192,7 +7395,16 @@ version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c42e6f1735c5f00f51e43e28d6634141f2bcad10931b2609ddd74a86d751260" dependencies = [ - "secp256k1-sys", + "secp256k1-sys 0.4.2", +] + +[[package]] +name = "secp256k1" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26947345339603ae8395f68e2f3d85a6b0a8ddfe6315818e80b8504415099db0" +dependencies = [ + "secp256k1-sys 0.5.2", ] [[package]] @@ -7204,6 +7416,15 @@ dependencies = [ "cc", ] +[[package]] +name = "secp256k1-sys" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "152e20a0fd0519390fc43ab404663af8a0b794273d2a91d60ad4a39f13ffe110" +dependencies = [ + "cc", +] + [[package]] name = "secrecy" version = "0.8.0" @@ -7842,7 +8063,7 @@ dependencies = [ "regex", "scale-info", "schnorrkel", - "secp256k1", + "secp256k1 0.21.3", "secrecy", "serde", "sp-core-hashing", @@ -7957,7 +8178,7 @@ dependencies = [ "log", "parity-scale-codec", "parking_lot 0.12.1", - "secp256k1", + "secp256k1 0.21.3", "sp-core", "sp-externalities", "sp-keystore", @@ -8761,6 +8982,19 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" +[[package]] +name = "token-serai" +version = "0.1.0" +dependencies = [ + "ink_env", + "ink_lang", + "ink_metadata", + "ink_primitives", + "ink_storage", + "parity-scale-codec", + "scale-info", +] + [[package]] name = "tokio" version = "1.20.0" diff --git a/Cargo.toml b/Cargo.toml index d6d2877c..52fb9cfb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,9 @@ members = [ "substrate/runtime", "substrate/consensus", - "substrate/node" + "substrate/node", + + "contracts/multisig", ] [profile.release] diff --git a/contracts/multisig/Cargo.toml b/contracts/multisig/Cargo.toml new file mode 100644 index 00000000..488a045d --- /dev/null +++ b/contracts/multisig/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "multisig-serai" +version = "0.1.0" +description = "An ink! tracker for Serai's current multisig" +license = "AGPL-3.0-only" +authors = ["Luke Parker "] +edition = "2021" + +[dependencies] +ink_primitives = { version = "3", default-features = false } +ink_metadata = { version = "3", default-features = false, features = ["derive"], optional = true } +ink_env = { version = "3", default-features = false } +ink_storage = { version = "3", default-features = false } +ink_lang = { version = "3", default-features = false } + +scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] } +scale-info = { version = "2", default-features = false, features = ["derive"], optional = true } + +[lib] +name = "multisig" +path = "lib.rs" +crate-type = [ + # Used for normal contract Wasm blobs. + "cdylib", +] + +[features] +default = ["std"] +std = [ + "ink_metadata/std", + "ink_env/std", + "ink_storage/std", + "ink_primitives/std", + "scale/std", + "scale-info/std", +] +ink-as-dependency = [] diff --git a/contracts/multisig/lib.rs b/contracts/multisig/lib.rs new file mode 100644 index 00000000..f0da67f6 --- /dev/null +++ b/contracts/multisig/lib.rs @@ -0,0 +1,184 @@ +#![cfg_attr(not(feature = "std"), no_std)] + +use ink_lang as ink; +use ink_env::{Environment, DefaultEnvironment, AccountId}; + +#[ink::chain_extension] +pub trait ValidatorsExtension { + type ErrorCode = (); + + /// Returns the amount of active validators on the current chain. + #[ink(extension = 0, handle_status = false, returns_result = false)] + fn active_validators_len() -> u16; + + /// Returns the ID for the current validator set for the current chain. + // TODO: Decide if this should be an increasing unsigned integer instead of a hash. + #[ink(extension = 1, handle_status = false, returns_result = false)] + fn validator_set_id() -> [u8; 32]; + + /// Returns if the specified account is an active validator for the current chain. + #[ink(extension = 2, handle_status = false, returns_result = false)] + fn is_active_validator(account: &AccountId) -> bool; +} + +pub struct ValidatorEnvironment; +impl Environment for ValidatorEnvironment { + const MAX_EVENT_TOPICS: usize = ::MAX_EVENT_TOPICS; + + type AccountId = ::AccountId; + type Balance = ::Balance; + type Hash = ::Hash; + type BlockNumber = ::BlockNumber; + type Timestamp = ::Timestamp; + + type ChainExtension = ValidatorsExtension; +} + +#[ink::contract(env = crate::ValidatorEnvironment)] +mod multisig { + use scale::Encode; + + use ink_storage::{traits::SpreadAllocate, Mapping}; + use ink_env::{hash::Blake2x256, hash_encoded}; + + /// A contract which tracks the current multisig keys. + #[ink(storage)] + #[derive(SpreadAllocate)] + pub struct Multisig { + /// Validator set currently holding the multisig. + validator_set: [u8; 32], + /// Mapping from a curve's index to the multisig's current public key for it. + // This is a mapping due to ink's eager loading. Considering we're right now only considering + // secp256k1 and Ed25519, it may be notably more efficient to use a Vec here. + keys: Mapping>, + /// Voter + Keys -> Voted already or not + voted: Mapping<(AccountId, [u8; 32]), ()>, + /// Validator Set + Keys -> Vote Count + votes: Mapping<([u8; 32], [u8; 32]), u16>, + } + + /// Event emitted when a new set of multisig keys is voted on. Only for the first vote on a set + // of keys will they be present in this event. + #[ink(event)] + pub struct Vote { + /// Validator who issued the vote. + #[ink(topic)] + validator: AccountId, + /// Validator set for which keys are being generated. + #[ink(topic)] + validator_set: [u8; 32], + /// Hash of the keys voted on. + #[ink(topic)] + hash: [u8; 32], + /// Keys voted on. + keys: Vec>, + } + + /// Event emitted when the new keys are fully generated for all curves, having been fully voted + /// on. + #[ink(event)] + pub struct KeyGen { + #[ink(topic)] + hash: [u8; 32], + } + + /// The Multisig error types. + #[derive(Debug, PartialEq, Eq, scale::Encode, scale::Decode)] + #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] + pub enum Error { + /// Returned if a curve index doesn't have a key registered for it. + NonExistentCurve, + /// Returned if a non-validator is voting. + NotValidator, + /// Returned if this validator set already generated keys. + AlreadyGeneratedKeys, + /// Returned if this validator has already voted for these keys. + AlreadyVoted, + } + + /// The Multisig result type. + pub type Result = core::result::Result; + + impl Multisig { + /// Deploys the Multisig contract. + #[ink(constructor)] + pub fn new() -> Self { + ink_lang::utils::initialize_contract(|_| {}) + } + + /// Validator set currently holding the multisig. + #[ink(message)] + pub fn validator_set(&self) -> [u8; 32] { + self.validator_set + } + + /// Returns the key currently in-use for a given curve ID. This is then bound to a given chain + /// by applying a personalized additive offset, as done by the processor. Each chain then has + /// its own way of receiving funds to these keys, leaving this not for usage by wallets, nor + /// the processor which is expected to track events for this information. This is really solely + /// for debugging purposes. + #[ink(message)] + pub fn key(&self, curve: u8) -> Result> { + self.keys.get(curve).ok_or(Error::NonExistentCurve) + } + + // TODO: voted + // TODO: votes + + fn hash(value: &T) -> [u8; 32] { + let mut output = [0; 32]; + hash_encoded::(value, &mut output); + output + } + + /// Vote for a given set of keys. + #[ink(message)] + pub fn vote(&mut self, keys: Vec>) -> Result<()> { + if keys.len() > 256 { + Err(Error::NonExistentCurve)?; + } + + let validator = self.env().caller(); + if !self.env().extension().is_active_validator(&validator) { + Err(Error::NotValidator)?; + } + + let validator_set = self.env().extension().validator_set_id(); + if self.validator_set == validator_set { + Err(Error::AlreadyGeneratedKeys)?; + } + + let keys_hash = Self::hash(&keys); + if self.voted.get((validator, keys_hash)).is_some() { + Err(Error::AlreadyVoted)?; + } + self.voted.insert((validator, keys_hash), &()); + + let votes = if let Some(votes) = self.votes.get((validator_set, keys_hash)) { + self.env().emit_event(Vote { validator, validator_set, hash: keys_hash, keys: vec![] }); + votes + 1 + } else { + self.env().emit_event(Vote { + validator, + validator_set, + hash: keys_hash, + keys: keys.clone(), + }); + 1 + }; + // We could skip writing this if we've reached consensus, yet best to keep our ducks in a row + self.votes.insert((validator_set, keys_hash), &votes); + + // If we've reached consensus, action this. + if votes == self.env().extension().active_validators_len() { + self.validator_set = validator_set; + for (k, key) in keys.iter().enumerate() { + self.keys.insert(u8::try_from(k).unwrap(), key); + } + self.env().emit_event(KeyGen { hash: keys_hash }); + } + + Ok(()) + } + } +} diff --git a/docs/protocol/Multisig.md b/docs/protocol/Multisig.md new file mode 100644 index 00000000..92562922 --- /dev/null +++ b/docs/protocol/Multisig.md @@ -0,0 +1,27 @@ +# Multisig + +The multisig is represented on chain by the `Multisig` contract. + +### `vote(keys: Vec>)` + +Lets a validator vote on a set of keys. Once all validators have voted on these +keys, it becomes the tracked set of keys for incoming funds. + +The old keys are eligible to still receive transactions for a provided grace +period. This means nodes are expected to track and oraclize incoming +transactions for both sets of keys. At the end of the grace period, the old keys +are dropped from consideration, and all funds are forwarded to the new keys at +the next transaction interval for a given chain. + +The old keys are expected to process outbounds until they forward their funds, +at which point the new keys are expected to process outbounds. + +Unlike transactions in, which is confirmed as part of the BFT process, a 100% +vote is used here. While the BFT process would confirm that keys were generated +and enough nodes acknowledge them the wallet would be spendable from, it does +not confirm fault tolerance. If the other 33% of nodes failed to receive their +key shares somehow, the multisig which is intended to be t-of-n would instead be +t-of-t. + +Accordingly, validators are allowed to vote multiple times, and the first key +set to receive the necessary votes becomes the new key set.