From f1f1341bc3efd63e3dcd3a738596d12a65bbf3fd Mon Sep 17 00:00:00 2001 From: Sergio Chouhy <41742639+schouhy@users.noreply.github.com> Date: Tue, 24 Oct 2023 11:01:19 -0300 Subject: [PATCH] Stark: Implement Stone compatible grinding (#616) * implement stone strategy for grinding * fmt * clippy * fmt * update docs * fix typo * change test --- docs/src/starks/protocol.md | 8 +- provers/stark/src/grinding.rs | 177 +++++++++++++++++++++++++--------- provers/stark/src/prover.rs | 5 +- provers/stark/src/verifier.rs | 22 ++--- 4 files changed, 144 insertions(+), 68 deletions(-) diff --git a/docs/src/starks/protocol.md b/docs/src/starks/protocol.md index 1fb4c5626..679e04909 100644 --- a/docs/src/starks/protocol.md +++ b/docs/src/starks/protocol.md @@ -4,15 +4,13 @@ In this section we describe precisely the STARKs protocol used in Lambdaworks. We begin with some additional considerations and notation for most of the relevant objects and values to refer to them later on. +### Grinding +This is a technique to increase the soundness of the protocol by adding proof of work. It works as follows. At some fixed point in the protocol, the prover needs to find a string `nonce` such that `H(H(prefix || state || grinding_factor) || nonce)` has `grinding_factor` number of zeros to the left, where `H` is a hash function, `prefix` is the bit-string `0x0123456789abcded` and `state` is the state of the transcript. Here `x || y` denotes the concatenation of the bit-strings `x` and `y`. + ### Transcript The Fiat-Shamir heuristic is used to make the protocol noninteractive. We assume there is a transcript object to which values can be added and from which challenges can be sampled. -### Grinding -This is a technique to increase the soundness of the protocol by adding proof of work. It works as follows. At some fixed point in the protocol, a value $x$ is derived in a deterministic way from all the interactions between the prover and the verifier up to that point (the state of the transcript). The prover needs to find a string $y$ such that $H(x || y)$ begins with a predefined number of zeroes. Here $x || y$ denotes the concatenation of $x$ and $y$, seen as bit strings. -The number of zeroes is called the *grinding factor*. The hash function $H$ can be any hash function, independent of other hash functions used in the rest of the protocol. In Lambdaworks we use Keccak256. - - ## General notation - $\mathbb{F}$ denotes a finite field. diff --git a/provers/stark/src/grinding.rs b/provers/stark/src/grinding.rs index bf3871ea3..85ad84b26 100644 --- a/provers/stark/src/grinding.rs +++ b/provers/stark/src/grinding.rs @@ -1,78 +1,159 @@ use sha3::{Digest, Keccak256}; -/// Build data with the concatenation of transcript hash and value. -/// Computes the hash of this element and returns the number of -/// leading zeros in the resulting value (in the big-endian representation). +const PREFIX: [u8; 8] = [0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xed]; + +/// Checks if the bit-string `Hash(Hash(prefix || seed || grinding_factor) || nonce)` +/// has at least `grinding_factor` zeros to the left. +/// `prefix` is the bit-string `0x123456789abcded` +/// +/// # Parameters +/// +/// * `seed`: the input seed, +/// * `nonce`: the value to be tested, +/// * `grinding_factor`: the number of leading zeros needed. +/// +/// # Returns +/// +/// `true` if the number of leading zeros is at least `grinding_factor`, and `false` otherwise. +pub fn is_valid_nonce(seed: &[u8; 32], nonce: u64, grinding_factor: u8) -> bool { + let inner_hash = get_inner_hash(seed, grinding_factor); + let limit = 1 << (64 - grinding_factor); + is_valid_nonce_for_inner_hash(&inner_hash, nonce, limit) +} + +/// Performs grinding, returning a new nonce for the proof. +/// The nonce generated is such that: +/// Hash(Hash(prefix || seed || grinding_factor) || nonce) has at least `grinding_factor` zeros +/// to the left. +/// `prefix` is the bit-string `0x123456789abcded` /// /// # Parameters /// -/// * `transcript_challenge` - the hash value obtained from the transcript -/// * `value` - the value to be concatenated with the transcript hash -/// (i.e. a candidate nonce). +/// * `seed`: the input seed, +/// * `grinding_factor`: the number of leading zeros needed. /// /// # Returns /// -/// The number of leading zeros in the resulting hash value. +/// A `nonce` satisfying the required condition. +pub fn generate_nonce(seed: &[u8; 32], grinding_factor: u8) -> Option { + let inner_hash = get_inner_hash(seed, grinding_factor); + let limit = 1 << (64 - grinding_factor); + (0..u64::MAX) + .find(|&candidate_nonce| is_valid_nonce_for_inner_hash(&inner_hash, candidate_nonce, limit)) +} + +/// Checks if the leftmost 8 bytes of `Hash(inner_hash || candidate_nonce)` are less than `limit` +/// when interpreted as `u64`. #[inline(always)] -pub fn hash_transcript_with_int_and_get_leading_zeros( - transcript_challenge: &[u8; 32], - value: u64, -) -> u8 { +fn is_valid_nonce_for_inner_hash(inner_hash: &[u8; 32], candidate_nonce: u64, limit: u64) -> bool { let mut data = [0; 40]; - data[..32].copy_from_slice(transcript_challenge); - data[32..].copy_from_slice(&value.to_le_bytes()); + data[..32].copy_from_slice(inner_hash); + data[32..].copy_from_slice(&candidate_nonce.to_be_bytes()); let digest = Keccak256::digest(data); let seed_head = u64::from_be_bytes(digest[..8].try_into().unwrap()); - seed_head.trailing_zeros() as u8 + seed_head < limit } -/// Performs grinding, generating a new nonce for the proof. -/// The nonce generated is such that: -/// Hash(transcript_hash || nonce) has a number of leading zeros -/// greater or equal than `grinding_factor`. -/// -/// # Parameters -/// -/// * `transcript` - the hash of the transcript -/// * `grinding_factor` - the number of leading zeros needed -pub fn generate_nonce_with_grinding( - transcript_challenge: &[u8; 32], - grinding_factor: u8, -) -> Option { - (0..u64::MAX).find(|&candidate_nonce| { - hash_transcript_with_int_and_get_leading_zeros(transcript_challenge, candidate_nonce) - >= grinding_factor - }) +/// Returns the bit-string constructed as +/// Hash(prefix || seed || grinding_factor) +/// `prefix` is the bit-string `0x123456789abcded` +fn get_inner_hash(seed: &[u8; 32], grinding_factor: u8) -> [u8; 32] { + let mut inner_data = [0u8; 41]; + inner_data[0..8].copy_from_slice(&PREFIX); + inner_data[8..40].copy_from_slice(seed); + inner_data[40] = grinding_factor; + + let digest = Keccak256::digest(inner_data); + digest[..32].try_into().unwrap() } #[cfg(test)] mod test { - use sha3::{Digest, Keccak256}; + use crate::grinding::is_valid_nonce; #[test] - fn hash_transcript_with_int_and_get_leading_zeros_works() { - let transcript_challenge = [ - 226_u8, 27, 133, 168, 62, 203, 20, 59, 122, 230, 227, 33, 76, 44, 53, 150, 200, 45, - 136, 162, 249, 239, 142, 90, 204, 191, 45, 4, 53, 22, 103, 240, + fn test_invalid_nonce_grinding_factor_6() { + // This setting produces a hash with 5 leading zeros, therefore not enough for grinding + // factor 6. + let seed = [ + 174, 187, 26, 134, 6, 43, 222, 151, 140, 48, 52, 67, 69, 181, 177, 165, 111, 222, 148, + 92, 130, 241, 171, 2, 62, 34, 95, 159, 37, 116, 155, 217, ]; - let grinding_factor = 10; + let nonce = 4; + let grinding_factor = 6; + assert!(!is_valid_nonce(&seed, nonce, grinding_factor)); + } - let nonce = - super::generate_nonce_with_grinding(&transcript_challenge, grinding_factor).unwrap(); - assert_eq!(nonce, 33); + #[test] + fn test_invalid_nonce_grinding_factor_9() { + // This setting produces a hash with 8 leading zeros, therefore not enough for grinding + // factor 9. + let seed = [ + 174, 187, 26, 134, 6, 43, 222, 151, 140, 48, 52, 67, 69, 181, 177, 165, 111, 222, 148, + 92, 130, 241, 171, 2, 62, 34, 95, 159, 37, 116, 155, 217, + ]; + let nonce = 287; + let grinding_factor = 9; + assert!(!is_valid_nonce(&seed, nonce, grinding_factor)); + } - // check generated hash has more trailing_zeros than grinding_factor - let mut data = [0; 40]; - data[..32].copy_from_slice(&transcript_challenge); - data[32..].copy_from_slice(&nonce.to_le_bytes()); + #[test] + fn test_is_valid_nonce_grinding_factor_10() { + let seed = [ + 37, 68, 26, 150, 139, 142, 66, 175, 33, 47, 199, 160, 9, 109, 79, 234, 135, 254, 39, + 11, 225, 219, 206, 108, 224, 165, 25, 72, 189, 96, 218, 95, + ]; + let nonce = 0x5ba; + let grinding_factor = 10; + assert!(is_valid_nonce(&seed, nonce, grinding_factor)); + } - let digest = Keccak256::digest(data); + #[test] + fn test_is_valid_nonce_grinding_factor_20() { + let seed = [ + 37, 68, 26, 150, 139, 142, 66, 175, 33, 47, 199, 160, 9, 109, 79, 234, 135, 254, 39, + 11, 225, 219, 206, 108, 224, 165, 25, 72, 189, 96, 218, 95, + ]; + let nonce = 0x2c5db8; + let grinding_factor = 20; + assert!(is_valid_nonce(&seed, nonce, grinding_factor)); + } - let seed_head = u64::from_be_bytes(digest[..8].try_into().unwrap()); - let trailing_zeors = seed_head.trailing_zeros() as u8; + #[test] + fn test_invalid_nonce_grinding_factor_19() { + // This setting would pass for grinding factor 20 instead of 19. The nonce is invalid + // here because the grinding factor is part of the inner hash, changing the outer hash + // and the resulting number of leading zeros. + let seed = [ + 37, 68, 26, 150, 139, 142, 66, 175, 33, 47, 199, 160, 9, 109, 79, 234, 135, 254, 39, + 11, 225, 219, 206, 108, 224, 165, 25, 72, 189, 96, 218, 95, + ]; + let nonce = 0x2c5db8; + let grinding_factor = 19; + assert!(!is_valid_nonce(&seed, nonce, grinding_factor)); + } - assert!(trailing_zeors >= grinding_factor); + #[test] + fn test_is_valid_nonce_grinding_factor_30() { + let seed = [ + 37, 68, 26, 150, 139, 142, 66, 175, 33, 47, 199, 160, 9, 109, 79, 234, 135, 254, 39, + 11, 225, 219, 206, 108, 224, 165, 25, 72, 189, 96, 218, 95, + ]; + let nonce = 0x1ae839e1; + let grinding_factor = 30; + assert!(is_valid_nonce(&seed, nonce, grinding_factor)); + } + + #[test] + fn test_is_valid_nonce_grinding_factor_33() { + let seed = [ + 37, 68, 26, 150, 139, 142, 66, 175, 33, 47, 199, 160, 9, 109, 79, 234, 135, 254, 39, + 11, 225, 219, 206, 108, 224, 165, 25, 72, 189, 96, 218, 95, + ]; + let nonce = 0x4cc3123f; + let grinding_factor = 33; + assert!(is_valid_nonce(&seed, nonce, grinding_factor)); } } diff --git a/provers/stark/src/prover.rs b/provers/stark/src/prover.rs index 4f4a702f1..fda9cc829 100644 --- a/provers/stark/src/prover.rs +++ b/provers/stark/src/prover.rs @@ -26,7 +26,7 @@ use super::constraints::evaluator::ConstraintEvaluator; use super::domain::Domain; use super::frame::Frame; use super::fri::fri_decommit::FriDecommitment; -use super::grinding::generate_nonce_with_grinding; +use super::grinding; use super::proof::options::ProofOptions; use super::proof::stark::{DeepPolynomialOpening, StarkProof}; use super::trace::TraceTable; @@ -398,8 +398,7 @@ pub trait IsStarkProver { let security_bits = air.context().proof_options.grinding_factor; let mut nonce = 0; if security_bits > 0 { - let transcript_challenge = transcript.state(); - nonce = generate_nonce_with_grinding(&transcript_challenge, security_bits) + nonce = grinding::generate_nonce(&transcript.state(), security_bits) .expect("nonce not found"); transcript.append_bytes(&nonce.to_be_bytes()); } diff --git a/provers/stark/src/verifier.rs b/provers/stark/src/verifier.rs index f36cceee9..88539d9f6 100644 --- a/provers/stark/src/verifier.rs +++ b/provers/stark/src/verifier.rs @@ -23,7 +23,7 @@ use super::{ config::BatchedMerkleTreeBackend, domain::Domain, fri::fri_decommit::FriDecommitment, - grinding::hash_transcript_with_int_and_get_leading_zeros, + grinding, proof::{options::ProofOptions, stark::StarkProof}, traits::AIR, }; @@ -47,7 +47,7 @@ where pub zetas: Vec>, pub iotas: Vec, pub rap_challenges: A::RAPChallenges, - pub leading_zeros_count: u8, // number of leading zeros in the grinding + pub grinding_seed: [u8; 32], } pub type DeepPolynomialEvaluations = (Vec>, Vec>); @@ -174,15 +174,11 @@ pub trait IsStarkVerifier { transcript.append_field_element(&proof.fri_last_value); // Receive grinding value - // 1) Receive challenge from the transcript let security_bits = air.context().proof_options.grinding_factor; - let mut leading_zeros_count = 0; + let mut grinding_seed = [0u8; 32]; if security_bits > 0 { - let transcript_challenge = transcript.state(); - let nonce = proof.nonce; - leading_zeros_count = - hash_transcript_with_int_and_get_leading_zeros(&transcript_challenge, nonce); - transcript.append_bytes(&nonce.to_be_bytes()); + grinding_seed = transcript.state(); + transcript.append_bytes(&proof.nonce.to_be_bytes()); } // FRI query phase @@ -199,7 +195,7 @@ pub trait IsStarkVerifier { zetas, iotas, rap_challenges, - leading_zeros_count, + grinding_seed, } } @@ -688,8 +684,10 @@ pub trait IsStarkVerifier { ); // verify grinding - let grinding_factor = air.context().proof_options.grinding_factor; - if challenges.leading_zeros_count < grinding_factor { + let security_bits = air.context().proof_options.grinding_factor; + if security_bits > 0 + && !grinding::is_valid_nonce(&challenges.grinding_seed, proof.nonce, security_bits) + { error!("Grinding factor not satisfied"); return false; }