Skip to content

fix(legacy/netsync): auto-detect v2 txmeta wire format#954

Merged
icellan merged 3 commits into
bsv-blockchain:mainfrom
icellan:fix/netsync-txmeta-v2-wireformat
May 27, 2026
Merged

fix(legacy/netsync): auto-detect v2 txmeta wire format#954
icellan merged 3 commits into
bsv-blockchain:mainfrom
icellan:fix/netsync-txmeta-v2-wireformat

Conversation

@icellan

@icellan icellan commented May 27, 2026

Copy link
Copy Markdown
Contributor

Summary

processTXmetaBatchMessage in services/legacy/netsync/manager.go read the first 4 bytes as a v1 uint32 entry count unconditionally. If a future deploy flips validator_txmeta_wireFormat=v2, the producer emits messages with a 0xFF magic byte, an 8-byte header, and an 8-byte xxhash before each entry's tx hash. The legacy consumer would either log "truncated message" and ack silently, or misalign and announce garbled tx hashes to peers.

This mirrors the v2 detection added to services/subtreevalidation/txmetaHandler.go in #912:

  • Branch on data[0] == 0xFF → v2; otherwise v1.
  • v2: validate sub-version byte, read entry count from data[4:8], set offset = 8.
  • v2: per entry, skip the 8-byte xxhash before reading the 32-byte hash.

netsync discards the xxhash because it only announces ADDs to peers via txAnnounceBatcher — partition-aligned cache writes are a subtreevalidation concern.

Test plan

  • go test -race ./services/legacy/netsync/ — all v1 subtests still pass; 4 new v2 subtests added (happy path, DELETE-skipping, unknown sub-version, truncated header)
  • Confirmed red phase: the happy-path and DELETE v2 subtests fail against main (current code logs truncated message at entry N and announces nothing), proving the regression the fix addresses
  • go vet ./services/legacy/... clean
  • golangci-lint run ./services/legacy/netsync/... — 0 issues
  • go build ./... clean

