Skip to content

Commit

Permalink
Add ancestor store based in pebble in memory db.
Browse files Browse the repository at this point in the history
  • Loading branch information
carlos-romano committed Nov 20, 2024
1 parent e0c9a9b commit ce4679f
Show file tree
Hide file tree
Showing 3 changed files with 211 additions and 28 deletions.
102 changes: 77 additions & 25 deletions internal/block/header.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ package block

import (
"crypto/ed25519"

"sync"
"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"
Expand Down Expand Up @@ -35,42 +37,92 @@ type EpochMarker struct {
type WinningTicketMarker [jamtime.TimeslotsPerEpoch]Ticket

// AncestorStoreSingleton the in memory store for headers that need to be kept for 24 hours
// TODO replace with pebble
var AncestorStoreSingleton = &AncestorStore{
ancestorSet: make(map[crypto.Hash]*Header),
mu: sync.RWMutex{},
}
// TODO: Add 24 hours TTL
var AncestorStoreSingleton = NewAncestorStore()

// AncestorStore manages blockchain header storage using KVStore as the backend
type AncestorStore struct {
ancestorSet map[crypto.Hash]*Header
mu sync.RWMutex
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,
}
}

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

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

return nil
}

func (a *AncestorStore) GetAncestor(header *Header) *Header {
a.mu.RLock()
defer a.mu.RUnlock()
return a.ancestorSet[header.ParentHash]
// 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
}

func (a *AncestorStore) FindAncestor(fn func(header *Header) bool) *Header {
a.mu.RLock()
defer a.mu.RUnlock()
for _, h := range a.ancestorSet {
if fn(h) {
return h
// 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 nil

return Header{}, nil
}

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

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

"github.com/eigerco/strawberry/internal/common"
Expand Down Expand Up @@ -50,3 +51,133 @@ 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)
}
})
}
}
6 changes: 3 additions & 3 deletions internal/statetransition/state_transition.go
Original file line number Diff line number Diff line change
Expand Up @@ -914,7 +914,7 @@ func validateExtrinsicGuarantees(header block.Header, currentState *state.State,
}

// ∀x ∈ x ∶ ∃h ∈ A ∶ ht = xt ∧ H(h) = xl (149 v0.4.5)
ancestor := ancestorStore.FindAncestor(func(ancestor *block.Header) bool {
_, err := ancestorStore.FindAncestor(func(ancestor block.Header) bool {
encodedHeader, err := jam.Marshal(ancestor)
if err != nil {
return false
Expand All @@ -924,8 +924,8 @@ func validateExtrinsicGuarantees(header block.Header, currentState *state.State,
}
return false
})
if ancestor == nil {
return fmt.Errorf("no record of header found")
if err != nil {
return fmt.Errorf("no record of header found: %w", err)
}
}

Expand Down

0 comments on commit ce4679f

Please sign in to comment.