Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
97e2a01
feat: blob batching methods - nr only
MirandaWood Apr 15, 2025
52801e6
chore: fmt, more tests, rearranging
MirandaWood Apr 16, 2025
f655bf5
Merge remote-tracking branch 'origin/mw/blob-batching' into mw/blob-b…
MirandaWood Apr 21, 2025
c300c77
chore: renaming, cleanup
MirandaWood Apr 22, 2025
63ffe96
chore: add issue nums (hopefully force ci cache reset)
MirandaWood Apr 22, 2025
75d6d35
Merge remote-tracking branch 'origin/mw/blob-batching' into mw/blob-b…
MirandaWood Apr 22, 2025
5251cd1
feat: adding helpers, constants, docs, etc. for integration
MirandaWood Apr 24, 2025
ca0da9d
Merge remote-tracking branch 'origin/mw/blob-batching' into mw/blob-b…
MirandaWood May 7, 2025
d3ac058
chore: rename v -> blob_commitments_hash, move noir ref further up stack
MirandaWood May 13, 2025
1fc2c0d
Merge remote-tracking branch 'origin/mw/blob-batching' into mw/blob-b…
MirandaWood May 15, 2025
cda163a
chore: use updated methods from bignum, remove warnings
MirandaWood May 15, 2025
49c7be3
chore: switch bigcurve branch to remove visibility warnings
MirandaWood May 16, 2025
ee900fe
Merge remote-tracking branch 'origin/mw/blob-batching' into mw/blob-b…
MirandaWood May 16, 2025
55bc974
Merge remote-tracking branch 'origin/mw/blob-batching' into mw/blob-b…
MirandaWood May 20, 2025
cb146ae
fix: include is_inf in all serialization so recursion works
MirandaWood May 20, 2025
3398526
chore: update import
MirandaWood May 20, 2025
5f02ae1
feat: add point compression unit test
MirandaWood May 20, 2025
4c5c437
Merge remote-tracking branch 'origin/mw/blob-batching' into mw/blob-b…
MirandaWood May 22, 2025
8667c5c
Merge remote-tracking branch 'origin/mw/blob-batching' into mw/blob-b…
MirandaWood May 28, 2025
ff52662
chore: bump bignum
MirandaWood May 28, 2025
c75232b
feat: replace empty blob assumption
MirandaWood May 30, 2025
9e80ff2
feat: address some comments
MirandaWood May 30, 2025
ea19acd
chore: add extra check before blob acc init
MirandaWood Jun 2, 2025
92f6e5f
chore: renaming, bring down changes from integration branch, cleanup
MirandaWood Jun 2, 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
3 changes: 2 additions & 1 deletion noir-projects/noir-protocol-circuits/crates/blob/Nargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ authors = [""]
compiler_version = ">=0.30.0"

[dependencies]
bigint = { tag = "v0.6.0", git = "https://github.com/noir-lang/noir-bignum" }
bigint = { tag = "v0.6.1", git = "https://github.com/noir-lang/noir-bignum" }
bigcurve = { tag = "mw/bump", git = "https://github.com/noir-lang/noir_bigcurve" }
Comment thread
MirandaWood marked this conversation as resolved.
Outdated
types = { path = "../types" }
17 changes: 13 additions & 4 deletions noir-projects/noir-protocol-circuits/crates/blob/src/blob.nr
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ use types::{
utils::arrays::array_splice,
};

