Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
assets/actor_bundles.car.zst filter=lfs diff=lfs merge=lfs -text
src/beacon/drand_pb.rs linguist-generated=true
13 changes: 13 additions & 0 deletions proto/drand_pb.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
syntax = "proto3";

package drand_pb;

// https://github.com/drand/drand/blob/v2.1.7/protobuf/drand/api.proto#L42-L53
Comment thread
EclesioMeloJunior marked this conversation as resolved.
// Only the fields an unchained beacon entry needs. The rest are skipped on decode, as
// protobuf skips unknown fields: `previous_signature` is empty on unchained networks,
// `randomness` is deprecated and derived from the signature, and `metadata` names the beacon,
// which the gossip topic already identifies.
message PublicRandResponse {
uint64 round = 1;
bytes signature = 2;
}
55 changes: 55 additions & 0 deletions src/beacon/drand_pb.rs

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

4 changes: 4 additions & 0 deletions src/beacon/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@

pub mod beacon_entries;
mod drand;
// Only test fixtures decode it until gossiped entries are validated.
#[cfg(test)]
mod drand_pb;
pub mod metrics;
pub mod signatures;
pub use beacon_entries::*;
Expand All @@ -16,4 +19,5 @@ pub mod tests {
// tests in other modules.
pub mod drand;
pub mod fake_drand_server;
pub mod fake_drand_signer;
}
6 changes: 6 additions & 0 deletions src/beacon/signatures/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ pub use bls_signatures::{PublicKey as PublicKeyOnG1, Signature as SignatureOnG2}
const CSUITE_G1: &[u8] = b"BLS_SIG_BLS12381G1_XMD:SHA-256_SSWU_RO_NUL_";
const CSUITE_G2: &[u8] = b"BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_";

/// Signs `message` on G1 with `secret`, as an unchained `drand` network signs a round.
#[cfg(test)]
pub(in crate::beacon) fn sign_unchained(secret: blstrs::Scalar, message: &[u8]) -> SignatureOnG1 {
SignatureOnG1((G1Projective::hash_to_curve(message, CSUITE_G1, &[]) * secret).to_affine())
}

#[derive(Debug, Clone, Eq, PartialEq, derive_more::Deref)]
pub struct PublicKeyOnG2(pub(crate) G2Projective);

Expand Down
12 changes: 8 additions & 4 deletions src/beacon/tests/drand.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,18 @@ use std::borrow::Cow;
use std::sync::LazyLock;
use std::time::Duration;

/// Filecoin mainnet genesis timestamp and epoch duration, which the test beacons map rounds to.
pub const FIL_GENESIS_TIME: u64 = 1598306400;
pub const FIL_BLOCK_DELAY: u64 = 30;

/// Serves recorded `quicknet` responses, so no test below reaches a public relay.
static FAKE_DRAND: LazyLock<FakeDrandServer> =
LazyLock::new(|| FakeDrandServer::start(vec![FakeDrandChain::quicknet()]));

fn new_beacon_mainnet() -> DrandBeacon {
DrandBeacon::new(
1598306400,
30,
FIL_GENESIS_TIME,
FIL_BLOCK_DELAY,
&DrandConfig {
servers: vec![],
// https://api.drand.sh/8990e7a9aaed2ffed73dbd7092123d6f289930540d7651336225dc172e51b2ce/info
Expand All @@ -52,8 +56,8 @@ fn new_beacon_mainnet() -> DrandBeacon {

pub fn new_beacon_quicknet() -> DrandBeacon {
DrandBeacon::new(
1598306400,
30,
FIL_GENESIS_TIME,
FIL_BLOCK_DELAY,
&DrandConfig {
servers: vec![FAKE_DRAND.url().clone()],
// https://api.drand.sh/52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971/info
Expand Down
96 changes: 96 additions & 0 deletions src/beacon/tests/fake_drand_signer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// Copyright 2019-2026 ChainSafe Systems
// SPDX-License-Identifier: Apache-2.0, MIT

use quick_protobuf::{BytesReader, MessageRead as _, MessageWrite as _, Writer};

use super::drand::{FIL_BLOCK_DELAY, FIL_GENESIS_TIME};
use super::fake_drand_server::FakeDrandChain;
use crate::beacon::{
Beacon, BeaconEntry, ChainInfo, DrandBeacon, DrandConfig, DrandNetwork,
drand_pb::PublicRandResponse, signatures::sign_unchained,
};
use crate::utils::encoding::hex;
use blstrs::{G2Projective, Scalar};
use group::{Curve, Group};

/// Signs any round of a `quicknet`-like chain with a made-up key, and encodes it as `drand`
/// gossips it.
pub struct FakeDrandSigner {
secret: Scalar,
config: DrandConfig<'static>,
}

impl FakeDrandSigner {
pub fn new() -> Self {
// Arbitrary: any non-zero scalar makes a valid key pair.
let secret = Scalar::from(0xC0FFEEu64);
Comment thread
EclesioMeloJunior marked this conversation as resolved.
let public = G2Projective::generator() * secret;
let public_key = hex::encode(public.to_affine().to_compressed());
// Real `quicknet` timing, so rounds map to epochs as on mainnet; the key and hashes
// are made up.
let quicknet = FakeDrandChain::quicknet().info;
Self {
secret,
config: DrandConfig {
servers: vec![],
chain_info: ChainInfo {
public_key: public_key.into(),
period: quicknet.period,
genesis_time: quicknet.genesis_time,
hash: "0011".repeat(16).into(),
group_hash: "00".repeat(32).into(),
},
network_type: DrandNetwork::Quicknet, // unchained
// Would clash with the real quicknet config's collector.
register_metrics: false,
},
}
}

pub fn entry(&self, round: u64) -> BeaconEntry {
let signature = sign_unchained(self.secret, BeaconEntry::message_unchained(round).as_ref());
BeaconEntry::new(round, signature.0.to_compressed().to_vec())
}

/// `round`, signed, as a gossiped `PublicRandResponse`.
pub fn encode(&self, round: u64) -> Vec<u8> {
self.encode_raw(round, self.entry(round).signature())
}

/// A gossiped `PublicRandResponse` with any signature.
pub fn encode_raw(&self, round: u64, signature: &[u8]) -> Vec<u8> {
let mut out = Vec::new();
PublicRandResponse {
round,
signature: signature.to_vec(),
}
.write_message(&mut Writer::new(&mut out))
.unwrap();
out
}

pub fn decode(payload: &[u8]) -> BeaconEntry {
let response =
PublicRandResponse::from_reader(&mut BytesReader::from_bytes(payload), payload)
.unwrap();
BeaconEntry::new(response.round, response.signature)
}

pub fn beacon(&self) -> DrandBeacon {
DrandBeacon::new(FIL_GENESIS_TIME, FIL_BLOCK_DELAY, &self.config)
}
}

// Entries decoded from the gossip encoding verify against the fake chain's key.
#[test]
fn fake_drand_entries_verify() {
let d = FakeDrandSigner::new();
let entries: Vec<_> = (1..=5)
.map(|round| FakeDrandSigner::decode(&d.encode(round)))
.collect();
Comment thread
EclesioMeloJunior marked this conversation as resolved.
assert!(
d.beacon()
.verify_entries(&entries, &BeaconEntry::default())
.unwrap()
);
}
Loading