Skip to content

Commit

Permalink
Use secp256k1 for validator keys.
Browse files Browse the repository at this point in the history
  • Loading branch information
deuszx committed Feb 21, 2025
1 parent e4d3b92 commit 42cf98a
Show file tree
Hide file tree
Showing 31 changed files with 399 additions and 154 deletions.
3 changes: 2 additions & 1 deletion CLI.md
Original file line number Diff line number Diff line change
Expand Up @@ -404,11 +404,12 @@ Synchronizes a validator with the local state of chains

Add or modify a validator (admin only)

**Usage:** `linera set-validator [OPTIONS] --public-key <PUBLIC_KEY> --address <ADDRESS>`
**Usage:** `linera set-validator [OPTIONS] --public-key <PUBLIC_KEY> --account-key <ACCOUNT_KEY> --address <ADDRESS>`

###### **Options:**

* `--public-key <PUBLIC_KEY>` — The public key of the validator
* `--account-key <ACCOUNT_KEY>` — The public key of the account controlled by the validator
* `--address <ADDRESS>` — Network address
* `--votes <VOTES>` — Voting power

Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions linera-base/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ port-selector.workspace = true
zstd.workspace = true

[dev-dependencies]
bincode.workspace = true
linera-base = { path = ".", default-features = false, features = ["test"] }
linera-witty = { workspace = true, features = ["test"] }
test-case.workspace = true
Expand Down
39 changes: 22 additions & 17 deletions linera-base/src/crypto/ed25519.rs
Original file line number Diff line number Diff line change
Expand Up @@ -288,23 +288,6 @@ impl Ed25519Signature {
})
}

/// Checks an optional signature.
pub fn check_optional_signature<'de, T>(
signature: Option<&Self>,
value: &T,
author: &Ed25519PublicKey,
) -> Result<(), CryptoError>
where
T: BcsSignable<'de> + fmt::Debug,
{
match signature {
Some(sig) => sig.check(value, *author),
None => Err(CryptoError::MissingSignature {
type_name: T::type_name().to_string(),
}),
}
}

fn verify_batch_internal<'a, 'de, T, I>(
value: &'a T,
votes: I,
Expand Down Expand Up @@ -435,4 +418,26 @@ mod tests {
assert!(s.check(&tsx, addr1).is_err());
assert!(s.check(&foo, addr1).is_err());
}

#[test]
fn test_publickey_serialization() {
use crate::crypto::ed25519::Ed25519PublicKey;
let key_in = Ed25519PublicKey::test_key(0);
let s = serde_json::to_string(&key_in).unwrap();
let key_out: Ed25519PublicKey = serde_json::from_str(&s).unwrap();
assert_eq!(key_out, key_in);

let s = bincode::serialize(&key_in).unwrap();
let key_out: Ed25519PublicKey = bincode::deserialize(&s).unwrap();
assert_eq!(key_out, key_in);
}

#[test]
fn test_secretkey_serialization() {
use crate::crypto::ed25519::Ed25519SecretKey;
let key_in = Ed25519SecretKey::generate();
let s = serde_json::to_string(&key_in).unwrap();
let key_out: Ed25519SecretKey = serde_json::from_str(&s).unwrap();
assert_eq!(key_out.0.to_bytes(), key_in.0.to_bytes());
}
}
8 changes: 5 additions & 3 deletions linera-base/src/crypto/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@ use serde::{Deserialize, Serialize};
use thiserror::Error;

/// The public key of a validator.
pub type ValidatorPublicKey = ed25519::Ed25519PublicKey;
pub type ValidatorPublicKey = secp256k1::Secp256k1PublicKey;
/// The private key of a validator.
pub type ValidatorSecretKey = ed25519::Ed25519SecretKey;
pub type ValidatorSecretKey = secp256k1::Secp256k1SecretKey;
/// The signature of a validator.
pub type ValidatorSignature = ed25519::Ed25519Signature;
pub type ValidatorSignature = secp256k1::Secp256k1Signature;
/// The key pair of a validator.
pub type ValidatorKeypair = secp256k1::Secp256k1KeyPair;

/// The public key of a chain owner.
/// The corresponding private key is allowed to propose blocks
Expand Down
102 changes: 97 additions & 5 deletions linera-base/src/crypto/secp256k1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ pub static SECP256K1: LazyLock<Secp256k1<All>> = LazyLock::new(secp256k1::Secp25
pub struct Secp256k1SecretKey(pub secp256k1::SecretKey);

/// A secp256k1 public key.
#[derive(Eq, PartialEq, Copy, Clone)]
#[derive(Eq, PartialEq, Ord, PartialOrd, Copy, Clone, Hash)]
pub struct Secp256k1PublicKey(pub secp256k1::PublicKey);

/// Secp256k1 public/secret key pair.
Expand All @@ -36,12 +36,49 @@ pub struct Secp256k1KeyPair {
#[derive(Eq, PartialEq, Copy, Clone)]
pub struct Secp256k1Signature(pub secp256k1::ecdsa::Signature);

impl Secp256k1PublicKey {
/// A fake public key used for testing.
#[cfg(with_testing)]
pub fn test_key(seed: u8) -> Self {
use rand::SeedableRng;

let mut rng = rand::rngs::StdRng::seed_from_u64(seed as u64);
let secp = secp256k1::Secp256k1::signing_only();
Self(secp.generate_keypair(&mut rng).1)
}
}

impl fmt::Debug for Secp256k1SecretKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "<redacted for Secp256k1 secret key>")
}
}

