Skip to content

perf(validator): skip BDK script validation for trusted legacy IBD blocks#951

Merged
freemans13 merged 1 commit into
bsv-blockchain:mainfrom
freemans13:stu/validator-skip-script-validation
May 27, 2026
Merged

perf(validator): skip BDK script validation for trusted legacy IBD blocks#951
freemans13 merged 1 commit into
bsv-blockchain:mainfrom
freemans13:stu/validator-skip-script-validation

Conversation

@freemans13

Copy link
Copy Markdown
Collaborator

Summary

  • Adds an Options.SkipScriptValidation flag and WithSkipScriptValidation constructor in services/validator/options.go.
  • In TxValidator.ValidateTransaction, returns immediately before calling the BDK adapter when the flag is set.
  • Wires validator.WithSkipScriptValidation(true) into the per-tx Validate call inside PreValidateTransactions (services/legacy/netsync/handle_block.go).

Motivation

PreValidateTransactions is only ever reached via the quickValidationMode path (prepareSubtreesValidateTransactionsLegacyMode), and that path only runs when the block height is at or below the highest hard-coded checkpoint for the active network (quickValidationAllowed). PoW plus checkpoint linkage already establish the chain as canonical, so re-running BDK transaction validation (script execution, sigops, standardness, consensus checks) is pure overhead.

CPU profiling of mainnet IBD showed gobdk.TxValidator.ValidateTransaction (the cgo call) accounting for ~22% of teranode's CPU, even though the surrounding code path already trusts the chain via the checkpoint mechanism. This change eliminates that work on the trusted path.

Safety

  • The non-quickValidationMode pre-warm path (validateTransactions) deliberately does not set the new flag.
  • SkipScriptValidation is documented as MUST NOT be used on non-trusted paths, and the call-site comment explains why the legacy IBD path can safely set it.
  • BIP68 sequence-lock checks in Validator.validateTransaction are unaffected — they are gated separately on SkipPolicyChecks and CSV activation height (CSV is post-checkpoint on mainnet).

Test plan

  • go test ./services/validator/... ./services/legacy/netsync/... — 461 tests pass
  • go vet ./services/validator/... ./services/legacy/netsync/... — no issues
  • New unit tests:
    • TestWithSkipScriptValidation — option setter round-trip
    • TestSkipScriptValidationDefault — default is false
    • TestTxValidatorSkipsBDKWhenSkipScriptValidationSet — counting bdk validator confirms the BDK adapter is not invoked when the flag is set
  • Manual: observe mainnet legacy IBD throughput before/after on a checkpoint-anchored range (e.g. height < 250000)

…BD below checkpoint

PreValidateTransactions is only reached via the quickValidationMode path,
which only runs when the block height is at or below the highest hard-coded
checkpoint for the active network. PoW plus checkpoint linkage establish the
chain as canonical, so re-running BDK transaction validation (script
execution, sigops, standardness, consensus checks) is pure overhead.

Profiling mainnet IBD showed BDK.ValidateTransaction (the cgo call into
gobdk) accounting for ~22% of teranode's CPU during legacy catchup, even
though the surrounding code path already trusts the chain via the
checkpoint mechanism.

Adds an Options.SkipScriptValidation flag plus WithSkipScriptValidation
constructor. When set, TxValidator.ValidateTransaction returns immediately
before calling the BDK adapter. The flag is only wired from
PreValidateTransactions, which is documented (and structurally constrained
via quickValidationAllowed) to run only on trusted checkpoint-anchored
blocks. The non-quickValidationMode pre-warm path (validateTransactions)
deliberately does not set it.

Includes unit tests for the option setter, the default value, and a
counting BDK validator that asserts the bdk adapter is not invoked when
the flag is set.
@github-actions

github-actions Bot commented May 26, 2026

Copy link
Copy Markdown
Contributor

🤖 Claude Code Review

Status: Complete

Current Review:

No issues found.

This PR adds a performance optimization that skips BDK script validation during legacy IBD for blocks at or below hard-coded checkpoints. The implementation is well-structured with appropriate safety guards:

Correctness verified:

  • The flag is only set in PreValidateTransactions, which runs exclusively through the quickValidationMode path
  • quickValidationMode is structurally gated by quickValidationAllowed checking block height ≤ highest checkpoint
  • BIP68 validation (via ValidateBIP68 in Validator.validateTransaction:1495) is independent of SkipScriptValidation and will continue to run when blockHeight ≥ CSVHeight
  • The early return at TxValidator.go:158-160 occurs after checkInputs and checkFees but before BDK ValidateTransaction
  • Documentation clearly warns this MUST NOT be used on non-trusted paths
  • Non-quickValidationMode paths (validateTransactions) deliberately omit the flag
  • Tests confirm BDK adapter is not invoked when flag is set

@freemans13 freemans13 self-assigned this May 26, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Benchmark Comparison Report

Baseline: main (unknown)

Current: PR-951 (77c5bea)

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.713µ 1.705µ ~ 0.700
SplitSyncedParentMap_SetIfNotExists/256_buckets-4 61.65n 61.56n ~ 0.100
SplitSyncedParentMap_SetIfNotExists/16_buckets-4 61.53n 61.57n ~ 1.000
SplitSyncedParentMap_SetIfNotExists/1_bucket-4 61.61n 61.67n ~ 0.200
SplitSyncedParentMap_ConcurrentSetIfNotExists/256_buckets... 29.54n 30.35n ~ 0.200
SplitSyncedParentMap_ConcurrentSetIfNotExists/16_buckets_... 50.62n 50.50n ~ 0.700
SplitSyncedParentMap_ConcurrentSetIfNotExists/1_bucket_pa... 104.8n 105.3n ~ 0.400
MiningCandidate_Stringify_Short-4 268.8n 268.7n ~ 0.700
MiningCandidate_Stringify_Long-4 1.875µ 1.873µ ~ 1.000
MiningSolution_Stringify-4 962.6n 963.0n ~ 1.000
BlockInfo_MarshalJSON-4 1.757µ 1.751µ ~ 0.300
NewFromBytes-4 68.08n 74.44n ~ 0.400
AddTxBatchColumnar_Validation-4 1.336µ 1.359µ ~ 0.400
OffsetValidationLoop-4 296.6n 314.8n ~ 0.400
Mine_EasyDifficulty-4 60.10µ 60.19µ ~ 0.700
Mine_WithAddress-4 6.819µ 7.040µ ~ 0.100
BlockAssembler_AddTx-4 0.02626n 0.02600n ~ 1.000
AddNode-4 11.03 10.76 ~ 0.400
AddNodeWithMap-4 11.98 11.68 ~ 0.200
DiskTxMap_SetIfNotExists-4 3.279µ 3.346µ ~ 0.100
DiskTxMap_SetIfNotExists_Parallel-4 3.122µ 3.199µ ~ 0.400
DiskTxMap_ExistenceOnly-4 289.9n 288.5n ~ 1.000
Queue-4 185.9n 185.1n ~ 0.100
AtomicPointer-4 4.837n 5.098n ~ 0.400
ReorgOptimizations/DedupFilterPipeline/Old/10K-4 833.9µ 830.5µ ~ 1.000
ReorgOptimizations/DedupFilterPipeline/New/10K-4 772.9µ 766.7µ ~ 0.200
ReorgOptimizations/AllMarkFalse/Old/10K-4 100.2µ 100.6µ ~ 0.700
ReorgOptimizations/AllMarkFalse/New/10K-4 61.97µ 62.14µ ~ 0.400
ReorgOptimizations/HashSlicePool/Old/10K-4 52.06µ 50.97µ ~ 0.200
ReorgOptimizations/HashSlicePool/New/10K-4 11.36µ 11.25µ ~ 0.100
ReorgOptimizations/NodeFlags/Old/10K-4 4.428µ 4.600µ ~ 0.100
ReorgOptimizations/NodeFlags/New/10K-4 1.520µ 1.562µ ~ 0.400
ReorgOptimizations/DedupFilterPipeline/Old/100K-4 9.537m 8.961m ~ 0.400
ReorgOptimizations/DedupFilterPipeline/New/100K-4 9.164m 9.146m ~ 0.700
ReorgOptimizations/AllMarkFalse/Old/100K-4 1.053m 1.036m ~ 0.100
ReorgOptimizations/AllMarkFalse/New/100K-4 671.2µ 677.9µ ~ 0.200
ReorgOptimizations/HashSlicePool/Old/100K-4 655.4µ 659.4µ ~ 0.700
ReorgOptimizations/HashSlicePool/New/100K-4 315.1µ 335.9µ ~ 0.400
ReorgOptimizations/NodeFlags/Old/100K-4 48.16µ 49.62µ ~ 0.100
ReorgOptimizations/NodeFlags/New/100K-4 16.23µ 17.51µ ~ 0.100
TxMapSetIfNotExists-4 52.31n 51.78n ~ 0.400
TxMapSetIfNotExistsDuplicate-4 41.08n 40.31n ~ 0.100
ChannelSendReceive-4 587.5n 581.7n ~ 0.400
DirectSubtreeAdd/4_per_subtree-4 57.06n 56.59n ~ 1.000
DirectSubtreeAdd/64_per_subtree-4 29.07n 29.22n ~ 0.700
DirectSubtreeAdd/256_per_subtree-4 27.85n 27.86n ~ 0.400
DirectSubtreeAdd/1024_per_subtree-4 26.47n 26.64n ~ 0.200
DirectSubtreeAdd/2048_per_subtree-4 26.16n 26.12n ~ 1.000
SubtreeProcessorAdd/4_per_subtree-4 331.1n 318.0n ~ 0.400
SubtreeProcessorAdd/64_per_subtree-4 319.1n 295.9n ~ 0.100
SubtreeProcessorAdd/256_per_subtree-4 306.9n 309.1n ~ 1.000
SubtreeProcessorAdd/1024_per_subtree-4 286.2n 312.3n ~ 0.200
SubtreeProcessorAdd/2048_per_subtree-4 301.4n 337.5n ~ 0.100
SubtreeProcessorRotate/4_per_subtree-4 301.9n 320.7n ~ 0.200
SubtreeProcessorRotate/64_per_subtree-4 293.3n 329.6n ~ 0.100
SubtreeProcessorRotate/256_per_subtree-4 313.9n 359.0n ~ 0.400
SubtreeProcessorRotate/1024_per_subtree-4 332.3n 293.1n ~ 0.100
SubtreeNodeAddOnly/4_per_subtree-4 55.48n 55.54n ~ 0.800
SubtreeNodeAddOnly/64_per_subtree-4 36.16n 36.12n ~ 1.000
SubtreeNodeAddOnly/256_per_subtree-4 35.09n 35.07n ~ 1.000
SubtreeNodeAddOnly/1024_per_subtree-4 34.42n 34.48n ~ 0.500
SubtreeCreationOnly/4_per_subtree-4 111.2n 109.7n ~ 0.200
SubtreeCreationOnly/64_per_subtree-4 352.4n 347.7n ~ 0.200
SubtreeCreationOnly/256_per_subtree-4 1.246µ 1.237µ ~ 0.700
SubtreeCreationOnly/1024_per_subtree-4 3.854µ 3.762µ ~ 0.100
SubtreeCreationOnly/2048_per_subtree-4 6.876µ 6.902µ ~ 0.700
SubtreeProcessorOverheadBreakdown/64_per_subtree-4 281.0n 291.3n ~ 0.100
SubtreeProcessorOverheadBreakdown/1024_per_subtree-4 282.5n 301.8n ~ 0.100
ParallelGetAndSetIfNotExists/1k_nodes-4 2.017m 2.091m ~ 0.100
ParallelGetAndSetIfNotExists/10k_nodes-4 5.120m 5.277m ~ 0.100
ParallelGetAndSetIfNotExists/50k_nodes-4 7.346m 7.380m ~ 1.000
ParallelGetAndSetIfNotExists/100k_nodes-4 10.14m 10.11m ~ 0.700
SequentialGetAndSetIfNotExists/1k_nodes-4 1.824m 1.809m ~ 0.700
SequentialGetAndSetIfNotExists/10k_nodes-4 4.691m 4.645m ~ 1.000
SequentialGetAndSetIfNotExists/50k_nodes-4 17.18m 15.20m ~ 0.700
SequentialGetAndSetIfNotExists/100k_nodes-4 25.85m 28.38m ~ 0.100
ProcessOwnBlockSubtreeNodesParallel/1k_nodes-4 2.057m 2.072m ~ 0.400
ProcessOwnBlockSubtreeNodesParallel/10k_nodes-4 8.410m 8.596m ~ 0.200
ProcessOwnBlockSubtreeNodesParallel/100k_nodes-4 13.62m 14.04m ~ 0.700
ProcessOwnBlockSubtreeNodesSequential/1k_nodes-4 1.844m 1.893m ~ 0.700
ProcessOwnBlockSubtreeNodesSequential/10k_nodes-4 9.720m 10.279m ~ 1.000
ProcessOwnBlockSubtreeNodesSequential/100k_nodes-4 50.28m 55.25m ~ 0.400
CalcBlockWork-4 464.6n 470.9n ~ 0.100
CalculateWork-4 633.2n 643.4n ~ 0.700
BuildBlockLocatorString_Helpers/Size_10-4 1.346µ 1.344µ ~ 0.800
BuildBlockLocatorString_Helpers/Size_100-4 12.79µ 12.71µ ~ 0.100
BuildBlockLocatorString_Helpers/Size_1000-4 137.2µ 155.5µ ~ 0.700
CatchupWithHeaderCache-4 104.1m 104.2m ~ 0.100
SubtreeSizes/10k_tx_4_per_subtree-4 1.303m 1.315m ~ 0.200
SubtreeSizes/10k_tx_16_per_subtree-4 307.3µ 307.6µ ~ 0.700
SubtreeSizes/10k_tx_64_per_subtree-4 74.69µ 73.68µ ~ 0.700
SubtreeSizes/10k_tx_256_per_subtree-4 18.70µ 18.57µ ~ 0.100
SubtreeSizes/10k_tx_512_per_subtree-4 9.318µ 9.221µ ~ 0.700
SubtreeSizes/10k_tx_1024_per_subtree-4 4.613µ 4.632µ ~ 0.700
SubtreeSizes/10k_tx_2k_per_subtree-4 2.315µ 2.324µ ~ 1.000
BlockSizeScaling/10k_tx_64_per_subtree-4 73.17µ 73.72µ ~ 0.400
BlockSizeScaling/10k_tx_256_per_subtree-4 18.57µ 18.67µ ~ 0.400
BlockSizeScaling/10k_tx_1024_per_subtree-4 4.634µ 4.689µ ~ 0.700
BlockSizeScaling/50k_tx_64_per_subtree-4 384.4µ 386.7µ ~ 0.200
BlockSizeScaling/50k_tx_256_per_subtree-4 94.17µ 93.91µ ~ 1.000
BlockSizeScaling/50k_tx_1024_per_subtree-4 22.93µ 22.78µ ~ 0.400
SubtreeAllocations/small_subtrees_exists_check-4 152.3µ 155.4µ ~ 0.100
SubtreeAllocations/small_subtrees_data_fetch-4 158.7µ 161.1µ ~ 0.100
SubtreeAllocations/small_subtrees_full_validation-4 319.5µ 320.2µ ~ 0.400
SubtreeAllocations/medium_subtrees_exists_check-4 9.193µ 9.099µ ~ 0.700
SubtreeAllocations/medium_subtrees_data_fetch-4 9.394µ 9.432µ ~ 0.100
SubtreeAllocations/medium_subtrees_full_validation-4 18.80µ 18.84µ ~ 1.000
SubtreeAllocations/large_subtrees_exists_check-4 2.184µ 2.207µ ~ 1.000
SubtreeAllocations/large_subtrees_data_fetch-4 2.290µ 2.284µ ~ 0.400
SubtreeAllocations/large_subtrees_full_validation-4 4.698µ 4.693µ ~ 1.000
_BufferPoolAllocation/16KB-4 4.639µ 3.781µ ~ 0.700
_BufferPoolAllocation/32KB-4 8.277µ 8.519µ ~ 1.000
_BufferPoolAllocation/64KB-4 16.66µ 21.69µ ~ 0.100
_BufferPoolAllocation/128KB-4 31.16µ 35.78µ ~ 0.100
_BufferPoolAllocation/512KB-4 120.0µ 151.9µ ~ 0.100
_BufferPoolConcurrent/32KB-4 18.03µ 18.98µ ~ 0.700
_BufferPoolConcurrent/64KB-4 28.70µ 30.54µ ~ 0.200
_BufferPoolConcurrent/512KB-4 147.4µ 143.3µ ~ 0.200
_SubtreeDeserializationWithBufferSizes/16KB-4 618.2µ 618.3µ ~ 1.000
_SubtreeDeserializationWithBufferSizes/32KB-4 620.1µ 611.3µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/64KB-4 624.3µ 609.5µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/128KB-4 622.4µ 607.2µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/512KB-4 593.4µ 593.7µ ~ 1.000
_SubtreeDataDeserializationWithBufferSizes/16KB-4 37.21m 36.59m ~ 0.400
_SubtreeDataDeserializationWithBufferSizes/32KB-4 36.78m 36.45m ~ 0.400
_SubtreeDataDeserializationWithBufferSizes/64KB-4 36.80m 36.49m ~ 0.100
_SubtreeDataDeserializationWithBufferSizes/128KB-4 36.76m 36.42m ~ 0.200
_SubtreeDataDeserializationWithBufferSizes/512KB-4 36.55m 36.06m ~ 0.200
_PooledVsNonPooled/Pooled-4 830.9n 832.1n ~ 0.400
_PooledVsNonPooled/NonPooled-4 7.450µ 7.727µ ~ 0.100
_MemoryFootprint/Current_512KB_32concurrent-4 7.275µ 7.294µ ~ 1.000
_MemoryFootprint/Proposed_32KB_32concurrent-4 9.593µ 9.695µ ~ 1.000
_MemoryFootprint/Alternative_64KB_32concurrent-4 9.438µ 9.741µ ~ 0.700
_prepareTxsPerLevel-4 314.0m 314.4m ~ 1.000
_prepareTxsPerLevelOrdered-4 2.838m 3.218m ~ 0.100
_prepareTxsPerLevel_Comparison/Original-4 306.8m 310.9m ~ 0.200
_prepareTxsPerLevel_Comparison/Optimized-4 2.961m 3.028m ~ 0.700
StoreBlock_Sequential/BelowCSVHeight-4 324.8µ 328.4µ ~ 0.200
StoreBlock_Sequential/AboveCSVHeight-4 326.8µ 328.4µ ~ 0.700
GetUtxoHashes-4 262.2n 263.7n ~ 1.000
GetUtxoHashes_ManyOutputs-4 42.34µ 42.27µ ~ 1.000
_NewMetaDataFromBytes-4 230.9n 228.0n ~ 1.000
_Bytes-4 399.5n 393.2n ~ 0.100
_MetaBytes-4 135.8n 135.3n ~ 0.400

Threshold: >10% with p < 0.05 | Generated: 2026-05-26 16:52 UTC

@freemans13 freemans13 requested review from icellan and oskarszoon and removed request for icellan May 26, 2026 17:07
freemans13 added a commit to freemans13/teranode that referenced this pull request May 26, 2026
…deploy branch

# Conflicts:
#	services/legacy/netsync/handle_block_test.go

@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. Consensus-safe — semantically equivalent to svnode's -assumevalid script-skip with a stricter gate (compiled-in checkpoints vs operator opt-in).

Three independent constraints enforce the trust boundary: flag only set in PreValidateTransactions:888 → only reached via ValidateTransactionsLegacyMode when quickValidationMode == true (handle_block.go:398) → quickValidationAllowed true iff blockHeight ≤ highest checkpoint (mainnet 945000, testnet 1730000, regtest/STN false). Wrong-hash peers disconnected at manager.go:1622-1629; FSM RUN gate refuses RUN until tip ≥ highest checkpoint; merkle-root still verified.

Still enforced independent of the skip: coinbase rejection (TxValidator.go:140-142), coinbase-prevout shape (checkInputs:309-322), BIP68 sequence-lock (Validator.go:1495 — mainnet CSVHeight 419328 ≪ 945000 checkpoint), coinbase maturity at UTXO-store level, block merkle-root. Only thing actually skipped: BDK script execution / sigops / standardness / consensus at TxValidator.go:164.

Worth considering (not blocking):

  • Runtime guard refusing flag when blockHeight > highest checkpoint — currently honor-system gated.
  • Prom counter on skip site for IBD visibility.
  • BIP68 layering test (flag=true + non-final seq + height ≥ CSVHeight must reject).
  • assert.Equalrequire.Equal at TxValidator_test.go:133.

@sonarqubecloud

Copy link
Copy Markdown

@freemans13 freemans13 merged commit 0fd088e into bsv-blockchain:main May 27, 2026
27 of 28 checks passed
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