diff --git a/proto/evnode/v1/evnode.proto b/proto/evnode/v1/evnode.proto index f1ca34ae7..6048536a3 100644 --- a/proto/evnode/v1/evnode.proto +++ b/proto/evnode/v1/evnode.proto @@ -93,6 +93,7 @@ message Metadata { message Data { Metadata metadata = 1; repeated bytes txs = 2; + bytes cached_hash = 3; // Optional cached hash for performance } // SignedData is a data with a signature and a signer. diff --git a/types/data.go b/types/data.go index ebecac30f..ae7638258 100644 --- a/types/data.go +++ b/types/data.go @@ -33,6 +33,10 @@ type Metadata struct { type Data struct { *Metadata Txs Txs + + // cachedHash stores the computed hash to avoid recalculation + // This field is set once when Hash() is first called and never modified after that + cachedHash Hash } // SignedData combines Data and its signature. diff --git a/types/hashing.go b/types/hashing.go index f2c9006e1..15c6c400c 100644 --- a/types/hashing.go +++ b/types/hashing.go @@ -21,10 +21,22 @@ func (h *Header) Hash() Hash { // Hash returns hash of the Data func (d *Data) Hash() Hash { + // Return cached hash if already computed + if d.cachedHash != nil { + return d.cachedHash + } + + // Compute hash if not cached // Ignoring the marshal error for now to satisfy the go-header interface // Later on the usage of Hash should be replaced with DA commitment - dBytes, _ := d.MarshalBinary() - return leafHashOpt(sha256.New(), dBytes) + // Use MarshalBinaryWithoutCache to avoid circular dependency with cached hash + dBytes, _ := d.MarshalBinaryWithoutCache() + hash := leafHashOpt(sha256.New(), dBytes) + + // Cache the result - no synchronization needed since hash computation is idempotent + // If multiple goroutines compute this simultaneously, they'll get the same result + d.cachedHash = hash + return hash } // DACommitment returns the DA commitment of the Data excluding the Metadata diff --git a/types/hashing_test.go b/types/hashing_test.go index 1bcc8691d..43f7e0c12 100644 --- a/types/hashing_test.go +++ b/types/hashing_test.go @@ -48,7 +48,8 @@ func TestDataHash(t *testing.T) { hash1 := data.Hash() - dataBytes, err := data.MarshalBinary() + // Use MarshalBinaryWithoutCache for consistent hash calculation + dataBytes, err := data.MarshalBinaryWithoutCache() require.NoError(t, err) hasher := sha256.New() @@ -60,14 +61,36 @@ func TestDataHash(t *testing.T) { assert.Len(t, hash1, sha256.Size) assert.Equal(t, Hash(expectedHash), hash1, "Data hash should match manual calculation with prefix") - data.Txs = Txs{Tx("tx3")} - hash2 := data.Hash() + // Test that different Data objects with different Txs have different hashes + data2 := Data{ + Txs: Txs{Tx("tx3")}, + Metadata: &Metadata{ + ChainID: "test-chain", + Height: 1, + Time: 1234567890, + LastDataHash: []byte("lastdatahash"), + }, + } + hash2 := data2.Hash() assert.NotEqual(t, hash1, hash2, "Different data (Txs) should have different hashes") - data.Metadata.Height = 2 - hash3 := data.Hash() + // Test that different Data objects with different Metadata have different hashes + data3 := Data{ + Txs: Txs{Tx("tx1"), Tx("tx2")}, + Metadata: &Metadata{ + ChainID: "test-chain", + Height: 2, // Different height + Time: 1234567890, + LastDataHash: []byte("lastdatahash"), + }, + } + hash3 := data3.Hash() assert.NotEqual(t, hash1, hash3, "Different data (Metadata) should have different hashes") assert.NotEqual(t, hash2, hash3) + + // Test that calling Hash() multiple times on the same object returns the same result (caching) + hash1Again := data.Hash() + assert.Equal(t, hash1, hash1Again, "Hash should be cached and return same result") } // TestLeafHashOpt tests the helper function leafHashOpt directly. diff --git a/types/pb/evnode/v1/evnode.pb.go b/types/pb/evnode/v1/evnode.pb.go index 700b2182e..93c5748d6 100644 --- a/types/pb/evnode/v1/evnode.pb.go +++ b/types/pb/evnode/v1/evnode.pb.go @@ -423,6 +423,7 @@ type Data struct { state protoimpl.MessageState `protogen:"open.v1"` Metadata *Metadata `protobuf:"bytes,1,opt,name=metadata,proto3" json:"metadata,omitempty"` Txs [][]byte `protobuf:"bytes,2,rep,name=txs,proto3" json:"txs,omitempty"` + CachedHash []byte `protobuf:"bytes,3,opt,name=cached_hash,json=cachedHash,proto3" json:"cached_hash,omitempty"` // Optional cached hash for performance unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -471,6 +472,13 @@ func (x *Data) GetTxs() [][]byte { return nil } +func (x *Data) GetCachedHash() []byte { + if x != nil { + return x.CachedHash + } + return nil +} + // SignedData is a data with a signature and a signer. type SignedData struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -647,10 +655,12 @@ const file_evnode_v1_evnode_proto_rawDesc = "" + "\bchain_id\x18\x01 \x01(\tR\achainId\x12\x16\n" + "\x06height\x18\x02 \x01(\x04R\x06height\x12\x12\n" + "\x04time\x18\x03 \x01(\x04R\x04time\x12$\n" + - "\x0elast_data_hash\x18\x04 \x01(\fR\flastDataHash\"I\n" + + "\x0elast_data_hash\x18\x04 \x01(\fR\flastDataHash\"j\n" + "\x04Data\x12/\n" + "\bmetadata\x18\x01 \x01(\v2\x13.evnode.v1.MetadataR\bmetadata\x12\x10\n" + - "\x03txs\x18\x02 \x03(\fR\x03txs\"z\n" + + "\x03txs\x18\x02 \x03(\fR\x03txs\x12\x1f\n" + + "\vcached_hash\x18\x03 \x01(\fR\n" + + "cachedHash\"z\n" + "\n" + "SignedData\x12#\n" + "\x04data\x18\x01 \x01(\v2\x0f.evnode.v1.DataR\x04data\x12\x1c\n" + diff --git a/types/serialization.go b/types/serialization.go index ee78a8165..2ac77baa1 100644 --- a/types/serialization.go +++ b/types/serialization.go @@ -47,6 +47,11 @@ func (d *Data) MarshalBinary() ([]byte, error) { return proto.Marshal(d.ToProto()) } +// MarshalBinaryWithoutCache encodes Data without cached hash for hash calculation +func (d *Data) MarshalBinaryWithoutCache() ([]byte, error) { + return proto.Marshal(d.ToProtoWithoutCache()) +} + // UnmarshalBinary decodes binary form of Data into object. func (d *Data) UnmarshalBinary(data []byte) error { var pData pb.Data @@ -244,6 +249,20 @@ func (m *Metadata) FromProto(other *pb.Metadata) error { // ToProto converts Data into protobuf representation and returns it. func (d *Data) ToProto() *pb.Data { + var mProto *pb.Metadata + if d.Metadata != nil { + mProto = d.Metadata.ToProto() + } + return &pb.Data{ + Metadata: mProto, + Txs: txsToByteSlices(d.Txs), + CachedHash: d.cachedHash, + } +} + +// ToProtoWithoutCache converts Data to protobuf without the cached hash +// This is used for hash calculation to avoid circular dependency +func (d *Data) ToProtoWithoutCache() *pb.Data { var mProto *pb.Metadata if d.Metadata != nil { mProto = d.Metadata.ToProto() @@ -251,6 +270,7 @@ func (d *Data) ToProto() *pb.Data { return &pb.Data{ Metadata: mProto, Txs: txsToByteSlices(d.Txs), + // CachedHash is intentionally omitted for hash calculation } } @@ -270,6 +290,13 @@ func (d *Data) FromProto(other *pb.Data) error { d.Metadata = nil } d.Txs = byteSlicesToTxs(other.GetTxs()) + + // Restore cached hash from protobuf if available + if other.CachedHash != nil { + d.cachedHash = other.CachedHash + } else { + d.cachedHash = nil + } return nil }