Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add block request #268

Merged
merged 2 commits into from
Feb 19, 2025
Merged
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
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
Loading