Skip to content
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
64d59d9
First draft
davxy Feb 22, 2025
1d2eed8
Implement Schnorr signature for Bandersnatch
davxy Feb 26, 2025
be67253
Working integration
davxy Feb 26, 2025
3cbdf75
Removed old bandersnatch_vrfs
davxy Feb 26, 2025
fa5377a
Infallible prover/verifier construction with padding fallback
davxy Feb 26, 2025
7103b40
Sassafras primitives
davxy Feb 26, 2025
db08b53
Pallet builds
davxy Feb 26, 2025
7e060e4
Test run, but fail
davxy Feb 26, 2025
adb012e
Fixed tests
davxy Feb 26, 2025
3c4036a
Cleanup
davxy Feb 26, 2025
6156d40
remove from default feats
davxy Feb 26, 2025
a07c51a
more cleanup
davxy Feb 26, 2025
df014b8
Lock to rev
davxy Feb 27, 2025
9826d28
Cleanup
davxy Feb 28, 2025
479758a
Merge branch 'master' into davxy/introduce-ark-ec-vrfs
davxy Feb 28, 2025
1720113
Bump ark-ec-vrf rev
davxy Feb 28, 2025
4d24263
Use crates.io
davxy Feb 28, 2025
af82993
Fix
davxy Feb 28, 2025
5c1cc41
Resolve warnings
davxy Mar 1, 2025
5c8cdab
Bump ark-ec-vrfs version
davxy Mar 20, 2025
42503c6
Doc
davxy Mar 20, 2025
555ae8d
Backend renamed
davxy Mar 28, 2025
4704379
Merge branch 'master' into davxy/introduce-ark-ec-vrfs
skunert Apr 1, 2025
df12918
Lock update
davxy Apr 1, 2025
7b9a15a
Fix static keyring pks
davxy Apr 1, 2025
f0fd53e
Update from github-actions[bot] running command 'prdoc --audience run…
github-actions[bot] Apr 1, 2025
1ef9407
toml formatting
ggwpez Apr 1, 2025
ac11b52
Add bandersnatch feature to Zepter check
ggwpez Apr 1, 2025
a3a5da0
Propagate missign features
ggwpez Apr 1, 2025
704c9b1
Exclude from umbrella
ggwpez Apr 1, 2025
8cb5d3c
Merge branch 'master' into davxy/introduce-ark-ec-vrfs
ggwpez Apr 1, 2025
bbd4b1f
doc build
ggwpez Apr 1, 2025
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
618 changes: 273 additions & 345 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -614,6 +614,7 @@ ark-bls12-381-ext = { version = "0.4.1", default-features = false }
ark-bw6-761 = { version = "0.4.0", default-features = false }
ark-bw6-761-ext = { version = "0.4.1", default-features = false }
ark-ec = { version = "0.4.2", default-features = false }
ark-ec-vrfs = { version = "0.1.1", default-features = false }
ark-ed-on-bls12-377 = { version = "0.4.0", default-features = false }
ark-ed-on-bls12-377-ext = { version = "0.4.1", default-features = false }
ark-ed-on-bls12-381-bandersnatch = { version = "0.4.0", default-features = false }
Expand Down Expand Up @@ -1431,6 +1432,7 @@ overflow-checks = true
#
# This list is ordered alphabetically.
[profile.dev.package]
ark-ec-vrfs = { opt-level = 3 }
blake2 = { opt-level = 3 }
blake2b_simd = { opt-level = 3 }
chacha20poly1305 = { opt-level = 3 }
Expand Down
Binary file modified substrate/frame/sassafras/src/data/25_tickets_100_auths.bin
Binary file not shown.
53 changes: 17 additions & 36 deletions substrate/frame/sassafras/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,6 @@ pub use pallet::*;

const LOG_TARGET: &str = "sassafras::runtime";

// Contextual string used by the VRF to generate per-block randomness.
const RANDOMNESS_VRF_CONTEXT: &[u8] = b"SassafrasOnChainRandomness";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the motivation behind removing the context for vrf randomness?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Context strings are supplied by the dedicated utility functions to construct sassafras VrfInputs (e.g. the one to construct ticket)


// Max length for segments holding unsorted tickets.
const SEGMENT_MAX_SIZE: u32 = 128;