Notes

  • The constants txmetaWireV2Magic (0xFF) / txmetaWireV2Version (0x02) are now declared in three places: validator (producer), subtreevalidation/txmetaHandler.go, and now legacy/netsync/manager.go. Consolidating into a shared model/ helper is a small follow-up — left out here to keep the diff surgical.
  • The v2 magic-byte assumption (v1 messages can't start with 0xFF because that would require >4.27B entries) is the same one used by the subtreevalidation handler.

processTXmetaBatchMessage read the first 4 bytes as a v1 uint32 entry
count unconditionally. If validator_txmeta_wireFormat=v2 is enabled the
producer emits messages with a 0xFF magic byte, an 8-byte header, and
an 8-byte xxhash before each entry's tx hash; the legacy consumer would
either log "truncated message" and ack silently or misalign and announce
garbled tx hashes to peers.

Mirror the detection pattern from services/subtreevalidation/txmetaHandler.go
(PR bsv-blockchain#912): branch on data[0] == 0xFF, validate the sub-version, read the
entry count from bytes 4-8, and skip the per-entry xxhash. netsync
discards the xxhash because it only announces ADDs to peers via
txAnnounceBatcher and does no partition-aligned cache writes.

Added v2 subtests covering the happy path, DELETE-skipping, unknown
sub-version, and truncated header; the happy-path and DELETE tests
failed against the unpatched code, confirming the regression.
@github-actions

github-actions Bot commented May 27, 2026

Copy link
Copy Markdown
Contributor

🤖 Claude Code Review

Status: Complete


Current Review:

No issues found. The PR successfully addresses the v2 wire format auto-detection bug and implements comprehensive safeguards.

Analysis:

This PR fixes a critical bug where legacy/netsync would misparse or drop valid v1 txmeta messages when the entry count's little-endian representation begins with 0xFF (counts 255, 511, 767, etc.). The fix mirrors the hardened detection logic already deployed in subtreevalidation (#912).

Key Changes Verified:

  1. Wire format constants consolidated in stores/txmetacache/wire.go - single source of truth for all producers/consumers
  2. Detection logic hardened - requires full 4-byte header signature PLUS plausibility check on entry count before treating as v2
  3. Test coverage comprehensive - includes regression tests for count=255 and count=767 edge cases in both netsync and subtreevalidation
  4. Documentation updated - docs/references/kafkaMessageFormat.md accurately describes the wire format, detection rules, and the collision risk
  5. Error handling preserved - truncated/malformed messages are logged and acked (not redelivered)

Documentation Accuracy: All claims verified against implementation:

  • Wire format layouts match code in serializeTxMetaBatch (v1) and serializeTxMetaBatchV2 (v2)
  • Constants match: WireV2MinEntrySize=45, WireV1MinEntrySize=37
  • Detection algorithm in docs matches processTXmetaBatchMessage and txmetaHandler
  • Code examples are accurate representations of actual implementation

Previous Copilot Issues: All addressed in commit d0757af with test coverage and author responses.

Recommendation: Approve for merge.

Copilot AI 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.

Pull request overview

Updates the legacy netsync txmeta Kafka consumer to auto-detect and parse the newer v2 txmeta wire format (magic/header + per-entry xxhash prefix), preventing mis-parsing or silent drops when producers switch to validator_txmeta_wireFormat=v2.

Changes:

  • Add v2 header detection and per-entry xxhash skipping in processTXmetaBatchMessage.
  • Introduce v2 message builder + new v2-focused unit subtests for coinbase filtering, DELETE skipping, unknown version, and truncated header handling.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
services/legacy/netsync/manager.go Adds v2 wire-format detection + parsing adjustments (skip xxhash) for txmeta Kafka batches.
services/legacy/netsync/kafka_txmeta_test.go Adds a v2 message builder helper and v2 parsing/behavior subtests.

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

Comment thread services/legacy/netsync/manager.go Outdated
Comment thread services/legacy/netsync/kafka_txmeta_test.go
Comment thread services/legacy/netsync/manager.go Outdated
@github-actions

github-actions Bot commented May 27, 2026

Copy link
Copy Markdown
Contributor

Benchmark Comparison Report

Baseline: main (unknown)

Current: PR-954 (7ef80b1)

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.734µ 1.751µ ~ 0.700
SplitSyncedParentMap_SetIfNotExists/256_buckets-4 59.58n 59.32n ~ 0.400
SplitSyncedParentMap_SetIfNotExists/16_buckets-4 59.30n 62.02n ~ 0.700
SplitSyncedParentMap_SetIfNotExists/1_bucket-4 59.39n 59.32n ~ 0.200
SplitSyncedParentMap_ConcurrentSetIfNotExists/256_buckets... 32.51n 33.01n ~ 0.700
SplitSyncedParentMap_ConcurrentSetIfNotExists/16_buckets_... 56.84n 56.05n ~ 0.400
SplitSyncedParentMap_ConcurrentSetIfNotExists/1_bucket_pa... 148.4n 153.4n ~ 0.200
MiningCandidate_Stringify_Short-4 251.7n 251.0n ~ 0.400
MiningCandidate_Stringify_Long-4 1.762µ 1.775µ ~ 0.100
MiningSolution_Stringify-4 907.5n 908.9n ~ 0.400
BlockInfo_MarshalJSON-4 1.755µ 1.749µ ~ 0.600
NewFromBytes-4 128.8n 149.6n ~ 0.400
AddTxBatchColumnar_Validation-4 2.538µ 2.477µ ~ 0.200
OffsetValidationLoop-4 636.1n 636.7n ~ 0.800
Mine_EasyDifficulty-4 60.19µ 60.23µ ~ 0.400
Mine_WithAddress-4 6.825µ 6.850µ ~ 1.000
DiskTxMap_SetIfNotExists-4 4.001µ 3.659µ ~ 0.200
DiskTxMap_SetIfNotExists_Parallel-4 3.406µ 3.412µ ~ 1.000
DiskTxMap_ExistenceOnly-4 332.0n 327.6n ~ 0.700
Queue-4 191.0n 188.2n ~ 0.100
AtomicPointer-4 4.823n 4.824n ~ 1.000
ReorgOptimizations/DedupFilterPipeline/Old/10K-4 932.3µ 887.5µ ~ 0.100
ReorgOptimizations/DedupFilterPipeline/New/10K-4 843.3µ 801.6µ ~ 0.100
ReorgOptimizations/AllMarkFalse/Old/10K-4 110.8µ 107.5µ ~ 0.100
ReorgOptimizations/AllMarkFalse/New/10K-4 62.76µ 62.44µ ~ 0.200
ReorgOptimizations/HashSlicePool/Old/10K-4 57.51µ 56.14µ ~ 0.700
ReorgOptimizations/HashSlicePool/New/10K-4 11.63µ 11.80µ ~ 0.700
ReorgOptimizations/NodeFlags/Old/10K-4 5.058µ 4.749µ ~ 0.100
ReorgOptimizations/NodeFlags/New/10K-4 1.726µ 1.579µ ~ 0.100
ReorgOptimizations/DedupFilterPipeline/Old/100K-4 10.01m 10.49m ~ 1.000
ReorgOptimizations/DedupFilterPipeline/New/100K-4 11.11m 10.10m ~ 0.100
ReorgOptimizations/AllMarkFalse/Old/100K-4 1.155m 1.129m ~ 0.100
ReorgOptimizations/AllMarkFalse/New/100K-4 685.4µ 689.5µ ~ 0.700
ReorgOptimizations/HashSlicePool/Old/100K-4 691.9µ 641.4µ ~ 0.100
ReorgOptimizations/HashSlicePool/New/100K-4 304.6µ 286.7µ ~ 0.700
ReorgOptimizations/NodeFlags/Old/100K-4 48.74µ 51.16µ ~ 0.100
ReorgOptimizations/NodeFlags/New/100K-4 17.69µ 17.65µ ~ 0.700
TxMapSetIfNotExists-4 52.53n 52.37n ~ 0.400
TxMapSetIfNotExistsDuplicate-4 40.42n 40.58n ~ 0.700
ChannelSendReceive-4 641.8n 627.8n ~ 0.100
BlockAssembler_AddTx-4 0.02802n 0.02821n ~ 0.700
AddNode-4 11.28 11.55 ~ 1.000
AddNodeWithMap-4 12.30 11.58 ~ 1.000
DirectSubtreeAdd/4_per_subtree-4 57.93n 56.13n ~ 0.100
DirectSubtreeAdd/64_per_subtree-4 28.80n 29.31n ~ 0.700
DirectSubtreeAdd/256_per_subtree-4 27.76n 27.73n ~ 1.000
DirectSubtreeAdd/1024_per_subtree-4 26.42n 26.45n ~ 0.700
DirectSubtreeAdd/2048_per_subtree-4 26.03n 26.06n ~ 0.500
SubtreeProcessorAdd/4_per_subtree-4 300.8n 291.8n ~ 0.100
SubtreeProcessorAdd/64_per_subtree-4 290.9n 289.1n ~ 0.400
SubtreeProcessorAdd/256_per_subtree-4 296.7n 288.6n ~ 0.100
SubtreeProcessorAdd/1024_per_subtree-4 285.8n 275.5n ~ 0.100
SubtreeProcessorAdd/2048_per_subtree-4 282.5n 276.2n ~ 0.100
SubtreeProcessorRotate/4_per_subtree-4 280.7n 291.8n ~ 0.100
SubtreeProcessorRotate/64_per_subtree-4 291.0n 290.7n ~ 1.000
SubtreeProcessorRotate/256_per_subtree-4 285.2n 290.6n ~ 0.700
SubtreeProcessorRotate/1024_per_subtree-4 287.4n 290.2n ~ 0.700
SubtreeNodeAddOnly/4_per_subtree-4 55.33n 55.02n ~ 0.700
SubtreeNodeAddOnly/64_per_subtree-4 36.05n 35.90n ~ 0.100
SubtreeNodeAddOnly/256_per_subtree-4 35.06n 35.01n ~ 0.400
SubtreeNodeAddOnly/1024_per_subtree-4 34.53n 34.42n ~ 0.300
SubtreeCreationOnly/4_per_subtree-4 109.8n 110.2n ~ 0.100
SubtreeCreationOnly/64_per_subtree-4 349.9n 343.4n ~ 0.200
SubtreeCreationOnly/256_per_subtree-4 1.219µ 1.224µ ~ 0.800
SubtreeCreationOnly/1024_per_subtree-4 3.833µ 3.807µ ~ 0.700
SubtreeCreationOnly/2048_per_subtree-4 6.907µ 6.783µ ~ 0.100
SubtreeProcessorOverheadBreakdown/64_per_subtree-4 279.4n 276.5n ~ 0.100
SubtreeProcessorOverheadBreakdown/1024_per_subtree-4 281.0n 276.9n ~ 0.100
ParallelGetAndSetIfNotExists/1k_nodes-4 2.010m 1.985m ~ 0.200
ParallelGetAndSetIfNotExists/10k_nodes-4 5.163m 5.199m ~ 1.000
ParallelGetAndSetIfNotExists/50k_nodes-4 7.357m 7.227m ~ 0.700
ParallelGetAndSetIfNotExists/100k_nodes-4 10.12m 10.10m ~ 0.700
SequentialGetAndSetIfNotExists/1k_nodes-4 1.776m 1.781m ~ 0.400
SequentialGetAndSetIfNotExists/10k_nodes-4 4.487m 4.621m ~ 0.100
SequentialGetAndSetIfNotExists/50k_nodes-4 13.53m 14.45m ~ 0.100
SequentialGetAndSetIfNotExists/100k_nodes-4 24.88m 26.29m ~ 0.100
ProcessOwnBlockSubtreeNodesParallel/1k_nodes-4 2.081m 2.052m ~ 0.700
ProcessOwnBlockSubtreeNodesParallel/10k_nodes-4 8.353m 8.388m ~ 0.200
ProcessOwnBlockSubtreeNodesParallel/100k_nodes-4 13.43m 13.42m ~ 1.000
ProcessOwnBlockSubtreeNodesSequential/1k_nodes-4 1.808m 1.804m ~ 1.000
ProcessOwnBlockSubtreeNodesSequential/10k_nodes-4 8.049m 8.156m ~ 0.400
ProcessOwnBlockSubtreeNodesSequential/100k_nodes-4 43.51m 43.35m ~ 0.700
CalcBlockWork-4 360.6n 363.8n ~ 0.700
CalculateWork-4 496.0n 491.3n ~ 0.400
BuildBlockLocatorString_Helpers/Size_10-4 1.336µ 1.401µ ~ 0.400
BuildBlockLocatorString_Helpers/Size_100-4 15.12µ 13.87µ ~ 1.000
BuildBlockLocatorString_Helpers/Size_1000-4 126.2µ 133.3µ ~ 0.100
CatchupWithHeaderCache-4 104.1m 104.2m ~ 0.700
_BufferPoolAllocation/16KB-4 3.954µ 3.856µ ~ 0.400
_BufferPoolAllocation/32KB-4 8.205µ 7.600µ ~ 0.100
_BufferPoolAllocation/64KB-4 16.86µ 19.35µ ~ 0.100
_BufferPoolAllocation/128KB-4 27.05µ 28.15µ ~ 1.000
_BufferPoolAllocation/512KB-4 108.4µ 113.7µ ~ 0.700
_BufferPoolConcurrent/32KB-4 18.30µ 18.50µ ~ 0.700
_BufferPoolConcurrent/64KB-4 31.77µ 28.48µ ~ 0.100
_BufferPoolConcurrent/512KB-4 146.7µ 143.8µ ~ 0.400
_SubtreeDeserializationWithBufferSizes/16KB-4 648.1µ 609.7µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/32KB-4 654.4µ 597.6µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/64KB-4 658.2µ 595.5µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/128KB-4 686.3µ 602.6µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/512KB-4 644.8µ 579.6µ ~ 0.100
_SubtreeDataDeserializationWithBufferSizes/16KB-4 37.37m 35.89m ~ 0.100
_SubtreeDataDeserializationWithBufferSizes/32KB-4 37.00m 35.76m ~ 0.100
_SubtreeDataDeserializationWithBufferSizes/64KB-4 36.64m 35.77m ~ 0.100
_SubtreeDataDeserializationWithBufferSizes/128KB-4 36.34m 35.78m ~ 0.100
_SubtreeDataDeserializationWithBufferSizes/512KB-4 36.26m 35.95m ~ 0.100
_PooledVsNonPooled/Pooled-4 839.3n 832.0n ~ 0.700
_PooledVsNonPooled/NonPooled-4 8.161µ 7.516µ ~ 0.700
_MemoryFootprint/Current_512KB_32concurrent-4 6.610µ 6.514µ ~ 0.100
_MemoryFootprint/Proposed_32KB_32concurrent-4 12.107µ 9.313µ ~ 0.100
_MemoryFootprint/Alternative_64KB_32concurrent-4 9.825µ 8.858µ ~ 0.100
_prepareTxsPerLevel-4 412.4m 400.1m ~ 0.200
_prepareTxsPerLevelOrdered-4 4.221m 4.163m ~ 1.000
_prepareTxsPerLevel_Comparison/Original-4 407.7m 398.8m ~ 0.200
_prepareTxsPerLevel_Comparison/Optimized-4 4.164m 4.247m ~ 1.000
SubtreeSizes/10k_tx_4_per_subtree-4 1.244m 1.264m ~ 0.400
SubtreeSizes/10k_tx_16_per_subtree-4 297.9µ 295.3µ ~ 1.000
SubtreeSizes/10k_tx_64_per_subtree-4 70.05µ 70.59µ ~ 0.100
SubtreeSizes/10k_tx_256_per_subtree-4 17.57µ 17.65µ ~ 1.000
SubtreeSizes/10k_tx_512_per_subtree-4 8.899µ 8.964µ ~ 1.000
SubtreeSizes/10k_tx_1024_per_subtree-4 4.342µ 4.337µ ~ 1.000
SubtreeSizes/10k_tx_2k_per_subtree-4 2.187µ 2.200µ ~ 0.400
BlockSizeScaling/10k_tx_64_per_subtree-4 68.43µ 69.37µ ~ 0.200
BlockSizeScaling/10k_tx_256_per_subtree-4 17.28µ 17.26µ ~ 0.400
BlockSizeScaling/10k_tx_1024_per_subtree-4 4.316µ 4.287µ ~ 0.800
BlockSizeScaling/50k_tx_64_per_subtree-4 364.1µ 364.3µ ~ 1.000
BlockSizeScaling/50k_tx_256_per_subtree-4 87.41µ 86.10µ ~ 0.100
BlockSizeScaling/50k_tx_1024_per_subtree-4 21.57µ 21.67µ ~ 0.100
SubtreeAllocations/small_subtrees_exists_check-4 149.0µ 147.6µ ~ 0.100
SubtreeAllocations/small_subtrees_data_fetch-4 157.8µ 156.2µ ~ 1.000
SubtreeAllocations/small_subtrees_full_validation-4 308.0µ 306.6µ ~ 1.000
SubtreeAllocations/medium_subtrees_exists_check-4 8.708µ 8.790µ ~ 0.100
SubtreeAllocations/medium_subtrees_data_fetch-4 9.214µ 9.263µ ~ 1.000
SubtreeAllocations/medium_subtrees_full_validation-4 17.30µ 17.58µ ~ 0.700
SubtreeAllocations/large_subtrees_exists_check-4 2.058µ 2.065µ ~ 1.000
SubtreeAllocations/large_subtrees_data_fetch-4 2.194µ 2.203µ ~ 0.700
SubtreeAllocations/large_subtrees_full_validation-4 4.331µ 4.325µ ~ 0.400
StoreBlock_Sequential/BelowCSVHeight-4 338.7µ 332.9µ ~ 0.100
StoreBlock_Sequential/AboveCSVHeight-4 333.6µ 340.6µ ~ 0.200
GetUtxoHashes-4 268.6n 266.5n ~ 0.600
GetUtxoHashes_ManyOutputs-4 49.13µ 49.35µ ~ 0.200
_NewMetaDataFromBytes-4 226.7n 230.2n ~ 0.200
_Bytes-4 420.5n 420.8n ~ 0.400
_MetaBytes-4 138.3n 138.1n ~ 0.400

Threshold: >10% with p < 0.05 | Generated: 2026-05-27 10:48 UTC

@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. Mirrors the v2 detection in #912 cleanly.

Parse logic checks out end-to-end: 4 distinct truncation guards, no unchecked slice indexing, no panic possible on malformed input. Magic-byte collision is unreachable (v1 entry count would need to be ≥ 4,278,190,080). Sub-version validation rejects anything ≠ 0x02 with Errorf "unknown v2 wire version N" + ack-discard — that's the forward-compat seam working as designed.

One deliberate divergence from txmetaHandler.go worth noting: manager.go return nil on a truncated entry (drops the whole batch), txmetaHandler.go break (commits the parsed prefix). Both are correct for their respective consumers — netsync only announces ADDs to peers, so a truncated batch should drop entirely rather than leak garbled tx hashes.

Red-phase claim verified — v2 happy-path and DELETE-skip subtests fail against unpatched main (0xFF first byte misread as v1 entry count 767, loop exits truncated after 0 announcements). All 11 subtests pass -race.

icellan added 2 commits May 27, 2026 11:53
The v2 magic-byte detection was unsafe: v1 messages with entry counts
255, 511, 767, ... begin with byte 0xFF (the LE-uint32 low byte), so the
naive `data[0] == 0xFF` check misclassified them. Count 767 in
particular aliases the entire 4-byte v2 header `0xFF 0x02 0x00 0x00`
exactly. Both legacy/netsync (this PR) and subtreevalidation (PR bsv-blockchain#912)
had the bug, silently dropping the netsync announce or garbling the
txmeta cache write.

Detection now requires the full 4-byte header signature plus a
plausibility check on the resulting v2 entry count
(entries * minEntrySize must fit in the remaining buffer); any failure
falls through to v1 parsing instead of dropping the message.

Move the wire-format constants (action bytes, magic, version, header
length, min entry size) into a new stores/txmetacache/wire.go so the
producer (validator) and all consumers (subtreevalidation,
legacy/netsync) reference one source of truth. Drops three local copies
and prevents future drift.

Regression tests cover the count=255 and count=767 collisions in both
the netsync consumer and the subtreevalidation handler, asserting that
the v1 dispatch path runs and the v2 path does not.

Addresses Copilot review feedback on PR bsv-blockchain#954.
Adds the same plausibility check on the v1 fallback path of the
subtreevalidation txmeta handler that the v2 path already had:
`entries * minEntrySize ≤ remainingBuffer`. Without this, a malformed
or malicious v1 message could declare ~4B entries and trigger an
oversized pool-buffer pre-allocation (~128GB) before the per-entry
truncation check would kick in. The bound now uses the shared
WireV1MinEntrySize constant added to stores/txmetacache/wire.go and
mirrors the v2 detection-time check in shape.

Renames TestServer_txmetaHandler_V2_UnknownVersion to reflect its new
semantics — an unknown v2 sub-version now falls back to v1 parsing
rather than being rejected outright. The test now also asserts that
the v2 dispatch path is NOT taken, which is the actual invariant.
Adds TestServer_txmetaHandler_V1_ImplausibleEntryCount_IsRejected to
cover the new bound.

Rewrites the Transaction Metadata Message Format section of
docs/references/kafkaMessageFormat.md to describe the actual binary
batch wire format (v1 and v2) with the multi-byte detection rules and
plausibility checks. The previous protobuf-shaped description was
stale from before the batched binary format landed; this PR widens
the gap by introducing v2, so the doc is updated as part of the same
change. Index entries adjusted to match.
@sonarqubecloud

Copy link
Copy Markdown

@icellan icellan merged commit 413396e into bsv-blockchain:main May 27, 2026
25 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.

4 participants