-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Add block request
- Loading branch information
Showing
5 changed files
with
495 additions
and
59 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") | ||
} |
Oops, something went wrong.