Update DA encoding/verifier to v1.1

This commit is contained in:
mgonen 2025-05-20 13:45:53 +03:00
parent 2c5c3860f0
commit 5cb2f9223c
3 changed files with 77 additions and 143 deletions

View File

@ -1,5 +1,5 @@
from dataclasses import dataclass
from hashlib import sha3_256
from hashlib import sha256
from itertools import chain, zip_longest, compress
from typing import List, Generator, Self, Sequence
@ -41,9 +41,8 @@ class Bitfield(List[bool]):
pass
def build_blob_id(aggregated_column_commitment: Commitment, row_commitments: Sequence[Commitment]) -> BlobId:
hasher = sha3_256()
hasher.update(bytes(aggregated_column_commitment))
def build_blob_id(row_commitments: Sequence[Commitment]) -> BlobId:
hasher = sha256()
for c in row_commitments:
hasher.update(bytes(c))
return hasher.digest()

View File

@ -1,5 +1,5 @@
from dataclasses import dataclass
from itertools import batched, chain
from itertools import batched
from typing import List, Sequence, Tuple
from hashlib import blake2b
@ -10,6 +10,8 @@ from da.kzg_rs import kzg, rs
from da.kzg_rs.common import GLOBAL_PARAMETERS, ROOTS_OF_UNITY, BYTES_PER_FIELD_ELEMENT
from da.kzg_rs.poly import Polynomial
# Domain separation tag
_DST = b"NOMOS_DA_V1"
@dataclass
class DAEncoderParams:
@ -23,10 +25,7 @@ class EncodedData:
chunked_data: ChunksMatrix
extended_matrix: ChunksMatrix
row_commitments: List[Commitment]
row_proofs: List[List[Proof]]
column_commitments: List[Commitment]
aggregated_column_commitment: Commitment
aggregated_column_proofs: List[Proof]
combined_column_proofs: List[Proof]
class DAEncoder:
@ -65,72 +64,44 @@ class DAEncoder:
)
return ChunksMatrix(__rs_encode_row(row) for row in chunks_matrix)
def _derive_challenge(self, commitments: Sequence[Commitment]) -> BLSFieldElement:
h = blake2b(digest_size=31)
h.update(_DST)
for com in commitments:
h.update(bytes(com))
digest31 = h.digest()
# pad to 32 bytes
return BLSFieldElement.from_bytes(digest31 + b'\x00')
@staticmethod
def _compute_rows_proofs(
chunks_matrix: ChunksMatrix,
polynomials: Sequence[Polynomial],
row_commitments: Sequence[Commitment]
) -> List[List[Proof]]:
proofs = []
for row, poly, commitment in zip(chunks_matrix, polynomials, row_commitments):
proofs.append(
[
kzg.generate_element_proof(i, poly, GLOBAL_PARAMETERS, ROOTS_OF_UNITY)
for i in range(len(row))
]
)
return proofs
def _combined_polynomial(
polys: Sequence[Polynomial], h: BLSFieldElement
) -> Polynomial:
combined = Polynomial.zero()
power = BLSFieldElement(1)
for poly in polys:
combined = combined + poly * int(power)
power = power * h
return combined
def _compute_column_kzg_commitments(self, chunks_matrix: ChunksMatrix) -> List[Tuple[Polynomial, Commitment]]:
return self._compute_row_kzg_commitments(chunks_matrix.transposed())
@staticmethod
def _compute_aggregated_column_commitment(
column_commitments: Sequence[Commitment]
) -> Tuple[Polynomial, Commitment]:
data = bytes(chain.from_iterable(
DAEncoder.hash_commitment_blake2b31(commitment)
for commitment in column_commitments
))
return kzg.bytes_to_commitment(data, GLOBAL_PARAMETERS)
@staticmethod
def _compute_aggregated_column_proofs(
polynomial: Polynomial,
column_commitments: Sequence[Commitment],
) -> List[Proof]:
def _compute_combined_column_proofs(self, combined_poly: Polynomial) -> List[Proof]:
total_cols = self.params.column_count * 2
return [
kzg.generate_element_proof(i, polynomial, GLOBAL_PARAMETERS, ROOTS_OF_UNITY)
for i in range(len(column_commitments))
kzg.generate_element_proof(j, combined_poly, GLOBAL_PARAMETERS, ROOTS_OF_UNITY)
for j in range(total_cols)
]
def encode(self, data: bytes) -> EncodedData:
chunks_matrix = self._chunkify_data(data)
row_polynomials, row_commitments = zip(*self._compute_row_kzg_commitments(chunks_matrix))
extended_matrix = self._rs_encode_rows(chunks_matrix)
row_proofs = self._compute_rows_proofs(extended_matrix, row_polynomials, row_commitments)
column_polynomials, column_commitments = zip(*self._compute_column_kzg_commitments(extended_matrix))
aggregated_column_polynomial, aggregated_column_commitment = (
self._compute_aggregated_column_commitment(column_commitments)
)
aggregated_column_proofs = self._compute_aggregated_column_proofs(
aggregated_column_polynomial, column_commitments
)
h = self._derive_challenge(row_commitments)
combined_poly = self._combined_polynomial(row_polynomials, h)
combined_column_proofs = self._compute_combined_column_proofs(combined_poly)
result = EncodedData(
data,
chunks_matrix,
extended_matrix,
row_commitments,
row_proofs,
column_commitments,
aggregated_column_commitment,
aggregated_column_proofs
combined_column_proofs
)
return result
@staticmethod
def hash_commitment_blake2b31(commitment: Commitment) -> bytes:
return (
# digest size must be 31 bytes as we cannot encode 32 without risking overflowing the BLS_MODULUS
int.from_bytes(blake2b(bytes(commitment), digest_size=31).digest())
).to_bytes(32, byteorder="big") # rewrap into 32 padded bytes for the field elements, EC library dependant

View File

@ -1,7 +1,6 @@
from dataclasses import dataclass
from hashlib import sha3_256
from typing import List, Sequence, Set
from hashlib import blake2b
from eth2spec.deneb.mainnet import BLSFieldElement
from eth2spec.eip7594.mainnet import (
KZGCommitment as Commitment,
@ -9,98 +8,63 @@ from eth2spec.eip7594.mainnet import (
)
import da.common
from da.common import Column, Chunk, BlobId
from da.encoder import DAEncoder
from da.common import Column, Chunk, BlobId, build_blob_id
from da.kzg_rs import kzg
from da.kzg_rs.common import ROOTS_OF_UNITY, GLOBAL_PARAMETERS, BLS_MODULUS
# Domain separation tag
_DST = b"NOMOS_DA_V1"
@dataclass
class DABlob:
class DAShare:
column: Column
column_idx: int
column_commitment: Commitment
aggregated_column_commitment: Commitment
aggregated_column_proof: Proof
rows_commitments: List[Commitment]
rows_proofs: List[Proof]
def blob_id(self) -> bytes:
return da.common.build_blob_id(self.aggregated_column_commitment, self.rows_commitments)
def column_id(self) -> bytes:
return sha3_256(self.column.as_bytes()).digest()
combined_column_proof: Proof
row_commitments: List[Commitment]
def blob_id(self) -> BlobId:
return build_blob_id(self.row_commitments)
class DAVerifier:
@staticmethod
def _verify_column(
column: Column,
column_idx: int,
column_commitment: Commitment,
aggregated_column_commitment: Commitment,
aggregated_column_proof: Proof,
) -> bool:
# 1. compute commitment for column
_, computed_column_commitment = kzg.bytes_to_commitment(column.as_bytes(), GLOBAL_PARAMETERS)
# 2. If computed column commitment != column commitment, fail
if column_commitment != computed_column_commitment:
return False
# 3. compute column hash
column_hash = DAEncoder.hash_commitment_blake2b31(column_commitment)
# 4. Check proof with commitment and proof over the aggregated column commitment
chunk = BLSFieldElement.from_bytes(column_hash)
return kzg.verify_element_proof(
chunk, aggregated_column_commitment, aggregated_column_proof, column_idx, ROOTS_OF_UNITY
)
@staticmethod
def _verify_chunk(chunk: Chunk, commitment: Commitment, proof: Proof, index: int) -> bool:
chunk = BLSFieldElement(int.from_bytes(bytes(chunk)) % BLS_MODULUS)
return kzg.verify_element_proof(chunk, commitment, proof, index, ROOTS_OF_UNITY)
@staticmethod
def _verify_chunks(
chunks: Sequence[Chunk],
commitments: Sequence[Commitment],
proofs: Sequence[Proof],
index: int
) -> bool:
if not (len(chunks) == len(commitments) == len(proofs)):
return False
for chunk, commitment, proof in zip(chunks, commitments, proofs):
if not DAVerifier._verify_chunk(chunk, commitment, proof, index):
return False
return True
def verify(self, blob: DABlob) -> bool:
def _derive_challenge(row_commitments: List[Commitment]) -> BLSFieldElement:
"""
Verify the integrity of the given blob.
This function must be idempotent. The implementer should ensure that
repeated verification attempts do not result in inconsistent states.
Args:
blob (DABlob): The blob to verify.
Returns:
bool: True if the blob is verified successfully, False otherwise.
Derive a FiatShamir challenge scalar h from the row commitments:
h = BLAKE2b-31( DST || bytes(com1) || bytes(com2) || ... )
"""
is_column_verified = DAVerifier._verify_column(
blob.column,
blob.column_idx,
blob.column_commitment,
blob.aggregated_column_commitment,
blob.aggregated_column_proof,
)
if not is_column_verified:
return False
h = blake2b(digest_size=31)
h.update(_DST)
for com in row_commitments:
h.update(bytes(com))
digest31 = h.digest() # 31 bytes
# pad to 32 bytes for field element conversion
padded = digest31 + b'\x00'
return BLSFieldElement.from_bytes(padded)
are_chunks_verified = DAVerifier._verify_chunks(
blob.column, blob.rows_commitments, blob.rows_proofs, blob.column_idx
)
if not are_chunks_verified:
return False
@staticmethod
def verify(blob: DAShare) -> bool:
"""
Verifies that blob.column.chunks at index blob.column_idx is consistent
with the row commitments and the single column proof.
# Ensure idempotency: Implementers should define how to avoid redundant verification.
return True
Returns True if verification succeeds, False otherwise.
"""
# 1. Derive challenge
h = DAVerifier._derive_challenge(blob.row_commitments)
# 2. Reconstruct combined commitment: com_C = sum_{i=0..l-1} h^i * row_commitments[i]
com_C = blob.row_commitments[0]
power = h
for com in blob.row_commitments[1:]:
com_C = com_C + com * int(power)
power = power * h
# 3. Compute combined evaluation v = sum_{i=0..l-1} (h^i * column_data[i])
v = BLSFieldElement(0)
power = BLSFieldElement(1)
for chunk in blob.column.chunks:
x = BLSFieldElement(int.from_bytes(bytes(chunk), byteorder="big"))
v = v + x * power
power = power * h
# 4. Verify the single KZG proof for evaluation at point w^{column_idx}
return kzg.verify_element_proof(v,com_C,blob.combined_column_proof,blob.column_idx,ROOTS_OF_UNITY)