mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-08 20:29:23 +00:00
Use a MuSig signature to publish validator set key pairs to Serai
The processor/coordinator flow still has to be rewritten.
This commit is contained in:
7
Cargo.lock
generated
7
Cargo.lock
generated
@@ -8754,7 +8754,9 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bitcoin",
|
"bitcoin",
|
||||||
"ciphersuite",
|
"ciphersuite",
|
||||||
|
"frost-schnorrkel",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
|
"modular-frost",
|
||||||
"monero-serai",
|
"monero-serai",
|
||||||
"parity-scale-codec",
|
"parity-scale-codec",
|
||||||
"rand_core 0.6.4",
|
"rand_core 0.6.4",
|
||||||
@@ -8764,6 +8766,7 @@ dependencies = [
|
|||||||
"subxt",
|
"subxt",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -10995,11 +10998,15 @@ dependencies = [
|
|||||||
name = "validator-sets-pallet"
|
name = "validator-sets-pallet"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"ciphersuite",
|
||||||
|
"dkg",
|
||||||
"frame-support",
|
"frame-support",
|
||||||
"frame-system",
|
"frame-system",
|
||||||
|
"hashbrown 0.13.2",
|
||||||
"parity-scale-codec",
|
"parity-scale-codec",
|
||||||
"scale-info",
|
"scale-info",
|
||||||
"serai-primitives",
|
"serai-primitives",
|
||||||
|
"sp-application-crypto",
|
||||||
"sp-core",
|
"sp-core",
|
||||||
"validator-sets-primitives",
|
"validator-sets-primitives",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ all-features = true
|
|||||||
rustdoc-args = ["--cfg", "docsrs"]
|
rustdoc-args = ["--cfg", "docsrs"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
zeroize = "^1.5"
|
||||||
thiserror = { version = "1", optional = true }
|
thiserror = { version = "1", optional = true }
|
||||||
|
|
||||||
scale = { package = "parity-scale-codec", version = "3" }
|
scale = { package = "parity-scale-codec", version = "3" }
|
||||||
@@ -28,6 +29,17 @@ bitcoin = { version = "0.30", optional = true }
|
|||||||
ciphersuite = { path = "../../crypto/ciphersuite", version = "0.3", optional = true }
|
ciphersuite = { path = "../../crypto/ciphersuite", version = "0.3", optional = true }
|
||||||
monero-serai = { path = "../../coins/monero", version = "0.1.4-alpha", optional = true }
|
monero-serai = { path = "../../coins/monero", version = "0.1.4-alpha", optional = true }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
lazy_static = "1"
|
||||||
|
|
||||||
|
rand_core = "0.6"
|
||||||
|
|
||||||
|
ciphersuite = { path = "../../crypto/ciphersuite", features = ["ristretto"] }
|
||||||
|
frost = { package = "modular-frost", path = "../../crypto/frost", features = ["tests"] }
|
||||||
|
schnorrkel = { path = "../../crypto/schnorrkel", package = "frost-schnorrkel" }
|
||||||
|
|
||||||
|
tokio = "1"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
serai = ["thiserror", "scale-info", "subxt"]
|
serai = ["thiserror", "scale-info", "subxt"]
|
||||||
|
|
||||||
@@ -38,10 +50,3 @@ monero = ["coins", "ciphersuite/ed25519", "monero-serai"]
|
|||||||
# Assumes the default usage is to use Serai as a DEX, which doesn't actually
|
# Assumes the default usage is to use Serai as a DEX, which doesn't actually
|
||||||
# require connecting to a Serai node
|
# require connecting to a Serai node
|
||||||
default = ["bitcoin", "monero"]
|
default = ["bitcoin", "monero"]
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
lazy_static = "1"
|
|
||||||
|
|
||||||
rand_core = "0.6"
|
|
||||||
|
|
||||||
tokio = "1"
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ impl Serai {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn execute_batch(&self, batch: SignedBatch) -> Encoded {
|
pub fn execute_batch(batch: SignedBatch) -> Encoded {
|
||||||
self.unsigned::<InInstructions, _>(&in_instructions::Call::<Runtime>::execute_batch { batch })
|
Self::unsigned::<InInstructions, _>(&in_instructions::Call::<Runtime>::execute_batch { batch })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -269,7 +269,7 @@ impl Serai {
|
|||||||
.map_err(SeraiError::RpcError)
|
.map_err(SeraiError::RpcError)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn unsigned<P: 'static, C: Encode>(&self, call: &C) -> Encoded {
|
fn unsigned<P: 'static, C: Encode>(call: &C) -> Encoded {
|
||||||
// TODO: Should Serai purge the old transaction code AND set this to 0/1?
|
// TODO: Should Serai purge the old transaction code AND set this to 0/1?
|
||||||
const TRANSACTION_VERSION: u8 = 4;
|
const TRANSACTION_VERSION: u8 = 4;
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
|
use sp_core::sr25519::Signature;
|
||||||
|
|
||||||
use serai_runtime::{validator_sets, ValidatorSets, Runtime};
|
use serai_runtime::{validator_sets, ValidatorSets, Runtime};
|
||||||
pub use validator_sets::primitives;
|
pub use validator_sets::primitives;
|
||||||
use primitives::{ValidatorSet, ValidatorSetData, KeyPair};
|
use primitives::{ValidatorSet, ValidatorSetData, KeyPair};
|
||||||
|
|
||||||
use subxt::tx::Payload;
|
use subxt::utils::Encoded;
|
||||||
|
|
||||||
use crate::{primitives::NetworkId, Serai, SeraiError, Composite, scale_value, scale_composite};
|
use crate::{primitives::NetworkId, Serai, SeraiError, scale_value};
|
||||||
|
|
||||||
const PALLET: &str = "ValidatorSets";
|
const PALLET: &str = "ValidatorSets";
|
||||||
|
|
||||||
@@ -20,15 +22,6 @@ impl Serai {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_vote_events(
|
|
||||||
&self,
|
|
||||||
block: [u8; 32],
|
|
||||||
) -> Result<Vec<ValidatorSetsEvent>, SeraiError> {
|
|
||||||
self
|
|
||||||
.events::<ValidatorSets, _>(block, |event| matches!(event, ValidatorSetsEvent::Vote { .. }))
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_key_gen_events(
|
pub async fn get_key_gen_events(
|
||||||
&self,
|
&self,
|
||||||
block: [u8; 32],
|
block: [u8; 32],
|
||||||
@@ -52,17 +45,35 @@ impl Serai {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_validator_set_musig_key(
|
||||||
|
&self,
|
||||||
|
set: ValidatorSet,
|
||||||
|
) -> Result<Option<[u8; 32]>, SeraiError> {
|
||||||
|
self
|
||||||
|
.storage(
|
||||||
|
PALLET,
|
||||||
|
"MuSigKeys",
|
||||||
|
Some(vec![scale_value(set)]),
|
||||||
|
self.get_latest_block_hash().await?,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn get_keys(&self, set: ValidatorSet) -> Result<Option<KeyPair>, SeraiError> {
|
pub async fn get_keys(&self, set: ValidatorSet) -> Result<Option<KeyPair>, SeraiError> {
|
||||||
self
|
self
|
||||||
.storage(PALLET, "Keys", Some(vec![scale_value(set)]), self.get_latest_block_hash().await?)
|
.storage(PALLET, "Keys", Some(vec![scale_value(set)]), self.get_latest_block_hash().await?)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn vote(network: NetworkId, key_pair: KeyPair) -> Payload<Composite<()>> {
|
pub fn set_validator_set_keys(
|
||||||
Payload::new(
|
network: NetworkId,
|
||||||
PALLET,
|
key_pair: KeyPair,
|
||||||
"vote",
|
signature: Signature,
|
||||||
scale_composite(validator_sets::Call::<Runtime>::vote { network, key_pair }),
|
) -> Encoded {
|
||||||
)
|
Self::unsigned::<ValidatorSets, _>(&validator_sets::Call::<Runtime>::set_keys {
|
||||||
|
network,
|
||||||
|
key_pair,
|
||||||
|
signature,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,9 +9,10 @@ use serai_client::{
|
|||||||
primitives::{Batch, SignedBatch},
|
primitives::{Batch, SignedBatch},
|
||||||
InInstructionsEvent,
|
InInstructionsEvent,
|
||||||
},
|
},
|
||||||
|
Serai,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::common::{serai, tx::publish_tx, validator_sets::vote_in_keys};
|
use crate::common::{serai, tx::publish_tx, validator_sets::set_validator_set_keys};
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub async fn provide_batch(batch: Batch) -> [u8; 32] {
|
pub async fn provide_batch(batch: Batch) -> [u8; 32] {
|
||||||
@@ -24,15 +25,15 @@ pub async fn provide_batch(batch: Batch) -> [u8; 32] {
|
|||||||
keys
|
keys
|
||||||
} else {
|
} else {
|
||||||
let keys = (pair.public(), vec![].try_into().unwrap());
|
let keys = (pair.public(), vec![].try_into().unwrap());
|
||||||
vote_in_keys(set, keys.clone()).await;
|
set_validator_set_keys(set, keys.clone()).await;
|
||||||
keys
|
keys
|
||||||
};
|
};
|
||||||
assert_eq!(keys.0, pair.public());
|
assert_eq!(keys.0, pair.public());
|
||||||
|
|
||||||
let block = publish_tx(
|
let block = publish_tx(&Serai::execute_batch(SignedBatch {
|
||||||
&serai
|
batch: batch.clone(),
|
||||||
.execute_batch(SignedBatch { batch: batch.clone(), signature: pair.sign(&batch.encode()) }),
|
signature: pair.sign(&batch.encode()),
|
||||||
)
|
}))
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let batches = serai.get_batch_events(block).await.unwrap();
|
let batches = serai.get_batch_events(block).await.unwrap();
|
||||||
|
|||||||
@@ -1,42 +1,68 @@
|
|||||||
use sp_core::Pair;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use zeroize::Zeroizing;
|
||||||
|
use rand_core::OsRng;
|
||||||
|
|
||||||
|
use scale::Encode;
|
||||||
|
|
||||||
|
use sp_core::{Pair, sr25519::Signature};
|
||||||
|
|
||||||
|
use ciphersuite::{group::GroupEncoding, Ciphersuite, Ristretto};
|
||||||
|
use frost::dkg::musig::*;
|
||||||
|
use schnorrkel::Schnorrkel;
|
||||||
|
|
||||||
use serai_client::{
|
use serai_client::{
|
||||||
subxt::config::extrinsic_params::BaseExtrinsicParamsBuilder,
|
primitives::insecure_pair_from_name,
|
||||||
primitives::{SeraiAddress, insecure_pair_from_name},
|
|
||||||
validator_sets::{
|
validator_sets::{
|
||||||
primitives::{ValidatorSet, KeyPair},
|
primitives::{ValidatorSet, KeyPair},
|
||||||
ValidatorSetsEvent,
|
ValidatorSetsEvent,
|
||||||
},
|
},
|
||||||
PairSigner, Serai,
|
Serai,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::common::{serai, tx::publish_tx};
|
use crate::common::{serai, tx::publish_tx};
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub async fn vote_in_keys(set: ValidatorSet, key_pair: KeyPair) -> [u8; 32] {
|
pub async fn set_validator_set_keys(set: ValidatorSet, key_pair: KeyPair) -> [u8; 32] {
|
||||||
let pair = insecure_pair_from_name("Alice");
|
let pair = insecure_pair_from_name("Alice");
|
||||||
let public = pair.public();
|
let public = pair.public();
|
||||||
|
|
||||||
let serai = serai().await;
|
let serai = serai().await;
|
||||||
|
let public_key = <Ristretto as Ciphersuite>::read_G::<&[u8]>(&mut public.0.as_ref()).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
serai.get_validator_set_musig_key(set).await.unwrap().unwrap(),
|
||||||
|
musig_key::<Ristretto>(&[public_key]).unwrap().to_bytes()
|
||||||
|
);
|
||||||
|
|
||||||
|
let secret_key = <Ristretto as Ciphersuite>::read_F::<&[u8]>(
|
||||||
|
&mut pair.as_ref().secret.to_bytes()[.. 32].as_ref(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(Ristretto::generator() * secret_key, public_key);
|
||||||
|
let threshold_keys = musig::<Ristretto>(&Zeroizing::new(secret_key), &[public_key]).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
serai.get_validator_set_musig_key(set).await.unwrap().unwrap(),
|
||||||
|
threshold_keys.group_key().to_bytes()
|
||||||
|
);
|
||||||
|
|
||||||
|
let sig = frost::tests::sign_without_caching(
|
||||||
|
&mut OsRng,
|
||||||
|
frost::tests::algorithm_machines(
|
||||||
|
&mut OsRng,
|
||||||
|
Schnorrkel::new(b"substrate"),
|
||||||
|
&HashMap::from([(threshold_keys.params().i(), threshold_keys.into())]),
|
||||||
|
),
|
||||||
|
&key_pair.encode(),
|
||||||
|
);
|
||||||
|
|
||||||
// Vote in a key pair
|
// Vote in a key pair
|
||||||
let address = SeraiAddress::from(pair.public());
|
let block = publish_tx(&Serai::set_validator_set_keys(
|
||||||
let block = publish_tx(
|
set.network,
|
||||||
&serai
|
key_pair.clone(),
|
||||||
.sign(
|
Signature(sig.to_bytes()),
|
||||||
&PairSigner::new(pair),
|
))
|
||||||
&Serai::vote(set.network, key_pair.clone()),
|
|
||||||
serai.get_nonce(&address).await.unwrap(),
|
|
||||||
BaseExtrinsicParamsBuilder::new(),
|
|
||||||
)
|
|
||||||
.unwrap(),
|
|
||||||
)
|
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
serai.get_vote_events(block).await.unwrap(),
|
|
||||||
vec![ValidatorSetsEvent::Vote { voter: public, set, key_pair: key_pair.clone(), votes: 1 }]
|
|
||||||
);
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
serai.get_key_gen_events(block).await.unwrap(),
|
serai.get_key_gen_events(block).await.unwrap(),
|
||||||
vec![ValidatorSetsEvent::KeyGen { set, key_pair: key_pair.clone() }]
|
vec![ValidatorSetsEvent::KeyGen { set, key_pair: key_pair.clone() }]
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ use rand_core::{RngCore, OsRng};
|
|||||||
|
|
||||||
use sp_core::{sr25519::Public, Pair};
|
use sp_core::{sr25519::Public, Pair};
|
||||||
|
|
||||||
|
use ciphersuite::{group::GroupEncoding, Ciphersuite, Ristretto};
|
||||||
|
use frost::dkg::musig::musig_key;
|
||||||
|
|
||||||
use serai_client::{
|
use serai_client::{
|
||||||
primitives::{NETWORKS, NetworkId, insecure_pair_from_name},
|
primitives::{NETWORKS, NetworkId, insecure_pair_from_name},
|
||||||
validator_sets::{
|
validator_sets::{
|
||||||
@@ -12,10 +15,10 @@ use serai_client::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
mod common;
|
mod common;
|
||||||
use common::{serai, validator_sets::vote_in_keys};
|
use common::{serai, validator_sets::set_validator_set_keys};
|
||||||
|
|
||||||
serai_test!(
|
serai_test!(
|
||||||
async fn vote_keys() {
|
async fn set_validator_set_keys_test() {
|
||||||
let network = NetworkId::Bitcoin;
|
let network = NetworkId::Bitcoin;
|
||||||
let set = ValidatorSet { session: Session(0), network };
|
let set = ValidatorSet { session: Session(0), network };
|
||||||
|
|
||||||
@@ -51,14 +54,20 @@ serai_test!(
|
|||||||
assert_eq!(set_data.network, NETWORKS[&NetworkId::Bitcoin]);
|
assert_eq!(set_data.network, NETWORKS[&NetworkId::Bitcoin]);
|
||||||
let participants_ref: &[_] = set_data.participants.as_ref();
|
let participants_ref: &[_] = set_data.participants.as_ref();
|
||||||
assert_eq!(participants_ref, [(public, set_data.bond)].as_ref());
|
assert_eq!(participants_ref, [(public, set_data.bond)].as_ref());
|
||||||
|
|
||||||
let block = vote_in_keys(set, key_pair.clone()).await;
|
|
||||||
|
|
||||||
// While the vote_in_keys function should handle this, it's beneficial to independently test it
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
serai.get_vote_events(block).await.unwrap(),
|
serai.get_validator_set_musig_key(set).await.unwrap().unwrap(),
|
||||||
vec![ValidatorSetsEvent::Vote { voter: public, set, key_pair: key_pair.clone(), votes: 1 }]
|
musig_key::<Ristretto>(&[<Ristretto as Ciphersuite>::read_G::<&[u8]>(
|
||||||
|
&mut public.0.as_ref()
|
||||||
|
)
|
||||||
|
.unwrap()])
|
||||||
|
.unwrap()
|
||||||
|
.to_bytes()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let block = set_validator_set_keys(set, key_pair.clone()).await;
|
||||||
|
|
||||||
|
// While the set_validator_set_keys function should handle this, it's beneficial to
|
||||||
|
// independently test it
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
serai.get_key_gen_events(block).await.unwrap(),
|
serai.get_key_gen_events(block).await.unwrap(),
|
||||||
vec![ValidatorSetsEvent::KeyGen { set, key_pair: key_pair.clone() }]
|
vec![ValidatorSetsEvent::KeyGen { set, key_pair: key_pair.clone() }]
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ pub mod pallet {
|
|||||||
// Match to be exhaustive
|
// Match to be exhaustive
|
||||||
let batch = match call {
|
let batch = match call {
|
||||||
Call::execute_batch { ref batch } => batch,
|
Call::execute_batch { ref batch } => batch,
|
||||||
_ => Err(InvalidTransaction::Call)?,
|
Call::__Ignore(_, _) => unreachable!(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let network = batch.batch.network;
|
let network = batch.batch.network;
|
||||||
|
|||||||
@@ -174,7 +174,7 @@ impl Contains<RuntimeCall> for CallFilter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let RuntimeCall::ValidatorSets(call) = call {
|
if let RuntimeCall::ValidatorSets(call) = call {
|
||||||
return matches!(call, validator_sets::Call::vote { .. });
|
return matches!(call, validator_sets::Call::set_keys { .. });
|
||||||
}
|
}
|
||||||
|
|
||||||
false
|
false
|
||||||
|
|||||||
@@ -12,19 +12,28 @@ all-features = true
|
|||||||
rustdoc-args = ["--cfg", "docsrs"]
|
rustdoc-args = ["--cfg", "docsrs"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
hashbrown = { version = "0.13", default-features = false }
|
||||||
|
|
||||||
|
ciphersuite = { version = "0.3", path = "../../../crypto/ciphersuite", default-features = false, features = ["ristretto"] }
|
||||||
|
dkg = { version = "0.4", path = "../../../crypto/dkg", default-features = false }
|
||||||
|
|
||||||
scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] }
|
scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] }
|
||||||
scale-info = { version = "2", default-features = false, features = ["derive"] }
|
scale-info = { version = "2", default-features = false, features = ["derive"] }
|
||||||
|
|
||||||
sp-core = { git = "https://github.com/serai-dex/substrate", default-features = false }
|
sp-core = { git = "https://github.com/serai-dex/substrate", default-features = false }
|
||||||
|
sp-application-crypto = { git = "https://github.com/serai-dex/substrate", default-features = false }
|
||||||
|
|
||||||
frame-system = { git = "https://github.com/serai-dex/substrate", default-features = false }
|
frame-system = { git = "https://github.com/serai-dex/substrate", default-features = false }
|
||||||
frame-support = { git = "https://github.com/serai-dex/substrate", default-features = false }
|
frame-support = { git = "https://github.com/serai-dex/substrate", default-features = false }
|
||||||
|
|
||||||
serai-primitives = { path = "../..//primitives", default-features = false }
|
serai-primitives = { path = "../../primitives", default-features = false }
|
||||||
validator-sets-primitives = { path = "../primitives", default-features = false }
|
validator-sets-primitives = { path = "../primitives", default-features = false }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
std = [
|
std = [
|
||||||
|
"ciphersuite/std",
|
||||||
|
"dkg/std",
|
||||||
|
|
||||||
"scale/std",
|
"scale/std",
|
||||||
"scale-info/std",
|
"scale-info/std",
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,9 @@
|
|||||||
pub mod pallet {
|
pub mod pallet {
|
||||||
use scale_info::TypeInfo;
|
use scale_info::TypeInfo;
|
||||||
|
|
||||||
|
use sp_core::sr25519::{Public, Signature};
|
||||||
|
use sp_application_crypto::RuntimePublic;
|
||||||
|
|
||||||
use frame_system::pallet_prelude::*;
|
use frame_system::pallet_prelude::*;
|
||||||
use frame_support::pallet_prelude::*;
|
use frame_support::pallet_prelude::*;
|
||||||
|
|
||||||
@@ -13,7 +16,7 @@ pub mod pallet {
|
|||||||
use primitives::*;
|
use primitives::*;
|
||||||
|
|
||||||
#[pallet::config]
|
#[pallet::config]
|
||||||
pub trait Config: frame_system::Config<AccountId = sp_core::sr25519::Public> + TypeInfo {
|
pub trait Config: frame_system::Config<AccountId = Public> + TypeInfo {
|
||||||
type RuntimeEvent: IsType<<Self as frame_system::Config>::RuntimeEvent> + From<Event<Self>>;
|
type RuntimeEvent: IsType<<Self as frame_system::Config>::RuntimeEvent> + From<Event<Self>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,59 +49,57 @@ pub mod pallet {
|
|||||||
pub type ValidatorSets<T: Config> =
|
pub type ValidatorSets<T: Config> =
|
||||||
StorageMap<_, Twox64Concat, ValidatorSet, ValidatorSetData, OptionQuery>;
|
StorageMap<_, Twox64Concat, ValidatorSet, ValidatorSetData, OptionQuery>;
|
||||||
|
|
||||||
|
/// The MuSig key for a validator set.
|
||||||
|
#[pallet::storage]
|
||||||
|
#[pallet::getter(fn musig_key)]
|
||||||
|
pub type MuSigKeys<T: Config> = StorageMap<_, Twox64Concat, ValidatorSet, Public, OptionQuery>;
|
||||||
|
|
||||||
/// The key pair for a given validator set instance.
|
/// The key pair for a given validator set instance.
|
||||||
#[pallet::storage]
|
#[pallet::storage]
|
||||||
#[pallet::getter(fn keys)]
|
#[pallet::getter(fn keys)]
|
||||||
pub type Keys<T: Config> = StorageMap<_, Twox64Concat, ValidatorSet, KeyPair, OptionQuery>;
|
pub type Keys<T: Config> = StorageMap<_, Twox64Concat, ValidatorSet, KeyPair, OptionQuery>;
|
||||||
|
|
||||||
/// If an account has voted for a specific key pair or not.
|
|
||||||
// This prevents a validator from voting multiple times.
|
|
||||||
#[pallet::storage]
|
|
||||||
#[pallet::getter(fn voted)]
|
|
||||||
pub type Voted<T: Config> =
|
|
||||||
StorageMap<_, Blake2_128Concat, (T::AccountId, KeyPair), (), OptionQuery>;
|
|
||||||
|
|
||||||
/// How many times a key pair has been voted for. Once consensus is reached, the keys will be
|
|
||||||
/// adopted.
|
|
||||||
#[pallet::storage]
|
|
||||||
#[pallet::getter(fn vote_count)]
|
|
||||||
pub type VoteCount<T: Config> =
|
|
||||||
StorageMap<_, Blake2_128Concat, (ValidatorSet, KeyPair), u16, ValueQuery>;
|
|
||||||
|
|
||||||
#[pallet::event]
|
#[pallet::event]
|
||||||
#[pallet::generate_deposit(pub(super) fn deposit_event)]
|
#[pallet::generate_deposit(pub(super) fn deposit_event)]
|
||||||
pub enum Event<T: Config> {
|
pub enum Event<T: Config> {
|
||||||
NewSet {
|
NewSet { set: ValidatorSet },
|
||||||
set: ValidatorSet,
|
KeyGen { set: ValidatorSet, key_pair: KeyPair },
|
||||||
},
|
|
||||||
Vote {
|
|
||||||
voter: T::AccountId,
|
|
||||||
set: ValidatorSet,
|
|
||||||
key_pair: KeyPair,
|
|
||||||
// Amount of votes the key now has
|
|
||||||
votes: u16,
|
|
||||||
},
|
|
||||||
KeyGen {
|
|
||||||
set: ValidatorSet,
|
|
||||||
key_pair: KeyPair,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pallet::genesis_build]
|
#[pallet::genesis_build]
|
||||||
impl<T: Config> GenesisBuild<T> for GenesisConfig<T> {
|
impl<T: Config> GenesisBuild<T> for GenesisConfig<T> {
|
||||||
fn build(&self) {
|
fn build(&self) {
|
||||||
|
use ciphersuite::{group::GroupEncoding, Ciphersuite, Ristretto};
|
||||||
|
|
||||||
|
let hash_set = self.participants.iter().map(|key| key.0).collect::<hashbrown::HashSet<[u8; 32]>>();
|
||||||
|
if hash_set.len() != self.participants.len()
|
||||||
|
{
|
||||||
|
panic!("participants contained duplicates");
|
||||||
|
}
|
||||||
|
|
||||||
let mut participants = Vec::new();
|
let mut participants = Vec::new();
|
||||||
|
let mut keys = Vec::new();
|
||||||
for participant in self.participants.clone() {
|
for participant in self.participants.clone() {
|
||||||
|
keys.push(
|
||||||
|
<Ristretto as Ciphersuite>::read_G::<&[u8]>(
|
||||||
|
&mut participant.0.as_ref(),
|
||||||
|
)
|
||||||
|
.expect("invalid participant"),
|
||||||
|
);
|
||||||
participants.push((participant, self.bond));
|
participants.push((participant, self.bond));
|
||||||
}
|
}
|
||||||
let participants = BoundedVec::try_from(participants).unwrap();
|
let participants = BoundedVec::try_from(participants).unwrap();
|
||||||
|
|
||||||
for (id, network) in self.networks.clone() {
|
for (id, network) in self.networks.clone() {
|
||||||
let set = ValidatorSet { session: Session(0), network: id };
|
let set = ValidatorSet { session: Session(0), network: id };
|
||||||
|
// TODO: Should this be split up? Substrate will read this entire struct into mem on every
|
||||||
|
// read, not just accessed variables
|
||||||
ValidatorSets::<T>::set(
|
ValidatorSets::<T>::set(
|
||||||
set,
|
set,
|
||||||
Some(ValidatorSetData { bond: self.bond, network, participants: participants.clone() }),
|
Some(ValidatorSetData { bond: self.bond, network, participants: participants.clone() }),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
MuSigKeys::<T>::set(set, Some(Public(dkg::musig::musig_key::<Ristretto>(&keys).unwrap().to_bytes())));
|
||||||
Pallet::<T>::deposit_event(Event::NewSet { set })
|
Pallet::<T>::deposit_event(Event::NewSet { set })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -108,63 +109,90 @@ pub mod pallet {
|
|||||||
pub enum Error<T> {
|
pub enum Error<T> {
|
||||||
/// Validator Set doesn't exist.
|
/// Validator Set doesn't exist.
|
||||||
NonExistentValidatorSet,
|
NonExistentValidatorSet,
|
||||||
/// Non-validator is voting.
|
|
||||||
NotValidator,
|
|
||||||
/// Validator Set already generated keys.
|
/// Validator Set already generated keys.
|
||||||
AlreadyGeneratedKeys,
|
AlreadyGeneratedKeys,
|
||||||
/// Vvalidator has already voted for these keys.
|
/// An invalid MuSig signature was provided.
|
||||||
AlreadyVoted,
|
BadSignature,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Config> Pallet<T> {
|
||||||
|
fn verify_signature(
|
||||||
|
set: ValidatorSet,
|
||||||
|
key_pair: &KeyPair,
|
||||||
|
signature: &Signature,
|
||||||
|
) -> Result<(), Error<T>> {
|
||||||
|
if Keys::<T>::get(set).is_some() {
|
||||||
|
Err(Error::AlreadyGeneratedKeys)?
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(musig_key) = MuSigKeys::<T>::get(set) else {
|
||||||
|
Err(Error::NonExistentValidatorSet)?
|
||||||
|
};
|
||||||
|
if !musig_key.verify(&key_pair.encode(), signature) {
|
||||||
|
Err(Error::BadSignature)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pallet::call]
|
#[pallet::call]
|
||||||
impl<T: Config> Pallet<T> {
|
impl<T: Config> Pallet<T> {
|
||||||
#[pallet::call_index(0)]
|
#[pallet::call_index(0)]
|
||||||
#[pallet::weight(0)] // TODO
|
#[pallet::weight(0)] // TODO
|
||||||
pub fn vote(origin: OriginFor<T>, network: NetworkId, key_pair: KeyPair) -> DispatchResult {
|
pub fn set_keys(
|
||||||
let signer = ensure_signed(origin)?;
|
origin: OriginFor<T>,
|
||||||
// TODO: Do we need to check the key is within the length bounds?
|
network: NetworkId,
|
||||||
// The docs suggest the BoundedVec will create/write, yet not read, which could be an issue
|
key_pair: KeyPair,
|
||||||
// if it can be passed in
|
signature: Signature,
|
||||||
|
) -> DispatchResult {
|
||||||
|
ensure_none(origin)?;
|
||||||
|
|
||||||
// TODO: Get session
|
// TODO: Get session
|
||||||
let session: Session = Session(0);
|
let session: Session = Session(0);
|
||||||
|
|
||||||
// Confirm a key hasn't been set for this set instance
|
// Confirm a key hasn't been set for this set instance
|
||||||
let set = ValidatorSet { session, network };
|
let set = ValidatorSet { session, network };
|
||||||
if Keys::<T>::get(set).is_some() {
|
Self::verify_signature(set, &key_pair, &signature)?;
|
||||||
Err(Error::<T>::AlreadyGeneratedKeys)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Confirm the signer is a validator in the set
|
|
||||||
let data = ValidatorSets::<T>::get(set).ok_or(Error::<T>::NonExistentValidatorSet)?;
|
|
||||||
if !data.participants.iter().any(|participant| participant.0 == signer) {
|
|
||||||
Err(Error::<T>::NotValidator)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Confirm this signer hasn't already voted for these keys
|
|
||||||
if Voted::<T>::get((&signer, &key_pair)).is_some() {
|
|
||||||
Err(Error::<T>::AlreadyVoted)?;
|
|
||||||
}
|
|
||||||
Voted::<T>::set((&signer, &key_pair), Some(()));
|
|
||||||
|
|
||||||
// Add their vote
|
|
||||||
let votes = VoteCount::<T>::mutate((set, &key_pair), |value| {
|
|
||||||
*value += 1;
|
|
||||||
*value
|
|
||||||
});
|
|
||||||
|
|
||||||
Self::deposit_event(Event::Vote { voter: signer, set, key_pair: key_pair.clone(), votes });
|
|
||||||
|
|
||||||
// If we've reached consensus, set the key
|
|
||||||
if usize::try_from(votes).unwrap() == data.participants.len() {
|
|
||||||
Keys::<T>::set(set, Some(key_pair.clone()));
|
Keys::<T>::set(set, Some(key_pair.clone()));
|
||||||
Self::deposit_event(Event::KeyGen { set, key_pair });
|
Self::deposit_event(Event::KeyGen { set, key_pair });
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[pallet::validate_unsigned]
|
||||||
|
impl<T: Config> ValidateUnsigned for Pallet<T> {
|
||||||
|
type Call = Call<T>;
|
||||||
|
|
||||||
|
fn validate_unsigned(_: TransactionSource, call: &Self::Call) -> TransactionValidity {
|
||||||
|
// Match to be exhaustive
|
||||||
|
let (network, key_pair, signature) = match call {
|
||||||
|
Call::set_keys { network, ref key_pair, ref signature } => (network, key_pair, signature),
|
||||||
|
Call::__Ignore(_, _) => unreachable!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: Get the latest session
|
||||||
|
let session = Session(0);
|
||||||
|
|
||||||
|
let set = ValidatorSet { session, network: *network };
|
||||||
|
match Self::verify_signature(set, key_pair, signature) {
|
||||||
|
Err(Error::AlreadyGeneratedKeys) => Err(InvalidTransaction::Stale)?,
|
||||||
|
Err(Error::NonExistentValidatorSet) | Err(Error::BadSignature) => Err(InvalidTransaction::BadProof)?,
|
||||||
|
Err(Error::__Ignore(_, _)) => unreachable!(),
|
||||||
|
Ok(()) => (),
|
||||||
|
}
|
||||||
|
|
||||||
|
ValidTransaction::with_tag_prefix("validator-sets")
|
||||||
|
.and_provides(set)
|
||||||
|
// Set a 10 block longevity, though this should be included in the next block
|
||||||
|
.longevity(10)
|
||||||
|
.propagate(true)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Support session rotation
|
// TODO: Support session rotation
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user