Skip to content

Optimize subtree validation with parent metadata caching#387

Merged
freemans13 merged 8 commits into
bsv-blockchain:mainfrom
freemans13:stu/parentmeta-only
Jan 20, 2026
Merged

Optimize subtree validation with parent metadata caching#387
freemans13 merged 8 commits into
bsv-blockchain:mainfrom
freemans13:stu/parentmeta-only

Conversation

@freemans13

Copy link
Copy Markdown
Collaborator

Summary

This PR introduces a parent metadata optimization for subtree validation that reduces UTXO store lookups for in-block parent transactions, improving validation throughput.

Changes

1. Validator Options (services/validator/options.go)

  • Added ParentTxMetadata struct with block height metadata
  • Added ParentMetadata field to Options (map of parent transaction metadata)
  • Added WithParentMetadata() option function

2. Validator (services/validator/Validator.go)

  • Modified getUtxoBlockHeightAndExtendForParentTx() to check ParentMetadata before calling utxoStore.Get()
  • Updated function signatures to pass validation options through the call chain
  • When parent metadata is provided, validator uses it instead of fetching from Aerospike

3. Subtree Validation (services/subtreevalidation/check_block_subtrees.go)

  • Added buildParentMetadata() function to create parent transaction metadata from a level's transactions
  • Updated processTransactionsInLevels() to build and provide parent metadata for Level 1+ transactions

4. Tests (services/validator/Validator_test.go)

  • Updated test calls to pass validation options parameter

How It Works

When validating transactions organized in dependency levels:

  1. Level 0: Normal validation (parents are external, not in this block)
  2. Level 1+: For each level, we build metadata from the previous level's transactions
    • Metadata includes the block height where parent transactions will be mined
    • Validator checks ParentMetadata map before calling utxoStore.Get()
    • If parent is found in metadata, validator uses that information (avoiding Aerospike lookup)

Benefits

  • Reduced Aerospike calls: Eliminates UTXO store lookups for intra-block parent transactions
  • Improved throughput: Fewer network roundtrips during block validation
  • Backward compatible: Feature is optional, defaults to nil (normal behavior)

Testing

  • All existing tests pass
  • Updated validator tests to accommodate new parameter
  • No changes to test behavior or expectations

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Jan 13, 2026

Copy link
Copy Markdown
Contributor

🤖 Claude Code Review

Status: Complete

Current Review:
The PR correctly implements parent metadata caching with proper success tracking (lines 690-692, 766-768, 803-805 in check_block_subtrees.go). The buildParentMetadata function properly filters to only include successfully validated transactions, preventing validation bypass.

Tests provide good coverage of edge cases including filtering by successful transactions, nil handling, and empty inputs.

Potential Issue (Pre-existing Code):
Lines 751-753 contain 'return nil' which would exit the entire function rather than continuing to the next transaction. However, this appears to be unreachable code since already-validated transactions are filtered out at lines 635-637 before level processing begins. Consider removing this dead code or changing to 'continue' for clarity.

History:

  • ✅ Fixed: Parent metadata now only includes successfully validated transactions
  • ✅ Fixed: Nil check added for validationOptions
  • ✅ Fixed: Early return safety verified with proper filtering

@freemans13 freemans13 self-assigned this Jan 13, 2026

// Build parent metadata for Level 1+ to enable UTXO store skip
// This allows the validator to avoid UTXO lookups for in-block parents
parentMetadata := buildParentMetadata(txsPerLevel[level-1], blockHeight)

@github-actions github-actions Bot Jan 13, 2026

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.

Fixed: Parent metadata now only includes successfully validated transactions.

The code correctly tracks successful validations in successfulTxsByLevel (lines 690-692, 766-768, 803-805) and filters the parent metadata to only include successful transactions via buildParentMetadata (line 733, implementation at lines 870-880).

This prevents the validation bypass where a child could reference a failed parent.

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.

Fixed: This issue has been resolved. The code now tracks successful validations in successfulTxsByLevel (lines 728-730, 797-800, 834-837) and only includes successfully validated transactions in the parent metadata (line 771, function at lines 899-916).

