Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions proto/evnode/v1/evnode.proto
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ message Metadata {
message Data {
Metadata metadata = 1;
repeated bytes txs = 2;
bytes cached_hash = 3; // Optional cached hash for performance
Copy link
Member

Choose a reason for hiding this comment

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

Do we really need to add this? Just caching the last one in the manager is sufficient right? Sure it is less optimized that cache it for all, but the logic that omits the cache for the serialization can be confusing if you use the api directly .

Copy link
Contributor Author

Choose a reason for hiding this comment

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

in the flow, every time we get we would rehash since we rarely to almost never keep the data struct alive for multiple checks on the hash after we get from the DB. This would make the hash cache nullified, evaluating if this cache is worth it if we remove it from the proto

Copy link
Member

Choose a reason for hiding this comment

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

don't we only care about height-1 anyway?

}

// SignedData is a data with a signature and a signer.
Expand Down
4 changes: 4 additions & 0 deletions types/data.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
16 changes: 14 additions & 2 deletions types/hashing.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
33 changes: 28 additions & 5 deletions types/hashing_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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.
Expand Down
14 changes: 12 additions & 2 deletions types/pb/evnode/v1/evnode.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

27 changes: 27 additions & 0 deletions types/serialization.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -244,13 +249,28 @@ 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()
}
return &pb.Data{
Metadata: mProto,
Txs: txsToByteSlices(d.Txs),
// CachedHash is intentionally omitted for hash calculation
}
}

Expand All @@ -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
}

Expand Down
Loading