impl Serialize for Secp256k1SecretKey {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::ser::Serializer,
{
// This is only used for JSON configuration.
assert!(serializer.is_human_readable());
serializer.serialize_str(&hex::encode(self.0.secret_bytes()))
}
}

impl<'de> Deserialize<'de> for Secp256k1SecretKey {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::de::Deserializer<'de>,
{
// This is only used for JSON configuration.
assert!(deserializer.is_human_readable());
let str = String::deserialize(deserializer)?;
let bytes = hex::decode(&str).map_err(serde::de::Error::custom)?;
let sk = secp256k1::SecretKey::from_slice(&bytes).map_err(serde::de::Error::custom)?;
Ok(Secp256k1SecretKey(sk))
}
}

impl Serialize for Secp256k1PublicKey {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
Expand All @@ -50,7 +87,10 @@ impl Serialize for Secp256k1PublicKey {
if serializer.is_human_readable() {
serializer.serialize_str(&hex::encode(self.0.serialize()))
} else {
serializer.serialize_newtype_struct("Secp256k1PublicKey", &self.0)
#[derive(Serialize)]
#[serde(rename = "Secp256k1PublicKey")]
struct Foo<'a>(&'a secp256k1::PublicKey);
Foo(&self.0).serialize(serializer)
}
}
}
Expand All @@ -69,7 +109,6 @@ impl<'de> Deserialize<'de> for Secp256k1PublicKey {
#[derive(Deserialize)]
#[serde(rename = "Secp256k1PublicKey")]
struct Foo(secp256k1::PublicKey);

let value = Foo::deserialize(deserializer)?;
Ok(Self(value.0))
}
Expand Down Expand Up @@ -128,9 +167,17 @@ impl Secp256k1KeyPair {

impl Secp256k1SecretKey {
/// Returns a public key for the given secret key.
pub fn to_public(&self) -> Secp256k1PublicKey {
pub fn public(&self) -> Secp256k1PublicKey {
Secp256k1PublicKey(self.0.public_key(&SECP256K1))
}

/// Copies the key pair, **including the secret key**.
///
/// The `Clone` and `Copy` traits are deliberately not implemented for `Secp256k1SecretKey` to prevent
/// accidental copies of secret keys.
pub fn copy(&self) -> Self {
Self(self.0)
}
}

impl Secp256k1Signature {
Expand Down Expand Up @@ -159,6 +206,28 @@ impl Secp256k1Signature {
type_name: T::type_name().to_string(),
})
}

pub fn verify_batch<'a, 'de, T, I>(value: &'a T, votes: I) -> Result<(), CryptoError>
where
T: BcsSignable<'de> + fmt::Debug,
I: IntoIterator<Item = &'a (Secp256k1PublicKey, Secp256k1Signature)>,
{
let message = Message::from_digest(CryptoHash::new(value).as_bytes().0);
for (author, signature) in votes {
SECP256K1
.verify_ecdsa(&message, &signature.0, &author.0)
.expect("Invalid signature");
}
Ok(())
}

/// Converts the signature to a byte array.
/// Expects the signature to be serialized in compact form.
pub fn from_slice<A: AsRef<[u8]>>(bytes: A) -> Result<Self, CryptoError> {
let sig = secp256k1::ecdsa::Signature::from_compact(bytes.as_ref())
.map_err(CryptoError::Secp256k1Error)?;
Ok(Secp256k1Signature(sig))
}
}

impl Serialize for Secp256k1Signature {
Expand Down Expand Up @@ -210,9 +279,10 @@ impl fmt::Debug for Secp256k1Signature {
}

doc_scalar!(Secp256k1Signature, "A secp256k1 signature value");
doc_scalar!(Secp256k1PublicKey, "A secp256k1 public key value");

#[cfg(with_testing)]
mod secp256k1_tests {
mod tests {
#[test]
fn test_signatures() {
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -240,4 +310,26 @@ mod secp256k1_tests {
assert!(s.check(&tsx, &keypair1.public_key).is_err());
assert!(s.check(&foo, &keypair1.public_key).is_err());
}

#[test]
fn test_publickey_serialization() {
use crate::crypto::secp256k1::Secp256k1PublicKey;
let key_in = Secp256k1PublicKey::test_key(0);
let s = serde_json::to_string(&key_in).unwrap();
let key_out: Secp256k1PublicKey = serde_json::from_str(&s).unwrap();
assert_eq!(key_out, key_in);

let s = bincode::serialize(&key_in).unwrap();
let key_out: Secp256k1PublicKey = bincode::deserialize(&s).unwrap();
assert_eq!(key_out, key_in);
}

#[test]
fn test_secretkey_deserialization() {
use crate::crypto::secp256k1::{Secp256k1KeyPair, Secp256k1SecretKey};
let key_in = Secp256k1KeyPair::generate().secret_key;
let s = serde_json::to_string(&key_in).unwrap();
let key_out: Secp256k1SecretKey = serde_json::from_str(&s).unwrap();
assert_eq!(key_out, key_in);
}
}
4 changes: 2 additions & 2 deletions linera-chain/src/chain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -608,7 +608,7 @@ where
self.execution_state.system.ownership.get().clone(),
BlockHeight(0),
local_time,
maybe_committee.flat_map(|(_, committee)| committee.keys_and_weights()),
maybe_committee.flat_map(|(_, committee)| committee.fallback_keys_and_weights()),
)
}

Expand Down Expand Up @@ -1040,7 +1040,7 @@ where
let maybe_committee = self.execution_state.system.current_committee().into_iter();
let ownership = self.execution_state.system.ownership.get().clone();
let fallback_owners =
maybe_committee.flat_map(|(_, committee)| committee.keys_and_weights());
maybe_committee.flat_map(|(_, committee)| committee.fallback_keys_and_weights());
self.pending_validated_blobs.clear();
self.pending_proposed_blobs.clear();
self.manager
Expand Down
12 changes: 6 additions & 6 deletions linera-chain/src/data_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -810,21 +810,21 @@ impl BlockProposal {

impl LiteVote {
/// Uses the signing key to create a signed object.
pub fn new(value: LiteValue, round: Round, key_pair: &ValidatorSecretKey) -> Self {
pub fn new(value: LiteValue, round: Round, secret_key: &ValidatorSecretKey) -> Self {
let hash_and_round = VoteValue(value.value_hash, round, value.kind);
let signature = ValidatorSignature::new(&hash_and_round, key_pair);
let signature = ValidatorSignature::new(&hash_and_round, secret_key);
Self {
value,
round,
public_key: key_pair.public(),
public_key: secret_key.public(),
signature,
}
}

/// Verifies the signature in the vote.
pub fn check(&self) -> Result<(), ChainError> {
let hash_and_round = VoteValue(self.value.value_hash, self.round, self.value.kind);
Ok(self.signature.check(&hash_and_round, self.public_key)?)
Ok(self.signature.check(&hash_and_round, &self.public_key)?)
}
}

Expand Down Expand Up @@ -858,7 +858,7 @@ impl<'a, T> SignatureAggregator<'a, T> {
T: CertificateValue,
{
let hash_and_round = VoteValue(self.partial.hash(), self.partial.round, T::KIND);
signature.check(&hash_and_round, public_key)?;
signature.check(&hash_and_round, &public_key)?;
// Check that each validator only appears once.
ensure!(
!self.used_validators.contains(&public_key),
Expand Down Expand Up @@ -916,7 +916,7 @@ pub(crate) fn check_signatures(
);
// All that is left is checking signatures!
let hash_and_round = VoteValue(value_hash, round, certificate_kind);
ValidatorSignature::verify_batch(&hash_and_round, signatures.iter().map(|(v, s)| (v, s)))?;
ValidatorSignature::verify_batch(&hash_and_round, signatures.iter())?;
Ok(())
}

Expand Down
3 changes: 2 additions & 1 deletion linera-chain/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
//! Test utilities
use linera_base::{
crypto::AccountSecretKey,
crypto::{AccountPublicKey, AccountSecretKey},
data_types::{Amount, BlockHeight, Round, Timestamp},
hashed::Hashed,
identifiers::{ChainId, Owner},
Expand Down Expand Up @@ -139,6 +139,7 @@ impl<T: CertificateValue> VoteTestExt<T> for Vote<T> {
let state = ValidatorState {
network_address: "".to_string(),
votes: 100,
account_public_key: AccountPublicKey::test_key(1),
};
let committee = Committee::new(
vec![(self.public_key, state)].into_iter().collect(),
Expand Down
8 changes: 6 additions & 2 deletions linera-chain/src/unit_tests/chain_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,10 @@ fn make_admin_message_id(height: BlockHeight) -> MessageId {
}

fn make_open_chain_config() -> OpenChainConfig {
let committee = Committee::make_simple(vec![ValidatorPublicKey::test_key(1)]);
let committee = Committee::make_simple(vec![(
ValidatorPublicKey::test_key(1),
AccountPublicKey::test_key(1),
)]);
OpenChainConfig {
ownership: ChainOwnership::single(AccountPublicKey::test_key(0).into()),
admin_id: admin_id(),
Expand All @@ -112,7 +115,7 @@ async fn test_block_size_limit() {
let mut chain = ChainStateView::new(chain_id).await;

// The size of the executed valid block below.
let maximum_executed_block_size = 685;
let maximum_executed_block_size = 720;

// Initialize the chain.
let mut config = make_open_chain_config();
Expand All @@ -124,6 +127,7 @@ async fn test_block_size_limit() {
ValidatorState {
network_address: ValidatorPublicKey::test_key(1).to_string(),
votes: 1,
account_public_key: AccountPublicKey::test_key(1),
},
)]),
ResourceControlPolicy {
Expand Down
Loading

0 comments on commit 42cf98a

Please sign in to comment.