Skip to content

Commit

Permalink
Stark: Implement Stone compatible grinding (#616)
Browse files Browse the repository at this point in the history
* implement stone strategy for grinding

* fmt

* clippy

* fmt

* update docs

* fix typo

* change test
  • Loading branch information
schouhy authored Oct 24, 2023
1 parent f940e14 commit f1f1341
Show file tree
Hide file tree
Showing 4 changed files with 144 additions and 68 deletions.
8 changes: 3 additions & 5 deletions docs/src/starks/protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
177 changes: 129 additions & 48 deletions provers/stark/src/grinding.rs
Original file line number Diff line number Diff line change
@@ -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<u64> {
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<u64> {
(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));
}
}
5 changes: 2 additions & 3 deletions provers/stark/src/prover.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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());
}
Expand Down
22 changes: 10 additions & 12 deletions provers/stark/src/verifier.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand All @@ -47,7 +47,7 @@ where
pub zetas: Vec<FieldElement<F>>,
pub iotas: Vec<usize>,
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<F> = (Vec<FieldElement<F>>, Vec<FieldElement<F>>);
Expand Down Expand Up @@ -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
Expand All @@ -199,7 +195,7 @@ pub trait IsStarkVerifier {
zetas,
iotas,
rap_challenges,
leading_zeros_count,
grinding_seed,
}
}

Expand Down Expand Up @@ -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;
}
Expand Down

0 comments on commit f1f1341

Please sign in to comment.