Skip to content

validator: align finality with sv-node (nLockTime/BIP68/candidate-MTP) + migrate fee/consolidation to BDK [MVP-4632 pt.1]#975

Merged
ctnguyen merged 9 commits into
bsv-blockchain:mainfrom
ctnguyen:Feature/MvP-4632
May 29, 2026
Merged

validator: align finality with sv-node (nLockTime/BIP68/candidate-MTP) + migrate fee/consolidation to BDK [MVP-4632 pt.1]#975
ctnguyen merged 9 commits into
bsv-blockchain:mainfrom
ctnguyen:Feature/MvP-4632

Conversation

@ctnguyen

@ctnguyen ctnguyen commented May 28, 2026

Copy link
Copy Markdown
Collaborator

Summary

First of a split series for MVP-4632 (kept small for reviewability). Brings the
Teranode validator's transaction-finality logic into exact agreement with
sv-node, and moves fee + consolidation validation out of Teranode into BDK so
there is a single source of truth.

⚠️ Consensus impact

This changes which transactions are considered final, and which time a block is
judged against. Reviewers/operators must read this section.

  • nLockTime finality is now strict < (was <=), matching sv-node
    IsFinalTx. A tx whose locktime equals the comparison height/MTP is no longer
    final. Teranode was previously looser than sv-node at the boundary.
  • Finality time source is now era-correct, matching sv-node
    ContextualCheckBlock:
    • pre-CSV: candidate block time
    • post-CSV: candidate parent median-time-past (BIP113, 11-header MTP)
  • BIP68 time-based sequence locks subtract 1, matching
    CalculateSequenceLocks (boundary-only; post-Genesis short-circuits earlier).
  • Zero-txid input check removed from Teranode; defer to BDK
    implCheckPrevOutputs (IsNull() semantics — Teranode was stricter than
    sv-node here).

Action before merge: confirm TTN block history contains no boundary-equality
(locktime == limit) tx already mined — such a block would now be rejected.

Fee / consolidation → BDK

Deletes Teranode checkFees / isConsolidationTx / isDustReturnTx (and ~1.8k
lines of their tests); pushes MinMiningTxFee + 4 consolidation settings into BDK
at startup (fatal-on-error); maps BDK's InsufficientFee DoS code into the reject
path. Net deletion — logic now lives once, in BDK.

Plumbing

New Options.CandidateBlockTime / Options.CandidateParentMedianTime
(+ proto field 9, additive/backwards-compatible), populated on the mainline
block-validation paths in legacy/netsync and subtreevalidation. Post-CSV MTP
is derived from the candidate parent via GetBlockHeaders(parentHash, 11) with a
hash-keyed walkParentChain fallback for reorg-race cache recovery.

BDK bump

gobdkv1.2.5-0.20260526081552-cdfa7814ee5d (adds the fee/consolidation
setters this PR consumes).

Testing

New: candidate-block-time + candidate-parent-MTP API tests, finality-selection
tests, BIP68 boundary test, median-timestamp tests (netsync + subtreevalidation).
Removed: fee/consolidation unit tests (logic moved to BDK, covered there).

Scope / follow-ups

  • Part 1 of MVP-4632; remaining work in follow-up PRs.
  • services/validator/CheckSubtree (peer-facing gRPC) intentionally does not
    populate candidate times — request carries no block header.

Relates to: MVP-4632


Task mapping (provenance)