Expand Down Expand Up @@ -202,6 +199,12 @@ pub mod pallet {
#[pallet::getter(fn randomness_accumulator)]
pub(crate) type RandomnessAccumulator<T> = StorageValue<_, Randomness, ValueQuery>;

/// Per slot randomness used to feed the randomness accumulator.
///
/// The value is ephemeral and is cleared on block finalization.
#[pallet::storage]
pub(crate) type SlotRandomness<T> = StorageValue<_, Randomness>;

/// The configuration for the current epoch.
#[pallet::storage]
#[pallet::getter(fn config)]
Expand Down Expand Up @@ -272,13 +275,7 @@ pub mod pallet {

/// Ring verifier data for the current epoch.
#[pallet::storage]
pub type RingVerifierData<T: Config> = StorageValue<_, vrf::RingVerifierData>;

/// Slot claim VRF pre-output used to generate per-slot randomness.
///
/// The value is ephemeral and is cleared on block finalization.
#[pallet::storage]
pub(crate) type ClaimTemporaryData<T> = StorageValue<_, vrf::VrfPreOutput>;
pub type RingVerifierData<T: Config> = StorageValue<_, vrf::RingVerifierKey>;

/// Genesis configuration for Sassafras protocol.
#[pallet::genesis_config]
Expand Down Expand Up @@ -326,12 +323,8 @@ pub mod pallet {
Self::post_genesis_initialize(claim.slot);
}

let randomness_pre_output = claim
.vrf_signature
.pre_outputs
.get(0)
.expect("Valid claim must have VRF signature; qed");
ClaimTemporaryData::<T>::put(randomness_pre_output);
let randomness = claim.vrf_signature.pre_output.make_bytes();
SlotRandomness::<T>::put(randomness);

let trigger_weight = T::EpochChangeTrigger::trigger::<T>(block_num);

Expand All @@ -343,15 +336,8 @@ pub mod pallet {
// to the accumulator. If we've determined that this block was the first in
// a new epoch, the changeover logic has already occurred at this point
// (i.e. `enact_epoch_change` has already been called).
let randomness_input = vrf::slot_claim_input(
&Self::randomness(),
CurrentSlot::<T>::get(),
EpochIndex::<T>::get(),
);
let randomness_pre_output = ClaimTemporaryData::<T>::take()
let randomness = SlotRandomness::<T>::take()
.expect("Unconditionally populated in `on_initialize`; `on_finalize` is always called after; qed");
let randomness = randomness_pre_output
.make_bytes::<RANDOMNESS_LENGTH>(RANDOMNESS_VRF_CONTEXT, &randomness_input);
Self::deposit_slot_randomness(&randomness);

// Check if we are in the epoch's second half.
Expand Down Expand Up @@ -399,7 +385,9 @@ pub mod pallet {
return Err("Tickets shall be submitted in the first epoch half".into())
}

let Some(verifier) = RingVerifierData::<T>::get().map(|v| v.into()) else {
let Some(verifier) =
RingVerifierData::<T>::get().map(|vk| vrf::RingContext::verifier_no_context(vk))
else {
warn!(target: LOG_TARGET, "Ring verifier key not initialized");
return Err("Ring verifier key not initialized".into())
};
Expand All @@ -424,15 +412,8 @@ pub mod pallet {
for ticket in tickets {
debug!(target: LOG_TARGET, "Checking ring proof");

let Some(ticket_id_pre_output) = ticket.signature.pre_outputs.get(0) else {
debug!(target: LOG_TARGET, "Missing ticket VRF pre-output from ring signature");
continue
};
let ticket_id_input =
vrf::ticket_id_input(&randomness, ticket.body.attempt_idx, epoch_idx);

// Check threshold constraint
let ticket_id = vrf::make_ticket_id(&ticket_id_input, &ticket_id_pre_output);
let ticket_id = vrf::make_ticket_id(&ticket.signature.pre_output);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So ticket id used to explicitly depends on the ticket_id_input but not anymore? why?

Copy link
Member Author

@davxy davxy Apr 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

preout = input * secret
ticket_id = hash(preout) // aka vrf output

THis is exactly what we're doing in JAM and prescribed by the GP

if ticket_id >= ticket_threshold {
debug!(target: LOG_TARGET, "Ignoring ticket over threshold ({:032x} >= {:032x})", ticket_id, ticket_threshold);
continue
Expand All @@ -445,6 +426,8 @@ pub mod pallet {
}

// Check ring signature
let ticket_id_input =
vrf::ticket_id_input(&randomness, ticket.body.attempt_idx, epoch_idx);
let sign_data = vrf::ticket_body_sign_data(&ticket.body, ticket_id_input);
if !ticket.signature.ring_vrf_verify(&sign_data, &verifier) {
debug!(target: LOG_TARGET, "Proof verification failure for ticket ({:032x})", ticket_id);
Expand Down Expand Up @@ -585,9 +568,7 @@ impl<T: Config> Pallet<T> {
let pks: Vec<_> = authorities.iter().map(|auth| *auth.as_ref()).collect();

debug!(target: LOG_TARGET, "Building ring verifier (ring size: {})", pks.len());
let verifier_data = ring_ctx
.verifier_data(&pks)
.expect("Failed to build ring verifier. This is a bug");
let verifier_data = ring_ctx.verifier_key(&pks);

RingVerifierData::<T>::put(verifier_data);
}
Expand Down
4 changes: 2 additions & 2 deletions substrate/frame/sassafras/src/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ pub fn make_prover(pair: &AuthorityPair) -> RingProver {
.collect();

log::debug!("Building prover. Ring size: {}", pks.len());
let prover = ring_ctx.prover(&pks, prover_idx.unwrap()).unwrap();
let prover = ring_ctx.prover(&pks, prover_idx.unwrap());
log::debug!("Done");

prover
Expand All @@ -201,7 +201,7 @@ pub fn make_ticket_body(attempt_idx: u32, pair: &AuthorityPair) -> (TicketId, Ti
let ticket_id_input = vrf::ticket_id_input(&randomness, attempt_idx, epoch);
let ticket_id_pre_output = pair.as_inner_ref().vrf_pre_output(&ticket_id_input);

let id = vrf::make_ticket_id(&ticket_id_input, &ticket_id_pre_output);
let id = vrf::make_ticket_id(&ticket_id_pre_output);

// Make a dummy ephemeral public that hopefully is unique within one test instance.
// In the tests, the values within the erased public are just used to compare
Expand Down
34 changes: 17 additions & 17 deletions substrate/frame/sassafras/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ fn on_first_block_after_genesis() {

// Post-initialization status

assert!(ClaimTemporaryData::<Test>::exists());
assert!(SlotRandomness::<Test>::exists());
common_assertions();
println!("[DEBUG] {}", b2h(Sassafras::randomness_accumulator()));
assert_eq!(
Expand All @@ -273,12 +273,11 @@ fn on_first_block_after_genesis() {

// Post-finalization status

assert!(!ClaimTemporaryData::<Test>::exists());
assert!(!SlotRandomness::<Test>::exists());
common_assertions();
println!("[DEBUG] {}", b2h(Sassafras::randomness_accumulator()));
assert_eq!(
Sassafras::randomness_accumulator(),
h2b("9f2b9fd19a772c34d437dcd8b84a927e73a5cb43d3d1cd00093223d60d2b4843"),
h2b("95a508cf10f877cf0457af3503a6cb3192763d5c15a7b9a58e40dc543efae889"),
);

// Header data check
Expand Down Expand Up @@ -332,23 +331,24 @@ fn on_normal_block() {

// Post-initialization status

assert!(ClaimTemporaryData::<Test>::exists());
assert!(SlotRandomness::<Test>::exists());
common_assertions();
println!("[DEBUG] {}", b2h(Sassafras::randomness_accumulator()));
assert_eq!(
Sassafras::randomness_accumulator(),
h2b("9f2b9fd19a772c34d437dcd8b84a927e73a5cb43d3d1cd00093223d60d2b4843"),
h2b("95a508cf10f877cf0457af3503a6cb3192763d5c15a7b9a58e40dc543efae889"),
);

let header = finalize_block(end_block);

// Post-finalization status

assert!(!ClaimTemporaryData::<Test>::exists());
assert!(!SlotRandomness::<Test>::exists());
common_assertions();
println!("[DEBUG] {}", b2h(Sassafras::randomness_accumulator()));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to keep this?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can remove it

assert_eq!(
Sassafras::randomness_accumulator(),
h2b("be9261adb9686dfd3f23f8a276b7acc7f4beb3137070beb64c282ac22d84cbf0"),
h2b("5465cb257ad20cd4b9400a9fc85af7b1e2e72b59debd8ca06580dfb76bfca394"),
);

// Header data check
Expand Down Expand Up @@ -389,34 +389,34 @@ fn produce_epoch_change_digest_no_config() {

// Post-initialization status

assert!(ClaimTemporaryData::<Test>::exists());
assert!(SlotRandomness::<Test>::exists());
common_assertions();
println!("[DEBUG] {}", b2h(Sassafras::next_randomness()));
assert_eq!(
Sassafras::next_randomness(),
h2b("d3a18b857af6ecc7b52f047107e684fff0058b5722d540a296d727e37eaa55b3"),
h2b("c4d374ed47b71e1c29e57143db23861916ff2d0c59ead4c51070d42ff4af2830"),
);
println!("[DEBUG] {}", b2h(Sassafras::randomness_accumulator()));
assert_eq!(
Sassafras::randomness_accumulator(),
h2b("bf0f1228f4ff953c8c1bda2cceb668bf86ea05d7ae93e26d021c9690995d5279"),
h2b("c6d84d1f389853959c39271a38010f2f27abe6ff56cc419cf9e89eafcae1ab5e"),
);

let header = finalize_block(end_block);

// Post-finalization status

assert!(!ClaimTemporaryData::<Test>::exists());
assert!(!SlotRandomness::<Test>::exists());
common_assertions();
println!("[DEBUG] {}", b2h(Sassafras::next_randomness()));
assert_eq!(
Sassafras::next_randomness(),
h2b("d3a18b857af6ecc7b52f047107e684fff0058b5722d540a296d727e37eaa55b3"),
h2b("c4d374ed47b71e1c29e57143db23861916ff2d0c59ead4c51070d42ff4af2830"),
);
println!("[DEBUG] {}", b2h(Sassafras::randomness_accumulator()));
assert_eq!(
Sassafras::randomness_accumulator(),
h2b("8a1ceb346036c386d021264b10912c8b656799668004c4a487222462b394cd89"),
h2b("6ca02b90e14ef11b3855069794da7e9d4007526b0588c426c3e3533b0b6ade7a"),
);

// Header data check
Expand Down Expand Up @@ -670,7 +670,7 @@ fn block_allowed_to_skip_epochs() {

// Post-initialization status

assert!(ClaimTemporaryData::<Test>::exists());
assert!(SlotRandomness::<Test>::exists());
assert_eq!(Sassafras::genesis_slot(), start_slot);
assert_eq!(Sassafras::current_slot(), start_slot + offset);
assert_eq!(Sassafras::epoch_index(), 4);
Expand Down Expand Up @@ -829,9 +829,9 @@ fn submit_tickets_with_ring_proof_check_works() {
// Check state after submission
assert_eq!(
TicketsMeta::<Test>::get(),
TicketsMetadata { unsorted_tickets_count: 16, tickets_count: [0, 0] },
TicketsMetadata { unsorted_tickets_count: 13, tickets_count: [0, 0] },
);
assert_eq!(UnsortedSegments::<Test>::get(0).len(), 16);
assert_eq!(UnsortedSegments::<Test>::get(0).len(), 13);
assert_eq!(UnsortedSegments::<Test>::get(1).len(), 0);

finalize_block(start_block);
Expand Down
82 changes: 21 additions & 61 deletions substrate/primitives/consensus/sassafras/src/vrf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,88 +24,48 @@ use codec::Encode;
use sp_consensus_slots::Slot;

pub use sp_core::bandersnatch::{
ring_vrf::{RingProver, RingVerifier, RingVerifierData, RingVrfSignature},
ring_vrf::{RingProver, RingVerifier, RingVerifierKey, RingVrfSignature},
vrf::{VrfInput, VrfPreOutput, VrfSignData, VrfSignature},
};

/// Ring VRF domain size for Sassafras consensus.
pub const RING_VRF_DOMAIN_SIZE: u32 = 2048;
/// Ring size (aka authorities count) for Sassafras consensus.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how come ring size was 2048 before?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The size is arbitrary at this point. But I can make it a generic maybe

pub const RING_SIZE: usize = 1024;

/// Bandersnatch VRF [`RingContext`] specialization for Sassafras using [`RING_VRF_DOMAIN_SIZE`].
pub type RingContext = sp_core::bandersnatch::ring_vrf::RingContext<RING_VRF_DOMAIN_SIZE>;
/// Bandersnatch VRF [`RingContext`] specialization for Sassafras using [`RING_SIZE`].
pub type RingContext = sp_core::bandersnatch::ring_vrf::RingContext<RING_SIZE>;

fn vrf_input_from_data(
domain: &[u8],
data: impl IntoIterator<Item = impl AsRef<[u8]>>,
) -> VrfInput {
let buf = data.into_iter().fold(Vec::new(), |mut buf, item| {
let bytes = item.as_ref();
buf.extend_from_slice(bytes);
let len = u8::try_from(bytes.len()).expect("private function with well known inputs; qed");
buf.push(len);
buf
});
VrfInput::new(domain, buf)
}

/// VRF input to claim slot ownership during block production.
/// TODO
pub fn slot_claim_input(randomness: &Randomness, slot: Slot, epoch: u64) -> VrfInput {
vrf_input_from_data(
b"sassafras-claim-v1.0",
[randomness.as_slice(), &slot.to_le_bytes(), &epoch.to_le_bytes()],
)
let v = [b"sassafras-ticket", randomness.as_slice(), &slot.to_le_bytes(), &epoch.to_le_bytes()]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

b"sassafras-ticket" should really be defined as a constant rather than hidden here in the code.

On dropping the length, it should be OK if the it the input is constant length but maybe specify that somewhere in the comment with specifying the pre-set length of each components.

.concat();
VrfInput::new(&v[..])
}

/// Signing-data to claim slot ownership during block production.
pub fn slot_claim_sign_data(randomness: &Randomness, slot: Slot, epoch: u64) -> VrfSignData {
let input = slot_claim_input(randomness, slot, epoch);
VrfSignData::new_unchecked(
b"sassafras-slot-claim-transcript-v1.0",
Option::<&[u8]>::None,
Some(input),
)
let v = [b"sassafras-ticket", randomness.as_slice(), &slot.to_le_bytes(), &epoch.to_le_bytes()]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

b"sassafras-ticket" should be pre defined constant.

.concat();
VrfSignData::new(&v[..], &[])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we are getting rid of including the length in the transcript because we are only accepting a constant length, maybe vrfSignData should police the length?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alistair's comment on this: "The question then is how does the VRF input appear in Hashes. If we are just heading the VRF inputs alone to a curve, then that is fine. If we are ever appending anything after the VRF input in a hash, whether the hash to curve or the Fiat-Shamir hash, then we have a collision issue. If the appended things are always fixed length it might be fine. Or the nonce generation hash. A collision there for some massage /input that can be generated without knowing the secret key would be fatal." (the last one is refering to the nonce in Schnorr signature I believe.

Copy link
Member Author

@davxy davxy Apr 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The question then is how does the VRF input appear in Hashes

I'm not sure I understood the question.
The raw vrf input is :

[b"sassafras-ticket", randomness.as_slice(), &attempt.to_le_bytes(), &epoch.to_le_bytes()]

This sequence of bytes is used to construct VrfInput via the new method . Internally it uses Elligator2.
The returned VrfInput is already a point on the curve

}

/// VRF input to generate the ticket id.
pub fn ticket_id_input(randomness: &Randomness, attempt: u32, epoch: u64) -> VrfInput {
vrf_input_from_data(
b"sassafras-ticket-v1.0",
[randomness.as_slice(), &attempt.to_le_bytes(), &epoch.to_le_bytes()],
)
}

/// VRF input to generate the revealed key.
pub fn revealed_key_input(randomness: &Randomness, attempt: u32, epoch: u64) -> VrfInput {
vrf_input_from_data(
b"sassafras-revealed-v1.0",
[randomness.as_slice(), &attempt.to_le_bytes(), &epoch.to_le_bytes()],
)
let v =
[b"sassafras-ticket", randomness.as_slice(), &attempt.to_le_bytes(), &epoch.to_le_bytes()]
.concat();
VrfInput::new(&v[..])
}

/// Data to be signed via ring-vrf.
pub fn ticket_body_sign_data(ticket_body: &TicketBody, ticket_id_input: VrfInput) -> VrfSignData {
VrfSignData::new_unchecked(
b"sassafras-ticket-body-transcript-v1.0",
Some(ticket_body.encode().as_slice()),
Some(ticket_id_input),
)
VrfSignData { vrf_input: ticket_id_input, aux_data: ticket_body.encode() }
}

/// Make ticket-id from the given VRF input and pre-output.
/// Make ticket-id from the given VRF pre-output.
///
/// Input should have been obtained via [`ticket_id_input`].
/// Pre-output should have been obtained from the input directly using the vrf
/// secret key or from the vrf signature pre-outputs.
pub fn make_ticket_id(input: &VrfInput, pre_output: &VrfPreOutput) -> TicketId {
let bytes = pre_output.make_bytes::<16>(b"ticket-id", input);
/// secret key or from the vrf signature pre-output.
pub fn make_ticket_id(preout: &VrfPreOutput) -> TicketId {
let bytes: [u8; 16] = preout.make_bytes()[..16].try_into().unwrap();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why aren't we depending on input anymore?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because IETF vrf defines preout = input*secret.
Thus preout already depends on input

u128::from_le_bytes(bytes)
}

/// Make revealed key seed from a given VRF input and pre-output.
///
/// Input should have been obtained via [`revealed_key_input`].
/// Pre-output should have been obtained from the input directly using the vrf
/// secret key or from the vrf signature pre-outputs.
pub fn make_revealed_key_seed(input: &VrfInput, pre_output: &VrfPreOutput) -> [u8; 32] {
pre_output.make_bytes::<32>(b"revealed-seed", input)
}
Loading
Loading