fn convert_blob_fields(blob_as_fields: [Field; FIELDS_PER_BLOB]) -> [F; FIELDS_PER_BLOB] {
// TODO(MW): remove pub when fully moved to batching
pub(crate) fn convert_blob_fields(
blob_as_fields: [Field; FIELDS_PER_BLOB],
) -> [F; FIELDS_PER_BLOB] {
let mut blob: [F; FIELDS_PER_BLOB] = [F::zero(); FIELDS_PER_BLOB];
for i in 0..FIELDS_PER_BLOB {
blob[i] = F::from(blob_as_fields[i]);
Expand Down Expand Up @@ -42,7 +45,11 @@ pub fn check_block_blob_sponge(
sponge_hash
}

fn compute_challenge(hashed_blobs_fields: Field, kzg_commitment: BlobCommitment) -> Field {
// TODO(MW): remove pub when fully moved to batching
pub(crate) fn compute_challenge(
hashed_blobs_fields: Field,
kzg_commitment: BlobCommitment,
) -> Field {
let preimage = [hashed_blobs_fields, kzg_commitment.inner[0], kzg_commitment.inner[1]];
let challenge = std::hash::poseidon2::Poseidon2::hash(preimage, 3);
challenge
Expand All @@ -52,7 +59,8 @@ fn compute_challenge(hashed_blobs_fields: Field, kzg_commitment: BlobCommitment)
// we don't actually need to operate on it so we've simply encoded it as fitting inside a
// [Field; 2], since two 254-bit fields more-than covers 381+1=382 bits.
// See yarn-project/foundation/src/blob/index.ts -> commitmentToFields() for encoding
fn evaluate_blob(
// TODO(MW): remove pub when fully moved to batching
pub(crate) fn evaluate_blob(
blob_as_fields: [Field; FIELDS_PER_BLOB],
kzg_commitment: BlobCommitment,
hashed_blobs_fields: Field,
Expand Down Expand Up @@ -112,8 +120,9 @@ pub fn evaluate_blobs(
* @param ys - the many y_i's of the blob.
*
* @return y = p(z)
* TODO(MW): remove pub when fully moved to batching
*/
fn barycentric_evaluate_blob_at_z(z: F, ys: [F; FIELDS_PER_BLOB]) -> F {
pub(crate) fn barycentric_evaluate_blob_at_z(z: F, ys: [F; FIELDS_PER_BLOB]) -> F {
// Note: it's more efficient (saving 30k constraints) to compute:
// ___d-1
// \ / y_i \
Expand Down
311 changes: 311 additions & 0 deletions noir-projects/noir-protocol-circuits/crates/blob/src/blob_batching.nr
Original file line number Diff line number Diff line change
@@ -0,0 +1,311 @@
use crate::{
blob::{
barycentric_evaluate_blob_at_z, check_block_blob_sponge, compute_challenge,
convert_blob_fields,
},
blob_batching_public_inputs::{
BatchingBlobCommitment, BlobAccumulationInputs, BlobPublicInputsAcc,
compress_to_blob_commitment,
},
blob_public_inputs::BlobCommitment,
};
use bigcurve::curves::bls12_381::BLS12_381 as BLSPoint;
use bigint::{BigNumTrait, BLS12_381_Fr as F};
use types::{
abis::sponge_blob::SpongeBlob,
constants::{BLOBS_PER_BLOCK, FIELDS_PER_BLOB},
traits::is_empty,
utils::arrays::array_splice,
};

// Evaluates a single blob:
// - Evaluates the blob at shared challenge z and returns result y_i
// - Calculates this blob's challenge z_i (= H(H(blob_i), C_i))
fn evaluate_blob_for_batching(
blob_as_fields: [Field; FIELDS_PER_BLOB],
kzg_commitment: BatchingBlobCommitment,
Comment thread
MirandaWood marked this conversation as resolved.
hashed_blobs_fields: Field,
challenge_z: Field,
) -> (Field, F) {
let challenge_z_as_bignum = F::from(challenge_z);
let blob = convert_blob_fields(blob_as_fields);

let y_i: F = barycentric_evaluate_blob_at_z(challenge_z_as_bignum, blob);
let z_i: Field = compute_challenge(
hashed_blobs_fields,
// TODO(MW): At some point BatchingBlobCommitment will replace BlobCommitment and we won't need this silly conversion
BlobCommitment { inner: kzg_commitment.to_compressed_fields() },
);

(z_i, y_i)
}

// Evaluates each blob required for a block:
// - Hashes all fields in the block's blobs (to use for the challenges z_i)
// - Compresses each of the blob's injected commitments (")
// - Evaluates each blob individually to find its challenge z_i & evaluation y_i
// - Updates the batched blob accumulator
pub fn evaluate_blobs_and_batch(
blobs_as_fields: [Field; FIELDS_PER_BLOB * BLOBS_PER_BLOCK],
kzg_commitments_points: [BLSPoint; BLOBS_PER_BLOCK],
mut sponge_blob: SpongeBlob,
challenge_z: Field, // shared over epoch, verified in root
challenge_gamma: F, // shared over epoch, verified in root
start_accumulator: BlobPublicInputsAcc,
) -> BlobPublicInputsAcc {
// See components.nr out_sponge definition as to why we copy here:
let mut end_accumulator = start_accumulator;
// Note that with multiple blobs per block, each blob uses the same hashed_blobs_fields in:
// z_i = H(hashed_blobs_fields, kzg_commitment[0], kzg_commitment[1])
// This is ok, because each commitment is unique to the blob, and we need hashed_blobs_fields to encompass
// all fields in the blob, which it does.
let hashed_blobs_fields = check_block_blob_sponge(blobs_as_fields, sponge_blob);
for i in 0..BLOBS_PER_BLOCK {
let single_blob_fields = array_splice(blobs_as_fields, i * FIELDS_PER_BLOB);
Comment thread
iAmMichaelConnor marked this conversation as resolved.
let c_i = compress_to_blob_commitment(kzg_commitments_points[i]);
let (z_i, y_i) =
evaluate_blob_for_batching(single_blob_fields, c_i, hashed_blobs_fields, challenge_z);
if !(y_i.is_zero()) & !(single_blob_fields[0] == 0) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

(single_blob_fields[0] != 0) might be easier to read?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why do we skip the accumulation when the first field is 0? Isn't it possible that the first field can be 0 and followed by non-zero values? Like when the offset falls on the log data that is 0.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This was copied over from the old blob methods which I think are now outdated (when I started on batching IIRC we did not include 0s at all?) - good catch! I will edit this, I think y_i being 0 may be sufficient to ensure we have an empty blob but I'll think on it

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'm actually not sure whether y_i being 0 is enough to say the blob is empty - for example it could be possible for a blob to have two items which end up having coefficients in the polynomial which are negatives of each other. I'm not sure it's even possible for a user to actually inject many blob fields since they are mostly hashes, but to be on the safe side I added a check on the number of fields in the sponge.
Unfortunately it is a range check, so relatively expensive! However if it is required, we could in future reduce the number of gates by commandeering the existing check in check_block_blob_sponge's hash that we haven't added more blob elements than we claim. If during that check we pass i * FIELDS_PER_BLOB and are above the number of fields in the blob, we could return a flag to say 'blob i is empty'. But I'd rather avoid adding that if y_i being 0 is sufficient then ending up having to remove it!
(Sorry for the essay!!)

Comment thread
MirandaWood marked this conversation as resolved.
Outdated
// Only accumulate if the blob is non empty
if is_empty(end_accumulator) {
// TODO(MW): move this if is_empty() statement to .accumulate()?
Comment thread
MirandaWood marked this conversation as resolved.
Outdated
// Init only if accumulator is empty:
// - This will be checked in root, where the left input's start acc will be constrained to be zero
// - No other accs can be zero since each block_merge checks left's end acc == right's start acc
end_accumulator = BlobPublicInputsAcc::init(
BlobAccumulationInputs { z_i, y_i, c_i },
challenge_gamma,
);
} else {
end_accumulator = end_accumulator.accumulate(
BlobAccumulationInputs { z_i, y_i, c_i },
challenge_gamma,
);
}
Comment thread
iAmMichaelConnor marked this conversation as resolved.
}
}
end_accumulator
}

mod tests {
use crate::{
blob_batching::evaluate_blobs_and_batch,
blob_batching_public_inputs::{BlobPublicInputsAcc, compress_to_blob_commitment},
};
use bigcurve::{BigCurveTrait, curves::bls12_381::BLS12_381 as BLSPoint};
use bigint::{BigNumTrait, BLS12_381_Fq as Q, BLS12_381_Fr as F};
use types::{
abis::sponge_blob::SpongeBlob,
constants::{BLOBS_PER_BLOCK, FIELDS_PER_BLOB},
hash::sha256_to_field,
tests::utils::pad_end,
traits::Empty,
};

// All hardcoded values in this test are taken from yarn-project/foundation/src/blob/blob_batching.test.ts -> 'should construct and verify a batched blob of 400 items'
#[test]
unconstrained fn test_400_batched() {
// We evaluate 1 blob of 400 items using the batch methods.
// This ensures a block with a single blob will work:
let mut blob: [Field; FIELDS_PER_BLOB] = [0; FIELDS_PER_BLOB];
for i in 0..400 {
blob[i] = 3;
}
let mut sponge_blob = SpongeBlob::new(400);
sponge_blob.absorb(blob, 400);
let kzg_commitments_in = [
BLSPoint {
x: Q {
limbs: [
0xa971c7e8d8292be943d05bccebcfea,
0xcddefc3721a54895a7a45e77504dd1,
0x5fe972914ba3616033e2748bbaa6db,
0x12803d,
],
},
y: Q {
limbs: [
0x71bde5210b6cae1530202c8a928127,
0x5e7d987fb4afc5bcee960c6fc0628c,
0x64801e9aff2901eb6916e65c51f280,
0x1996ca,
],
},
is_infinity: false,
},
BLSPoint::point_at_infinity(),
BLSPoint::point_at_infinity(),
];
// = z_0
let final_z = 0x135d767e8b86b949d264be7a6b71d257c538893f3cef60c95d76ba420df18c3c;
// = H(y_0, z_0)
let final_gamma = F {
limbs: [0xda0ebb0c577c62d5954852cf7a8863, 0xaac05db8dabf148f011d29f2d308e4, 0x0b28],
};
// Evaluation
let res = evaluate_blobs_and_batch(
pad_end(blob, 0),
kzg_commitments_in,
sponge_blob,
final_z,
final_gamma,
BlobPublicInputsAcc::empty(),
);
let final_acc = res.finalize();

assert_eq(final_acc.z, final_z);
assert_eq(F::from(final_acc.gamma), final_gamma);
// Since i = 1, gamma_pow = gamma^1 = gamma:
assert_eq(final_acc.gamma_pow, final_gamma);

// y is a BLS field with value 0x212c4f0c0ee5e7dd037110686a4639d191dde7b57ab99b51e4b06e7d827b6c4c
let expected_y: F = F {
limbs: [0xdde7b57ab99b51e4b06e7d827b6c4c, 0x4f0c0ee5e7dd037110686a4639d191, 0x212c],
};
assert_eq(final_acc.y, expected_y);

// Since i = 1, v is just the sha256 hash of the single (compressed) commitment
let expected_v = sha256_to_field(
compress_to_blob_commitment(kzg_commitments_in[0]).compressed,
);
assert_eq(final_acc.v, expected_v);

// Since i = 1, C = gamma^0 * C_0 = C_0
assert_eq(final_acc.c, kzg_commitments_in[0]);
}

// All hardcoded values in this test are taken from yarn-project/foundation/src/blob/blob_batching.test.ts -> 'should construct and verify a batch of 3 full blobs'
#[test]
unconstrained fn test_full_blobs_batched() {
// Fill three blobs completely with different values (to avoid a constant polynomial)
let mut blob: [Field; FIELDS_PER_BLOB * BLOBS_PER_BLOCK] =
[0; FIELDS_PER_BLOB * BLOBS_PER_BLOCK];
for j in 0..BLOBS_PER_BLOCK {
for i in 0..FIELDS_PER_BLOB {
blob[j * FIELDS_PER_BLOB + i] = ((j + 3) * (i + 1)) as Field;
}
}
// Absorb the values into a sponge
let mut sponge_blob = SpongeBlob::new(FIELDS_PER_BLOB * BLOBS_PER_BLOCK);
sponge_blob.absorb(blob, FIELDS_PER_BLOB * BLOBS_PER_BLOCK);
// Init. injected values:
// - Commitments are injected and checked for correctness on L1 via acc.v
let kzg_commitments_in = [
BLSPoint {
x: Q {
limbs: [
0x2627fc88755984d7f002e5ef0e6b3e,
0x0ea98f6a26672e17f919eb020b00ee,
0xea6e5173f2ef1bedbb07bfa9ac6ed8,
0x01c6e6,
],
},
y: Q {
limbs: [
0xaa96f04ba1d419683f218cc4f15a3f,
0x8887c5e719583b765309e4b3d18752,
0x9ff512de37b8582f7167fdfbb29539,
0x18f531,
],
},
is_infinity: false,
},
BLSPoint {
x: Q {
limbs: [
0x71556bb7217816fbb3f822fc873740,
0x9c57d93d7fd33a388e13e95cfdba95,
0x4f0ddbdc9d6a3653cd825ebd9f5730,
0x12324e,
],
},
y: Q {
limbs: [
0xabf9f60fc773ef1802a706d6b170a4,
0x788f9000166d54151ac05df44e63be,
0x0a8b45ead129885bb12837fb59033b,
0x12aadd,
],
},
is_infinity: false,
},
BLSPoint {
x: Q {
limbs: [
0x69afb94a09e713e7fb94e26f33c3ed,
0x8161293f65480c3b7bad57aaef1984,
0xc34d68dc32d1ecd46f46ec4c969bb1,
0x0d97ef,
],
},
y: Q {
limbs: [
0x9837a79d9fa4d0370198419b273360,
0x9e7340f07732e2cb3d51db22b1dcb3,
0x8285e8cad42f634bb51ad7d2c68a12,
0x07db3c,
],
},
is_infinity: false,
},
];
// - The final z value is injected and checked for correctness in root (see below final_acc)
let final_z = 0x02d6a54e591ada73e5eea35188a02ac87779f4293ea3e7d675fa50ae7ff332ce;
// - The final gamma value is injected and checked for correctness in root (see below final_acc)
let final_gamma = F {
limbs: [0x281287a8d44071d216177e06a02327, 0x16571aa3dcfef75c2447c705c6c68a, 0x16f2],
};
// Init. the accumulator
let start_acc = BlobPublicInputsAcc::empty();
// Evaluate all three blobs and iteratively accumulate the results
let output = evaluate_blobs_and_batch(
blob,
kzg_commitments_in,
sponge_blob,
final_z,
final_gamma,
start_acc,
);
// Finalize the output (actually done in the root circuit)
let final_acc = output.finalize();

assert_eq(final_acc.z, final_z);
assert_eq(F::from(final_acc.gamma), final_gamma);
assert_eq(final_acc.gamma_pow, final_gamma.__pow(F::from(BLOBS_PER_BLOCK as Field)));

// y is a BLS Fr field with value 0x0cd2fd9a46ba70fd7f212d08ec7283024b0b1ff9446b1f78a482fb7443e49b57
let expected_y = F {
limbs: [0x0b1ff9446b1f78a482fb7443e49b57, 0xfd9a46ba70fd7f212d08ec7283024b, 0x0cd2],
};

// C is a BLS point with value:
// x: 0x0f2f5f62cc6c3ab4c1ac1abcb9da9677e12796a76064f68c0d4f659f25a046a6d42616100269935afcb1b98c85d5e93e,
// y: 0x0af1e4abfa449daf65201c2b24507b1058d8ea9bf82ff948a1d01912615c4a8e507160da282e6c41bab917c868923254,
let expected_c = BLSPoint {
x: Q {
limbs: [
0x2616100269935afcb1b98c85d5e93e,
0x96a76064f68c0d4f659f25a046a6d4,
0x62cc6c3ab4c1ac1abcb9da9677e127,
0x0f2f5f,
],
},
y: Q {
limbs: [
0x7160da282e6c41bab917c868923254,
0xea9bf82ff948a1d01912615c4a8e50,
0xabfa449daf65201c2b24507b1058d8,
0x0af1e4,
],
},
is_infinity: false,
};

// y is a BN Fr field with value 0x00d2f7bffbc5a9008207a188e348e753087f54557a686efd7f74c90cac52a9a1
let expected_v = 0xd2f7bffbc5a9008207a188e348e753087f54557a686efd7f74c90cac52a9a1;

assert_eq(final_acc.y, expected_y);
assert_eq(final_acc.c, expected_c);
assert_eq(final_acc.v, expected_v);
}
}
Loading