Skip to content

vocone: refactor, fixes, persist mempool and finish block missing methods#1415

Merged
p4u merged 2 commits into
mainfrom
f/vocone
Mar 24, 2026
Merged

vocone: refactor, fixes, persist mempool and finish block missing methods#1415
p4u merged 2 commits into
mainfrom
f/vocone

Conversation

@p4u

@p4u p4u commented Mar 24, 2026

Copy link
Copy Markdown
Member

No description provided.

…hods

Signed-off-by: Pau Escrich <p4u@dabax.net>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR refactors Vocone’s single-node block production to persist blocks and mempool contents across restarts, adds missing block/tx retrieval plumbing, and expands test coverage around persistence and shutdown behavior.

Changes:

  • Replace in-memory mempool with a persistent KV-backed mempool and add pruning support.
  • Add a persistent blockstore with block metadata + hash→height reverse index and implement GetBlockByHeight/GetBlockByHash/GetTx plumbing.
  • Update Vocone Start to be context-cancellable and add extensive persistence/graceful shutdown tests.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
vocone/vocone.go Refactors startup and block production loop, adds context-driven lifecycle, persistent DB handles, and wiring for new block/mempool methods.
vocone/mempool.go Implements persistent mempool storage, load-on-start, and block preparation draining logic.
vocone/blockstore.go Implements block metadata persistence, tx keying, block reconstruction, and hash→height lookup.
vocone/vocone_test.go Adds new unit/e2e tests covering persistence, retrieval, key encoding, shutdown, and SetBulkTxCosts behavior.
cmd/voconed/voconed.go Updates daemon to run Start(ctx) and cancel on SIGTERM, then close resources.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread vocone/vocone.go Outdated
Comment thread vocone/vocone.go
Comment thread vocone/mempool.go
Comment thread vocone/blockstore.go
Comment thread cmd/voconed/voconed.go
Comment thread vocone/vocone.go
@p4u

p4u commented Mar 24, 2026

Copy link
Copy Markdown
Member Author

@copilot open a new pull request to apply changes based on the comments in this thread

Copilot AI commented Mar 24, 2026

Copy link
Copy Markdown

@p4u I've opened a new pull request, #1416, to work on those changes. Once the pull request is ready, I'll request review from you.

@coveralls

coveralls commented Mar 24, 2026

Copy link
Copy Markdown

Pull Request Test Coverage Report for Build 23496891038

Details

  • 373 of 525 (71.05%) changed or added relevant lines in 5 files are covered.
  • 5 unchanged lines in 1 file lost coverage.
  • Overall coverage increased (+0.1%) to 62.807%

Changes Missing Coverage Covered Lines Changed/Added Lines %
api/api_types.go 0 1 0.0%
vochain/cometbft.go 0 1 0.0%
vocone/blockstore.go 124 161 77.02%
vocone/vocone.go 133 179 74.3%
vocone/mempool.go 116 183 63.39%
Files with Coverage Reduction New Missed Lines %
vocone/vocone.go 5 72.85%
Totals Coverage Status
Change from base Build 20163668817: 0.1%
Covered Lines: 17150
Relevant Lines: 27306

💛 - Coveralls

@p4u p4u force-pushed the f/vocone branch 2 times, most recently from 02a2e29 to 71d7a0f Compare March 24, 2026 14:06
@p4u

p4u commented Mar 24, 2026

Copy link
Copy Markdown
Member Author

@copilot review

Copilot AI commented Mar 24, 2026

Copy link
Copy Markdown

@p4u I've opened a new pull request, #1417, to work on those changes. Once the pull request is ready, I'll request review from you.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 7 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread vocone/mempool.go Outdated
Comment on lines +207 to +210
// Update the in-memory index.
vc.mempoolMtx.Lock()
vc.mempoolKeys = vc.mempoolKeys[len(consumedKeys):]
vc.mempoolMtx.Unlock()

Copilot AI Mar 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

commitMempoolCleanup updates vc.mempoolKeys by slicing off len(consumedKeys) entries. If mempoolPrune (or any other mutation) removes items from the front of vc.mempoolKeys between prepareBlock() unlocking and this cleanup, len(consumedKeys) can exceed the current queue length or remove the wrong pending txs (panic or dropped txs). Update the in-memory index by removing the specific consumedKeys (e.g., build a set of consumed keys and filter vc.mempoolKeys), and guard against length mismatches.