Task What was done Where
T7_teranode Removed the teranode-side zero-txid block; defer to BDK's implCheckPrevOutputs. services/validator/TxValidator.go
T2a + T19a ValidLockTime flipped to strict >; policy/next-tip finality now runs in all eras using tip MTP; coinbase rejection moved ahead of the finality block. Test setups updated. util/lock_time.go, util/lock_time_test.go, services/validator/Validator.go, services/validator/Validator_test.go, services/propagation/Server_test.go
T20_extra BIP68 time-based branch now subtracts 1 to match sv-node CalculateSequenceLocks; boundary test added. services/validator/TxValidator.go, services/validator/TxValidator_bip68_test.go
T21_extra (teranode side) Pushed MinMiningTxFee (via math.Round) + 4 consolidation settings into BDK at startup with fatal-on-error; deleted checkFees/isConsolidationTx/isDustReturnTx and their tests; mapped BDK's new InsufficientFee DoS code into the teranode reject path; refreshed policy longdescs. services/validator/ScriptVerifierGoBDK.go, services/validator/TxValidator.go, services/validator/Validator_test.go, services/validator/TxValidator_test.go, settings/policy_settings.go
T19b Added Options.CandidateBlockTime + WithCandidateBlockTime; validateInternal consumes it on the pre-CSV consensus path with soft-fall when unset. Populated at the mainline block-validation callsites in legacy/netsync (3 sites) and subtreevalidation/check_block_subtrees.go (2 sites; processTransactionsInLevels signature extended). Peer-facing Server.go CheckSubtree intentionally unpopulated (gRPC request has no block header). services/validator/options.go, services/validator/Validator.go, services/legacy/netsync/handle_block.go, services/subtreevalidation/check_block_subtrees.go, services/subtreevalidation/Server.go
T2b + T19c Added Options.CandidateParentMedianTime (proto field 9, parallel HTTP-fallback wiring); always populated on mainline block-validation paths via GetBlockHeaders(parentHash, 11) + chain re-anchor, with a hash-keyed walkParentChain fallback for reorg-race cache-poisoning recovery. services/validator/options.go, services/validator/Validator.go, services/validator/Client.go, services/validator/Server.go, services/validator/validator_api/validator_api.proto, services/legacy/netsync/handle_block.go, services/subtreevalidation/check_block_subtrees.go, services/subtreevalidation/Server.go

@github-actions

github-actions Bot commented May 28, 2026

Copy link
Copy Markdown
Contributor

🤖 Claude Code Review

Status: Complete


This PR implements significant validator changes for correct finality time handling across pre-CSV and post-CSV eras, matching bitcoin-sv behavior. The implementation is well-structured with comprehensive testing.

Key Changes:

  • BDK upgrade: Moves fee validation into BDK, removes duplicated Teranode checks
  • Lock time finality: Changes from >= to strict > comparison (bitcoin-sv IsFinalTx)
  • Finality time selection: Pre-CSV uses candidate block time, post-CSV uses candidate-parent MTP
  • Reorg safety: Candidate-parent MTP calculation includes chain re-anchoring and hash-keyed fallback
  • BIP68 boundary fix: Time-based sequence locks now subtract 1 (matches bitcoin-sv)

Strengths:

  • Excellent test coverage for edge cases (reorg races, chain re-anchoring, era boundaries)
  • Clear documentation explaining bitcoin-sv equivalence
  • Defensive error handling (hard-errors instead of silent degradation)
  • Comprehensive fallback mechanisms for concurrent reorg scenarios

No critical issues found. The implementation follows Bitcoin consensus rules correctly and handles edge cases defensively.

@oskarszoon oskarszoon left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Approve with changes. Core consensus migration is correct (strict ValidLockTime matches bitcoin-sv IsFinalTx, BIP68 -1 matches CalculateSequenceLocks, finality selection matches ContextualCheckBlock, T7 zero-txid aligns to bitcoin-sv IsNull). BDK migration coverage at cdfa78 confirmed. Fix the items below before merge:

P0 — era-routing branch is untested. validateTransactions decides between CandidateBlockTime and CandidateParentMedianTime based on blockHeightUint32 < CSVHeight. All PreValidateTransactions unit tests pass literal 0, 0 for both — only the soft-fall path is exercised, never the era-selection logic the PR introduces. TestSyncManager_validateTransactions is t.Skip()'d. Add two table cases (pre-CSV and post-CSV heights) that assert the era-correct field is set.

P1 — silent zero on height conversion (services/legacy/netsync/handle_block.go:1187). The if blockHeightUint32, hErr := ...; hErr == nil { ... } block silently drops both candidateBlockTime and candidateParentMedianTime to zero on failure → soft-fall (the path this PR is eliminating). Outer guard at line 1216 catches the error today, but the duplicate conversion is fragile. Convert once at top and return the error immediately, matching validateTransactionsLegacyMode:654.

P1 — walkParentChain breaks silently on nil header (handle_block.go:773 + check_block_subtrees.go:1243). When GetBlockHeader returns (nil, meta, nil) the loop breaks and the caller computes a median from however many headers were collected — no error. Near genesis this is correct, mid-chain on transient cache miss it silently yields an incomplete MTP. Treat (nil, _, nil) as an error inside walkParentChain, or only tolerate short returns when blockHeight < 11.

P1 — CheckSubtreeFromBlock peer-priority path leaves CandidateParentMedianTime unpopulated (services/subtreevalidation/Server.go:759-780). Mainline paths close the finality-MTP race; peer-priority falls back to tip MTP. Two nodes processing the same subtree under different tip-MTP snapshots can diverge on finality. The request has PreviousBlockHash — call fetchCandidateParentMedianTime(ctx, req.GetPreviousBlockHash()) here too.

Discussion (non-blocking):

  • Strict < ValidLockTime flip is consensus-correct vs bitcoin-sv. Confirm TTN history doesn't contain a boundary-equality tx mined into a block — if clean, no further action.
  • BDK migration boundary under-tested in teranode: no startup-config panic test, no e2e fee-reject through mapBDKValidationError. BDK side is covered; add the teranode bridge tests.

P2s (extract walkParentChain to shared package, deduplicate MTP fetch, HTTP parse warnings, InsufficientFee wire-divergence with sv-node) are tracked in the review notes — happy to file follow-up issues.

@ctnguyen ctnguyen requested a review from oskarszoon May 29, 2026 03:51
@ctnguyen ctnguyen force-pushed the Feature/MvP-4632 branch from 1f023d5 to a6112a0 Compare May 29, 2026 04:02
@github-actions

github-actions Bot commented May 29, 2026

Copy link
Copy Markdown
Contributor

Benchmark Comparison Report

Baseline: main (unknown)

Current: PR-975 (a812399)

Summary

  • Regressions: 0
  • Improvements: 0
  • Unchanged: 144
  • Significance level: p < 0.05
All benchmark results (sec/op)
Benchmark Baseline Current Change p-value
_NewBlockFromBytes-4 1.752µ 1.775µ ~ 0.600
SplitSyncedParentMap_SetIfNotExists/256_buckets-4 61.91n 61.88n ~ 1.000
SplitSyncedParentMap_SetIfNotExists/16_buckets-4 61.59n 61.59n ~ 1.000
SplitSyncedParentMap_SetIfNotExists/1_bucket-4 61.64n 61.72n ~ 1.000
SplitSyncedParentMap_ConcurrentSetIfNotExists/256_buckets... 30.51n 30.15n ~ 1.000
SplitSyncedParentMap_ConcurrentSetIfNotExists/16_buckets_... 51.30n 50.53n ~ 0.700
SplitSyncedParentMap_ConcurrentSetIfNotExists/1_bucket_pa... 105.2n 105.0n ~ 1.000
MiningCandidate_Stringify_Short-4 259.3n 264.8n ~ 0.200
MiningCandidate_Stringify_Long-4 1.934µ 1.945µ ~ 0.200
MiningSolution_Stringify-4 974.4n 989.4n ~ 0.100
BlockInfo_MarshalJSON-4 1.792µ 1.821µ ~ 0.100
NewFromBytes-4 143.1n 127.9n ~ 0.100
AddTxBatchColumnar_Validation-4 2.533µ 2.555µ ~ 0.700
OffsetValidationLoop-4 544.2n 547.7n ~ 0.100
Mine_EasyDifficulty-4 68.01µ 67.22µ ~ 0.700
Mine_WithAddress-4 7.027µ 7.623µ ~ 0.100
BlockAssembler_AddTx-4 0.02887n 0.02861n ~ 0.400
AddNode-4 11.98 11.44 ~ 0.400
AddNodeWithMap-4 12.50 11.79 ~ 0.100
DirectSubtreeAdd/4_per_subtree-4 58.91n 61.89n ~ 0.700
DirectSubtreeAdd/64_per_subtree-4 29.22n 29.21n ~ 0.500
DirectSubtreeAdd/256_per_subtree-4 28.10n 28.21n ~ 0.400
DirectSubtreeAdd/1024_per_subtree-4 26.77n 26.72n ~ 0.400
DirectSubtreeAdd/2048_per_subtree-4 26.30n 26.39n ~ 0.400
SubtreeProcessorAdd/4_per_subtree-4 298.1n 298.3n ~ 1.000
SubtreeProcessorAdd/64_per_subtree-4 291.0n 287.2n ~ 0.700
SubtreeProcessorAdd/256_per_subtree-4 289.6n 287.9n ~ 0.400
SubtreeProcessorAdd/1024_per_subtree-4 279.4n 281.1n ~ 1.000
SubtreeProcessorAdd/2048_per_subtree-4 279.1n 280.5n ~ 1.000
SubtreeProcessorRotate/4_per_subtree-4 287.2n 287.0n ~ 1.000
SubtreeProcessorRotate/64_per_subtree-4 283.2n 290.4n ~ 0.700
SubtreeProcessorRotate/256_per_subtree-4 282.2n 286.2n ~ 0.100
SubtreeProcessorRotate/1024_per_subtree-4 284.8n 288.2n ~ 0.700
SubtreeNodeAddOnly/4_per_subtree-4 55.69n 56.05n ~ 0.100
SubtreeNodeAddOnly/64_per_subtree-4 36.30n 36.40n ~ 0.800
SubtreeNodeAddOnly/256_per_subtree-4 35.33n 35.48n ~ 1.000
SubtreeNodeAddOnly/1024_per_subtree-4 34.71n 34.66n ~ 1.000
SubtreeCreationOnly/4_per_subtree-4 114.2n 113.9n ~ 0.800
SubtreeCreationOnly/64_per_subtree-4 372.5n 369.0n ~ 0.400
SubtreeCreationOnly/256_per_subtree-4 1.294µ 1.285µ ~ 0.100
SubtreeCreationOnly/1024_per_subtree-4 3.987µ 4.031µ ~ 0.400
SubtreeCreationOnly/2048_per_subtree-4 7.400µ 7.369µ ~ 0.700
SubtreeProcessorOverheadBreakdown/64_per_subtree-4 284.2n 279.1n ~ 0.800
SubtreeProcessorOverheadBreakdown/1024_per_subtree-4 284.6n 281.6n ~ 0.400
ParallelGetAndSetIfNotExists/1k_nodes-4 2.056m 2.023m ~ 0.100
ParallelGetAndSetIfNotExists/10k_nodes-4 5.218m 5.381m ~ 0.100
ParallelGetAndSetIfNotExists/50k_nodes-4 7.431m 7.733m ~ 0.200
ParallelGetAndSetIfNotExists/100k_nodes-4 10.15m 10.57m ~ 0.100
SequentialGetAndSetIfNotExists/1k_nodes-4 1.807m 1.782m ~ 0.200
SequentialGetAndSetIfNotExists/10k_nodes-4 4.593m 4.573m ~ 1.000
SequentialGetAndSetIfNotExists/50k_nodes-4 13.69m 14.89m ~ 0.100
SequentialGetAndSetIfNotExists/100k_nodes-4 25.29m 26.13m ~ 0.100
ProcessOwnBlockSubtreeNodesParallel/1k_nodes-4 2.107m 2.109m ~ 1.000
ProcessOwnBlockSubtreeNodesParallel/10k_nodes-4 8.662m 8.563m ~ 0.100
ProcessOwnBlockSubtreeNodesParallel/100k_nodes-4 14.01m 14.03m ~ 1.000
ProcessOwnBlockSubtreeNodesSequential/1k_nodes-4 1.818m 1.821m ~ 1.000
ProcessOwnBlockSubtreeNodesSequential/10k_nodes-4 9.170m 8.527m ~ 0.400
ProcessOwnBlockSubtreeNodesSequential/100k_nodes-4 44.99m 44.75m ~ 0.700
DiskTxMap_SetIfNotExists-4 3.782µ 3.640µ ~ 0.700
DiskTxMap_SetIfNotExists_Parallel-4 3.520µ 3.347µ ~ 0.100
DiskTxMap_ExistenceOnly-4 342.3n 318.4n ~ 0.100
Queue-4 188.0n 188.0n ~ 1.000
AtomicPointer-4 3.824n 3.738n ~ 0.400
ReorgOptimizations/DedupFilterPipeline/Old/10K-4 876.4µ 843.3µ ~ 0.100
ReorgOptimizations/DedupFilterPipeline/New/10K-4 794.6µ 761.5µ ~ 0.100
ReorgOptimizations/AllMarkFalse/Old/10K-4 115.5µ 108.2µ ~ 0.100
ReorgOptimizations/AllMarkFalse/New/10K-4 64.30µ 64.40µ ~ 0.400
ReorgOptimizations/HashSlicePool/Old/10K-4 57.05µ 56.69µ ~ 1.000
ReorgOptimizations/HashSlicePool/New/10K-4 11.15µ 11.06µ ~ 0.700
ReorgOptimizations/NodeFlags/Old/10K-4 4.895µ 4.506µ ~ 0.100
ReorgOptimizations/NodeFlags/New/10K-4 1.576µ 1.512µ ~ 0.100
ReorgOptimizations/DedupFilterPipeline/Old/100K-4 9.361m 9.217m ~ 0.700
ReorgOptimizations/DedupFilterPipeline/New/100K-4 9.328m 10.085m ~ 0.100
ReorgOptimizations/AllMarkFalse/Old/100K-4 1.070m 1.087m ~ 0.100
ReorgOptimizations/AllMarkFalse/New/100K-4 704.3µ 706.7µ ~ 0.700
ReorgOptimizations/HashSlicePool/Old/100K-4 579.1µ 521.9µ ~ 0.700
ReorgOptimizations/HashSlicePool/New/100K-4 204.8µ 194.7µ ~ 0.200
ReorgOptimizations/NodeFlags/Old/100K-4 47.99µ 48.44µ ~ 0.400
ReorgOptimizations/NodeFlags/New/100K-4 16.71µ 17.75µ ~ 0.100
TxMapSetIfNotExists-4 49.30n 49.82n ~ 0.100
TxMapSetIfNotExistsDuplicate-4 41.00n 43.94n ~ 0.100
ChannelSendReceive-4 603.7n 580.5n ~ 0.400
CalcBlockWork-4 518.0n 508.6n ~ 0.400
CalculateWork-4 704.5n 701.9n ~ 0.100
BuildBlockLocatorString_Helpers/Size_10-4 1.371µ 1.360µ ~ 0.100
BuildBlockLocatorString_Helpers/Size_100-4 14.95µ 14.42µ ~ 0.700
BuildBlockLocatorString_Helpers/Size_1000-4 129.3µ 129.8µ ~ 0.700
CatchupWithHeaderCache-4 104.6m 104.7m ~ 1.000
_prepareTxsPerLevel-4 422.6m 406.6m ~ 0.200
_prepareTxsPerLevelOrdered-4 3.924m 3.761m ~ 0.700
_prepareTxsPerLevel_Comparison/Original-4 420.9m 416.3m ~ 0.400
_prepareTxsPerLevel_Comparison/Optimized-4 3.506m 3.547m ~ 0.700
_BufferPoolAllocation/16KB-4 5.123µ 3.826µ ~ 0.100
_BufferPoolAllocation/32KB-4 7.650µ 9.568µ ~ 0.700
_BufferPoolAllocation/64KB-4 16.61µ 20.09µ ~ 0.400
_BufferPoolAllocation/128KB-4 27.17µ 31.20µ ~ 0.200
_BufferPoolAllocation/512KB-4 122.8µ 127.7µ ~ 0.400
_BufferPoolConcurrent/32KB-4 19.52µ 19.20µ ~ 1.000
_BufferPoolConcurrent/64KB-4 31.13µ 30.52µ ~ 0.100
_BufferPoolConcurrent/512KB-4 149.1µ 151.4µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/16KB-4 700.1µ 653.4µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/32KB-4 717.8µ 645.3µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/64KB-4 686.2µ 655.2µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/128KB-4 674.3µ 658.6µ ~ 0.400
_SubtreeDeserializationWithBufferSizes/512KB-4 617.5µ 633.8µ ~ 0.700
_SubtreeDataDeserializationWithBufferSizes/16KB-4 36.96m 37.00m ~ 1.000
_SubtreeDataDeserializationWithBufferSizes/32KB-4 36.80m 37.44m ~ 0.700
_SubtreeDataDeserializationWithBufferSizes/64KB-4 36.65m 36.81m ~ 0.200
_SubtreeDataDeserializationWithBufferSizes/128KB-4 36.49m 36.61m ~ 0.400
_SubtreeDataDeserializationWithBufferSizes/512KB-4 36.19m 36.60m ~ 0.200
_PooledVsNonPooled/Pooled-4 832.3n 830.0n ~ 0.100
_PooledVsNonPooled/NonPooled-4 8.029µ 7.252µ ~ 0.100
_MemoryFootprint/Current_512KB_32concurrent-4 7.074µ 6.790µ ~ 0.400
_MemoryFootprint/Proposed_32KB_32concurrent-4 9.951µ 9.484µ ~ 0.100
_MemoryFootprint/Alternative_64KB_32concurrent-4 9.690µ 9.106µ ~ 0.100
SubtreeSizes/10k_tx_4_per_subtree-4 1.297m 1.270m ~ 0.200
SubtreeSizes/10k_tx_16_per_subtree-4 303.4µ 297.0µ ~ 0.700
SubtreeSizes/10k_tx_64_per_subtree-4 73.04µ 71.65µ ~ 0.200
SubtreeSizes/10k_tx_256_per_subtree-4 17.86µ 18.06µ ~ 0.100
SubtreeSizes/10k_tx_512_per_subtree-4 8.818µ 8.754µ ~ 1.000
SubtreeSizes/10k_tx_1024_per_subtree-4 4.427µ 4.411µ ~ 0.700
SubtreeSizes/10k_tx_2k_per_subtree-4 2.213µ 2.182µ ~ 0.100
BlockSizeScaling/10k_tx_64_per_subtree-4 69.17µ 70.03µ ~ 0.700
BlockSizeScaling/10k_tx_256_per_subtree-4 17.63µ 17.51µ ~ 0.400
BlockSizeScaling/10k_tx_1024_per_subtree-4 4.345µ 4.373µ ~ 0.700
BlockSizeScaling/50k_tx_64_per_subtree-4 363.9µ 372.7µ ~ 0.100
BlockSizeScaling/50k_tx_256_per_subtree-4 88.14µ 88.36µ ~ 1.000
BlockSizeScaling/50k_tx_1024_per_subtree-4 21.62µ 22.06µ ~ 0.200
SubtreeAllocations/small_subtrees_exists_check-4 151.5µ 149.2µ ~ 0.100
SubtreeAllocations/small_subtrees_data_fetch-4 158.4µ 159.0µ ~ 0.700
SubtreeAllocations/small_subtrees_full_validation-4 312.2µ 311.1µ ~ 0.700
SubtreeAllocations/medium_subtrees_exists_check-4 8.978µ 8.856µ ~ 0.400
SubtreeAllocations/medium_subtrees_data_fetch-4 9.597µ 9.345µ ~ 0.100
SubtreeAllocations/medium_subtrees_full_validation-4 17.47µ 17.75µ ~ 0.200
SubtreeAllocations/large_subtrees_exists_check-4 2.104µ 2.114µ ~ 0.500
SubtreeAllocations/large_subtrees_data_fetch-4 2.235µ 2.300µ ~ 0.200
SubtreeAllocations/large_subtrees_full_validation-4 4.336µ 4.440µ ~ 0.100
StoreBlock_Sequential/BelowCSVHeight-4 336.2µ 328.6µ ~ 0.400
StoreBlock_Sequential/AboveCSVHeight-4 327.9µ 335.9µ ~ 0.100
GetUtxoHashes-4 267.1n 269.1n ~ 0.400
GetUtxoHashes_ManyOutputs-4 46.97µ 45.53µ ~ 0.200
_NewMetaDataFromBytes-4 215.0n 215.2n ~ 1.000
_Bytes-4 398.7n 401.4n ~ 0.200
_MetaBytes-4 140.2n 140.7n ~ 1.000

Threshold: >10% with p < 0.05 | Generated: 2026-05-29 11:58 UTC

@ordishs ordishs left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approval

Solid work — the design is well-reasoned, doc comments are top-tier, and test pinning is more thorough than most consensus-affecting PRs. Approving with a recommendation to address the three silent-failure / contract concerns below before merge. The rest of the review notes are reasonable follow-ups.

Please address before merge

1. walkParentChain should not silently truncate on nil headers (services/legacy/netsync/handle_block.go:145-167, also duplicated in services/subtreevalidation/check_block_subtrees.go)

if header == nil {
    break
}

BIP113 MTP requires exactly 11 headers. An early break silently computes MTP over a smaller window. Gated by blockHeight >= CSVHeight so unreachable on mainnet, but on regtest or test setups walking close to genesis this produces a wrong MTP. Return an error when the loop terminates with fewer than depth headers due to a nil response.

2. extractValidationParams silently ignores ParseUint errors (services/validator/Server.go:737-746)

if v, err := strconv.ParseUint(candidateBlockTimeStr, 10, 32); err == nil {
    options.CandidateBlockTime = uint32(v)
}

A malformed value (candidateBlockTime=abc) leaves the field at 0 and silently degrades to skip-finality (pre-CSV) or tip-MTP fallback (post-CSV). Add a warn log on parse failure so future regressions on the HTTP fallback path don't ship silently.

3. selectFinalityComparisonTime post-CSV soft-fall masks caller bugs (services/validator/Validator.go:324-358)

The post-CSV default arm soft-falls to blockState.MedianTime when CandidateParentMedianTime == 0. The doc comment correctly states this is reserved for CheckSubtree, but a block-validation caller that forgets to populate the field gets the soft-fall instead of a hard error — exactly the asynchronous-MTP race this whole change exists to eliminate.

Consider enforcing the contract in the API rather than via documentation. Options:

  • A required-field flag (Options.RequireCandidateParentMedianTime) that block-validation callers set
  • A separate validator entry point (ValidateForBlock) that requires the field
  • A distinguished sentinel ("explicitly unset") so the soft-fall only fires when explicitly opted into

Documentation-only contracts on async race-prone paths tend to erode.

Other notes (non-blocking)

The full review with positive observations and additional non-blocking items (helper duplication, missing panic-path tests, comment placement in CheckSubtree, MinConsolidationInputMaturity not wired, consensus-tightening notes) is in a separate comment.

@ctnguyen ctnguyen force-pushed the Feature/MvP-4632 branch from a6112a0 to 143337f Compare May 29, 2026 08:36
@ctnguyen ctnguyen force-pushed the Feature/MvP-4632 branch 2 times, most recently from b9730f3 to 12b0c5b Compare May 29, 2026 10:29
@ctnguyen ctnguyen force-pushed the Feature/MvP-4632 branch from 12b0c5b to 9a53d36 Compare May 29, 2026 11:43
@socket-security

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Updatedgolang/​github.com/​bitcoin-sv/​bdk/​module/​gobdk@​v1.2.5-0.20260518042519-1ca0c6519af9 ⏵ v1.2.5-0.20260526081552-cdfa7814ee5d98 +1100100100100

View full report

@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
71.3% Coverage on New Code (required ≥ 80%)
8.4% Duplication on New Code (required ≤ 3%)

See analysis details on SonarQube Cloud

@oskarszoon oskarszoon left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Re-reviewed at 9a53d36ff. All blocking items from my previous review and ordishs's three are fixed — verified, not just acknowledged:

  • Era-routing now tested (TestCandidateFinalityTimesForBlock_EraSelection, pre/post-CSV + boundary).
  • Height conversion done once + error returned; silent-zero path gone.
  • walkParentChain hard-errors on nil header / nil link in both the netsync and subtreevalidation copies — no more silent short-window MTP.
  • CheckSubtreeFromBlock populates CandidateParentMedianTime on the peer path, behind the existing parent-resolution check (no peer-DoS).
  • selectFinalityComparisonTime post-CSV soft-fall removed — a caller that doesn't populate the field now hard-errors instead of silently falling back to tip MTP. Right fix.
  • extractValidationParams warns on parse failure.

Core consensus re-confirmed (strict < IsFinalTx, BIP68 −1, era selection); build/vet/tests clean; BDK pin unchanged at cdfa78.

One thing before merge, not a code change: confirm TTN block history has no already-mined boundary-equality (locktime == limit) tx — the strict < flip would reject such a block on revalidation. Mainline is safe by construction.

Non-blocking follow-ups, fine as separate issues: teranode-side BDK boundary tests (startup-config panic + e2e fee-reject; BDK side is already covered), a "don't expose externally" guard on the HTTP candidate-time params, and extracting the duplicated walkParentChain helper. Approving.

@oskarszoon oskarszoon changed the title Mvp-4632 First PR validator: align finality with sv-node (nLockTime/BIP68/candidate-MTP) + migrate fee/consolidation to BDK [MVP-4632 pt.1] May 29, 2026
@ctnguyen ctnguyen merged commit e9f6a3f into bsv-blockchain:main May 29, 2026
29 of 30 checks passed
@ctnguyen ctnguyen deleted the Feature/MvP-4632 branch June 1, 2026 04:07
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.

3 participants