Files
zeekayandHanzo Dev b1cc108c9f gofmt
Co-authored-by: Hanzo Dev <[email protected]>
2026-09-08 01:51:51 -07:00

129 lines
4.0 KiB
Go

// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
// See the file LICENSE for licensing terms.
package tree
import (
"context"
"maps"
"slices"
"sync"
"github.com/luxfi/ids"
chain "github.com/luxfi/vm/chain"
)
// Tree handles the propagation of block acceptance and rejection to inner
// blocks.
//
// The Tree is needed because:
// 1. The consensus engine guarantees that for each verified block, either
// Accept() or Reject() are eventually called, and they are called only once.
// The proposervm must maintain these invariants for the wrapped VM.
// 2. A given inner block may be wrapped into multiple different proposervm
// blocks (e.g. same inner block generated by two validators).
//
// The Tree prevents Accept() and Reject() from being called multiple times on
// the same inner block by:
// 1. tracking inner blocks in a tree-like structure, to be able to easily spot
// siblings
// 2. rejecting an inner block only when one of the siblings is accepted.
// Rejection of a proposervm block does not imply its inner block rejection
// (it may be held by a different proposervm block).
type Tree interface {
// Add places the block in the tree
Add(chain.Block)
// Get returns the block that was added to this tree whose parent and ID
// match the provided block. If non-exists, then false will be returned.
Get(chain.Block) (chain.Block, bool)
// Accept marks the provided block as accepted and rejects every conflicting
// block.
Accept(context.Context, chain.Block) error
}
type tree struct {
// lock guards [nodes]. The proposervm calls Add/Get (during Verify) and
// Accept concurrently from the consensus engine's handler goroutines, so
// every access to [nodes] must hold this lock. Accept makes block callouts
// (Accept/Reject on the inner VM) only AFTER releasing the lock, so this
// lock is a leaf lock and cannot deadlock against the inner VM.
lock sync.RWMutex
// parentID -> childID -> childBlock
nodes map[ids.ID]map[ids.ID]chain.Block
}
func New() Tree {
return &tree{
nodes: make(map[ids.ID]map[ids.ID]chain.Block),
}
}
func (t *tree) Add(blk chain.Block) {
t.lock.Lock()
defer t.lock.Unlock()
parentID := blk.Parent()
children, exists := t.nodes[parentID]
if !exists {
children = make(map[ids.ID]chain.Block)
t.nodes[parentID] = children
}
children[blk.ID()] = blk
}
func (t *tree) Get(blk chain.Block) (chain.Block, bool) {
t.lock.RLock()
defer t.lock.RUnlock()
parentID := blk.Parent()
children := t.nodes[parentID]
originalBlk, exists := children[blk.ID()]
return originalBlk, exists
}
func (t *tree) Accept(ctx context.Context, blk chain.Block) error {
// accept the provided block. This callout is made before any map mutation,
// preserving the original semantics: if Accept fails the tree is unchanged.
if err := blk.Accept(ctx); err != nil {
return err
}
// Phase 1 (locked, no callouts): detach the accepted block's subtree from
// the node map and collect every conflicting block that must be rejected.
// Collecting the full reject set under the lock — rather than interleaving
// map reads with Reject callouts — is what keeps this lock a leaf lock.
t.lock.Lock()
parentID := blk.Parent()
children := t.nodes[parentID]
delete(children, blk.ID())
delete(t.nodes, parentID)
// frontier holds blocks still to be expanded; rejected accumulates the full
// set of blocks to reject (each exactly once).
frontier := slices.Collect(maps.Values(children))
rejected := make([]chain.Block, 0, len(frontier))
for len(frontier) > 0 {
i := len(frontier) - 1
child := frontier[i]
frontier = frontier[:i]
rejected = append(rejected, child)
// mark the progeny of this block as being rejectable
childID := child.ID()
frontier = append(frontier, slices.Collect(maps.Values(t.nodes[childID]))...)
delete(t.nodes, childID)
}
t.lock.Unlock()
// Phase 2 (unlocked): reject all conflicting blocks via inner-VM callouts.
for _, child := range rejected {
if err := child.Reject(ctx); err != nil {
return err
}
}
return nil
}