Suggested change
// Update the in-memory index.
vc.mempoolMtx.Lock()
vc.mempoolKeys = vc.mempoolKeys[len(consumedKeys):]
vc.mempoolMtx.Unlock()
// Update the in-memory index by removing the specific consumed keys.
vc.mempoolMtx.Lock()
defer vc.mempoolMtx.Unlock()
if len(consumedKeys) > len(vc.mempoolKeys) {
// This can happen if other routines (e.g., pruning) removed entries
// from the front of the queue between prepareBlock and this cleanup.
log.Warnw("mempool cleanup mismatch: more consumed keys than pending keys",
"consumed", len(consumedKeys), "pending", len(vc.mempoolKeys))
}
consumedSet := make(map[string]struct{}, len(consumedKeys))
for _, key := range consumedKeys {
consumedSet[string(key)] = struct{}{}
}
if len(consumedSet) == 0 {
return
}
// Filter vc.mempoolKeys in place, keeping only keys that were not consumed.
dst := vc.mempoolKeys[:0]
for _, key := range vc.mempoolKeys {
if _, ok := consumedSet[string(key)]; !ok {
dst = append(dst, key)
}
}
vc.mempoolKeys = dst

Copilot uses AI. Check for mistakes.
Comment thread vocone/mempool.go
Comment on lines +30 to +41

// Iterate over all pending transactions and rebuild the key list.
vc.mempoolKeys = nil
if err := vc.mempoolDB.Iterate([]byte(prefixMempool), func(key, _ []byte) bool {
keyCopy := make([]byte, len(key))
copy(keyCopy, key)
vc.mempoolKeys = append(vc.mempoolKeys, keyCopy)
return true
}); err != nil {
return fmt.Errorf("could not iterate mempool: %w", err)
}

Copilot AI Mar 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

loadMempool only restores vc.mempoolSeq from the persisted mpseq key. If mpseq is missing/corrupt (or behind), addTx will start reusing sequence numbers and can overwrite existing mempool entries. Consider deriving mempoolSeq from the highest recovered mp/ key when mpseq is absent or lower than the max observed key.

Suggested change
// Iterate over all pending transactions and rebuild the key list.
vc.mempoolKeys = nil
if err := vc.mempoolDB.Iterate([]byte(prefixMempool), func(key, _ []byte) bool {
keyCopy := make([]byte, len(key))
copy(keyCopy, key)
vc.mempoolKeys = append(vc.mempoolKeys, keyCopy)
return true
}); err != nil {
return fmt.Errorf("could not iterate mempool: %w", err)
}
// Track the maximum sequence number observed in existing mempool keys.
maxSeq := vc.mempoolSeq
// Iterate over all pending transactions and rebuild the key list.
vc.mempoolKeys = nil
if err := vc.mempoolDB.Iterate([]byte(prefixMempool), func(key, _ []byte) bool {
keyCopy := make([]byte, len(key))
copy(keyCopy, key)
vc.mempoolKeys = append(vc.mempoolKeys, keyCopy)
// Derive sequence from the key if it contains a numeric suffix.
// We assume the sequence is stored in the last 8 bytes of the key.
if len(key) >= len(prefixMempool)+8 {
seq := binary.BigEndian.Uint64(key[len(key)-8:])
if seq > maxSeq {
maxSeq = seq
}
}
return true
}); err != nil {
return fmt.Errorf("could not iterate mempool: %w", err)
}
// Ensure mempoolSeq is at least as large as the highest recovered key sequence.
if maxSeq > vc.mempoolSeq {
vc.mempoolSeq = maxSeq
}

Copilot uses AI. Check for mistakes.
Comment thread vocone/blockstore.go
Comment on lines +44 to +56
wTx := vc.blockStore.WriteTx()
defer wTx.Discard()
if err := wTx.Set(metaKey(height), data); err != nil {
return err
}
// Store hash→height reverse index for GetBlockByHash lookups.
if len(blockHash) > 0 {
heightBytes := make([]byte, 8)
binary.BigEndian.PutUint64(heightBytes, uint64(height))
if err := wTx.Set(blockHashKey(blockHash), heightBytes); err != nil {
return err
}
}

Copilot AI Mar 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

storeBlockMeta writes/overwrites metaKey(height) and also writes a hash→height reverse index. If storeBlockMeta is called again for the same height (e.g., crash after writing meta but before CommitState, then re-execution with a different timestamp/hash), the old blockhash/ entry is never deleted, leaving stale hash→height mappings and causing GetBlockByHash to return an unrelated block. When overwriting existing metadata, delete the prior reverse-index key for the previous hash if it differs.

Copilot uses AI. Check for mistakes.
Comment thread vocone/vocone.go
if err := vc.mempoolDB.Close(); err != nil {
errs = append(errs, fmt.Errorf("close mempool db: %w", err))
}
}

