Initial multisig tracking contract in ink

This commit is contained in:
Luke Parker 2022-07-17 17:17:23 -04:00
parent be921ab2d3
commit 5583bf3447
5 changed files with 488 additions and 4 deletions

240
Cargo.lock generated
View file

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

View file

@ -15,7 +15,9 @@ members = [
"substrate/runtime",
"substrate/consensus",
"substrate/node"
"substrate/node",
"contracts/multisig",
]
[profile.release]

View file

@ -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 <lukeparker5132@gmail.com>"]
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 = []

184
contracts/multisig/lib.rs Normal file
View file

@ -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 = <DefaultEnvironment as Environment>::MAX_EVENT_TOPICS;
type AccountId = <DefaultEnvironment as Environment>::AccountId;
type Balance = <DefaultEnvironment as Environment>::Balance;
type Hash = <DefaultEnvironment as Environment>::Hash;
type BlockNumber = <DefaultEnvironment as Environment>::BlockNumber;
type Timestamp = <DefaultEnvironment as Environment>::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<u8, Vec<u8>>,
/// 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<Vec<u8>>,
}
/// 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<T> = core::result::Result<T, Error>;
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<Vec<u8>> {
self.keys.get(curve).ok_or(Error::NonExistentCurve)
}
// TODO: voted
// TODO: votes
fn hash<T: Encode>(value: &T) -> [u8; 32] {
let mut output = [0; 32];
hash_encoded::<Blake2x256, _>(value, &mut output);
output
}
/// Vote for a given set of keys.
#[ink(message)]
pub fn vote(&mut self, keys: Vec<Vec<u8>>) -> 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(())
}
}
}

27
docs/protocol/Multisig.md Normal file
View file

@ -0,0 +1,27 @@
# Multisig
The multisig is represented on chain by the `Multisig` contract.
### `vote(keys: Vec<Vec<u8>>)`
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.