Skip to content

Commit

Permalink
Add header storage (#224)
Browse files Browse the repository at this point in the history
  • Loading branch information
bamzedev authored Jan 16, 2025
1 parent 52e6b2d commit 1712821
Show file tree
Hide file tree
Showing 6 changed files with 247 additions and 254 deletions.
95 changes: 0 additions & 95 deletions internal/block/header.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,8 @@ package block

import (
"crypto/ed25519"
"errors"
"fmt"

"github.com/eigerco/strawberry/pkg/db"
"github.com/eigerco/strawberry/pkg/db/pebble"

"github.com/eigerco/strawberry/internal/common"
"github.com/eigerco/strawberry/internal/crypto"
"github.com/eigerco/strawberry/internal/jamtime"
Expand Down Expand Up @@ -60,94 +56,3 @@ func HeaderFromBytes(data []byte) (Header, error) {
}
return header, nil
}

// AncestorStoreSingleton the in memory store for headers that need to be kept for 24 hours
// TODO: Add 24 hours TTL
var AncestorStoreSingleton = NewAncestorStore()

// AncestorStore manages blockchain header storage using KVStore as the backend
type AncestorStore struct {
store db.KVStore
}

// NewAncestorStore creates a new in-memory ancestor store using KVStore
func NewAncestorStore() *AncestorStore {
store, err := pebble.NewKVStore()
if err != nil {
panic(fmt.Errorf("failed to initialize store: %w", err))
}

return &AncestorStore{
store: store,
}
}

// StoreHeader stores a header in the database
func (a *AncestorStore) StoreHeader(header Header) error {
encodedHeader, err := jam.Marshal(header)
if err != nil {
return fmt.Errorf("failed to marshal header: %w", err)
}
hash := crypto.HashData(encodedHeader)

if err := a.store.Put(hash[:], encodedHeader); err != nil {
return fmt.Errorf("failed to store header: %w", err)
}

return nil
}

// GetAncestor retrieves the parent header for the given header
func (a *AncestorStore) GetAncestor(header Header) (Header, error) {
encodedHeader, err := a.store.Get(header.ParentHash[:])
if err != nil {
if errors.Is(err, pebble.ErrNotFound) {
return Header{}, nil
}
return Header{}, fmt.Errorf("failed to get ancestor: %w", err)
}

var ancestorHeader Header
if err := jam.Unmarshal(encodedHeader, &ancestorHeader); err != nil {
return Header{}, fmt.Errorf("failed to unmarshal header: %w", err)
}

return ancestorHeader, nil
}

// FindAncestor finds a header that matches the given predicate
func (a *AncestorStore) FindAncestor(fn func(header Header) bool) (Header, error) {
iter, err := a.store.NewIterator(nil, nil)
if err != nil {
return Header{}, fmt.Errorf("failed to create iterator: %w", err)
}
defer func(iter db.Iterator) {
err := iter.Close()
if err != nil {
panic(fmt.Errorf("failed to close iterator: %w", err))
}
}(iter)

for valid := iter.Next(); valid; valid = iter.Next() {
value, err := iter.Value()
if err != nil {
return Header{}, fmt.Errorf("failed to get value: %w", err)
}

var header Header
if err := jam.Unmarshal(value, &header); err != nil {
return Header{}, fmt.Errorf("failed to unmarshal header: %w", err)
}

if fn(header) {
return header, nil
}
}

return Header{}, nil
}

// Close closes the underlying store
func (a *AncestorStore) Close() error {
return a.store.Close()
}
131 changes: 0 additions & 131 deletions internal/block/header_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package block

import (
"crypto/ed25519"
"github.com/eigerco/strawberry/internal/jamtime"
"testing"

"github.com/eigerco/strawberry/internal/common"
Expand Down Expand Up @@ -51,133 +50,3 @@ func Test_HeaderEncodeDecode(t *testing.T) {

assert.Equal(t, h, h2)
}

func TestNewAncestorStore(t *testing.T) {
store := NewAncestorStore()
defer store.Close()

assert.NotEmpty(t, store.store)
}

func createTestHeader(parentHash crypto.Hash, slot jamtime.Timeslot) Header {
return Header{
ParentHash: parentHash,
TimeSlotIndex: slot,
// Other fields can be left at zero values for testing
}
}

func TestStoreAndGetHeader(t *testing.T) {
store := NewAncestorStore()
defer store.Close()

// Create a parent header with a zero ParentHash (genesis block)
parentHeader := Header{
ParentHash: crypto.Hash{}, // Genesis block has no parent
TimeSlotIndex: jamtime.Timeslot(1),
}

// Store the parent header
err := store.StoreHeader(parentHeader)
require.NoError(t, err)

// Calculate the hash of the parent header
encodedParentHeader, err := jam.Marshal(parentHeader)
require.NoError(t, err)
parentHeaderHash := crypto.HashData(encodedParentHeader)

// Create a child header referencing the parent header
childHeader := Header{
ParentHash: parentHeaderHash,
TimeSlotIndex: parentHeader.TimeSlotIndex + 1,
}

// Store the child header
err = store.StoreHeader(childHeader)
require.NoError(t, err)

// Retrieve the ancestor (parent header) of the child header
ancestorHeader, err := store.GetAncestor(childHeader)
require.NoError(t, err)
require.NotEmpty(t, ancestorHeader, "Ancestor should be found")

// Verify that the retrieved ancestor matches the original parent header
assert.Equal(t, parentHeader.ParentHash, ancestorHeader.ParentHash, "ParentHash should match")
assert.Equal(t, parentHeader.TimeSlotIndex, ancestorHeader.TimeSlotIndex, "TimeSlotIndex should match")
}

func TestGetNonExistentAncestor(t *testing.T) {
store := NewAncestorStore()
defer store.Close()

header := Header{
ParentHash: crypto.Hash{1, 2, 3}, // This hash doesn't exist in store
}

// Try to get a non-existent ancestor
retrieved, err := store.GetAncestor(header)
require.NoError(t, err)
assert.Empty(t, retrieved)
}

func TestFindAncestor(t *testing.T) {
store := NewAncestorStore()
defer store.Close()

// Store test headers
headers := []Header{
createTestHeader(crypto.Hash{1}, 1),
createTestHeader(crypto.Hash{2}, 2),
createTestHeader(crypto.Hash{3}, 3),
}

for _, h := range headers {
err := store.StoreHeader(h)
require.NoError(t, err)
}

testCases := []struct {
name string
predicate func(Header) bool
expectSlot jamtime.Timeslot
shouldFind bool
}{
{
name: "Find by specific timeslot",
predicate: func(h Header) bool {
return h.TimeSlotIndex == 2
},
expectSlot: 2,
shouldFind: true,
},
{
name: "Find non-existent timeslot",
predicate: func(h Header) bool {
return h.TimeSlotIndex == 99
},
shouldFind: false,
},
{
name: "Find by parent hash",
predicate: func(h Header) bool {
return h.ParentHash == crypto.Hash{2}
},
expectSlot: 2,
shouldFind: true,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
found, err := store.FindAncestor(tc.predicate)
require.NoError(t, err)

if tc.shouldFind {
require.NotEmpty(t, found)
assert.Equal(t, tc.expectSlot, found.TimeSlotIndex)
} else {
assert.Empty(t, found)
}
})
}
}
16 changes: 7 additions & 9 deletions internal/statetransition/state_transition.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"github.com/eigerco/strawberry/internal/safrole"
"github.com/eigerco/strawberry/internal/service"
"github.com/eigerco/strawberry/internal/state"
"github.com/eigerco/strawberry/internal/store"
"github.com/eigerco/strawberry/internal/validator"
"github.com/eigerco/strawberry/pkg/serialization/codec/jam"
)
Expand All @@ -38,14 +39,14 @@ const (
// TODO: all the calculations which are not dependent on intermediate / new state can be done in parallel
//
// it might be worth making State immutable and make it so that UpdateState returns a new State with all the updated fields
func UpdateState(s *state.State, newBlock block.Block) error {
func UpdateState(s *state.State, newBlock block.Block, chain *store.Chain) error {
if newBlock.Header.TimeSlotIndex.IsInFuture() {
return errors.New("invalid block, it is in the future")
}

newTimeState := CalculateNewTimeState(newBlock.Header)

if err := ValidateExtrinsicGuarantees(newBlock.Header, s, newBlock.Extrinsic.EG, s.CoreAssignments, newTimeState, block.AncestorStoreSingleton); err != nil {
if err := ValidateExtrinsicGuarantees(newBlock.Header, s, newBlock.Extrinsic.EG, s.CoreAssignments, newTimeState, chain); err != nil {
return fmt.Errorf("extrinsic guarantees validation failed, err: %w", err)
}

Expand Down Expand Up @@ -1254,7 +1255,7 @@ func ValidateExtrinsicGuarantees(
guarantees block.GuaranteesExtrinsic,
currentAssignment state.CoreAssignments,
newTimeslot jamtime.Timeslot,
ancestorStore *block.AncestorStore,
chain *store.Chain,
) error {
// [⋃ x∈β] K(x_p) ∪ [⋃ x∈ξ] x ∪ q ∪ a
pastWorkPackages := make(map[crypto.Hash]struct{})
Expand Down Expand Up @@ -1432,15 +1433,12 @@ func ValidateExtrinsicGuarantees(
}

// ∀x ∈ x ∶ ∃h ∈ A ∶ ht = xt ∧ H(h) = xl (eq. 11.34 0.5.0)
_, err = ancestorStore.FindAncestor(func(ancestor block.Header) bool {
encodedHeader, err := jam.Marshal(ancestor)
_, err = chain.FindHeader(func(ancestor block.Header) bool {
ancestorHash, err := ancestor.Hash()
if err != nil {
return false
}
if ancestor.TimeSlotIndex == context.LookupAnchor.Timeslot && crypto.HashData(encodedHeader) == context.LookupAnchor.HeaderHash {
return true
}
return false
return ancestor.TimeSlotIndex == context.LookupAnchor.Timeslot && ancestorHash == context.LookupAnchor.HeaderHash
})
if err != nil {
return fmt.Errorf("no record of header found: %w", err)
Expand Down
Loading

0 comments on commit 1712821

Please sign in to comment.