Copilot AI Mar 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Vocone.Close currently only closes blockStore and mempoolDB. The node also owns long-lived resources like App.State (state DB), Indexer, and Stats (which starts a goroutine and registers metrics). Not closing/stopping them can leak goroutines and keep DB files open. Consider closing vc.Indexer (if non-nil), calling vc.Stats.Close(), and closing vc.App.State (and any other owned resources) here.

Suggested change
}
}
// Close indexer if present.
if vc.Indexer != nil {
if err := vc.Indexer.Close(); err != nil {
errs = append(errs, fmt.Errorf("close indexer: %w", err))
}
}
// Close stats/metrics component if present.
if vc.Stats != nil {
if err := vc.Stats.Close(); err != nil {
errs = append(errs, fmt.Errorf("close stats: %w", err))
}
}
// Close application state DB if present.
if vc.App != nil && vc.App.State != nil {
if err := vc.App.State.Close(); err != nil {
errs = append(errs, fmt.Errorf("close app state: %w", err))
}
}

Copilot uses AI. Check for mistakes.
Comment thread vocone/vocone_test.go Outdated
Comment on lines +141 to +145
// Wait for a few blocks to be produced.
time.Sleep(time.Second * 3)
qt.Assert(t, vc.height.Load() >= 5, qt.IsTrue,
qt.Commentf("expected at least 5 blocks, got %d", vc.height.Load()))

Copilot AI Mar 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tests rely on fixed sleeps to wait for chain readiness/block production (e.g., time.Sleep(3s) then assert height>=5). This can be flaky under load/slow CI. Prefer polling with a timeout (e.g., loop until height reaches target or context deadline) so the test waits only as long as needed and fails deterministically if blocks are not produced.

Copilot uses AI. Check for mistakes.
Comment thread vocone/vocone_test.go Outdated
}
}()

time.Sleep(time.Second * 2)

Copilot AI Mar 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

newTestVoconeLite uses a fixed sleep to wait for startup (time.Sleep(2s)). This can make the suite slower and flaky when startup is slower than expected. Consider replacing it with a readiness wait (e.g., poll vc.height until >0 with a timeout) so tests don't depend on timing.

Suggested change
time.Sleep(time.Second * 2)
// Wait for vocone to become ready instead of using a fixed sleep.
deadline := time.Now().Add(5 * time.Second)
for {
// Consider vocone ready once it has produced at least one block.
if vc.height > 0 {
break
}
if time.Now().After(deadline) {
t.Fatalf("vocone did not become ready within timeout")
}
time.Sleep(10 * time.Millisecond)
}

Copilot uses AI. Check for mistakes.
Comment thread vocone/vocone_test.go Outdated
Comment on lines +176 to +205
// TestTransactionPersistence verifies that transactions are stored in the
// blockstore and can be retrieved after being included in a block.
func TestTransactionPersistence(t *testing.T) {
vc, cancel := newTestVoconeLite(t)
defer cancel()
defer vc.Close()

// Wait for a few blocks to be produced.
time.Sleep(time.Second * 3)
height := vc.height.Load()
qt.Assert(t, height >= 3, qt.IsTrue,
qt.Commentf("expected at least 3 blocks, got %d", height))

// Submit a raw transaction directly through addTx
// (we use a raw tx bytes that will fail validation, but we can still test
// the mempool → blockstore flow by checking blocks that are already produced).

// Instead, verify that blocks without txs still produce valid metadata.
for h := int64(1); h < height; h++ {
meta, err := vc.loadBlockMeta(h)
qt.Assert(t, err, qt.IsNil, qt.Commentf("block %d meta should exist", h))
qt.Assert(t, meta.Timestamp > 0, qt.IsTrue)

// Verify the block can be reconstructed.
blk := vc.getBlock(h)
qt.Assert(t, blk != nil, qt.IsTrue)
qt.Assert(t, blk.Height == h, qt.IsTrue)
qt.Assert(t, len(blk.Hash()) > 0, qt.IsTrue)
}
}

Copilot AI Mar 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TestTransactionPersistence (and its doc comment) claims to verify that transactions are stored and retrievable after inclusion in a block, but the test never submits a tx nor asserts tx retrieval; it only checks that block metadata exists for empty blocks. Either extend the test to actually add a valid tx and verify it can be fetched via getTx/getTxWithHash, or rename/reword the test to reflect what it currently validates.

Copilot uses AI. Check for mistakes.
Signed-off-by: Pau Escrich <p4u@dabax.net>
@p4u p4u merged commit b910212 into main Mar 24, 2026
11 of 12 checks passed
@p4u p4u deleted the f/vocone branch March 24, 2026 19:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants