Skip to content

Commit

Permalink
Add block request (#268)
Browse files Browse the repository at this point in the history
* Add block request
  • Loading branch information
bamzedev authored Feb 19, 2025
1 parent ccec214 commit 12424ca
Show file tree
Hide file tree
Showing 5 changed files with 495 additions and 59 deletions.
238 changes: 238 additions & 0 deletions internal/chain/service.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
package chain

import (
"errors"
"fmt"
"sync"
"time"

"github.com/eigerco/strawberry/internal/block"
"github.com/eigerco/strawberry/internal/crypto"
"github.com/eigerco/strawberry/internal/jamtime"
"github.com/eigerco/strawberry/internal/store"
"github.com/eigerco/strawberry/pkg/db/pebble"
"github.com/eigerco/strawberry/pkg/network"
)

// BlockService manages the node's view of the blockchain state, including:
// - Known leaf blocks (blocks with no known children)
// - Latest finalized block
// - Block storage and retrieval
//
// It handles block announcements according to UP 0 protocol specification,
// maintaining the set of leaf blocks and tracking finalization status.
type BlockService struct {
Mu sync.RWMutex
KnownLeaves map[crypto.Hash]jamtime.Timeslot // Maps leaf block hashes to their timeslots
LatestFinalized LatestFinalized // Tracks the most recently finalized block
Store *store.Chain // Persistent block storage
}

// LatestFinalized represents the latest finalized block in the chain.
// A block is considered finalized when it has a chain of 5 descendant blocks
// built on top of it according to the finalization rules.
type LatestFinalized struct {
Hash crypto.Hash // Hash of the finalized block
TimeSlotIndex jamtime.Timeslot // Timeslot of the finalized block
}

// Leaf represents a block with no known children (a tip of the chain).
// The BlockService tracks all known leaves to implement the UP 0 protocol's
// requirement of announcing all leaves in handshake messages.
type Leaf struct {
Hash crypto.Hash // Hash of the leaf block
TimeSlotIndex jamtime.Timeslot // Timeslot of the leaf block
}

// NewBlockService initializes a new BlockService with:
// - Empty leaf block set
// - Persistent block storage using PebbleDB
// - Genesis block as the latest finalized block
func NewBlockService() (*BlockService, error) {
kvStore, err := pebble.NewKVStore()
if err != nil {
return nil, err
}
chain := store.NewChain(kvStore)
bs := &BlockService{
Store: chain,
KnownLeaves: make(map[crypto.Hash]jamtime.Timeslot),
}
// Initialize by finding leaves and finalized block
if err := bs.initializeState(); err != nil {
// Log error but continue - we can recover state as we process blocks
fmt.Printf("Failed to initialize block manager state: %v\n", err)
}
return bs, nil
}

// initializeState sets up the initial blockchain state:
// 1. Creates and stores the genesis block
// 2. Sets genesis as the latest finalized block
//
// TODO: This is still a `mock` implementation.
func (bs *BlockService) initializeState() error {
// For now use genesis block
genesisHeader := block.Header{
ParentHash: crypto.Hash{1},
TimeSlotIndex: jamtime.Timeslot(1),
BlockAuthorIndex: 0,
}
hash, err := genesisHeader.Hash()
fmt.Printf("hash: %v\n", hash)
if err != nil {
return fmt.Errorf("failed to hash genesis block: %w", err)
}
if err := bs.Store.PutHeader(genesisHeader); err != nil {
return fmt.Errorf("failed to store genesis block: %w", err)
}
b := block.Block{
Header: genesisHeader,
}
if err := bs.Store.PutBlock(b); err != nil {
return fmt.Errorf("failed to store genesis block: %w", err)
}
bs.Mu.Lock()
defer bs.Mu.Unlock()
bs.LatestFinalized = LatestFinalized{
Hash: hash,
TimeSlotIndex: genesisHeader.TimeSlotIndex,
}
return nil
}

// checkFinalization determines if a block can be finalized by:
// 1. Walking back 5 generations from the given block hash
// 2. If a complete chain of 5 blocks exists, finalizing the oldest block
// 3. Updating the latest finalized pointer
// 4. Removing the finalized block from the leaf set if present
//
// Returns nil if finalization check succeeds, error if any operations fail.
// Note: May return nil even if finalization isn't possible (e.g., missing ancestors).
// This is due to genesis block handling and is not considered an error.
func (bs *BlockService) checkFinalization(hash crypto.Hash) error {
// Start from current header and walk back 6 generations
currentHash := hash
var ancestorChain []block.Header

// Walk back 6`` generations
for i := 0; i < 6; i++ {
header, err := bs.Store.GetHeader(currentHash)
if err != nil {
if errors.Is(err, store.ErrHeaderNotFound) {
// If we can't find a parent, we can't finalize
return nil
}
return fmt.Errorf("failed to get header in chain: %w", err)
}

ancestorChain = append(ancestorChain, header)
currentHash = header.ParentHash
}

// Get the oldest header (the one we'll finalize)
finalizeHeader := ancestorChain[len(ancestorChain)-1]
finalizeHash, err := finalizeHeader.Hash()
if err != nil {
return fmt.Errorf("failed to hash header during finalization: %w", err)
}

bs.RemoveLeaf(finalizeHash)
bs.UpdateLatestFinalized(finalizeHash, finalizeHeader.TimeSlotIndex)

return nil
}

// HandleNewHeader processes a new block header announcement by:
// 1. Storing the header in persistent storage
// 2. Updating the leaf block set (removing parent, adding new block)
// 3. Checking if the parent block can now be finalized
//
// This implements the core block processing logic required by UP 0 protocol,
// maintaining the node's view of chain tips and finalization status.
func (bs *BlockService) HandleNewHeader(header *block.Header) error {
// Get the header hash
hash, err := header.Hash()
if err != nil {
return fmt.Errorf("failed to hash header: %w", err)
}
// Need to verify this block is a descendant of latest finalized block
// before considering it as a potential leaf
isDescendant, err := bs.isDescendantOfFinalized(header)
if err != nil {
return fmt.Errorf("failed to check if block is descendant of finalized: %w", err)
}
if !isDescendant {
return fmt.Errorf("block %s is not a descendant of latest finalized block", hash)
}

// First store the header
if err := bs.Store.PutHeader(*header); err != nil {
return fmt.Errorf("failed to store header: %w", err)
}

// Only update leaves if this is a descendant of finalized block
bs.RemoveLeaf(header.ParentHash)
bs.AddLeaf(hash, header.TimeSlotIndex)

// Check if this creates a finalization condition starting from parent
if err := bs.checkFinalization(header.ParentHash); err != nil {
// Log but don't fail on finalization check errors
fmt.Printf("Failed to check finalization: %v\n", err)
}

return nil
}

// UpdateLatestFinalized updates the latest finalized block pointer.
func (bs *BlockService) UpdateLatestFinalized(hash crypto.Hash, slot jamtime.Timeslot) {
bs.Mu.Lock()
defer bs.Mu.Unlock()
bs.LatestFinalized = LatestFinalized{Hash: hash, TimeSlotIndex: slot}
network.LogBlockEvent(time.Now(), "finalizing", hash, slot.ToEpoch(), slot)
}

// AddLeaf adds a block to the set of known leaves.
func (bs *BlockService) AddLeaf(hash crypto.Hash, slot jamtime.Timeslot) {
bs.Mu.Lock()
defer bs.Mu.Unlock()
bs.KnownLeaves[hash] = slot
}

// RemoveLeaf removes a block from the set of known leaves.
func (bs *BlockService) RemoveLeaf(hash crypto.Hash) {
bs.Mu.Lock()
defer bs.Mu.Unlock()
delete(bs.KnownLeaves, hash)
}

// isDescendantOfFinalized checks if a block is a descendant of the latest finalized block
// by walking back through its ancestors until we either:
// - Find the latest finalized block (true)
// - Find a different block at the same height as latest finalized (false)
// - Can't find a parent (error)
func (bs *BlockService) isDescendantOfFinalized(header *block.Header) (bool, error) {
bs.Mu.RLock()
finalizedSlot := bs.LatestFinalized.TimeSlotIndex
finalizedHash := bs.LatestFinalized.Hash
bs.Mu.RUnlock()

current := header
for current.TimeSlotIndex > finalizedSlot {
parent, err := bs.Store.GetHeader(current.ParentHash)
if err != nil {
return false, fmt.Errorf("failed to get parent block: %w", err)
}
current = &parent
}

// If we found the finalized block, this is a descendant
if current.TimeSlotIndex == finalizedSlot {
currentHash, err := current.Hash()
if err != nil {
return false, err
}
return currentHash == finalizedHash, nil
}
return false, nil
}
77 changes: 77 additions & 0 deletions internal/chain/service_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package chain

import (
"testing"

"github.com/eigerco/strawberry/internal/block"
"github.com/eigerco/strawberry/internal/crypto"
"github.com/eigerco/strawberry/internal/jamtime"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestCheckFinalization(t *testing.T) {
// Create a new BlockService
bs, err := NewBlockService()
require.NoError(t, err)

// Create a chain of 7 headers (0->1->2->3->4->5->6)
// Block 1 will have 5 descendants (2,3,4,5,6) and should be finalized
var headers []block.Header
var hashes []crypto.Hash
parentHash := crypto.Hash{} // Zero hash for first block

// Create and store headers
for i := uint32(0); i < 7; i++ {
header := block.Header{
TimeSlotIndex: jamtime.Timeslot(i),
ParentHash: parentHash,
}

// Store the header
err := bs.Store.PutHeader(header)
require.NoError(t, err)

// Get and store the hash for next iteration
hash, err := header.Hash()
require.NoError(t, err)
parentHash = hash

headers = append(headers, header)
hashes = append(hashes, hash)
}

// Add some headers as leaves
bs.AddLeaf(hashes[6], 6) // Last one is a leaf

// Try to finalize using the last header
err = bs.checkFinalization(hashes[6])
require.NoError(t, err)

// Verify:
// 1. Header[1] should be finalized (having 5 descendants: 2,3,4,5,6)
// 2. Leaves should be updated
// 3. LatestFinalized should be set to header[1]

// Check LatestFinalized
assert.Equal(t, hashes[1], bs.LatestFinalized.Hash)
assert.Equal(t, headers[1].TimeSlotIndex, bs.LatestFinalized.TimeSlotIndex)

// Check leaves - only block 6 should remain as leaf
_, exists5 := bs.KnownLeaves[hashes[5]]
_, exists6 := bs.KnownLeaves[hashes[6]]
assert.False(t, exists5, "hash[5] should not be a leaf")
assert.True(t, exists6, "hash[6] should still be a leaf")
assert.Equal(t, 1, len(bs.KnownLeaves), "should only have one leaf")

// Try finalizing again with same hash - should not change anything
prevFinalized := bs.LatestFinalized
err = bs.checkFinalization(hashes[6])
require.NoError(t, err)
assert.Equal(t, prevFinalized, bs.LatestFinalized, "finalization should not change on second attempt")

// Try finalizing with non-existent hash
err = bs.checkFinalization(crypto.Hash{1, 2, 3})
require.NoError(t, err) // Should return nil error as per our implementation
assert.Equal(t, prevFinalized, bs.LatestFinalized, "finalization should not change with invalid hash")
}
Loading

0 comments on commit 12424ca

Please sign in to comment.