From 0b4f136825be518994655711d6a2790d60468a9d Mon Sep 17 00:00:00 2001 From: Taras Shchybovyk Date: Thu, 11 Dec 2025 20:58:07 -0800 Subject: [PATCH 01/12] - new method for hashing blobs - feed guardinan with blob hashes --- api/hashing/node_hashing.go | 25 ++++++++++++++++++++++++- node/grpc/server_v2.go | 17 +++++++++++++---- 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/api/hashing/node_hashing.go b/api/hashing/node_hashing.go index 90f5a40dff..401f57655f 100644 --- a/api/hashing/node_hashing.go +++ b/api/hashing/node_hashing.go @@ -19,7 +19,7 @@ const ValidatorStoreChunksRequestDomain = "validator.StoreChunksRequest" // HashStoreChunksRequest hashes the given StoreChunksRequest. func HashStoreChunksRequest(request *grpc.StoreChunksRequest) ([]byte, error) { - hasher := sha3.NewLegacyKeccak256() + hasher := sha3.New256() hasher.Write([]byte(ValidatorStoreChunksRequestDomain)) @@ -43,6 +43,29 @@ func HashStoreChunksRequest(request *grpc.StoreChunksRequest) ([]byte, error) { return hasher.Sum(nil), nil } +// HashStoreChunksRequestBlobHeaders returns a list of per-BlobHeader hashes (one per BlobCertificate). +func HashStoreChunksRequestBlobHeaders(request *grpc.StoreChunksRequest) ([][]byte, error) { + certs := request.GetBatch().GetBlobCertificates() + blobHeaderHashes := make([][]byte, len(certs)) + for i, cert := range certs { + if cert == nil { + return nil, fmt.Errorf("nil BlobCertificate at index %d", i) + } + header := cert.GetBlobHeader() + if header == nil { + return nil, fmt.Errorf("nil BlobHeader at index %d", i) + } + + h := sha3.New256() + if err := hashBlobHeader(h, header); err != nil { + return nil, fmt.Errorf("failed to hash blob header at index %d: %w", i, err) + } + blobHeaderHashes[i] = h.Sum(nil) + } + + return blobHeaderHashes, nil +} + func hashBlobCertificate(hasher hash.Hash, blobCertificate *common.BlobCertificate) error { err := hashBlobHeader(hasher, blobCertificate.GetBlobHeader()) if err != nil { diff --git a/node/grpc/server_v2.go b/node/grpc/server_v2.go index aa289c59a1..faa6e75dbc 100644 --- a/node/grpc/server_v2.go +++ b/node/grpc/server_v2.go @@ -12,6 +12,7 @@ import ( "github.com/Layr-Labs/eigenda/api" pb "github.com/Layr-Labs/eigenda/api/grpc/validator" + "github.com/Layr-Labs/eigenda/api/hashing" "github.com/Layr-Labs/eigenda/common" "github.com/Layr-Labs/eigenda/common/math" "github.com/Layr-Labs/eigenda/common/replay" @@ -179,7 +180,7 @@ func (s *ServerV2) StoreChunks(ctx context.Context, in *pb.StoreChunksRequest) ( return nil, api.NewErrorInvalidArg(fmt.Sprintf("failed to serialize batch header hash: %v", err)) } - hash, err := s.chunkAuthenticator.AuthenticateStoreChunksRequest(ctx, in, time.Now()) + _, err = s.chunkAuthenticator.AuthenticateStoreChunksRequest(ctx, in, time.Now()) if err != nil { //nolint:wrapcheck return nil, api.NewErrorInvalidArg(fmt.Sprintf("failed to authenticate request: %v", err)) @@ -191,11 +192,19 @@ func (s *ServerV2) StoreChunks(ctx context.Context, in *pb.StoreChunksRequest) ( fmt.Sprintf("disperser %d not authorized for on-demand payments", in.GetDisperserID())) } - timestamp := time.Unix(int64(in.GetTimestamp()), 0) - err = s.replayGuardian.VerifyRequest(hash, timestamp) + blobHeaderHashes, err := hashing.HashStoreChunksRequestBlobHeaders(in) if err != nil { //nolint:wrapcheck - return nil, api.NewErrorInvalidArg(fmt.Sprintf("failed to verify request: %v", err)) + return nil, api.NewErrorInvalidArg(fmt.Sprintf("failed to hash blob headers: %v", err)) + } + + timestamp := time.Unix(int64(in.GetTimestamp()), 0) + for i, blobHeaderHash := range blobHeaderHashes { + err = s.replayGuardian.VerifyRequest(blobHeaderHash, timestamp) + if err != nil { + //nolint:wrapcheck + return nil, api.NewErrorInvalidArg(fmt.Sprintf("failed to verify blob header hash at index %d: %v", i, err)) + } } for _, blobCert := range batch.BlobCertificates { From 5ce3c1e17a6f484f881203b2e558c371b3cdf190 Mon Sep 17 00:00:00 2001 From: Taras Shchybovyk Date: Fri, 12 Dec 2025 08:11:56 -0800 Subject: [PATCH 02/12] add timestamps to blob hash extraction --- api/hashing/node_hashing.go | 28 +++++++++++++++++++++++----- node/grpc/server_v2.go | 8 ++++---- 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/api/hashing/node_hashing.go b/api/hashing/node_hashing.go index 401f57655f..1551f08147 100644 --- a/api/hashing/node_hashing.go +++ b/api/hashing/node_hashing.go @@ -17,6 +17,16 @@ import ( // different type of object that has the same hash as a StoreChunksRequest. const ValidatorStoreChunksRequestDomain = "validator.StoreChunksRequest" +// TODO(taras): Very clumsy implementation. IMO hashing module has to be rewritten. +// As of now, package does two things: +// 1. Serializes message into "canonical" form (protobufs are not deterministic). +// 2. Hashes the canonical form. +// Those two things are independent, and should be separated. +type BlobHeaderHashWithTimestamp struct { + Hash []byte + Timestamp int64 +} + // HashStoreChunksRequest hashes the given StoreChunksRequest. func HashStoreChunksRequest(request *grpc.StoreChunksRequest) ([]byte, error) { hasher := sha3.New256() @@ -43,10 +53,11 @@ func HashStoreChunksRequest(request *grpc.StoreChunksRequest) ([]byte, error) { return hasher.Sum(nil), nil } -// HashStoreChunksRequestBlobHeaders returns a list of per-BlobHeader hashes (one per BlobCertificate). -func HashStoreChunksRequestBlobHeaders(request *grpc.StoreChunksRequest) ([][]byte, error) { +// HashStoreChunksRequestBlobHeaders returns a list of per-BlobHeader hashes (one per BlobCertificate) +// with the timestamp. +func HashStoreChunksRequestBlobHeaders(request *grpc.StoreChunksRequest) ([]BlobHeaderHashWithTimestamp, error) { certs := request.GetBatch().GetBlobCertificates() - blobHeaderHashes := make([][]byte, len(certs)) + out := make([]BlobHeaderHashWithTimestamp, len(certs)) for i, cert := range certs { if cert == nil { return nil, fmt.Errorf("nil BlobCertificate at index %d", i) @@ -55,15 +66,22 @@ func HashStoreChunksRequestBlobHeaders(request *grpc.StoreChunksRequest) ([][]by if header == nil { return nil, fmt.Errorf("nil BlobHeader at index %d", i) } + paymentHeader := header.GetPaymentHeader() + if paymentHeader == nil { + return nil, fmt.Errorf("nil PaymentHeader at index %d", i) + } h := sha3.New256() if err := hashBlobHeader(h, header); err != nil { return nil, fmt.Errorf("failed to hash blob header at index %d: %w", i, err) } - blobHeaderHashes[i] = h.Sum(nil) + out[i] = BlobHeaderHashWithTimestamp{ + Hash: h.Sum(nil), + Timestamp: paymentHeader.GetTimestamp(), + } } - return blobHeaderHashes, nil + return out, nil } func hashBlobCertificate(hasher hash.Hash, blobCertificate *common.BlobCertificate) error { diff --git a/node/grpc/server_v2.go b/node/grpc/server_v2.go index faa6e75dbc..cf525c596b 100644 --- a/node/grpc/server_v2.go +++ b/node/grpc/server_v2.go @@ -192,15 +192,15 @@ func (s *ServerV2) StoreChunks(ctx context.Context, in *pb.StoreChunksRequest) ( fmt.Sprintf("disperser %d not authorized for on-demand payments", in.GetDisperserID())) } - blobHeaderHashes, err := hashing.HashStoreChunksRequestBlobHeaders(in) + blobHeaders, err := hashing.HashStoreChunksRequestBlobHeaders(in) if err != nil { //nolint:wrapcheck return nil, api.NewErrorInvalidArg(fmt.Sprintf("failed to hash blob headers: %v", err)) } - timestamp := time.Unix(int64(in.GetTimestamp()), 0) - for i, blobHeaderHash := range blobHeaderHashes { - err = s.replayGuardian.VerifyRequest(blobHeaderHash, timestamp) + for i, blobHeader := range blobHeaders { + timestamp := time.Unix(blobHeader.Timestamp, 0) + err = s.replayGuardian.VerifyRequest(blobHeader.Hash, timestamp) if err != nil { //nolint:wrapcheck return nil, api.NewErrorInvalidArg(fmt.Sprintf("failed to verify blob header hash at index %d: %v", i, err)) From a3ad9e092c9f409c2f39a2acfa5ec16954dc63c7 Mon Sep 17 00:00:00 2001 From: Taras Shchybovyk Date: Sun, 14 Dec 2025 19:13:32 -0800 Subject: [PATCH 03/12] - separate serialization from hashing (found a bug in original serialization) - opt1: use in house serialization logic - opt2: use struct based encoder (way more cleaner) --- api/hashing/serialization/store_chunk.go | 268 ++++++++++++++++++ .../serialization/store_chunk_compat_test.go | 107 +++++++ api/hashing/serialization/store_chunk_v2.go | 187 ++++++++++++ go.mod | 1 + go.sum | 2 + 5 files changed, 565 insertions(+) create mode 100644 api/hashing/serialization/store_chunk.go create mode 100644 api/hashing/serialization/store_chunk_compat_test.go create mode 100644 api/hashing/serialization/store_chunk_v2.go diff --git a/api/hashing/serialization/store_chunk.go b/api/hashing/serialization/store_chunk.go new file mode 100644 index 0000000000..d955ca27ee --- /dev/null +++ b/api/hashing/serialization/store_chunk.go @@ -0,0 +1,268 @@ +package serialization + +import ( + "encoding/binary" + "fmt" + "math" + + commonv2 "github.com/Layr-Labs/eigenda/api/grpc/common/v2" + grpc "github.com/Layr-Labs/eigenda/api/grpc/validator" + "github.com/Layr-Labs/eigenda/api/hashing" +) + +// initialStoreChunksRequestCap is just a preallocation hint to reduce allocations. +// It's not a limit: `append` will grow the slice as needed. +const initialStoreChunksRequestCap = 512 + +const initialBlobHeaderCap = 512 + +type canonicalStoreChunksRequest struct { + Domain string + BatchHeader canonicalBatchHeader + BlobCertificates []canonicalBlobCertificate + DisperserID uint32 + Timestamp uint32 +} + +type canonicalBatchHeader struct { + Root []byte + ReferenceBlockNumber uint64 +} + +type canonicalBlobCertificate struct { + BlobHeader canonicalBlobHeader + Signature []byte + RelayKeys []uint32 +} + +type canonicalBlobHeader struct { + Version uint32 + // TODO(taras): QuorumNumbersLength is redundant. As QuorumNumbers is a list and length will + // the first uint32 in the list + QuorumNumbersLength uint32 + QuorumNumbers []uint32 + Commitment canonicalBlobCommitment + PaymentHeader canonicalPaymentHeader +} + +type canonicalBlobCommitment struct { + Commitment []byte + LengthCommitment []byte + LengthProof []byte + Length uint32 +} + +type canonicalPaymentHeader struct { + AccountId string + Timestamp int64 + CumulativePayment []byte +} + +func (h canonicalBatchHeader) serialize(dst []byte) ([]byte, error) { + var err error + dst, err = serializeBytes(dst, h.Root) + if err != nil { + return nil, fmt.Errorf("failed to serialize batch header root: %w", err) + } + dst = serializeU64(dst, h.ReferenceBlockNumber) + return dst, nil +} + +func (c canonicalBlobCommitment) serialize(dst []byte) ([]byte, error) { + var err error + dst, err = serializeBytes(dst, c.Commitment) + if err != nil { + return nil, fmt.Errorf("failed to serialize commitment: %w", err) + } + dst, err = serializeBytes(dst, c.LengthCommitment) + if err != nil { + return nil, fmt.Errorf("failed to serialize length commitment: %w", err) + } + dst, err = serializeBytes(dst, c.LengthProof) + if err != nil { + return nil, fmt.Errorf("failed to serialize length proof: %w", err) + } + dst = serializeU32(dst, c.Length) + return dst, nil +} + +func (h canonicalPaymentHeader) serialize(dst []byte) ([]byte, error) { + var err error + dst, err = serializeBytes(dst, []byte(h.AccountId)) + if err != nil { + return nil, fmt.Errorf("failed to serialize account id: %w", err) + } + dst = serializeI64(dst, h.Timestamp) + dst, err = serializeBytes(dst, h.CumulativePayment) + if err != nil { + return nil, fmt.Errorf("failed to serialize cumulative payment: %w", err) + } + return dst, nil +} + +func (h canonicalBlobHeader) serialize(dst []byte) ([]byte, error) { + dst = serializeU32(dst, h.Version) + dst = serializeU32(dst, h.QuorumNumbersLength) + var err error + dst, err = serializeU32Slice(dst, h.QuorumNumbers) + if err != nil { + return nil, fmt.Errorf("failed to serialize quorum numbers: %w", err) + } + dst, err = h.Commitment.serialize(dst) + if err != nil { + return nil, err + } + dst, err = h.PaymentHeader.serialize(dst) + if err != nil { + return nil, err + } + return dst, nil +} + +func (c canonicalBlobCertificate) serialize(dst []byte) ([]byte, error) { + var err error + dst, err = c.BlobHeader.serialize(dst) + if err != nil { + return nil, err + } + dst, err = serializeBytes(dst, c.Signature) + if err != nil { + return nil, fmt.Errorf("failed to serialize signature: %w", err) + } + dst, err = serializeU32Slice(dst, c.RelayKeys) + if err != nil { + return nil, fmt.Errorf("failed to serialize relay keys: %w", err) + } + return dst, nil +} + +func (r canonicalStoreChunksRequest) serialize(dst []byte) ([]byte, error) { + dst = append(dst, []byte(r.Domain)...) + + var err error + dst, err = r.BatchHeader.serialize(dst) + if err != nil { + return nil, err + } + + if len(r.BlobCertificates) > math.MaxUint32 { + return nil, fmt.Errorf("array is too long: %d", len(r.BlobCertificates)) + } + dst = serializeU32(dst, uint32(len(r.BlobCertificates))) + for i, cert := range r.BlobCertificates { + dst, err = cert.serialize(dst) + if err != nil { + return nil, fmt.Errorf("failed to serialize blob certificate at index %d: %w", i, err) + } + } + + dst = serializeU32(dst, r.DisperserID) + dst = serializeU32(dst, r.Timestamp) + return dst, nil +} + +func serializeU32(dst []byte, v uint32) []byte { + var b [4]byte + binary.BigEndian.PutUint32(b[:], v) + return append(dst, b[:]...) +} + +func serializeU64(dst []byte, v uint64) []byte { + var b [8]byte + binary.BigEndian.PutUint64(b[:], v) + return append(dst, b[:]...) +} + +func serializeI64(dst []byte, v int64) []byte { + return serializeU64(dst, uint64(v)) +} + +func serializeBytes(dst []byte, b []byte) ([]byte, error) { + if len(b) > math.MaxUint32 { + return nil, fmt.Errorf("byte array is too long: %d", len(b)) + } + dst = serializeU32(dst, uint32(len(b))) + dst = append(dst, b...) + return dst, nil +} + +func serializeU32Slice(dst []byte, s []uint32) ([]byte, error) { + if len(s) > math.MaxUint32 { + return nil, fmt.Errorf("uint32 array is too long: %d", len(s)) + } + dst = serializeU32(dst, uint32(len(s))) + for _, v := range s { + dst = serializeU32(dst, v) + } + return dst, nil +} + +func SerializeStoreChunksRequest(request *grpc.StoreChunksRequest) ([]byte, error) { + if request.GetBatch() == nil || request.GetBatch().GetHeader() == nil { + return nil, fmt.Errorf("missing batch/header") + } + + canonicalRequest := canonicalStoreChunksRequest{ + Domain: hashing.ValidatorStoreChunksRequestDomain, + BatchHeader: canonicalBatchHeader{ + Root: request.GetBatch().GetHeader().GetBatchRoot(), + ReferenceBlockNumber: request.GetBatch().GetHeader().GetReferenceBlockNumber(), + }, + BlobCertificates: make([]canonicalBlobCertificate, len(request.GetBatch().GetBlobCertificates())), + DisperserID: request.GetDisperserID(), + Timestamp: request.GetTimestamp(), + } + for i, cert := range request.GetBatch().GetBlobCertificates() { + if cert == nil || cert.GetBlobHeader() == nil || + cert.GetBlobHeader().GetCommitment() == nil || + cert.GetBlobHeader().GetPaymentHeader() == nil || + cert.GetSignature() == nil || + cert.GetRelayKeys() == nil { + return nil, fmt.Errorf("missing blob certificate fields at index %d", i) + } + canonicalRequest.BlobCertificates[i] = canonicalBlobCertificate{ + BlobHeader: canonicalBlobHeader{ + Version: cert.GetBlobHeader().GetVersion(), + QuorumNumbersLength: uint32(len(cert.GetBlobHeader().GetQuorumNumbers())), + QuorumNumbers: cert.GetBlobHeader().GetQuorumNumbers(), + Commitment: canonicalBlobCommitment{ + Commitment: cert.GetBlobHeader().GetCommitment().GetCommitment(), + LengthCommitment: cert.GetBlobHeader().GetCommitment().GetLengthCommitment(), + LengthProof: cert.GetBlobHeader().GetCommitment().GetLengthProof(), + Length: cert.GetBlobHeader().GetCommitment().GetLength(), + }, + PaymentHeader: canonicalPaymentHeader{ + AccountId: cert.GetBlobHeader().GetPaymentHeader().GetAccountId(), + Timestamp: cert.GetBlobHeader().GetPaymentHeader().GetTimestamp(), + CumulativePayment: cert.GetBlobHeader().GetPaymentHeader().GetCumulativePayment(), + }, + }, + Signature: cert.GetSignature(), + RelayKeys: cert.GetRelayKeys(), + } + } + + out := make([]byte, 0, initialStoreChunksRequestCap) + return canonicalRequest.serialize(out) +} + +func SerializeBlobHeader(header *commonv2.BlobHeader) ([]byte, error) { + if header == nil || header.GetCommitment() == nil || header.GetPaymentHeader() == nil { + return nil, fmt.Errorf("missing blob header fields") + } + canonicalHeader := canonicalBlobHeader{ + Version: header.GetVersion(), + QuorumNumbersLength: uint32(len(header.GetQuorumNumbers())), + QuorumNumbers: header.GetQuorumNumbers(), + Commitment: canonicalBlobCommitment{ + Commitment: header.GetCommitment().GetCommitment(), + }, + PaymentHeader: canonicalPaymentHeader{ + AccountId: header.GetPaymentHeader().GetAccountId(), + Timestamp: header.GetPaymentHeader().GetTimestamp(), + CumulativePayment: header.GetPaymentHeader().GetCumulativePayment(), + }, + } + out := make([]byte, 0, initialBlobHeaderCap) + return canonicalHeader.serialize(out) +} diff --git a/api/hashing/serialization/store_chunk_compat_test.go b/api/hashing/serialization/store_chunk_compat_test.go new file mode 100644 index 0000000000..ddabc0b248 --- /dev/null +++ b/api/hashing/serialization/store_chunk_compat_test.go @@ -0,0 +1,107 @@ +package serialization + +import ( + "bytes" + "testing" + + commonv1 "github.com/Layr-Labs/eigenda/api/grpc/common" + commonv2 "github.com/Layr-Labs/eigenda/api/grpc/common/v2" + grpc "github.com/Layr-Labs/eigenda/api/grpc/validator" +) + +func TestSerializeStoreChunksRequest_V1MatchesV2(t *testing.T) { + req := &grpc.StoreChunksRequest{ + Batch: &commonv2.Batch{ + Header: &commonv2.BatchHeader{ + BatchRoot: []byte{0x01, 0x02, 0x03, 0x04}, + ReferenceBlockNumber: 42, + }, + BlobCertificates: []*commonv2.BlobCertificate{ + { + BlobHeader: &commonv2.BlobHeader{ + Version: 1, + QuorumNumbers: []uint32{0, 2, 7}, + Commitment: &commonv1.BlobCommitment{ + Commitment: []byte{0xaa, 0xbb}, + LengthCommitment: []byte{0x10, 0x11, 0x12}, + LengthProof: []byte{0x20}, + Length: 123, + }, + PaymentHeader: &commonv2.PaymentHeader{ + AccountId: "0xabc", + Timestamp: 999, + CumulativePayment: []byte{0x09, 0x08, 0x07}, + }, + }, + Signature: []byte{0xde, 0xad, 0xbe, 0xef}, + RelayKeys: []uint32{5, 6}, + }, + { + BlobHeader: &commonv2.BlobHeader{ + Version: 2, + QuorumNumbers: []uint32{1}, + Commitment: &commonv1.BlobCommitment{ + Commitment: []byte{0x01}, + LengthCommitment: []byte{}, + LengthProof: []byte{0xff, 0xee}, + Length: 0, + }, + PaymentHeader: &commonv2.PaymentHeader{ + AccountId: "0xdef", + Timestamp: 123456789, + CumulativePayment: []byte{0x01, 0x00}, + }, + }, + Signature: []byte{0x00}, + RelayKeys: []uint32{0}, + }, + }, + }, + DisperserID: 7, + Timestamp: 55, + } + + b1, err := SerializeStoreChunksRequest(req) + if err != nil { + t.Fatalf("SerializeStoreChunksRequest: %v", err) + } + b2, err := SerializeStoreChunksRequestV2(req) + if err != nil { + t.Fatalf("SerializeStoreChunksRequestV2: %v", err) + } + + if !bytes.Equal(b1, b2) { + t.Fatalf("serialization mismatch\nv1=%x\nv2=%x", b1, b2) + } +} + +func TestSerializeBlobHeader_V1MatchesV2(t *testing.T) { + hdr := &commonv2.BlobHeader{ + Version: 3, + QuorumNumbers: []uint32{9, 8}, + Commitment: &commonv1.BlobCommitment{ + Commitment: []byte{0x01, 0x02, 0x03}, + LengthCommitment: []byte{0x99}, + LengthProof: []byte{0x88}, + Length: 777, + }, + PaymentHeader: &commonv2.PaymentHeader{ + AccountId: "0x1234", + Timestamp: 123, + CumulativePayment: []byte{0x01}, + }, + } + + b1, err := SerializeBlobHeader(hdr) + if err != nil { + t.Fatalf("SerializeBlobHeader: %v", err) + } + b2, err := SerializeBlobHeaderV2(hdr) + if err != nil { + t.Fatalf("SerializeBlobHeaderV2: %v", err) + } + + if !bytes.Equal(b1, b2) { + t.Fatalf("blob header serialization mismatch\nv1=%x\nv2=%x", b1, b2) + } +} diff --git a/api/hashing/serialization/store_chunk_v2.go b/api/hashing/serialization/store_chunk_v2.go new file mode 100644 index 0000000000..3827c98de8 --- /dev/null +++ b/api/hashing/serialization/store_chunk_v2.go @@ -0,0 +1,187 @@ +package serialization + +import ( + "bytes" + "fmt" + "math" + + commonv2 "github.com/Layr-Labs/eigenda/api/grpc/common/v2" + grpc "github.com/Layr-Labs/eigenda/api/grpc/validator" + "github.com/Layr-Labs/eigenda/api/hashing" + "github.com/lunixbochs/struc" +) + +// This file provides a struc-based encoder that preserves the *exact* byte layout of +// store_chunk.go's manual serializer. +// +// Wire-format invariants preserved: +// - Big-endian integers (struc defaults to big-endian) +// - For []byte and []uint32: a uint32 length prefix, followed by elements/bytes +// - Domain is written as raw bytes (no length prefix) at the start +// - Redundant QuorumNumbersLength field is preserved (it appears before the slice length prefix) + +type canonicalStoreChunksRequestBodyV2 struct { + BatchHeader canonicalBatchHeaderV2 + + BlobCertificatesLen uint32 `struc:"uint32,sizeof=BlobCertificates"` + BlobCertificates []canonicalBlobCertificateV2 + + DisperserID uint32 + Timestamp uint32 +} + +type canonicalBatchHeaderV2 struct { + RootLen uint32 `struc:"uint32,sizeof=Root"` + Root []byte + + ReferenceBlockNumber uint64 +} + +type canonicalBlobCertificateV2 struct { + BlobHeader canonicalBlobHeaderV2 + + SignatureLen uint32 `struc:"uint32,sizeof=Signature"` + Signature []byte + + RelayKeysLen uint32 `struc:"uint32,sizeof=RelayKeys"` + RelayKeys []uint32 +} + +type canonicalBlobHeaderV2 struct { + Version uint32 + + // Kept for backwards-compatible encoding: this is written first... + QuorumNumbersLength uint32 + // ...then the real slice length prefix (same value) followed by elements. + QuorumNumbersLen uint32 `struc:"uint32,sizeof=QuorumNumbers"` + QuorumNumbers []uint32 + + Commitment canonicalBlobCommitmentV2 + PaymentHeader canonicalPaymentHeaderV2 +} + +type canonicalBlobCommitmentV2 struct { + CommitmentLen uint32 `struc:"uint32,sizeof=Commitment"` + Commitment []byte + + LengthCommitmentLen uint32 `struc:"uint32,sizeof=LengthCommitment"` + LengthCommitment []byte + + LengthProofLen uint32 `struc:"uint32,sizeof=LengthProof"` + LengthProof []byte + + Length uint32 +} + +type canonicalPaymentHeaderV2 struct { + // store_chunk.go encodes AccountId as serializeBytes([]byte(string)) + AccountIdLen uint32 `struc:"uint32,sizeof=AccountId"` + AccountId []byte + + Timestamp int64 + + CumulativePaymentLen uint32 `struc:"uint32,sizeof=CumulativePayment"` + CumulativePayment []byte +} + +func SerializeStoreChunksRequestV2(request *grpc.StoreChunksRequest) ([]byte, error) { + if request.GetBatch() == nil || request.GetBatch().GetHeader() == nil { + return nil, fmt.Errorf("missing batch/header") + } + + certs := request.GetBatch().GetBlobCertificates() + if len(certs) > math.MaxUint32 { + return nil, fmt.Errorf("array is too long: %d", len(certs)) + } + + body := canonicalStoreChunksRequestBodyV2{ + BatchHeader: canonicalBatchHeaderV2{ + Root: request.GetBatch().GetHeader().GetBatchRoot(), + ReferenceBlockNumber: request.GetBatch().GetHeader().GetReferenceBlockNumber(), + }, + BlobCertificates: make([]canonicalBlobCertificateV2, len(certs)), + DisperserID: request.GetDisperserID(), + Timestamp: request.GetTimestamp(), + } + + for i, cert := range certs { + if cert == nil || cert.GetBlobHeader() == nil || + cert.GetBlobHeader().GetCommitment() == nil || + cert.GetBlobHeader().GetPaymentHeader() == nil { + return nil, fmt.Errorf("missing blob certificate fields at index %d", i) + } + + bh := cert.GetBlobHeader() + commitment := bh.GetCommitment() + payment := bh.GetPaymentHeader() + + qnums := bh.GetQuorumNumbers() + qnLen := uint32(len(qnums)) + + body.BlobCertificates[i] = canonicalBlobCertificateV2{ + BlobHeader: canonicalBlobHeaderV2{ + Version: bh.GetVersion(), + QuorumNumbersLength: qnLen, + QuorumNumbers: qnums, + Commitment: canonicalBlobCommitmentV2{ + Commitment: commitment.GetCommitment(), + LengthCommitment: commitment.GetLengthCommitment(), + LengthProof: commitment.GetLengthProof(), + Length: commitment.GetLength(), + }, + PaymentHeader: canonicalPaymentHeaderV2{ + AccountId: []byte(payment.GetAccountId()), + Timestamp: payment.GetTimestamp(), + CumulativePayment: payment.GetCumulativePayment(), + }, + }, + Signature: cert.GetSignature(), + RelayKeys: cert.GetRelayKeys(), + } + } + + var buf bytes.Buffer + buf.Grow(initialStoreChunksRequestCap) + + // IMPORTANT: preserve store_chunk.go behavior: raw domain bytes, no length prefix + _, _ = buf.WriteString(hashing.ValidatorStoreChunksRequestDomain) + + if err := struc.Pack(&buf, &body); err != nil { + return nil, fmt.Errorf("failed to pack canonical StoreChunksRequest: %w", err) + } + return buf.Bytes(), nil +} + +func SerializeBlobHeaderV2(header *commonv2.BlobHeader) ([]byte, error) { + if header == nil || header.GetCommitment() == nil || header.GetPaymentHeader() == nil { + return nil, fmt.Errorf("missing blob header fields") + } + + qnums := header.GetQuorumNumbers() + qnLen := uint32(len(qnums)) + + // Preserve current SerializeBlobHeader behavior from store_chunk.go: + // it only sets Commitment.Commitment and leaves the rest empty/zero. + ch := canonicalBlobHeaderV2{ + Version: header.GetVersion(), + QuorumNumbersLength: qnLen, + QuorumNumbers: qnums, + Commitment: canonicalBlobCommitmentV2{ + Commitment: header.GetCommitment().GetCommitment(), + // LengthCommitment / LengthProof / Length remain empty/zero by design + }, + PaymentHeader: canonicalPaymentHeaderV2{ + AccountId: []byte(header.GetPaymentHeader().GetAccountId()), + Timestamp: header.GetPaymentHeader().GetTimestamp(), + CumulativePayment: header.GetPaymentHeader().GetCumulativePayment(), + }, + } + + var buf bytes.Buffer + buf.Grow(initialBlobHeaderCap) + + if err := struc.Pack(&buf, &ch); err != nil { + return nil, fmt.Errorf("failed to pack canonical BlobHeader: %w", err) + } + return buf.Bytes(), nil +} diff --git a/go.mod b/go.mod index ab6e357b94..526e665d96 100644 --- a/go.mod +++ b/go.mod @@ -264,6 +264,7 @@ require ( require ( github.com/go-viper/mapstructure/v2 v2.4.0 + github.com/lunixbochs/struc v0.0.0-20241101090106-8d528fa2c543 github.com/spf13/viper v1.21.0 ) diff --git a/go.sum b/go.sum index ef250ff136..b2f4df2413 100644 --- a/go.sum +++ b/go.sum @@ -374,6 +374,8 @@ github.com/lmittmann/tint v1.0.4 h1:LeYihpJ9hyGvE0w+K2okPTGUdVLfng1+nDNVR4vWISc= github.com/lmittmann/tint v1.0.4/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE= github.com/lufia/plan9stats v0.0.0-20240226150601-1dcf7310316a h1:3Bm7EwfUQUvhNeKIkUct/gl9eod1TcXuj8stxvi/GoI= github.com/lufia/plan9stats v0.0.0-20240226150601-1dcf7310316a/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k= +github.com/lunixbochs/struc v0.0.0-20241101090106-8d528fa2c543 h1:GxMuVb9tJajC1QpbQwYNY1ZAo1EIE8I+UclBjOfjz/M= +github.com/lunixbochs/struc v0.0.0-20241101090106-8d528fa2c543/go.mod h1:vy1vK6wD6j7xX6O6hXe621WabdtNkou2h7uRtTfRMyg= github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= From 687a86401f34bb4bde99c24f7fd4194120ab499f Mon Sep 17 00:00:00 2001 From: Taras Shchybovyk Date: Sun, 14 Dec 2025 19:40:09 -0800 Subject: [PATCH 04/12] - use dedicated blob function to extract hashes and timestamps --- api/hashing/blob_header.go | 48 +++++++++++++ api/hashing/node_hashing.go | 41 ----------- api/hashing/serialization/store_chunk.go | 7 +- api/hashing/serialization/store_chunk_v2.go | 3 +- api/hashing/store_chunk_request.go | 29 ++++++++ api/hashing/store_chunk_request_test.go | 75 +++++++++++++++++++++ node/grpc/server_v2.go | 2 +- 7 files changed, 159 insertions(+), 46 deletions(-) create mode 100644 api/hashing/blob_header.go create mode 100644 api/hashing/store_chunk_request.go create mode 100644 api/hashing/store_chunk_request_test.go diff --git a/api/hashing/blob_header.go b/api/hashing/blob_header.go new file mode 100644 index 0000000000..1587286b6b --- /dev/null +++ b/api/hashing/blob_header.go @@ -0,0 +1,48 @@ +package hashing + +import ( + "fmt" + + grpc "github.com/Layr-Labs/eigenda/api/grpc/validator" + "github.com/Layr-Labs/eigenda/api/hashing/serialization" + "golang.org/x/crypto/sha3" +) + +// BlobHeaderHashWithTimestamp is a tuple of a blob header hash and the timestamp of the blob header. +type BlobHeaderHashWithTimestamp struct { + Hash []byte + Timestamp int64 +} + +// BlobHeadersHashesAndTimestamps returns a list of per-BlobHeader hashes (one per BlobCertificate) +// with the timestamp. +func BlobHeadersHashesAndTimestamps(request *grpc.StoreChunksRequest) ([]BlobHeaderHashWithTimestamp, error) { + certs := request.GetBatch().GetBlobCertificates() + out := make([]BlobHeaderHashWithTimestamp, len(certs)) + for i, cert := range certs { + if cert == nil { + return nil, fmt.Errorf("nil BlobCertificate at index %d", i) + } + header := cert.GetBlobHeader() + if header == nil { + return nil, fmt.Errorf("nil BlobHeader at index %d", i) + } + paymentHeader := header.GetPaymentHeader() + if paymentHeader == nil { + return nil, fmt.Errorf("nil PaymentHeader at index %d", i) + } + + headerBytes, err := serialization.SerializeBlobHeader(header) + if err != nil { + return nil, fmt.Errorf("failed to serialize blob header at index %d: %w", i, err) + } + hasher := sha3.New256() + _, _ = hasher.Write(headerBytes) + out[i] = BlobHeaderHashWithTimestamp{ + Hash: hasher.Sum(nil), + Timestamp: paymentHeader.GetTimestamp(), + } + } + + return out, nil +} diff --git a/api/hashing/node_hashing.go b/api/hashing/node_hashing.go index 1551f08147..1e71dced32 100644 --- a/api/hashing/node_hashing.go +++ b/api/hashing/node_hashing.go @@ -17,16 +17,6 @@ import ( // different type of object that has the same hash as a StoreChunksRequest. const ValidatorStoreChunksRequestDomain = "validator.StoreChunksRequest" -// TODO(taras): Very clumsy implementation. IMO hashing module has to be rewritten. -// As of now, package does two things: -// 1. Serializes message into "canonical" form (protobufs are not deterministic). -// 2. Hashes the canonical form. -// Those two things are independent, and should be separated. -type BlobHeaderHashWithTimestamp struct { - Hash []byte - Timestamp int64 -} - // HashStoreChunksRequest hashes the given StoreChunksRequest. func HashStoreChunksRequest(request *grpc.StoreChunksRequest) ([]byte, error) { hasher := sha3.New256() @@ -53,37 +43,6 @@ func HashStoreChunksRequest(request *grpc.StoreChunksRequest) ([]byte, error) { return hasher.Sum(nil), nil } -// HashStoreChunksRequestBlobHeaders returns a list of per-BlobHeader hashes (one per BlobCertificate) -// with the timestamp. -func HashStoreChunksRequestBlobHeaders(request *grpc.StoreChunksRequest) ([]BlobHeaderHashWithTimestamp, error) { - certs := request.GetBatch().GetBlobCertificates() - out := make([]BlobHeaderHashWithTimestamp, len(certs)) - for i, cert := range certs { - if cert == nil { - return nil, fmt.Errorf("nil BlobCertificate at index %d", i) - } - header := cert.GetBlobHeader() - if header == nil { - return nil, fmt.Errorf("nil BlobHeader at index %d", i) - } - paymentHeader := header.GetPaymentHeader() - if paymentHeader == nil { - return nil, fmt.Errorf("nil PaymentHeader at index %d", i) - } - - h := sha3.New256() - if err := hashBlobHeader(h, header); err != nil { - return nil, fmt.Errorf("failed to hash blob header at index %d: %w", i, err) - } - out[i] = BlobHeaderHashWithTimestamp{ - Hash: h.Sum(nil), - Timestamp: paymentHeader.GetTimestamp(), - } - } - - return out, nil -} - func hashBlobCertificate(hasher hash.Hash, blobCertificate *common.BlobCertificate) error { err := hashBlobHeader(hasher, blobCertificate.GetBlobHeader()) if err != nil { diff --git a/api/hashing/serialization/store_chunk.go b/api/hashing/serialization/store_chunk.go index d955ca27ee..18d86f0223 100644 --- a/api/hashing/serialization/store_chunk.go +++ b/api/hashing/serialization/store_chunk.go @@ -7,7 +7,6 @@ import ( commonv2 "github.com/Layr-Labs/eigenda/api/grpc/common/v2" grpc "github.com/Layr-Labs/eigenda/api/grpc/validator" - "github.com/Layr-Labs/eigenda/api/hashing" ) // initialStoreChunksRequestCap is just a preallocation hint to reduce allocations. @@ -16,6 +15,10 @@ const initialStoreChunksRequestCap = 512 const initialBlobHeaderCap = 512 +// validatorStoreChunksRequestDomain is the StoreChunksRequest hash domain prefix. +// Kept here to avoid an import cycle (hashing <-> serialization). +const validatorStoreChunksRequestDomain = "validator.StoreChunksRequest" + type canonicalStoreChunksRequest struct { Domain string BatchHeader canonicalBatchHeader @@ -203,7 +206,7 @@ func SerializeStoreChunksRequest(request *grpc.StoreChunksRequest) ([]byte, erro } canonicalRequest := canonicalStoreChunksRequest{ - Domain: hashing.ValidatorStoreChunksRequestDomain, + Domain: validatorStoreChunksRequestDomain, BatchHeader: canonicalBatchHeader{ Root: request.GetBatch().GetHeader().GetBatchRoot(), ReferenceBlockNumber: request.GetBatch().GetHeader().GetReferenceBlockNumber(), diff --git a/api/hashing/serialization/store_chunk_v2.go b/api/hashing/serialization/store_chunk_v2.go index 3827c98de8..c1b629bdac 100644 --- a/api/hashing/serialization/store_chunk_v2.go +++ b/api/hashing/serialization/store_chunk_v2.go @@ -7,7 +7,6 @@ import ( commonv2 "github.com/Layr-Labs/eigenda/api/grpc/common/v2" grpc "github.com/Layr-Labs/eigenda/api/grpc/validator" - "github.com/Layr-Labs/eigenda/api/hashing" "github.com/lunixbochs/struc" ) @@ -144,7 +143,7 @@ func SerializeStoreChunksRequestV2(request *grpc.StoreChunksRequest) ([]byte, er buf.Grow(initialStoreChunksRequestCap) // IMPORTANT: preserve store_chunk.go behavior: raw domain bytes, no length prefix - _, _ = buf.WriteString(hashing.ValidatorStoreChunksRequestDomain) + _, _ = buf.WriteString(validatorStoreChunksRequestDomain) if err := struc.Pack(&buf, &body); err != nil { return nil, fmt.Errorf("failed to pack canonical StoreChunksRequest: %w", err) diff --git a/api/hashing/store_chunk_request.go b/api/hashing/store_chunk_request.go new file mode 100644 index 0000000000..041a0206c6 --- /dev/null +++ b/api/hashing/store_chunk_request.go @@ -0,0 +1,29 @@ +package hashing + +import ( + "fmt" + + grpc "github.com/Layr-Labs/eigenda/api/grpc/validator" + "github.com/Layr-Labs/eigenda/api/hashing/serialization" + "golang.org/x/crypto/sha3" +) + +func HashStoreChunksRequest_Canonical(request *grpc.StoreChunksRequest) ([]byte, error) { + canonicalRequest, err := serialization.SerializeStoreChunksRequest(request) + if err != nil { + return nil, fmt.Errorf("failed to serialize store chunks request: %w", err) + } + hasher := sha3.New256() + _, _ = hasher.Write(canonicalRequest) + return hasher.Sum(nil), nil +} + +func HashStoreChunksRequest_V2_Canonical(request *grpc.StoreChunksRequest) ([]byte, error) { + canonicalRequest, err := serialization.SerializeStoreChunksRequestV2(request) + if err != nil { + return nil, fmt.Errorf("failed to serialize store chunks request: %w", err) + } + hasher := sha3.New256() + _, _ = hasher.Write(canonicalRequest) + return hasher.Sum(nil), nil +} diff --git a/api/hashing/store_chunk_request_test.go b/api/hashing/store_chunk_request_test.go new file mode 100644 index 0000000000..a5d884b651 --- /dev/null +++ b/api/hashing/store_chunk_request_test.go @@ -0,0 +1,75 @@ +package hashing + +import ( + "testing" + + commonv1 "github.com/Layr-Labs/eigenda/api/grpc/common" + commonv2 "github.com/Layr-Labs/eigenda/api/grpc/common/v2" + grpc "github.com/Layr-Labs/eigenda/api/grpc/validator" + "github.com/stretchr/testify/require" +) + +func TestHashStoreChunksRequest_CanonicalMatchesHasherImplementation(t *testing.T) { + req := &grpc.StoreChunksRequest{ + Batch: &commonv2.Batch{ + Header: &commonv2.BatchHeader{ + BatchRoot: []byte{0x01, 0x02, 0x03, 0x04}, + ReferenceBlockNumber: 42, + }, + BlobCertificates: []*commonv2.BlobCertificate{ + { + BlobHeader: &commonv2.BlobHeader{ + Version: 1, + QuorumNumbers: []uint32{0, 2, 7}, + Commitment: &commonv1.BlobCommitment{ + Commitment: []byte{0xaa, 0xbb}, + LengthCommitment: []byte{0x10, 0x11, 0x12}, + LengthProof: []byte{0x20}, + Length: 123, + }, + PaymentHeader: &commonv2.PaymentHeader{ + AccountId: "0xabc", + Timestamp: 999, + CumulativePayment: []byte{0x09, 0x08, 0x07}, + }, + }, + Signature: []byte{0xde, 0xad, 0xbe, 0xef}, + RelayKeys: []uint32{5, 6}, + }, + { + BlobHeader: &commonv2.BlobHeader{ + Version: 2, + QuorumNumbers: []uint32{1}, + Commitment: &commonv1.BlobCommitment{ + Commitment: []byte{0x01}, + LengthCommitment: []byte{}, + LengthProof: []byte{0xff, 0xee}, + Length: 0, + }, + PaymentHeader: &commonv2.PaymentHeader{ + AccountId: "0xdef", + Timestamp: 123456789, + CumulativePayment: []byte{0x01, 0x00}, + }, + }, + Signature: []byte{0x00}, + RelayKeys: []uint32{0}, + }, + }, + }, + DisperserID: 7, + Timestamp: 55, + } + + h1, err := HashStoreChunksRequest(req) + require.NoError(t, err) + + h2, err := HashStoreChunksRequest_Canonical(req) + require.NoError(t, err) + + h3, err := HashStoreChunksRequest_V2_Canonical(req) + require.NoError(t, err) + + require.Equal(t, h1, h2, "canonical (manual) serializer hash must match HashStoreChunksRequest") + require.Equal(t, h1, h3, "canonical (struc) serializer hash must match HashStoreChunksRequest") +} diff --git a/node/grpc/server_v2.go b/node/grpc/server_v2.go index cf525c596b..945d84c3b4 100644 --- a/node/grpc/server_v2.go +++ b/node/grpc/server_v2.go @@ -192,7 +192,7 @@ func (s *ServerV2) StoreChunks(ctx context.Context, in *pb.StoreChunksRequest) ( fmt.Sprintf("disperser %d not authorized for on-demand payments", in.GetDisperserID())) } - blobHeaders, err := hashing.HashStoreChunksRequestBlobHeaders(in) + blobHeaders, err := hashing.BlobHeadersHashesAndTimestamps(in) if err != nil { //nolint:wrapcheck return nil, api.NewErrorInvalidArg(fmt.Sprintf("failed to hash blob headers: %v", err)) From bd923fe7694bf8a9b8300563b38c03a814beab3a Mon Sep 17 00:00:00 2001 From: Taras Shchybovyk Date: Mon, 15 Dec 2025 08:59:50 -0800 Subject: [PATCH 05/12] - fix timestamp conversion --- .gitignore | 1 + api/hashing/blob_header.go | 10 +++++++--- node/grpc/server_v2.go | 3 +-- node/mock/testdata.go | 7 ++++--- 4 files changed, 13 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index 906b341134..c2e6cb80e4 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,7 @@ contracts/broadcast lightnode/docker/build-info.txt lightnode/docker/args.sh +.cache .idea .env .vscode diff --git a/api/hashing/blob_header.go b/api/hashing/blob_header.go index 1587286b6b..a80897dec2 100644 --- a/api/hashing/blob_header.go +++ b/api/hashing/blob_header.go @@ -2,6 +2,7 @@ package hashing import ( "fmt" + "time" grpc "github.com/Layr-Labs/eigenda/api/grpc/validator" "github.com/Layr-Labs/eigenda/api/hashing/serialization" @@ -11,7 +12,7 @@ import ( // BlobHeaderHashWithTimestamp is a tuple of a blob header hash and the timestamp of the blob header. type BlobHeaderHashWithTimestamp struct { Hash []byte - Timestamp int64 + Timestamp time.Time } // BlobHeadersHashesAndTimestamps returns a list of per-BlobHeader hashes (one per BlobCertificate) @@ -39,8 +40,11 @@ func BlobHeadersHashesAndTimestamps(request *grpc.StoreChunksRequest) ([]BlobHea hasher := sha3.New256() _, _ = hasher.Write(headerBytes) out[i] = BlobHeaderHashWithTimestamp{ - Hash: hasher.Sum(nil), - Timestamp: paymentHeader.GetTimestamp(), + Hash: hasher.Sum(nil), + Timestamp: time.Unix( + paymentHeader.GetTimestamp()/int64(time.Second), + paymentHeader.GetTimestamp()%int64(time.Second), + ), } } diff --git a/node/grpc/server_v2.go b/node/grpc/server_v2.go index 945d84c3b4..2eee2fe140 100644 --- a/node/grpc/server_v2.go +++ b/node/grpc/server_v2.go @@ -199,8 +199,7 @@ func (s *ServerV2) StoreChunks(ctx context.Context, in *pb.StoreChunksRequest) ( } for i, blobHeader := range blobHeaders { - timestamp := time.Unix(blobHeader.Timestamp, 0) - err = s.replayGuardian.VerifyRequest(blobHeader.Hash, timestamp) + err = s.replayGuardian.VerifyRequest(blobHeader.Hash, blobHeader.Timestamp) if err != nil { //nolint:wrapcheck return nil, api.NewErrorInvalidArg(fmt.Sprintf("failed to verify blob header hash at index %d: %v", i, err)) diff --git a/node/mock/testdata.go b/node/mock/testdata.go index 2d1c8e10aa..589e9a0f43 100644 --- a/node/mock/testdata.go +++ b/node/mock/testdata.go @@ -3,6 +3,7 @@ package mock import ( "math/big" "testing" + "time" "github.com/Layr-Labs/eigenda/core" v2 "github.com/Layr-Labs/eigenda/core/v2" @@ -33,7 +34,7 @@ func MockBatch(t *testing.T) ([]v2.BlobKey, *v2.Batch, []map[core.QuorumID]core. QuorumNumbers: []core.QuorumID{0, 1}, PaymentMetadata: core.PaymentMetadata{ AccountID: account0Addr, - Timestamp: 5, + Timestamp: time.Now().UnixNano(), CumulativePayment: big.NewInt(100), }, } @@ -43,7 +44,7 @@ func MockBatch(t *testing.T) ([]v2.BlobKey, *v2.Batch, []map[core.QuorumID]core. QuorumNumbers: []core.QuorumID{0, 1}, PaymentMetadata: core.PaymentMetadata{ AccountID: account1Addr, - Timestamp: 6, + Timestamp: time.Now().UnixNano(), CumulativePayment: big.NewInt(200), }, } @@ -53,7 +54,7 @@ func MockBatch(t *testing.T) ([]v2.BlobKey, *v2.Batch, []map[core.QuorumID]core. QuorumNumbers: []core.QuorumID{1, 2}, PaymentMetadata: core.PaymentMetadata{ AccountID: account2Addr, - Timestamp: 7, + Timestamp: time.Now().UnixNano(), CumulativePayment: big.NewInt(300), }, } From 756d97f8354a6d29aba61377dedbb55496de5442 Mon Sep 17 00:00:00 2001 From: Taras Shchybovyk Date: Mon, 15 Dec 2025 09:51:25 -0800 Subject: [PATCH 06/12] - add missing fields for blob Header --- api/hashing/serialization/store_chunk.go | 9 +++++++-- api/hashing/serialization/store_chunk_v2.go | 6 ++++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/api/hashing/serialization/store_chunk.go b/api/hashing/serialization/store_chunk.go index 18d86f0223..f3f1a91fb8 100644 --- a/api/hashing/serialization/store_chunk.go +++ b/api/hashing/serialization/store_chunk.go @@ -225,7 +225,9 @@ func SerializeStoreChunksRequest(request *grpc.StoreChunksRequest) ([]byte, erro } canonicalRequest.BlobCertificates[i] = canonicalBlobCertificate{ BlobHeader: canonicalBlobHeader{ - Version: cert.GetBlobHeader().GetVersion(), + Version: cert.GetBlobHeader().GetVersion(), + // TODO(taras): QuorumNumbersLength is redundant. As QuorumNumbers is a list and length will + // the first uint32 in the list QuorumNumbersLength: uint32(len(cert.GetBlobHeader().GetQuorumNumbers())), QuorumNumbers: cert.GetBlobHeader().GetQuorumNumbers(), Commitment: canonicalBlobCommitment{ @@ -258,7 +260,10 @@ func SerializeBlobHeader(header *commonv2.BlobHeader) ([]byte, error) { QuorumNumbersLength: uint32(len(header.GetQuorumNumbers())), QuorumNumbers: header.GetQuorumNumbers(), Commitment: canonicalBlobCommitment{ - Commitment: header.GetCommitment().GetCommitment(), + Commitment: header.GetCommitment().GetCommitment(), + LengthCommitment: header.GetCommitment().GetLengthCommitment(), + LengthProof: header.GetCommitment().GetLengthProof(), + Length: header.GetCommitment().GetLength(), }, PaymentHeader: canonicalPaymentHeader{ AccountId: header.GetPaymentHeader().GetAccountId(), diff --git a/api/hashing/serialization/store_chunk_v2.go b/api/hashing/serialization/store_chunk_v2.go index c1b629bdac..3b5189141e 100644 --- a/api/hashing/serialization/store_chunk_v2.go +++ b/api/hashing/serialization/store_chunk_v2.go @@ -166,8 +166,10 @@ func SerializeBlobHeaderV2(header *commonv2.BlobHeader) ([]byte, error) { QuorumNumbersLength: qnLen, QuorumNumbers: qnums, Commitment: canonicalBlobCommitmentV2{ - Commitment: header.GetCommitment().GetCommitment(), - // LengthCommitment / LengthProof / Length remain empty/zero by design + Commitment: header.GetCommitment().GetCommitment(), + LengthCommitment: header.GetCommitment().GetLengthCommitment(), + LengthProof: header.GetCommitment().GetLengthProof(), + Length: header.GetCommitment().GetLength(), }, PaymentHeader: canonicalPaymentHeaderV2{ AccountId: []byte(header.GetPaymentHeader().GetAccountId()), From 23ea2afe2559636aafe1093bbdbfb14a15288897 Mon Sep 17 00:00:00 2001 From: Taras Shchybovyk Date: Mon, 15 Dec 2025 10:18:18 -0800 Subject: [PATCH 07/12] move new hash and serialization code to v2 --- api/hashing/{ => v2}/blob_header.go | 4 ++-- api/hashing/{serialization => v2/serialize}/store_chunk.go | 2 +- .../serialize}/store_chunk_compat_test.go | 2 +- .../{serialization => v2/serialize}/store_chunk_v2.go | 2 +- api/hashing/{ => v2}/store_chunk_request.go | 6 +++--- api/hashing/{ => v2}/store_chunk_request_test.go | 3 ++- node/grpc/server_v2.go | 4 ++-- 7 files changed, 12 insertions(+), 11 deletions(-) rename api/hashing/{ => v2}/blob_header.go (92%) rename api/hashing/{serialization => v2/serialize}/store_chunk.go (99%) rename api/hashing/{serialization => v2/serialize}/store_chunk_compat_test.go (99%) rename api/hashing/{serialization => v2/serialize}/store_chunk_v2.go (99%) rename api/hashing/{ => v2}/store_chunk_request.go (76%) rename api/hashing/{ => v2}/store_chunk_request_test.go (95%) diff --git a/api/hashing/blob_header.go b/api/hashing/v2/blob_header.go similarity index 92% rename from api/hashing/blob_header.go rename to api/hashing/v2/blob_header.go index a80897dec2..97ba56de1e 100644 --- a/api/hashing/blob_header.go +++ b/api/hashing/v2/blob_header.go @@ -5,7 +5,7 @@ import ( "time" grpc "github.com/Layr-Labs/eigenda/api/grpc/validator" - "github.com/Layr-Labs/eigenda/api/hashing/serialization" + "github.com/Layr-Labs/eigenda/api/hashing/v2/serialize" "golang.org/x/crypto/sha3" ) @@ -33,7 +33,7 @@ func BlobHeadersHashesAndTimestamps(request *grpc.StoreChunksRequest) ([]BlobHea return nil, fmt.Errorf("nil PaymentHeader at index %d", i) } - headerBytes, err := serialization.SerializeBlobHeader(header) + headerBytes, err := serialize.SerializeBlobHeader(header) if err != nil { return nil, fmt.Errorf("failed to serialize blob header at index %d: %w", i, err) } diff --git a/api/hashing/serialization/store_chunk.go b/api/hashing/v2/serialize/store_chunk.go similarity index 99% rename from api/hashing/serialization/store_chunk.go rename to api/hashing/v2/serialize/store_chunk.go index f3f1a91fb8..3c4c459442 100644 --- a/api/hashing/serialization/store_chunk.go +++ b/api/hashing/v2/serialize/store_chunk.go @@ -1,4 +1,4 @@ -package serialization +package serialize import ( "encoding/binary" diff --git a/api/hashing/serialization/store_chunk_compat_test.go b/api/hashing/v2/serialize/store_chunk_compat_test.go similarity index 99% rename from api/hashing/serialization/store_chunk_compat_test.go rename to api/hashing/v2/serialize/store_chunk_compat_test.go index ddabc0b248..73c0a0f6a9 100644 --- a/api/hashing/serialization/store_chunk_compat_test.go +++ b/api/hashing/v2/serialize/store_chunk_compat_test.go @@ -1,4 +1,4 @@ -package serialization +package serialize import ( "bytes" diff --git a/api/hashing/serialization/store_chunk_v2.go b/api/hashing/v2/serialize/store_chunk_v2.go similarity index 99% rename from api/hashing/serialization/store_chunk_v2.go rename to api/hashing/v2/serialize/store_chunk_v2.go index 3b5189141e..05ee7d34c3 100644 --- a/api/hashing/serialization/store_chunk_v2.go +++ b/api/hashing/v2/serialize/store_chunk_v2.go @@ -1,4 +1,4 @@ -package serialization +package serialize import ( "bytes" diff --git a/api/hashing/store_chunk_request.go b/api/hashing/v2/store_chunk_request.go similarity index 76% rename from api/hashing/store_chunk_request.go rename to api/hashing/v2/store_chunk_request.go index 041a0206c6..451b0b0d61 100644 --- a/api/hashing/store_chunk_request.go +++ b/api/hashing/v2/store_chunk_request.go @@ -4,12 +4,12 @@ import ( "fmt" grpc "github.com/Layr-Labs/eigenda/api/grpc/validator" - "github.com/Layr-Labs/eigenda/api/hashing/serialization" + "github.com/Layr-Labs/eigenda/api/hashing/v2/serialize" "golang.org/x/crypto/sha3" ) func HashStoreChunksRequest_Canonical(request *grpc.StoreChunksRequest) ([]byte, error) { - canonicalRequest, err := serialization.SerializeStoreChunksRequest(request) + canonicalRequest, err := serialize.SerializeStoreChunksRequest(request) if err != nil { return nil, fmt.Errorf("failed to serialize store chunks request: %w", err) } @@ -19,7 +19,7 @@ func HashStoreChunksRequest_Canonical(request *grpc.StoreChunksRequest) ([]byte, } func HashStoreChunksRequest_V2_Canonical(request *grpc.StoreChunksRequest) ([]byte, error) { - canonicalRequest, err := serialization.SerializeStoreChunksRequestV2(request) + canonicalRequest, err := serialize.SerializeStoreChunksRequestV2(request) if err != nil { return nil, fmt.Errorf("failed to serialize store chunks request: %w", err) } diff --git a/api/hashing/store_chunk_request_test.go b/api/hashing/v2/store_chunk_request_test.go similarity index 95% rename from api/hashing/store_chunk_request_test.go rename to api/hashing/v2/store_chunk_request_test.go index a5d884b651..331b5f0b80 100644 --- a/api/hashing/store_chunk_request_test.go +++ b/api/hashing/v2/store_chunk_request_test.go @@ -6,6 +6,7 @@ import ( commonv1 "github.com/Layr-Labs/eigenda/api/grpc/common" commonv2 "github.com/Layr-Labs/eigenda/api/grpc/common/v2" grpc "github.com/Layr-Labs/eigenda/api/grpc/validator" + "github.com/Layr-Labs/eigenda/api/hashing" "github.com/stretchr/testify/require" ) @@ -61,7 +62,7 @@ func TestHashStoreChunksRequest_CanonicalMatchesHasherImplementation(t *testing. Timestamp: 55, } - h1, err := HashStoreChunksRequest(req) + h1, err := hashing.HashStoreChunksRequest(req) require.NoError(t, err) h2, err := HashStoreChunksRequest_Canonical(req) diff --git a/node/grpc/server_v2.go b/node/grpc/server_v2.go index 2eee2fe140..efa4f924f2 100644 --- a/node/grpc/server_v2.go +++ b/node/grpc/server_v2.go @@ -12,7 +12,7 @@ import ( "github.com/Layr-Labs/eigenda/api" pb "github.com/Layr-Labs/eigenda/api/grpc/validator" - "github.com/Layr-Labs/eigenda/api/hashing" + hashingv2 "github.com/Layr-Labs/eigenda/api/hashing/v2" "github.com/Layr-Labs/eigenda/common" "github.com/Layr-Labs/eigenda/common/math" "github.com/Layr-Labs/eigenda/common/replay" @@ -192,7 +192,7 @@ func (s *ServerV2) StoreChunks(ctx context.Context, in *pb.StoreChunksRequest) ( fmt.Sprintf("disperser %d not authorized for on-demand payments", in.GetDisperserID())) } - blobHeaders, err := hashing.BlobHeadersHashesAndTimestamps(in) + blobHeaders, err := hashingv2.BlobHeadersHashesAndTimestamps(in) if err != nil { //nolint:wrapcheck return nil, api.NewErrorInvalidArg(fmt.Sprintf("failed to hash blob headers: %v", err)) From c18aaee237209fb6112f754642f92646d8ef8d90 Mon Sep 17 00:00:00 2001 From: Taras Shchybovyk Date: Mon, 15 Dec 2025 10:53:20 -0800 Subject: [PATCH 08/12] - remove manual serialization to reduce liability (code is a liability) --- api/hashing/v2/serialize/store_chunk.go | 276 ------------------ .../v2/serialize/store_chunk_compat_test.go | 107 ------- ...ore_chunk_v2.go => store_chunk_request.go} | 58 ++-- api/hashing/v2/store_chunk_request.go | 12 +- api/hashing/v2/store_chunk_request_test.go | 12 +- 5 files changed, 37 insertions(+), 428 deletions(-) delete mode 100644 api/hashing/v2/serialize/store_chunk.go delete mode 100644 api/hashing/v2/serialize/store_chunk_compat_test.go rename api/hashing/v2/serialize/{store_chunk_v2.go => store_chunk_request.go} (77%) diff --git a/api/hashing/v2/serialize/store_chunk.go b/api/hashing/v2/serialize/store_chunk.go deleted file mode 100644 index 3c4c459442..0000000000 --- a/api/hashing/v2/serialize/store_chunk.go +++ /dev/null @@ -1,276 +0,0 @@ -package serialize - -import ( - "encoding/binary" - "fmt" - "math" - - commonv2 "github.com/Layr-Labs/eigenda/api/grpc/common/v2" - grpc "github.com/Layr-Labs/eigenda/api/grpc/validator" -) - -// initialStoreChunksRequestCap is just a preallocation hint to reduce allocations. -// It's not a limit: `append` will grow the slice as needed. -const initialStoreChunksRequestCap = 512 - -const initialBlobHeaderCap = 512 - -// validatorStoreChunksRequestDomain is the StoreChunksRequest hash domain prefix. -// Kept here to avoid an import cycle (hashing <-> serialization). -const validatorStoreChunksRequestDomain = "validator.StoreChunksRequest" - -type canonicalStoreChunksRequest struct { - Domain string - BatchHeader canonicalBatchHeader - BlobCertificates []canonicalBlobCertificate - DisperserID uint32 - Timestamp uint32 -} - -type canonicalBatchHeader struct { - Root []byte - ReferenceBlockNumber uint64 -} - -type canonicalBlobCertificate struct { - BlobHeader canonicalBlobHeader - Signature []byte - RelayKeys []uint32 -} - -type canonicalBlobHeader struct { - Version uint32 - // TODO(taras): QuorumNumbersLength is redundant. As QuorumNumbers is a list and length will - // the first uint32 in the list - QuorumNumbersLength uint32 - QuorumNumbers []uint32 - Commitment canonicalBlobCommitment - PaymentHeader canonicalPaymentHeader -} - -type canonicalBlobCommitment struct { - Commitment []byte - LengthCommitment []byte - LengthProof []byte - Length uint32 -} - -type canonicalPaymentHeader struct { - AccountId string - Timestamp int64 - CumulativePayment []byte -} - -func (h canonicalBatchHeader) serialize(dst []byte) ([]byte, error) { - var err error - dst, err = serializeBytes(dst, h.Root) - if err != nil { - return nil, fmt.Errorf("failed to serialize batch header root: %w", err) - } - dst = serializeU64(dst, h.ReferenceBlockNumber) - return dst, nil -} - -func (c canonicalBlobCommitment) serialize(dst []byte) ([]byte, error) { - var err error - dst, err = serializeBytes(dst, c.Commitment) - if err != nil { - return nil, fmt.Errorf("failed to serialize commitment: %w", err) - } - dst, err = serializeBytes(dst, c.LengthCommitment) - if err != nil { - return nil, fmt.Errorf("failed to serialize length commitment: %w", err) - } - dst, err = serializeBytes(dst, c.LengthProof) - if err != nil { - return nil, fmt.Errorf("failed to serialize length proof: %w", err) - } - dst = serializeU32(dst, c.Length) - return dst, nil -} - -func (h canonicalPaymentHeader) serialize(dst []byte) ([]byte, error) { - var err error - dst, err = serializeBytes(dst, []byte(h.AccountId)) - if err != nil { - return nil, fmt.Errorf("failed to serialize account id: %w", err) - } - dst = serializeI64(dst, h.Timestamp) - dst, err = serializeBytes(dst, h.CumulativePayment) - if err != nil { - return nil, fmt.Errorf("failed to serialize cumulative payment: %w", err) - } - return dst, nil -} - -func (h canonicalBlobHeader) serialize(dst []byte) ([]byte, error) { - dst = serializeU32(dst, h.Version) - dst = serializeU32(dst, h.QuorumNumbersLength) - var err error - dst, err = serializeU32Slice(dst, h.QuorumNumbers) - if err != nil { - return nil, fmt.Errorf("failed to serialize quorum numbers: %w", err) - } - dst, err = h.Commitment.serialize(dst) - if err != nil { - return nil, err - } - dst, err = h.PaymentHeader.serialize(dst) - if err != nil { - return nil, err - } - return dst, nil -} - -func (c canonicalBlobCertificate) serialize(dst []byte) ([]byte, error) { - var err error - dst, err = c.BlobHeader.serialize(dst) - if err != nil { - return nil, err - } - dst, err = serializeBytes(dst, c.Signature) - if err != nil { - return nil, fmt.Errorf("failed to serialize signature: %w", err) - } - dst, err = serializeU32Slice(dst, c.RelayKeys) - if err != nil { - return nil, fmt.Errorf("failed to serialize relay keys: %w", err) - } - return dst, nil -} - -func (r canonicalStoreChunksRequest) serialize(dst []byte) ([]byte, error) { - dst = append(dst, []byte(r.Domain)...) - - var err error - dst, err = r.BatchHeader.serialize(dst) - if err != nil { - return nil, err - } - - if len(r.BlobCertificates) > math.MaxUint32 { - return nil, fmt.Errorf("array is too long: %d", len(r.BlobCertificates)) - } - dst = serializeU32(dst, uint32(len(r.BlobCertificates))) - for i, cert := range r.BlobCertificates { - dst, err = cert.serialize(dst) - if err != nil { - return nil, fmt.Errorf("failed to serialize blob certificate at index %d: %w", i, err) - } - } - - dst = serializeU32(dst, r.DisperserID) - dst = serializeU32(dst, r.Timestamp) - return dst, nil -} - -func serializeU32(dst []byte, v uint32) []byte { - var b [4]byte - binary.BigEndian.PutUint32(b[:], v) - return append(dst, b[:]...) -} - -func serializeU64(dst []byte, v uint64) []byte { - var b [8]byte - binary.BigEndian.PutUint64(b[:], v) - return append(dst, b[:]...) -} - -func serializeI64(dst []byte, v int64) []byte { - return serializeU64(dst, uint64(v)) -} - -func serializeBytes(dst []byte, b []byte) ([]byte, error) { - if len(b) > math.MaxUint32 { - return nil, fmt.Errorf("byte array is too long: %d", len(b)) - } - dst = serializeU32(dst, uint32(len(b))) - dst = append(dst, b...) - return dst, nil -} - -func serializeU32Slice(dst []byte, s []uint32) ([]byte, error) { - if len(s) > math.MaxUint32 { - return nil, fmt.Errorf("uint32 array is too long: %d", len(s)) - } - dst = serializeU32(dst, uint32(len(s))) - for _, v := range s { - dst = serializeU32(dst, v) - } - return dst, nil -} - -func SerializeStoreChunksRequest(request *grpc.StoreChunksRequest) ([]byte, error) { - if request.GetBatch() == nil || request.GetBatch().GetHeader() == nil { - return nil, fmt.Errorf("missing batch/header") - } - - canonicalRequest := canonicalStoreChunksRequest{ - Domain: validatorStoreChunksRequestDomain, - BatchHeader: canonicalBatchHeader{ - Root: request.GetBatch().GetHeader().GetBatchRoot(), - ReferenceBlockNumber: request.GetBatch().GetHeader().GetReferenceBlockNumber(), - }, - BlobCertificates: make([]canonicalBlobCertificate, len(request.GetBatch().GetBlobCertificates())), - DisperserID: request.GetDisperserID(), - Timestamp: request.GetTimestamp(), - } - for i, cert := range request.GetBatch().GetBlobCertificates() { - if cert == nil || cert.GetBlobHeader() == nil || - cert.GetBlobHeader().GetCommitment() == nil || - cert.GetBlobHeader().GetPaymentHeader() == nil || - cert.GetSignature() == nil || - cert.GetRelayKeys() == nil { - return nil, fmt.Errorf("missing blob certificate fields at index %d", i) - } - canonicalRequest.BlobCertificates[i] = canonicalBlobCertificate{ - BlobHeader: canonicalBlobHeader{ - Version: cert.GetBlobHeader().GetVersion(), - // TODO(taras): QuorumNumbersLength is redundant. As QuorumNumbers is a list and length will - // the first uint32 in the list - QuorumNumbersLength: uint32(len(cert.GetBlobHeader().GetQuorumNumbers())), - QuorumNumbers: cert.GetBlobHeader().GetQuorumNumbers(), - Commitment: canonicalBlobCommitment{ - Commitment: cert.GetBlobHeader().GetCommitment().GetCommitment(), - LengthCommitment: cert.GetBlobHeader().GetCommitment().GetLengthCommitment(), - LengthProof: cert.GetBlobHeader().GetCommitment().GetLengthProof(), - Length: cert.GetBlobHeader().GetCommitment().GetLength(), - }, - PaymentHeader: canonicalPaymentHeader{ - AccountId: cert.GetBlobHeader().GetPaymentHeader().GetAccountId(), - Timestamp: cert.GetBlobHeader().GetPaymentHeader().GetTimestamp(), - CumulativePayment: cert.GetBlobHeader().GetPaymentHeader().GetCumulativePayment(), - }, - }, - Signature: cert.GetSignature(), - RelayKeys: cert.GetRelayKeys(), - } - } - - out := make([]byte, 0, initialStoreChunksRequestCap) - return canonicalRequest.serialize(out) -} - -func SerializeBlobHeader(header *commonv2.BlobHeader) ([]byte, error) { - if header == nil || header.GetCommitment() == nil || header.GetPaymentHeader() == nil { - return nil, fmt.Errorf("missing blob header fields") - } - canonicalHeader := canonicalBlobHeader{ - Version: header.GetVersion(), - QuorumNumbersLength: uint32(len(header.GetQuorumNumbers())), - QuorumNumbers: header.GetQuorumNumbers(), - Commitment: canonicalBlobCommitment{ - Commitment: header.GetCommitment().GetCommitment(), - LengthCommitment: header.GetCommitment().GetLengthCommitment(), - LengthProof: header.GetCommitment().GetLengthProof(), - Length: header.GetCommitment().GetLength(), - }, - PaymentHeader: canonicalPaymentHeader{ - AccountId: header.GetPaymentHeader().GetAccountId(), - Timestamp: header.GetPaymentHeader().GetTimestamp(), - CumulativePayment: header.GetPaymentHeader().GetCumulativePayment(), - }, - } - out := make([]byte, 0, initialBlobHeaderCap) - return canonicalHeader.serialize(out) -} diff --git a/api/hashing/v2/serialize/store_chunk_compat_test.go b/api/hashing/v2/serialize/store_chunk_compat_test.go deleted file mode 100644 index 73c0a0f6a9..0000000000 --- a/api/hashing/v2/serialize/store_chunk_compat_test.go +++ /dev/null @@ -1,107 +0,0 @@ -package serialize - -import ( - "bytes" - "testing" - - commonv1 "github.com/Layr-Labs/eigenda/api/grpc/common" - commonv2 "github.com/Layr-Labs/eigenda/api/grpc/common/v2" - grpc "github.com/Layr-Labs/eigenda/api/grpc/validator" -) - -func TestSerializeStoreChunksRequest_V1MatchesV2(t *testing.T) { - req := &grpc.StoreChunksRequest{ - Batch: &commonv2.Batch{ - Header: &commonv2.BatchHeader{ - BatchRoot: []byte{0x01, 0x02, 0x03, 0x04}, - ReferenceBlockNumber: 42, - }, - BlobCertificates: []*commonv2.BlobCertificate{ - { - BlobHeader: &commonv2.BlobHeader{ - Version: 1, - QuorumNumbers: []uint32{0, 2, 7}, - Commitment: &commonv1.BlobCommitment{ - Commitment: []byte{0xaa, 0xbb}, - LengthCommitment: []byte{0x10, 0x11, 0x12}, - LengthProof: []byte{0x20}, - Length: 123, - }, - PaymentHeader: &commonv2.PaymentHeader{ - AccountId: "0xabc", - Timestamp: 999, - CumulativePayment: []byte{0x09, 0x08, 0x07}, - }, - }, - Signature: []byte{0xde, 0xad, 0xbe, 0xef}, - RelayKeys: []uint32{5, 6}, - }, - { - BlobHeader: &commonv2.BlobHeader{ - Version: 2, - QuorumNumbers: []uint32{1}, - Commitment: &commonv1.BlobCommitment{ - Commitment: []byte{0x01}, - LengthCommitment: []byte{}, - LengthProof: []byte{0xff, 0xee}, - Length: 0, - }, - PaymentHeader: &commonv2.PaymentHeader{ - AccountId: "0xdef", - Timestamp: 123456789, - CumulativePayment: []byte{0x01, 0x00}, - }, - }, - Signature: []byte{0x00}, - RelayKeys: []uint32{0}, - }, - }, - }, - DisperserID: 7, - Timestamp: 55, - } - - b1, err := SerializeStoreChunksRequest(req) - if err != nil { - t.Fatalf("SerializeStoreChunksRequest: %v", err) - } - b2, err := SerializeStoreChunksRequestV2(req) - if err != nil { - t.Fatalf("SerializeStoreChunksRequestV2: %v", err) - } - - if !bytes.Equal(b1, b2) { - t.Fatalf("serialization mismatch\nv1=%x\nv2=%x", b1, b2) - } -} - -func TestSerializeBlobHeader_V1MatchesV2(t *testing.T) { - hdr := &commonv2.BlobHeader{ - Version: 3, - QuorumNumbers: []uint32{9, 8}, - Commitment: &commonv1.BlobCommitment{ - Commitment: []byte{0x01, 0x02, 0x03}, - LengthCommitment: []byte{0x99}, - LengthProof: []byte{0x88}, - Length: 777, - }, - PaymentHeader: &commonv2.PaymentHeader{ - AccountId: "0x1234", - Timestamp: 123, - CumulativePayment: []byte{0x01}, - }, - } - - b1, err := SerializeBlobHeader(hdr) - if err != nil { - t.Fatalf("SerializeBlobHeader: %v", err) - } - b2, err := SerializeBlobHeaderV2(hdr) - if err != nil { - t.Fatalf("SerializeBlobHeaderV2: %v", err) - } - - if !bytes.Equal(b1, b2) { - t.Fatalf("blob header serialization mismatch\nv1=%x\nv2=%x", b1, b2) - } -} diff --git a/api/hashing/v2/serialize/store_chunk_v2.go b/api/hashing/v2/serialize/store_chunk_request.go similarity index 77% rename from api/hashing/v2/serialize/store_chunk_v2.go rename to api/hashing/v2/serialize/store_chunk_request.go index 05ee7d34c3..f98fb39124 100644 --- a/api/hashing/v2/serialize/store_chunk_v2.go +++ b/api/hashing/v2/serialize/store_chunk_request.go @@ -19,25 +19,32 @@ import ( // - Domain is written as raw bytes (no length prefix) at the start // - Redundant QuorumNumbersLength field is preserved (it appears before the slice length prefix) -type canonicalStoreChunksRequestBodyV2 struct { - BatchHeader canonicalBatchHeaderV2 +// initialBufCap is a preallocation hint to reduce allocations. +const initialBufCap = 512 + +// validatorStoreChunksRequestDomain is the StoreChunksRequest hash domain prefix. +// Kept here to avoid an import cycle (hashing <-> serialization). +const validatorStoreChunksRequestDomain = "validator.StoreChunksRequest" + +type canonicalStoreChunksRequestBody struct { + BatchHeader canonicalBatchHeader BlobCertificatesLen uint32 `struc:"uint32,sizeof=BlobCertificates"` - BlobCertificates []canonicalBlobCertificateV2 + BlobCertificates []canonicalBlobCertificate DisperserID uint32 Timestamp uint32 } -type canonicalBatchHeaderV2 struct { +type canonicalBatchHeader struct { RootLen uint32 `struc:"uint32,sizeof=Root"` Root []byte ReferenceBlockNumber uint64 } -type canonicalBlobCertificateV2 struct { - BlobHeader canonicalBlobHeaderV2 +type canonicalBlobCertificate struct { + BlobHeader canonicalBlobHeader SignatureLen uint32 `struc:"uint32,sizeof=Signature"` Signature []byte @@ -46,7 +53,7 @@ type canonicalBlobCertificateV2 struct { RelayKeys []uint32 } -type canonicalBlobHeaderV2 struct { +type canonicalBlobHeader struct { Version uint32 // Kept for backwards-compatible encoding: this is written first... @@ -55,11 +62,11 @@ type canonicalBlobHeaderV2 struct { QuorumNumbersLen uint32 `struc:"uint32,sizeof=QuorumNumbers"` QuorumNumbers []uint32 - Commitment canonicalBlobCommitmentV2 - PaymentHeader canonicalPaymentHeaderV2 + Commitment canonicalBlobCommitment + PaymentHeader canonicalPaymentHeader } -type canonicalBlobCommitmentV2 struct { +type canonicalBlobCommitment struct { CommitmentLen uint32 `struc:"uint32,sizeof=Commitment"` Commitment []byte @@ -72,7 +79,7 @@ type canonicalBlobCommitmentV2 struct { Length uint32 } -type canonicalPaymentHeaderV2 struct { +type canonicalPaymentHeader struct { // store_chunk.go encodes AccountId as serializeBytes([]byte(string)) AccountIdLen uint32 `struc:"uint32,sizeof=AccountId"` AccountId []byte @@ -83,7 +90,7 @@ type canonicalPaymentHeaderV2 struct { CumulativePayment []byte } -func SerializeStoreChunksRequestV2(request *grpc.StoreChunksRequest) ([]byte, error) { +func SerializeStoreChunksRequest(request *grpc.StoreChunksRequest) ([]byte, error) { if request.GetBatch() == nil || request.GetBatch().GetHeader() == nil { return nil, fmt.Errorf("missing batch/header") } @@ -93,12 +100,12 @@ func SerializeStoreChunksRequestV2(request *grpc.StoreChunksRequest) ([]byte, er return nil, fmt.Errorf("array is too long: %d", len(certs)) } - body := canonicalStoreChunksRequestBodyV2{ - BatchHeader: canonicalBatchHeaderV2{ + body := canonicalStoreChunksRequestBody{ + BatchHeader: canonicalBatchHeader{ Root: request.GetBatch().GetHeader().GetBatchRoot(), ReferenceBlockNumber: request.GetBatch().GetHeader().GetReferenceBlockNumber(), }, - BlobCertificates: make([]canonicalBlobCertificateV2, len(certs)), + BlobCertificates: make([]canonicalBlobCertificate, len(certs)), DisperserID: request.GetDisperserID(), Timestamp: request.GetTimestamp(), } @@ -117,18 +124,18 @@ func SerializeStoreChunksRequestV2(request *grpc.StoreChunksRequest) ([]byte, er qnums := bh.GetQuorumNumbers() qnLen := uint32(len(qnums)) - body.BlobCertificates[i] = canonicalBlobCertificateV2{ - BlobHeader: canonicalBlobHeaderV2{ + body.BlobCertificates[i] = canonicalBlobCertificate{ + BlobHeader: canonicalBlobHeader{ Version: bh.GetVersion(), QuorumNumbersLength: qnLen, QuorumNumbers: qnums, - Commitment: canonicalBlobCommitmentV2{ + Commitment: canonicalBlobCommitment{ Commitment: commitment.GetCommitment(), LengthCommitment: commitment.GetLengthCommitment(), LengthProof: commitment.GetLengthProof(), Length: commitment.GetLength(), }, - PaymentHeader: canonicalPaymentHeaderV2{ + PaymentHeader: canonicalPaymentHeader{ AccountId: []byte(payment.GetAccountId()), Timestamp: payment.GetTimestamp(), CumulativePayment: payment.GetCumulativePayment(), @@ -140,9 +147,8 @@ func SerializeStoreChunksRequestV2(request *grpc.StoreChunksRequest) ([]byte, er } var buf bytes.Buffer - buf.Grow(initialStoreChunksRequestCap) + buf.Grow(initialBufCap) - // IMPORTANT: preserve store_chunk.go behavior: raw domain bytes, no length prefix _, _ = buf.WriteString(validatorStoreChunksRequestDomain) if err := struc.Pack(&buf, &body); err != nil { @@ -151,7 +157,7 @@ func SerializeStoreChunksRequestV2(request *grpc.StoreChunksRequest) ([]byte, er return buf.Bytes(), nil } -func SerializeBlobHeaderV2(header *commonv2.BlobHeader) ([]byte, error) { +func SerializeBlobHeader(header *commonv2.BlobHeader) ([]byte, error) { if header == nil || header.GetCommitment() == nil || header.GetPaymentHeader() == nil { return nil, fmt.Errorf("missing blob header fields") } @@ -161,17 +167,17 @@ func SerializeBlobHeaderV2(header *commonv2.BlobHeader) ([]byte, error) { // Preserve current SerializeBlobHeader behavior from store_chunk.go: // it only sets Commitment.Commitment and leaves the rest empty/zero. - ch := canonicalBlobHeaderV2{ + ch := canonicalBlobHeader{ Version: header.GetVersion(), QuorumNumbersLength: qnLen, QuorumNumbers: qnums, - Commitment: canonicalBlobCommitmentV2{ + Commitment: canonicalBlobCommitment{ Commitment: header.GetCommitment().GetCommitment(), LengthCommitment: header.GetCommitment().GetLengthCommitment(), LengthProof: header.GetCommitment().GetLengthProof(), Length: header.GetCommitment().GetLength(), }, - PaymentHeader: canonicalPaymentHeaderV2{ + PaymentHeader: canonicalPaymentHeader{ AccountId: []byte(header.GetPaymentHeader().GetAccountId()), Timestamp: header.GetPaymentHeader().GetTimestamp(), CumulativePayment: header.GetPaymentHeader().GetCumulativePayment(), @@ -179,7 +185,7 @@ func SerializeBlobHeaderV2(header *commonv2.BlobHeader) ([]byte, error) { } var buf bytes.Buffer - buf.Grow(initialBlobHeaderCap) + buf.Grow(initialBufCap) if err := struc.Pack(&buf, &ch); err != nil { return nil, fmt.Errorf("failed to pack canonical BlobHeader: %w", err) diff --git a/api/hashing/v2/store_chunk_request.go b/api/hashing/v2/store_chunk_request.go index 451b0b0d61..bcacc6257b 100644 --- a/api/hashing/v2/store_chunk_request.go +++ b/api/hashing/v2/store_chunk_request.go @@ -8,7 +8,7 @@ import ( "golang.org/x/crypto/sha3" ) -func HashStoreChunksRequest_Canonical(request *grpc.StoreChunksRequest) ([]byte, error) { +func HashStoreChunksRequest(request *grpc.StoreChunksRequest) ([]byte, error) { canonicalRequest, err := serialize.SerializeStoreChunksRequest(request) if err != nil { return nil, fmt.Errorf("failed to serialize store chunks request: %w", err) @@ -17,13 +17,3 @@ func HashStoreChunksRequest_Canonical(request *grpc.StoreChunksRequest) ([]byte, _, _ = hasher.Write(canonicalRequest) return hasher.Sum(nil), nil } - -func HashStoreChunksRequest_V2_Canonical(request *grpc.StoreChunksRequest) ([]byte, error) { - canonicalRequest, err := serialize.SerializeStoreChunksRequestV2(request) - if err != nil { - return nil, fmt.Errorf("failed to serialize store chunks request: %w", err) - } - hasher := sha3.New256() - _, _ = hasher.Write(canonicalRequest) - return hasher.Sum(nil), nil -} diff --git a/api/hashing/v2/store_chunk_request_test.go b/api/hashing/v2/store_chunk_request_test.go index 331b5f0b80..d72697a979 100644 --- a/api/hashing/v2/store_chunk_request_test.go +++ b/api/hashing/v2/store_chunk_request_test.go @@ -6,7 +6,7 @@ import ( commonv1 "github.com/Layr-Labs/eigenda/api/grpc/common" commonv2 "github.com/Layr-Labs/eigenda/api/grpc/common/v2" grpc "github.com/Layr-Labs/eigenda/api/grpc/validator" - "github.com/Layr-Labs/eigenda/api/hashing" + legacyhashing "github.com/Layr-Labs/eigenda/api/hashing" "github.com/stretchr/testify/require" ) @@ -62,15 +62,11 @@ func TestHashStoreChunksRequest_CanonicalMatchesHasherImplementation(t *testing. Timestamp: 55, } - h1, err := hashing.HashStoreChunksRequest(req) + h1, err := legacyhashing.HashStoreChunksRequest(req) require.NoError(t, err) - h2, err := HashStoreChunksRequest_Canonical(req) + h2, err := HashStoreChunksRequest(req) require.NoError(t, err) - h3, err := HashStoreChunksRequest_V2_Canonical(req) - require.NoError(t, err) - - require.Equal(t, h1, h2, "canonical (manual) serializer hash must match HashStoreChunksRequest") - require.Equal(t, h1, h3, "canonical (struc) serializer hash must match HashStoreChunksRequest") + require.Equal(t, h1, h2, "legacy (manual) serializer hash must match canonical (struc) serializer hash") } From a4502183a0d5417f37c3c3fca0cac158b1010821 Mon Sep 17 00:00:00 2001 From: Taras Shchybovyk Date: Mon, 15 Dec 2025 11:07:17 -0800 Subject: [PATCH 09/12] - update comments --- api/hashing/v2/blob_header.go | 4 +++- api/hashing/v2/serialize/store_chunk_request.go | 4 ++-- api/hashing/v2/store_chunk_request.go | 1 + node/grpc/server_v2.go | 1 + 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/api/hashing/v2/blob_header.go b/api/hashing/v2/blob_header.go index 97ba56de1e..55f5d27cd5 100644 --- a/api/hashing/v2/blob_header.go +++ b/api/hashing/v2/blob_header.go @@ -11,7 +11,9 @@ import ( // BlobHeaderHashWithTimestamp is a tuple of a blob header hash and the timestamp of the blob header. type BlobHeaderHashWithTimestamp struct { - Hash []byte + // Hash is canonical serialized blob header hash. + Hash []byte + // Timestamp is a timestamp from the payment header (seconds since epoch). Timestamp time.Time } diff --git a/api/hashing/v2/serialize/store_chunk_request.go b/api/hashing/v2/serialize/store_chunk_request.go index f98fb39124..ced8a99f1e 100644 --- a/api/hashing/v2/serialize/store_chunk_request.go +++ b/api/hashing/v2/serialize/store_chunk_request.go @@ -11,7 +11,7 @@ import ( ) // This file provides a struc-based encoder that preserves the *exact* byte layout of -// store_chunk.go's manual serializer. +// node_hashing.go's manual serializer+hash and separates the serialization from the hashing. // // Wire-format invariants preserved: // - Big-endian integers (struc defaults to big-endian) @@ -23,7 +23,7 @@ import ( const initialBufCap = 512 // validatorStoreChunksRequestDomain is the StoreChunksRequest hash domain prefix. -// Kept here to avoid an import cycle (hashing <-> serialization). +// Kept here to avoid an import cycle (hashing <-> serialize). const validatorStoreChunksRequestDomain = "validator.StoreChunksRequest" type canonicalStoreChunksRequestBody struct { diff --git a/api/hashing/v2/store_chunk_request.go b/api/hashing/v2/store_chunk_request.go index bcacc6257b..7dbf7693ba 100644 --- a/api/hashing/v2/store_chunk_request.go +++ b/api/hashing/v2/store_chunk_request.go @@ -8,6 +8,7 @@ import ( "golang.org/x/crypto/sha3" ) +// HashStoreChunksRequest hashes the given StoreChunksRequest using the canonical serialization. func HashStoreChunksRequest(request *grpc.StoreChunksRequest) ([]byte, error) { canonicalRequest, err := serialize.SerializeStoreChunksRequest(request) if err != nil { diff --git a/node/grpc/server_v2.go b/node/grpc/server_v2.go index efa4f924f2..b951b0993e 100644 --- a/node/grpc/server_v2.go +++ b/node/grpc/server_v2.go @@ -192,6 +192,7 @@ func (s *ServerV2) StoreChunks(ctx context.Context, in *pb.StoreChunksRequest) ( fmt.Sprintf("disperser %d not authorized for on-demand payments", in.GetDisperserID())) } + // Hash each blob header and verify the replay guardian. blobHeaders, err := hashingv2.BlobHeadersHashesAndTimestamps(in) if err != nil { //nolint:wrapcheck From 0d1921c0b0f83cfa22e0ff070cab74f6a8bdc1eb Mon Sep 17 00:00:00 2001 From: Taras Shchybovyk Date: Mon, 15 Dec 2025 11:12:20 -0800 Subject: [PATCH 10/12] - revert to legacy hashing code --- api/hashing/node_hashing.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/hashing/node_hashing.go b/api/hashing/node_hashing.go index 1e71dced32..90f5a40dff 100644 --- a/api/hashing/node_hashing.go +++ b/api/hashing/node_hashing.go @@ -19,7 +19,7 @@ const ValidatorStoreChunksRequestDomain = "validator.StoreChunksRequest" // HashStoreChunksRequest hashes the given StoreChunksRequest. func HashStoreChunksRequest(request *grpc.StoreChunksRequest) ([]byte, error) { - hasher := sha3.New256() + hasher := sha3.NewLegacyKeccak256() hasher.Write([]byte(ValidatorStoreChunksRequestDomain)) From f4bff5ea75312876d478fdd3d68d006e9dba47c7 Mon Sep 17 00:00:00 2001 From: Taras Shchybovyk Date: Mon, 15 Dec 2025 11:16:28 -0800 Subject: [PATCH 11/12] - update auth package with new (refactored) hashing (v2) --- node/auth/request_signing.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/node/auth/request_signing.go b/node/auth/request_signing.go index 4c2311e809..1eea6ae3d6 100644 --- a/node/auth/request_signing.go +++ b/node/auth/request_signing.go @@ -5,7 +5,8 @@ import ( "fmt" grpc "github.com/Layr-Labs/eigenda/api/grpc/validator" - "github.com/Layr-Labs/eigenda/api/hashing" + hashingv2 "github.com/Layr-Labs/eigenda/api/hashing/v2" + gethcommon "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" ) @@ -13,7 +14,7 @@ import ( // SignStoreChunksRequest signs the given StoreChunksRequest with the given private key. Does not // write the signature into the request. func SignStoreChunksRequest(key *ecdsa.PrivateKey, request *grpc.StoreChunksRequest) ([]byte, error) { - requestHash, err := hashing.HashStoreChunksRequest(request) + requestHash, err := hashingv2.HashStoreChunksRequest(request) if err != nil { return nil, fmt.Errorf("failed to hash request: %w", err) } @@ -29,7 +30,7 @@ func SignStoreChunksRequest(key *ecdsa.PrivateKey, request *grpc.StoreChunksRequ // VerifyStoreChunksRequest verifies the given signature of the given StoreChunksRequest with the given // public key. Returns the hash of the request. func VerifyStoreChunksRequest(key gethcommon.Address, request *grpc.StoreChunksRequest) ([]byte, error) { - requestHash, err := hashing.HashStoreChunksRequest(request) + requestHash, err := hashingv2.HashStoreChunksRequest(request) if err != nil { return nil, fmt.Errorf("failed to hash request: %w", err) } From cdea53aa2fbe37d789b8d37aa581bce8c9d880e9 Mon Sep 17 00:00:00 2001 From: Taras Shchybovyk Date: Mon, 15 Dec 2025 13:07:49 -0800 Subject: [PATCH 12/12] - move back to legacy keckak hasher for compatibility --- api/hashing/blob_header_v2_compat_test.go | 58 +++++++++++++++++++ ... => store_chunk_request_v2_compat_test.go} | 8 +-- api/hashing/v2/blob_header.go | 12 ++-- api/hashing/v2/store_chunk_request.go | 3 +- 4 files changed, 69 insertions(+), 12 deletions(-) create mode 100644 api/hashing/blob_header_v2_compat_test.go rename api/hashing/{v2/store_chunk_request_test.go => store_chunk_request_v2_compat_test.go} (88%) diff --git a/api/hashing/blob_header_v2_compat_test.go b/api/hashing/blob_header_v2_compat_test.go new file mode 100644 index 0000000000..c333fd39e5 --- /dev/null +++ b/api/hashing/blob_header_v2_compat_test.go @@ -0,0 +1,58 @@ +package hashing + +import ( + "testing" + "time" + + commonv1 "github.com/Layr-Labs/eigenda/api/grpc/common" + commonv2 "github.com/Layr-Labs/eigenda/api/grpc/common/v2" + grpc "github.com/Layr-Labs/eigenda/api/grpc/validator" + hashingv2 "github.com/Layr-Labs/eigenda/api/hashing/v2" + "github.com/stretchr/testify/require" + "golang.org/x/crypto/sha3" +) + +func TestV2BlobHeaderHashMatchesLegacyHashBlobHeader(t *testing.T) { + tsNanos := int64(1234567890123456) + header := &commonv2.BlobHeader{ + Version: 1, + QuorumNumbers: []uint32{0, 2, 7}, + Commitment: &commonv1.BlobCommitment{ + Commitment: []byte{0xaa, 0xbb}, + LengthCommitment: []byte{0x10, 0x11, 0x12}, + LengthProof: []byte{0x20}, + Length: 123, + }, + PaymentHeader: &commonv2.PaymentHeader{ + AccountId: "0xabc", + Timestamp: tsNanos, + CumulativePayment: []byte{0x09, 0x08, 0x07}, + }, + } + + // "Old" blob header hash: use the legacy streaming serializer+hash used by node_hashing.go. + legacyHasher := sha3.NewLegacyKeccak256() + err := hashBlobHeader(legacyHasher, header) + require.NoError(t, err) + legacyHash := legacyHasher.Sum(nil) + + req := &grpc.StoreChunksRequest{ + Batch: &commonv2.Batch{ + Header: &commonv2.BatchHeader{BatchRoot: []byte{0x01}, ReferenceBlockNumber: 1}, + BlobCertificates: []*commonv2.BlobCertificate{{ + BlobHeader: header, + Signature: []byte{0x00}, + RelayKeys: []uint32{1}, + }}, + }, + DisperserID: 1, + Timestamp: 1, + } + + got, err := hashingv2.BlobHeadersHashesAndTimestamps(req) + require.NoError(t, err) + require.Len(t, got, 1) + + require.Equal(t, legacyHash, got[0].Hash) + require.True(t, got[0].Timestamp.Equal(time.Unix(0, tsNanos))) +} diff --git a/api/hashing/v2/store_chunk_request_test.go b/api/hashing/store_chunk_request_v2_compat_test.go similarity index 88% rename from api/hashing/v2/store_chunk_request_test.go rename to api/hashing/store_chunk_request_v2_compat_test.go index d72697a979..532bedab84 100644 --- a/api/hashing/v2/store_chunk_request_test.go +++ b/api/hashing/store_chunk_request_v2_compat_test.go @@ -6,11 +6,11 @@ import ( commonv1 "github.com/Layr-Labs/eigenda/api/grpc/common" commonv2 "github.com/Layr-Labs/eigenda/api/grpc/common/v2" grpc "github.com/Layr-Labs/eigenda/api/grpc/validator" - legacyhashing "github.com/Layr-Labs/eigenda/api/hashing" + hashingv2 "github.com/Layr-Labs/eigenda/api/hashing/v2" "github.com/stretchr/testify/require" ) -func TestHashStoreChunksRequest_CanonicalMatchesHasherImplementation(t *testing.T) { +func TestHashStoreChunksRequestMatchesLegacyHashStoreChunksRequest(t *testing.T) { req := &grpc.StoreChunksRequest{ Batch: &commonv2.Batch{ Header: &commonv2.BatchHeader{ @@ -62,10 +62,10 @@ func TestHashStoreChunksRequest_CanonicalMatchesHasherImplementation(t *testing. Timestamp: 55, } - h1, err := legacyhashing.HashStoreChunksRequest(req) + h1, err := HashStoreChunksRequest(req) require.NoError(t, err) - h2, err := HashStoreChunksRequest(req) + h2, err := hashingv2.HashStoreChunksRequest(req) require.NoError(t, err) require.Equal(t, h1, h2, "legacy (manual) serializer hash must match canonical (struc) serializer hash") diff --git a/api/hashing/v2/blob_header.go b/api/hashing/v2/blob_header.go index 55f5d27cd5..d4f56cbbb0 100644 --- a/api/hashing/v2/blob_header.go +++ b/api/hashing/v2/blob_header.go @@ -13,7 +13,7 @@ import ( type BlobHeaderHashWithTimestamp struct { // Hash is canonical serialized blob header hash. Hash []byte - // Timestamp is a timestamp from the payment header (seconds since epoch). + // Timestamp is derived from PaymentHeader.Timestamp (nanoseconds since epoch). Timestamp time.Time } @@ -39,14 +39,12 @@ func BlobHeadersHashesAndTimestamps(request *grpc.StoreChunksRequest) ([]BlobHea if err != nil { return nil, fmt.Errorf("failed to serialize blob header at index %d: %w", i, err) } - hasher := sha3.New256() + // Must match legacy hashing (Keccak-256, not SHA3-256). + hasher := sha3.NewLegacyKeccak256() _, _ = hasher.Write(headerBytes) out[i] = BlobHeaderHashWithTimestamp{ - Hash: hasher.Sum(nil), - Timestamp: time.Unix( - paymentHeader.GetTimestamp()/int64(time.Second), - paymentHeader.GetTimestamp()%int64(time.Second), - ), + Hash: hasher.Sum(nil), + Timestamp: time.Unix(0, paymentHeader.GetTimestamp()), } } diff --git a/api/hashing/v2/store_chunk_request.go b/api/hashing/v2/store_chunk_request.go index 7dbf7693ba..cde79ee342 100644 --- a/api/hashing/v2/store_chunk_request.go +++ b/api/hashing/v2/store_chunk_request.go @@ -14,7 +14,8 @@ func HashStoreChunksRequest(request *grpc.StoreChunksRequest) ([]byte, error) { if err != nil { return nil, fmt.Errorf("failed to serialize store chunks request: %w", err) } - hasher := sha3.New256() + // Must match legacy hashing (Keccak-256, not SHA3-256). + hasher := sha3.NewLegacyKeccak256() _, _ = hasher.Write(canonicalRequest) return hasher.Sum(nil), nil }