mirror of
https://github.com/luxfi/dex.git
synced 2026-09-24 00:26:31 +00:00
fix(dchain): index processing blocks so plugin-transport Accept resolves them
The luxd plugin transport (vm/rpc) resolves Block.Accept by ID via VM.GetBlock — unlike Block.Verify, which carries the full block bytes (ParseBlock). The dexvm's GetBlock only indexed ACCEPTED blocks, so GetBlock(builtID) for a block the engine had just built returned ErrNotFound; the engine's self-finalize Accept (finalizeOwnProposal) became a silent no-op, the clob_* submitTx waiter (handler.go) hung forever, and the consensus forwarder wedged inside the hung Accept so NO further D-Chain blocks were ever built. The EVM survives the same ID-only Accept because geth resolves built-but-unaccepted blocks from its block cache; the dexvm had no equivalent. Add a processingBlocks index (built/verified-but-unaccepted blocks): BuildBlock and ParseBlock record into it (ParseBlock returns the stable instance so Verify's stashed overlay is the one Accept commits), GetBlock consults it after acceptedBlocks, and Accept/Reject prune it. Smallest change that makes the plugin Accept(ID) round-trip resolve the same instance built/verified — no transport, engine, or matcher change. TDD: TestVMAcceptViaGetBlock drives the exact plugin path BuildBlock -> GetBlock(id) -> Accept (proven to FAIL without the lookup: 'not found, the production stall'); TestVMParseVerifyAcceptByID covers the follower ParseBlock -> Verify -> GetBlock(id) -> Accept. Full pkg/dchain suite green CGO=0. Every prior test accepted the *Block from BuildBlock directly, skipping the ID-resolve path the live node uses.
This commit is contained in:
@@ -240,6 +240,7 @@ func (b *Block) Accept(ctx context.Context) error {
|
||||
b.vm.lastRoot = b.execRoot
|
||||
b.vm.acceptedBlocks[b.id] = b
|
||||
b.vm.heightIndex[b.height] = b.id
|
||||
delete(b.vm.processingBlocks, b.id) // now accepted; drop the processing entry
|
||||
b.applyToMemBooks()
|
||||
b.status = statusAccepted
|
||||
b.overlay = nil
|
||||
@@ -271,6 +272,7 @@ func (b *Block) Reject(ctx context.Context) error {
|
||||
b.overlay = nil
|
||||
}
|
||||
b.status = statusRejected
|
||||
delete(b.vm.processingBlocks, b.id) // rejected; drop the processing entry
|
||||
b.vm.mempool.Requeue(b.txs)
|
||||
return nil
|
||||
}
|
||||
|
||||
+31
-3
@@ -64,6 +64,16 @@ type VM struct {
|
||||
lastAcceptedHeight uint64
|
||||
lastRoot [Size]byte
|
||||
|
||||
// processingBlocks indexes built/verified blocks not yet accepted, so a
|
||||
// plugin-transport Accept — which carries only the block ID and re-resolves
|
||||
// the block via GetBlock (vm/rpc handleBlockAccept), unlike Verify which
|
||||
// carries the full bytes — can find the SAME instance (and its stashed Verify
|
||||
// overlay) to commit. Without it GetBlock(builtID) returns ErrNotFound, the
|
||||
// engine's self-finalize Accept is a silent no-op, and the submitTx waiter
|
||||
// hangs forever. The EVM gets this for free from geth's block cache. Pruned on
|
||||
// Accept/Reject. Guarded by vm.mu.
|
||||
processingBlocks map[ids.ID]*Block
|
||||
|
||||
// preferred is the block the engine asked us to build on next.
|
||||
preferred ids.ID
|
||||
|
||||
@@ -94,6 +104,7 @@ func (vm *VM) Initialize(ctx context.Context, init block.Init) error {
|
||||
vm.outcomes = newOutcomeRegistry()
|
||||
vm.books = map[[32]byte]*lx.OrderBook{}
|
||||
vm.acceptedBlocks = map[ids.ID]*Block{}
|
||||
vm.processingBlocks = map[ids.ID]*Block{}
|
||||
vm.heightIndex = map[uint64]ids.ID{}
|
||||
|
||||
// Load the head, or bootstrap genesis.
|
||||
@@ -386,6 +397,9 @@ func (vm *VM) BuildBlock(ctx context.Context) (block.Block, error) {
|
||||
}
|
||||
|
||||
blk := newBlock(vm, vm.lastAcceptedID, height, ts, res.root, txs)
|
||||
// Index the built block so a later Accept (which the plugin transport resolves
|
||||
// by ID via GetBlock, not by bytes) finds THIS instance to commit.
|
||||
vm.processingBlocks[blk.id] = blk
|
||||
vm.log.Debug("dchain built block", "height", height, "txs", len(txs), "fills", len(res.fills))
|
||||
return blk, nil
|
||||
}
|
||||
@@ -407,28 +421,42 @@ func (vm *VM) blockTimestamp() time.Time {
|
||||
}
|
||||
|
||||
// ParseBlock decodes block bytes into a Block. Used by the engine to reconstruct
|
||||
// a peer's proposed block; Verify then re-executes it.
|
||||
// a peer's proposed block; Verify then re-executes it. It returns a STABLE
|
||||
// instance per id: the accepted block if already accepted, else the processing
|
||||
// block already indexed (so the overlay Verify stashes on it is the one a later
|
||||
// Accept commits), else a freshly parsed block which it records as processing.
|
||||
func (vm *VM) ParseBlock(ctx context.Context, b []byte) (block.Block, error) {
|
||||
vm.mu.Lock()
|
||||
defer vm.mu.Unlock()
|
||||
// Return the cached accepted block if we already have it (stable identity).
|
||||
blk, err := parseBlock(vm, b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Return the cached accepted block if we already have it (stable identity).
|
||||
if existing, ok := vm.acceptedBlocks[blk.id]; ok {
|
||||
return existing, nil
|
||||
}
|
||||
// Reuse the processing instance if present, so Verify's stashed overlay is the
|
||||
// same one Accept commits; otherwise record this one as processing.
|
||||
if existing, ok := vm.processingBlocks[blk.id]; ok {
|
||||
return existing, nil
|
||||
}
|
||||
vm.processingBlocks[blk.id] = blk
|
||||
return blk, nil
|
||||
}
|
||||
|
||||
// GetBlock returns an accepted block by id, or database.ErrNotFound.
|
||||
// GetBlock returns a block by id — accepted or still processing (built/verified
|
||||
// but not yet accepted) — or database.ErrNotFound. The processing lookup is what
|
||||
// lets the plugin transport's ID-only Accept resolve the block it must commit.
|
||||
func (vm *VM) GetBlock(ctx context.Context, id ids.ID) (block.Block, error) {
|
||||
vm.mu.Lock()
|
||||
defer vm.mu.Unlock()
|
||||
if blk, ok := vm.acceptedBlocks[id]; ok {
|
||||
return blk, nil
|
||||
}
|
||||
if blk, ok := vm.processingBlocks[id]; ok {
|
||||
return blk, nil
|
||||
}
|
||||
return nil, database.ErrNotFound
|
||||
}
|
||||
|
||||
|
||||
@@ -523,6 +523,112 @@ func TestVMRestartLegacyGenesisDB(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestVMAcceptViaGetBlock reproduces the production consensus stall: the luxd
|
||||
// plugin transport (vm/rpc) resolves Block.Accept by ID via VM.GetBlock — unlike
|
||||
// Verify, which carries the full block bytes — and then calls Accept on whatever
|
||||
// GetBlock returns. Before the processing-block index, GetBlock(builtID) returned
|
||||
// ErrNotFound (only accepted blocks were indexed), so the engine's self-finalize
|
||||
// Accept was a silent no-op, the clob_* submitTx waiter hung forever, and no
|
||||
// further blocks were built. This drives the EXACT plugin-shaped path the live
|
||||
// node uses — BuildBlock -> GetBlock(id) -> Accept — which every existing test
|
||||
// skipped by accepting the *Block returned from BuildBlock directly.
|
||||
func TestVMAcceptViaGetBlock(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
db := memdb.New()
|
||||
pool := [32]byte{0x09}
|
||||
|
||||
vm, toEngine := newTestVM(t, db)
|
||||
|
||||
// A clob tx in the mempool, then the proposer's lifecycle exactly as the
|
||||
// engine drives it over the plugin transport.
|
||||
vm.mempool.Add(ensureMarketTx(t, pool))
|
||||
vm.mempool.Add(placePoolTx(t, pool, zapwire.SideSell, 101.0, 5.0, "maker-a"))
|
||||
drainSignals(toEngine)
|
||||
|
||||
built, err := vm.BuildBlock(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("BuildBlock: %v", err)
|
||||
}
|
||||
builtID := built.ID()
|
||||
|
||||
// THE PLUGIN-TRANSPORT CONTRACT: Verify the built block, then resolve it by ID
|
||||
// (not by holding the BuildBlock return) and Accept that instance.
|
||||
if err := built.Verify(ctx); err != nil {
|
||||
t.Fatalf("Verify: %v", err)
|
||||
}
|
||||
got, err := vm.GetBlock(ctx, builtID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetBlock(builtID) before accept: %v (the production stall)", err)
|
||||
}
|
||||
if got.ID() != builtID {
|
||||
t.Fatalf("GetBlock returned %s, want %s", got.ID(), builtID)
|
||||
}
|
||||
if err := got.Accept(ctx); err != nil {
|
||||
t.Fatalf("Accept via GetBlock: %v", err)
|
||||
}
|
||||
|
||||
// The head advanced and the accepted block is now resolvable by id; the
|
||||
// processing entry was pruned.
|
||||
if vm.lastAcceptedID != builtID {
|
||||
t.Fatalf("head = %s, want %s", vm.lastAcceptedID, builtID)
|
||||
}
|
||||
if _, ok := vm.processingBlocks[builtID]; ok {
|
||||
t.Fatalf("processing entry not pruned after Accept")
|
||||
}
|
||||
if _, err := vm.GetBlock(ctx, builtID); err != nil {
|
||||
t.Fatalf("GetBlock(head) after accept: %v", err)
|
||||
}
|
||||
if len(vm.books[pool].Orders) != 1 {
|
||||
t.Fatalf("resting orders = %d, want 1", len(vm.books[pool].Orders))
|
||||
}
|
||||
}
|
||||
|
||||
// TestVMParseVerifyAcceptByID covers the follower path: ParseBlock(bytes) ->
|
||||
// Verify -> Accept resolved by ID through GetBlock. ParseBlock must return a
|
||||
// STABLE instance so the overlay Verify stashes is the one Accept commits.
|
||||
func TestVMParseVerifyAcceptByID(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
pool := [32]byte{0x0A}
|
||||
|
||||
// Proposer builds the block bytes.
|
||||
dbP := memdb.New()
|
||||
vmP, engP := newTestVM(t, dbP)
|
||||
vmP.mempool.Add(ensureMarketTx(t, pool))
|
||||
vmP.mempool.Add(placePoolTx(t, pool, zapwire.SideBuy, 99.0, 4.0, "maker-c"))
|
||||
drainSignals(engP)
|
||||
prop, err := vmP.BuildBlock(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("proposer BuildBlock: %v", err)
|
||||
}
|
||||
raw := prop.Bytes()
|
||||
propID := prop.ID()
|
||||
|
||||
// Follower adopts the bytes the way the engine does: ParseBlock, Verify, then
|
||||
// resolve by ID and Accept.
|
||||
dbF := memdb.New()
|
||||
vmF, _ := newTestVM(t, dbF)
|
||||
parsed, err := vmF.ParseBlock(ctx, raw)
|
||||
if err != nil {
|
||||
t.Fatalf("follower ParseBlock: %v", err)
|
||||
}
|
||||
if parsed.ID() != propID {
|
||||
t.Fatalf("parsed id %s, want %s", parsed.ID(), propID)
|
||||
}
|
||||
if err := parsed.Verify(ctx); err != nil {
|
||||
t.Fatalf("follower Verify: %v", err)
|
||||
}
|
||||
got, err := vmF.GetBlock(ctx, propID)
|
||||
if err != nil {
|
||||
t.Fatalf("follower GetBlock(id): %v", err)
|
||||
}
|
||||
if err := got.Accept(ctx); err != nil {
|
||||
t.Fatalf("follower Accept via GetBlock: %v", err)
|
||||
}
|
||||
if vmF.lastAcceptedID != propID {
|
||||
t.Fatalf("follower head = %s, want %s", vmF.lastAcceptedID, propID)
|
||||
}
|
||||
}
|
||||
|
||||
// ensure the VM satisfies the consensus ChainVM interface at compile time.
|
||||
var _ block.ChainVM = (*VM)(nil)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user