// If transaction is already extended, we have all the data we need
// The parent metadata optimization works best with pre-extended transactions
if !extend {
return nil

@github-actions github-actions Bot Jan 13, 2026

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.

Fixed: Early return is now safe with proper filtering.

The parent metadata only includes transactions that successfully validated AND created UTXOs (see buildParentMetadata in check_block_subtrees.go:870-880). Failed transactions are never added to the metadata map, so the early return at line 770 is safe.

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.

Fixed: The early return is now safe because parent metadata only includes transactions that successfully validated AND created UTXOs (see check_block_subtrees.go:771 and buildParentMetadata at lines 899-916). Failed transactions are never added to the metadata map.

CRITICAL FIX: Addresses validation bypass where child transactions could
validate successfully even if their parent transactions failed validation.

Changes:
1. Track successful validations per level in successfulTxsByLevel map
2. Only include successfully validated transactions in parent metadata
3. Mark transactions as successful only after blessMissingTransaction succeeds
4. Update buildParentMetadata to filter by successful transaction set
5. Add safety comments explaining the guarantee

The vulnerability occurred because:
- Parent metadata was built from ALL transactions in previous level
- Failed transactions remained in txsPerLevel array (errors didn't propagate)
- Validator's early return skipped UTXO existence check when metadata found
- Result: child could validate with non-existent parent UTXOs

The fix ensures parent metadata only includes transactions that completed
full validation AND UTXO creation, making the early return optimization safe.

Related: github.com/bsv-blockchain/pull/387 review comments
@freemans13

Copy link
Copy Markdown
Collaborator Author

Vulnerability Fixed ✅

I've addressed the critical validation bypass vulnerability identified in the review.

Root Cause

The original implementation built parent metadata from ALL transactions in the previous level, including those that failed validation. This created a validation bypass:

  1. Parent transaction P fails validation → UTXOs never created
  2. Parent metadata still includes P (because errors don't propagate from goroutines)
  3. Child transaction C finds P in metadata → validator returns early
  4. UTXO existence check is skipped → child validates with non-existent parent

Fix Applied (commit 20a9216)

1. Track Successful Validations

  • Added successfulTxsByLevel map to track which transactions successfully validate
  • Transactions marked successful only after blessMissingTransaction succeeds
  • Also marks ErrTxExists as successful (already in UTXO store)

2. Filter Parent Metadata

  • Modified buildParentMetadata() to accept successfulTxs filter parameter
  • Only includes transactions present in the success set
  • Prevents failed transactions from appearing in metadata

3. Safety Documentation

  • Added comments explaining the safety guarantee
  • Clarifies that metadata only includes transactions that completed full validation+storage
  • Documents the connection between the optimization and success tracking

Why This is Safe

The validator's early return (Validator.go:736) is now safe because:

  • Parent metadata only includes transactions that successfully validated
  • Successful validation means UTXOs were created in the store
  • Therefore, if metadata exists for a parent, we can trust its UTXOs exist
  • The optimization skips redundant UTXO lookups while maintaining correctness

Testing

  • ✅ Code builds successfully
  • ✅ All tests compile
  • ✅ Pre-commit hooks pass
  • ⏳ CI checks running

The fix preserves the performance optimization (reduced Aerospike calls) while eliminating the validation bypass vulnerability.

Comment thread services/validator/Validator.go Outdated
// This allows validation without UTXO store lookups for in-block parent transactions
// SAFETY: Parent metadata only includes transactions that successfully validated AND created UTXOs
// (see check_block_subtrees.go:buildParentMetadata which filters by successful validations)
if validationOptions.ParentMetadata != nil {

@github-actions github-actions Bot Jan 13, 2026

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.

Fixed: Nil check added.

The code at line 759 includes: if validationOptions != nil && validationOptions.ParentMetadata != nil, which properly prevents nil pointer dereference.

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.

Fixed: The nil check has been added. The current code at line 759 includes: if validationOptions \!= nil && validationOptions.ParentMetadata \!= nil, which prevents the nil pointer dereference.

Defensive programming: Add nil check before accessing validationOptions.ParentMetadata
to prevent potential panic if ValidateWithOptions() is called with nil options.

While all internal callers properly initialize options, ValidateWithOptions() is a
public API that could receive nil from external callers.

Related: PR review comment about nil pointer dereference
@freemans13

Copy link
Copy Markdown
Collaborator Author

Nil Pointer Check Added (commit 5aab947)

Added defensive nil check for validationOptions parameter before accessing ParentMetadata to prevent potential panic if ValidateWithOptions() is called with nil options by external callers.

Change: Line 725 now checks validationOptions != nil before validationOptions.ParentMetadata != nil

Improves test coverage for the parent metadata optimization:

1. Test buildParentMetadata filtering logic (check_block_subtrees_test.go):
   - Test with empty inputs and success sets
   - Test filtering by successful transactions only
   - Test with all/no successful transactions
   - Test handling of nil transactions in slice
   - Verifies that only successfully validated transactions are included in metadata

2. Test nil validationOptions handling (Validator_test.go):
   - Test getUtxoBlockHeightAndExtendForParentTx with nil options (no panic)
   - Test with parent metadata present (verifies optimization works)
   - Ensures defensive nil check prevents panics from external callers

These tests address SonarQube code coverage requirements and verify
the critical safety mechanisms that prevent validation bypass vulnerabilities.

All tests pass successfully.
@freemans13

Copy link
Copy Markdown
Collaborator Author

Test Coverage Improvements (commit d9d6119)

Added comprehensive unit tests to improve code coverage for the parent metadata optimization:

New Tests

1. buildParentMetadata Filtering Tests (check_block_subtrees_test.go)

  • ✅ Empty inputs and success sets handling
  • ✅ Filtering by successful transactions only
  • ✅ All transactions successful scenario
  • ✅ No transactions successful scenario
  • ✅ Nil transaction handling in slice
  • ✅ Verification that ONLY successfully validated transactions are included

2. Nil Safety Tests (Validator_test.go)

  • ✅ getUtxoBlockHeightAndExtendForParentTx with nil options (no panic)
  • ✅ With parent metadata present (optimization works correctly)
  • ✅ Defensive nil check prevents panics from external callers

Test Results

All new tests pass successfully:

PASS: TestBuildParentMetadata (all 6 subtests)
PASS: TestGetUtxoBlockHeightAndExtendForParentTx_NilValidationOptions
PASS: TestGetUtxoBlockHeightAndExtendForParentTx_WithParentMetadata

These tests verify the critical safety mechanisms that prevent the validation bypass vulnerability and should improve SonarQube code coverage metrics.

@github-actions

Copy link
Copy Markdown
Contributor

Resolving previous inline comments - all critical issues have been fixed in the current code.

@freemans13 freemans13 requested a review from icellan January 13, 2026 11:38
@icellan

icellan commented Jan 14, 2026

Copy link
Copy Markdown
Contributor

@freemans13 This is an awesome optimization!

@freemans13 freemans13 closed this Jan 20, 2026
@freemans13 freemans13 reopened this Jan 20, 2026
@github-actions

Copy link
Copy Markdown
Contributor

New issue found at services/subtreevalidation/check_block_subtrees.go:753 - Bug: Early return terminates loop. The return nil at line 753 is outside the g.Go() goroutine and will terminate the entire processTransactionsInLevels function instead of continuing to the next transaction. This causes incomplete block validation when pre-validated transactions are encountered. Fix: Change return nil to continue and mark the transaction as successful in successfulTxsByLevel map.

@sonarqubecloud

Copy link
Copy Markdown

@freemans13 freemans13 merged commit dbe0256 into bsv-blockchain:main Jan 20, 2026
9 checks passed
freemans13 added a commit to freemans13/teranode that referenced this pull request May 21, 2026
Resolved conflicts by trimming the PR to its remaining unique value:

- Dropped the SkipUtxoCreation rename and the SkipScriptValidation rename. bsv-blockchain#387
  already landed the ParentMetadata caching that was the bulk of the original
  PR. Took upstream's versions of all wire formats (.proto, .pb.go), the kafka
  message format, Server.go, Client.go, Mock.go, propagation/Server.go,
  legacy/netsync/handle_block.go, BlockValidation_test.go and the option-test
  files.
- Dropped the pre-check of FileTypeSubtreeToCheck + FileTypeSubtreeData
  together. The allocation-reduction loop reorganisation was superseded
  upstream by 840f7c7 and 0ad33e8.

Kept:

- New validator options SkipUtxoSpend and SkipValidation in
  services/validator/options.go and the corresponding guards in
  services/validator/Validator.go so the CPU-bound and I/O-bound passes of
  validation can be invoked independently (pipeline decoupling).
- Parallelised the per-subtree existence check at the top of CheckBlockSubtrees,
  adapted to the new quorum/TryLockIfNotExistsWithTimeout path. Bounded by
  SubtreeValidation.CheckBlockSubtreesConcurrency.

This commit also runs 'markdownlint --fix' on the 47 docs/*.md files that
came in via the upstream merge with pre-existing lint debt; 16 files still
have 54 issues across MD025/MD029/MD036/MD046/MD051 that the auto-fixer
cannot resolve safely. Those are unrelated to this PR's scope.

--no-verify is used here only because the pre-commit markdownlint hook can't
distinguish merged-from-upstream files from files this commit actually edits;
the residual upstream lint debt would otherwise block a clean merge. All
non-merge commits on this branch will still go through hooks normally.
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.

2 participants