From 8c34f8a39e35c11ef916172976c0e4b6f7c5281a Mon Sep 17 00:00:00 2001 From: Daniel Sanchez Date: Wed, 13 Mar 2024 14:59:27 +0100 Subject: [PATCH] Da Dispersal (#80) * Added common bls types * Added verifier attestation build * Implement verification and aggregation in dispersal * Added certificate building tests * Added dispersal test --- da/common.py | 14 +++++++-- da/dispersal.py | 65 ++++++++++++++++++++++++++++------------ da/test_dispersal.py | 70 +++++++++++++++++++++++++++++++++++++------- da/test_encoder.py | 1 - da/test_verifier.py | 2 +- da/verifier.py | 15 ++++++---- 6 files changed, 128 insertions(+), 39 deletions(-) diff --git a/da/common.py b/da/common.py index cb1e3c9..8d428e3 100644 --- a/da/common.py +++ b/da/common.py @@ -2,12 +2,15 @@ from dataclasses import dataclass from itertools import chain, zip_longest from typing import List, Generator, Self -from eth2spec.eip7594.mainnet import Bytes32 + + +from eth2spec.eip7594.mainnet import Bytes32, KZGCommitment as Commitment class NodeId(Bytes32): pass + class Chunk(Bytes32): pass @@ -31,14 +34,19 @@ class ChunksMatrix(List[Row | Column]): return ChunksMatrix(self.columns) +BLSPublickey = bytes +BLSPrivateKey = int +BLSSignature = bytes @dataclass class Attestation: - pass + signature: BLSSignature @dataclass class Certificate: - pass + aggregated_signatures: BLSSignature + aggregated_column_commitment: Commitment + row_commitments: List[Commitment] diff --git a/da/dispersal.py b/da/dispersal.py index 1c8271f..2967e74 100644 --- a/da/dispersal.py +++ b/da/dispersal.py @@ -1,50 +1,77 @@ -from typing import List, Optional, Generator +from dataclasses import dataclass +from hashlib import sha3_256 +from typing import List, Optional, Generator, Sequence -from da.common import Certificate, NodeId +from py_ecc.bls import G2ProofOfPossession as bls_pop + +from da.common import Certificate, NodeId, BLSPublickey from da.encoder import EncodedData from da.verifier import DABlob, Attestation +@dataclass +class DispersalSettings: + nodes_ids: List[NodeId] + nodes_pubkey: List[BLSPublickey] + threshold: int + + class Dispersal: - def __init__(self, nodes: List[NodeId], threshold: int): - self.nodes = nodes - self.threshold = threshold + def __init__(self, settings: DispersalSettings): + self.settings = settings def _prepare_data(self, encoded_data: EncodedData) -> Generator[DABlob, None, None]: - assert len(encoded_data.row_commitments) == len(self.nodes) - assert len(encoded_data.row_proofs) == len(self.nodes) + assert len(encoded_data.column_commitments) == len(self.settings.nodes_ids) + assert len(encoded_data.aggregated_column_proofs) == len(self.settings.nodes_ids) columns = encoded_data.extended_matrix.columns column_commitments = encoded_data.column_commitments row_commitments = encoded_data.row_commitments rows_proofs = encoded_data.row_proofs aggregated_column_commitment = encoded_data.aggregated_column_commitment - aggregated_column_proof = encoded_data.aggregated_column_proof - for index, (column, column_commitment, row_proofs) in enumerate(zip(columns, column_commitments, rows_proofs)): + aggregated_column_proofs = encoded_data.aggregated_column_proofs + blobs_data = enumerate(zip(columns, column_commitments, zip(*rows_proofs), aggregated_column_proofs)) + for index, (column, column_commitment, row_proofs, column_proof) in blobs_data: blob = DABlob( index, column, column_commitment, aggregated_column_commitment, - aggregated_column_proof, + column_proof, row_commitments, row_proofs ) yield blob - def _send_and_await_response(self, node, encoded_data: EncodedData) -> Optional[Attestation]: + def _send_and_await_response(self, node: NodeId, blob: DABlob) -> Optional[Attestation]: pass - def _build_certificate(self, attestations: List[Attestation]): - pass + def _build_certificate(self, encoded_data: EncodedData, attestations: Sequence[Attestation]) -> Certificate: + assert len(attestations) >= self.settings.threshold + aggregated = bls_pop.Aggregate([attestation.signature for attestation in attestations]) + return Certificate( + aggregated_signatures=aggregated, + aggregated_column_commitment=encoded_data.aggregated_column_commitment, + row_commitments=encoded_data.row_commitments + ) - def _verify_attestation(self, attestation: Attestation) -> bool: - pass + @staticmethod + def _verify_attestation(public_key: BLSPublickey, attested_message: bytes, attestation: Attestation) -> bool: + return bls_pop.Verify(public_key, attested_message, attestation.signature) + + @staticmethod + def _build_attestation_message(encoded_data: EncodedData) -> bytes: + hasher = sha3_256() + hasher.update(bytes(encoded_data.aggregated_column_commitment)) + for c in encoded_data.row_commitments: + hasher.update(bytes(c)) + return hasher.digest() def disperse(self, encoded_data: EncodedData) -> Optional[Certificate]: attestations = [] - for node, blob in zip(self.nodes, self._prepare_data(encoded_data)): + attested_message = self._build_attestation_message(encoded_data) + for node, pk, blob in zip(self.settings.nodes_ids, self.settings.nodes_pubkey, self._prepare_data(encoded_data)): if attestation := self._send_and_await_response(node, blob): - if self._verify_attestation(attestation): + if self._verify_attestation(pk, attested_message, attestation): attestations.append(attestation) - if len(attestations) >= self.threshold: - return self._build_certificate(attestations) + if len(attestations) >= self.settings.threshold: + return self._build_certificate(encoded_data, attestations) diff --git a/da/test_dispersal.py b/da/test_dispersal.py index 04aafd8..5895b2c 100644 --- a/da/test_dispersal.py +++ b/da/test_dispersal.py @@ -1,20 +1,70 @@ +from hashlib import sha3_256 from unittest import TestCase -from da.dispersal import Dispersal, DABlob, EncodedData + +from .encoder import DAEncoderParams, DAEncoder +from .test_encoder import TestEncoder + +from da.common import NodeId, Attestation +from da.dispersal import Dispersal, EncodedData, DispersalSettings +from py_ecc.bls import G2ProofOfPossession as bls_pop + +from .verifier import DAVerifier, DABlob class TestDispersal(TestCase): + def setUp(self): + self.n_nodes = 16 + self.nodes_ids = [NodeId(x.to_bytes(length=32, byteorder='big')) for x in range(self.n_nodes)] + self.secret_keys = list(range(1, self.n_nodes+1)) + self.public_keys = [bls_pop.SkToPk(sk) for sk in self.secret_keys] + dispersal_settings = DispersalSettings( + self.nodes_ids, + self.public_keys, + self.n_nodes // 2 + 1 + ) + self.dispersal = Dispersal(dispersal_settings) + self.encoder_test = TestEncoder() + self.encoder_test.setUp() def test_build_certificate_insufficient_attestations(self): - pass + with self.assertRaises(AssertionError): + self.dispersal._build_certificate(None, []) def test_build_certificate_enough_attestations(self): - pass - - def test_prepare_data(self): - pass - - def test_verify_attestation(self): - pass + mock_encoded_data = EncodedData( + None, None, None, [], [], [], bytes(b"f"*48), [] + ) + mock_message = sha3_256(mock_encoded_data.aggregated_column_commitment).digest() + mock_attestations = [Attestation(bls_pop.Sign(sk, mock_message)) for sk in self.secret_keys] + certificate = self.dispersal._build_certificate(mock_encoded_data, mock_attestations) + self.assertIsNotNone(certificate) + self.assertEqual(certificate.aggregated_column_commitment, mock_encoded_data.aggregated_column_commitment) + self.assertEqual(certificate.row_commitments, []) + self.assertIsNotNone(certificate.aggregated_signatures) + self.assertTrue( + bls_pop.AggregateVerify(self.public_keys, [mock_message]*len(self.public_keys), certificate.aggregated_signatures) + ) def test_disperse(self): - pass + data = self.encoder_test.data + encoding_params = DAEncoderParams(column_count=self.n_nodes // 2, bytes_per_field_element=32) + encoded_data = DAEncoder(encoding_params).encode(data) + + # mock send and await method with local verifiers + def __send_and_await_response(node: NodeId, blob: DABlob): + sk = self.secret_keys[int.from_bytes(node)] + verifier = DAVerifier(sk) + return verifier.verify(blob) + # inject mock send and await method + self.dispersal._send_and_await_response = __send_and_await_response + + certificate = self.dispersal.disperse(encoded_data) + self.assertIsNotNone(certificate) + self.assertTrue( + bls_pop.AggregateVerify( + self.public_keys[:self.dispersal.settings.threshold], + [self.dispersal._build_attestation_message(encoded_data)]*self.dispersal.settings.threshold, + certificate.aggregated_signatures + ) + ) + diff --git a/da/test_encoder.py b/da/test_encoder.py index 24aad2f..d49a2f9 100644 --- a/da/test_encoder.py +++ b/da/test_encoder.py @@ -56,7 +56,6 @@ class TestEncoder(TestCase): ROOTS_OF_UNITY ) - def test_chunkify(self): encoder_settings = DAEncoderParams(column_count=2, bytes_per_field_element=32) elements = 10 diff --git a/da/test_verifier.py b/da/test_verifier.py index e6d3c13..747b6ae 100644 --- a/da/test_verifier.py +++ b/da/test_verifier.py @@ -11,7 +11,7 @@ from da.verifier import Attestation, DAVerifier, DABlob class TestVerifier(TestCase): def setUp(self): - self.verifier = DAVerifier(b"") + self.verifier = DAVerifier(1987) def test_verify_column(self): column = Column(int.to_bytes(i, length=32) for i in range(8)) diff --git a/da/verifier.py b/da/verifier.py index f8898e0..e9d483b 100644 --- a/da/verifier.py +++ b/da/verifier.py @@ -7,9 +7,9 @@ from eth2spec.eip7594.mainnet import ( KZGCommitment as Commitment, KZGProof as Proof, ) -from itertools import batched +from py_ecc.bls import G2ProofOfPossession as bls_pop -from da.common import Column, Chunk, Attestation +from da.common import Column, Chunk, Attestation, BLSPrivateKey from da.encoder import DAEncoder from da.kzg_rs import kzg from da.kzg_rs.common import ROOTS_OF_UNITY, GLOBAL_PARAMETERS, BLS_MODULUS @@ -27,7 +27,7 @@ class DABlob: class DAVerifier: - def __init__(self, sk: bytes): + def __init__(self, sk: BLSPrivateKey): self.sk = sk @staticmethod @@ -70,8 +70,13 @@ class DAVerifier: return False return True - def _build_attestation(self, _blob: DABlob) -> Attestation: - return Attestation() + def _build_attestation(self, blob: DABlob) -> Attestation: + hasher = sha3_256() + hasher.update(bytes(blob.aggregated_column_commitment)) + for c in blob.rows_commitments: + hasher.update(bytes(c)) + message = hasher.digest() + return Attestation(signature=bls_pop.Sign(self.sk, message)) def verify(self, blob: DABlob) -> Optional[Attestation]: is_column_verified = DAVerifier._verify_column(