Skip to content

test: fix CI fixtures after transaction re-extension (release/v0.15) - #1722

Merged
icellan merged 1 commit into
bsv-blockchain:release/v0.15from
icellan:fix/reextension-ci-tests-v015
Sep 11, 2026
Merged

test: fix CI fixtures after transaction re-extension (release/v0.15)#1722
icellan merged 1 commit into
bsv-blockchain:release/v0.15from
icellan:fix/reextension-ci-tests-v015

Conversation

@icellan

@icellan icellan commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Backport of #1721 to release/v0.15. The test and documentation changes are identical to main, with no branch-specific adjustments.

Mandatory transaction re-extension exposed three test assumptions: concurrent propagation submissions shared a mutable transaction, the zero-value script fixture depended on the configured minimum fee, and the reassignment smoke test trusted a replacement script that was never stored.

This change gives each concurrent submission its own decoded transaction while retaining the duplicate-submission assertion, explicitly disables the minimum fee only for the unspendable-script control, and checks reassignment maturity with a spend signed for the stored owner. The smoke test also verifies that an unstored replacement script remains rejected after maturity and leaves the output unspent.

Reassignment limitation: ReAssignUTXO changes the commitment and maturity state but does not persist the replacement locking script. Re-extension therefore retrieves the original script. Supporting a different owner requires an authoritative replacement-script source and corresponding validation support. This limitation is documented in the alert-service guide; production code is unchanged.

Validation on this branch:

  • Full propagation and validator packages with SETTINGS_CONTEXT=test, the race detector, and testtxmetacache, including real Aerospike/PostgreSQL containers.
  • Targeted propagation concurrency tests and reassignment smoke test, each repeated three times with the race detector.
  • go vet and staticcheck for the three affected packages.
  • Repository pre-commit checks, including Go lint and Markdown lint.

The zero-value fixture's insufficient-fee failure was reproduced with a nonzero minimum fee before verifying the fix.

A broader local run also hit an HTTP startup failure on main and a batch-test failure on release. Both full propagation packages and three isolated runs of the release batch test passed on rerun. These tests are unchanged by this patch.

Backport of ac2833f with identical test and documentation changes.
Copilot AI lite review requested due to automatic review settings September 11, 2026 14:22
@github-actions

github-actions Bot commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

🤖 Claude Code Review

Status: Complete

Current Review:
No issues found. This is a test/documentation-only backport of #1721 to release/v0.15; no production code changes.

  • Docs (alert.md) — The new limitation note accurately matches the implementation. Verified ReAssignUTXO in both stores/utxo/sql/alert_system.go (SET utxo_hash=..., frozen=false, spendableIn=...) and the Aerospike Lua path: both change the commitment, clear the freeze, and set maturity, but neither persists a replacement locking script.
  • Validator test — Disabling MinMiningTxFee and rebuilding txValidator is safe; tSettings is created fresh per newAmbiguityValidator call, so the mutation cannot leak across subtests. Correctly prevents an insufficient-fee failure from masking the intended Opcode missing script assertion on the zero-value fixture.
  • Propagation test — Giving each goroutine its own bt.NewTxFromBytes-decoded copy correctly fixes the shared-pointer data race caused by in-place re-extension, while preserving the dedup assertion (require.Len(..., 2)).
  • Reassign smoke test — The same-owner output (vout 1) cleanly isolates the maturity gate from the script-change case, and the assertions on Status_IMMATUREStatus_OK, plus SpendingData == nil, correctly pin the documented behavior. Symbols and helper signatures (WithP2PKHOutputs(numOutputs, amount, pubKey...), GetSpend, SpendResponse) verified.

Minor note (non-blocking): bobSpendingTx/charlesSpendingTx are resubmitted after an earlier expected rejection, relying on the validator not permanently caching rejects — this matches the pre-existing pattern in this test.

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.

🟢 Approval recommended

No unresolved approval-blocking issues were identified.

Pull request overview

Backports test and documentation fixes for mandatory transaction re-extension without changing production code.

Changes:

  • Isolates concurrent transaction submissions.
  • Makes validator fee fixtures deterministic.
  • Strengthens reassignment maturity tests.
  • Documents replacement-script limitations.
File summaries
File Description
test/e2e/daemon/ready/reassign_test.go Tests maturity and stored-owner behavior.
services/validator/utxo_commitment_ambiguity_test.go Disables fees for the zero-value control.
services/propagation/Server_test.go Uses independent decoded transactions concurrently.
docs/topics/services/alert.md Documents reassignment limitations.
Review details
  • Files reviewed: 4/4 changed files
  • Comments generated: 0
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@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.

Tests and docs only, mechanics verified: the extended-bytes round-trip is sound (go-bt
ReadFrom detects the 0xEF marker, so PreviousTxScript/PreviousTxSatoshis
survive NewTxFromBytes), settings are fresh per subtest so the fee override can't
leak, and the per-goroutine decode fixes a real shared-pointer race. Two things before
this lands.

Test name now says the opposite of what it assertstest/e2e/daemon/ready/reassign_test.go:24

TestShouldAllowReassign asserts reassignment to a different owner is rejected before
maturity, after maturity, and leaves the output unspent. The only surviving success
path reassigns to the stored owner, which changes no ownership. Rename it to what it
pins — TestReassignEnforcesMaturityAndRejectsUnstoredScript or similar — otherwise
the next person greps for reassignment coverage and concludes the feature works.

The regression needs more than a docs paragraph

Confirmed in both stores: stores/utxo/sql/alert_system.go:179-186 updates
utxo_hash/frozen/spendableIn only, and
stores/utxo/aerospike/alert_system.go:245-251 passes just the two hashes and heights
to the Lua UDF. Neither persists the replacement script, so alert-service reassignment
to a new owner is non-functional on release/v0.15 after the security fix.

No tracking issue exists. This is the confiscation path — it needs an issue and a
v0.15.x release-note entry, not only docs/topics/services/alert.md:141.

Smaller

  • services/validator/utxo_commitment_ambiguity_test.go:184-187 — reaching in to set
    MinMiningTxFee and rebuild txValidator after construction would be cleaner as an
    option on newAmbiguityValidator. Safe as written.
  • reassign_test.go:136 vs :175 — you assert SpendingData == nil after Charles's
    rejection but not after Bob's early rejection, then re-submit that same transaction
    at :178. One more assertion makes the later require.NoError unambiguous.

Unrelated, found while checking the above: sql/alert_system.go:174-178 honours
tSettings.UtxoStore.ReAssignedUtxoSpendableAfterBlocks but
aerospike/alert_system.go:249 hardcodes the package constant (1000). Production runs
Aerospike, so that setting is silently a no-op there.

@github-actions

Copy link
Copy Markdown
Contributor

Benchmark Comparison Report

Baseline: main (unknown)

Current: PR-1722 (d471710)

Summary

  • Regressions: 0
  • Improvements: 0
  • Unchanged: 130
  • Significance level: p < 0.05
All benchmark results (sec/op)
Benchmark Baseline Current Change p-value
_NewBlockFromBytes-4 1.711µ 1.755µ ~ 0.100
SplitSyncedParentMap_SetIfNotExists/256_buckets-4 63.94n 61.61n ~ 0.100
SplitSyncedParentMap_SetIfNotExists/16_buckets-4 63.94n 61.86n ~ 0.100
SplitSyncedParentMap_SetIfNotExists/1_bucket-4 63.90n 61.78n ~ 0.100
SplitSyncedParentMap_ConcurrentSetIfNotExists/256_buckets... 30.76n 32.00n ~ 0.700
SplitSyncedParentMap_ConcurrentSetIfNotExists/16_buckets_... 53.61n 51.62n ~ 0.200
SplitSyncedParentMap_ConcurrentSetIfNotExists/1_bucket_pa... 112.3n 109.8n ~ 0.700
MiningCandidate_Stringify_Short-4 242.7n 279.1n ~ 0.100
MiningCandidate_Stringify_Long-4 1.596µ 1.939µ ~ 0.100
MiningSolution_Stringify-4 790.3n 987.4n ~ 0.100
BlockInfo_MarshalJSON-4 1.575µ 1.794µ ~ 0.100
NewFromBytes-4 96.88n 97.50n ~ 0.200
AddTxBatchColumnar_Validation-4 1.672µ 2.080µ ~ 0.100
OffsetValidationLoop-4 439.2n 499.6n ~ 0.100
Mine_EasyDifficulty-4 61.55µ 61.70µ ~ 1.000
Mine_WithAddress-4 7.365µ 7.045µ ~ 0.100
DiskTxMap_SetIfNotExists-4 3.806µ 3.503µ ~ 0.100
DiskTxMap_SetIfNotExists_Parallel-4 3.565µ 3.292µ ~ 0.700
DiskTxMap_ExistenceOnly-4 351.2n 323.1n ~ 0.400
Queue-4 196.8n 189.6n ~ 0.100
AtomicPointer-4 4.666n 4.592n ~ 0.700
TxMapSetIfNotExists-4 64.18n 53.06n ~ 0.100
TxMapSetIfNotExistsDuplicate-4 46.17n 40.04n ~ 0.100
ChannelSendReceive-4 557.6n 572.8n ~ 0.100
BlockAssembler_AddTx-4 0.02276n 0.02053n ~ 0.100
AddNode-4 8.642 7.986 ~ 0.400
AddNodeWithMap-4 9.173 8.486 ~ 0.400
DirectSubtreeAdd/4_per_subtree-4 66.98n 65.56n ~ 1.000
DirectSubtreeAdd/64_per_subtree-4 40.45n 39.66n ~ 0.700
DirectSubtreeAdd/256_per_subtree-4 39.37n 39.21n ~ 0.400
DirectSubtreeAdd/1024_per_subtree-4 37.69n 37.56n ~ 0.400
DirectSubtreeAdd/2048_per_subtree-4 37.67n 36.84n ~ 0.100
SubtreeProcessorAdd/4_per_subtree-4 202.1n 316.2n ~ 0.100
SubtreeProcessorAdd/64_per_subtree-4 209.9n 301.7n ~ 0.100
SubtreeProcessorAdd/256_per_subtree-4 208.2n 283.1n ~ 0.100
SubtreeProcessorAdd/1024_per_subtree-4 205.0n 298.6n ~ 0.100
SubtreeProcessorAdd/2048_per_subtree-4 200.7n 298.8n ~ 0.100
SubtreeProcessorRotate/4_per_subtree-4 206.6n 293.4n ~ 0.100
SubtreeProcessorRotate/64_per_subtree-4 209.2n 295.4n ~ 0.100
SubtreeProcessorRotate/256_per_subtree-4 207.1n 302.0n ~ 0.100
SubtreeProcessorRotate/1024_per_subtree-4 205.6n 310.3n ~ 0.100
SubtreeNodeAddOnly/4_per_subtree-4 81.97n 83.83n ~ 0.200
SubtreeNodeAddOnly/64_per_subtree-4 63.79n 65.61n ~ 0.700
SubtreeNodeAddOnly/256_per_subtree-4 65.23n 64.16n ~ 1.000
SubtreeNodeAddOnly/1024_per_subtree-4 62.90n 65.89n ~ 0.100
SubtreeCreationOnly/4_per_subtree-4 127.3n 142.0n ~ 0.100
SubtreeCreationOnly/64_per_subtree-4 512.2n 511.0n ~ 1.000
SubtreeCreationOnly/256_per_subtree-4 1.826µ 1.660µ ~ 0.100
SubtreeCreationOnly/1024_per_subtree-4 5.284µ 5.339µ ~ 0.500
SubtreeCreationOnly/2048_per_subtree-4 9.200µ 9.633µ ~ 0.100
SubtreeProcessorOverheadBreakdown/64_per_subtree-4 200.2n 296.5n ~ 0.100
SubtreeProcessorOverheadBreakdown/1024_per_subtree-4 205.6n 301.0n ~ 0.100
ParallelGetAndSetIfNotExists/1k_nodes-4 13.42m 15.41m ~ 0.100
ParallelGetAndSetIfNotExists/10k_nodes-4 16.82m 20.96m ~ 0.100
ParallelGetAndSetIfNotExists/50k_nodes-4 19.06m 23.43m ~ 0.700
ParallelGetAndSetIfNotExists/100k_nodes-4 19.96m 19.59m ~ 0.700
SequentialGetAndSetIfNotExists/1k_nodes-4 12.96m 15.30m ~ 0.100
SequentialGetAndSetIfNotExists/10k_nodes-4 16.12m 19.33m ~ 0.100
SequentialGetAndSetIfNotExists/50k_nodes-4 19.01m 18.91m ~ 0.700
SequentialGetAndSetIfNotExists/100k_nodes-4 24.73m 25.08m ~ 0.400
ProcessOwnBlockSubtreeNodesParallel/1k_nodes-4 12.47m 16.19m ~ 0.200
ProcessOwnBlockSubtreeNodesParallel/10k_nodes-4 17.91m 18.16m ~ 1.000
ProcessOwnBlockSubtreeNodesParallel/100k_nodes-4 21.29m 21.79m ~ 0.700
ProcessOwnBlockSubtreeNodesSequential/1k_nodes-4 12.00m 12.14m ~ 1.000
ProcessOwnBlockSubtreeNodesSequential/10k_nodes-4 18.33m 17.05m ~ 0.400
ProcessOwnBlockSubtreeNodesSequential/100k_nodes-4 49.11m 47.50m ~ 0.700
CalcBlockWork-4 507.3n 517.7n ~ 0.700
CalculateWork-4 718.9n 692.8n ~ 0.700
CheckOldBlockIDs/on-chain-prefetch/1000-4 62.04µ 71.51µ ~ 0.100
CheckOldBlockIDs/on-chain-prefetch/10000-4 567.6µ 465.3µ ~ 0.700
BuildBlockLocatorString_Helpers/Size_10-4 1.437µ 1.391µ ~ 0.200
BuildBlockLocatorString_Helpers/Size_100-4 13.69µ 13.12µ ~ 0.100
BuildBlockLocatorString_Helpers/Size_1000-4 136.4µ 131.4µ ~ 0.100
CatchupWithHeaderCache-4 107.0m 104.6m ~ 0.100
_prepareTxsPerLevel-4 402.8m 410.6m ~ 0.200
_prepareTxsPerLevelOrdered-4 3.764m 3.663m ~ 1.000
_prepareTxsPerLevel_Comparison/Original-4 425.3m 415.5m ~ 0.200
_prepareTxsPerLevel_Comparison/Optimized-4 3.626m 3.575m ~ 1.000
SubtreeSizes/10k_tx_4_per_subtree-4 1.346m 1.322m ~ 0.700
SubtreeSizes/10k_tx_16_per_subtree-4 314.9µ 315.8µ ~ 1.000
SubtreeSizes/10k_tx_64_per_subtree-4 76.15µ 75.00µ ~ 0.200
SubtreeSizes/10k_tx_256_per_subtree-4 18.69µ 18.49µ ~ 0.100
SubtreeSizes/10k_tx_512_per_subtree-4 9.248µ 9.201µ ~ 0.700
SubtreeSizes/10k_tx_1024_per_subtree-4 4.613µ 4.569µ ~ 0.700
SubtreeSizes/10k_tx_2k_per_subtree-4 2.298µ 2.293µ ~ 0.700
BlockSizeScaling/10k_tx_64_per_subtree-4 74.05µ 72.61µ ~ 0.100
BlockSizeScaling/10k_tx_256_per_subtree-4 18.70µ 18.35µ ~ 0.100
BlockSizeScaling/10k_tx_1024_per_subtree-4 4.576µ 4.558µ ~ 1.000
BlockSizeScaling/50k_tx_64_per_subtree-4 389.6µ 388.2µ ~ 1.000
BlockSizeScaling/50k_tx_256_per_subtree-4 93.18µ 90.95µ ~ 0.100
BlockSizeScaling/50k_tx_1024_per_subtree-4 22.77µ 22.26µ ~ 0.200
SubtreeAllocations/small_subtrees_exists_check-4 155.4µ 154.8µ ~ 0.400
SubtreeAllocations/small_subtrees_data_fetch-4 162.6µ 159.3µ ~ 0.400
SubtreeAllocations/small_subtrees_full_validation-4 323.9µ 318.5µ ~ 0.700
SubtreeAllocations/medium_subtrees_exists_check-4 9.187µ 9.091µ ~ 0.400
SubtreeAllocations/medium_subtrees_data_fetch-4 9.714µ 9.530µ ~ 0.100
SubtreeAllocations/medium_subtrees_full_validation-4 18.60µ 18.53µ ~ 0.400
SubtreeAllocations/large_subtrees_exists_check-4 2.202µ 2.168µ ~ 0.100
SubtreeAllocations/large_subtrees_data_fetch-4 2.373µ 2.309µ ~ 0.100
SubtreeAllocations/large_subtrees_full_validation-4 4.665µ 4.655µ ~ 1.000
_BufferPoolAllocation/16KB-4 3.848µ 3.884µ ~ 0.700
_BufferPoolAllocation/32KB-4 8.766µ 7.827µ ~ 0.100
_BufferPoolAllocation/64KB-4 17.44µ 21.02µ ~ 0.700
_BufferPoolAllocation/128KB-4 33.60µ 34.96µ ~ 0.700
_BufferPoolAllocation/512KB-4 107.8µ 123.6µ ~ 0.400
_BufferPoolConcurrent/32KB-4 19.08µ 18.88µ ~ 1.000
_BufferPoolConcurrent/64KB-4 29.99µ 30.87µ ~ 0.700
_BufferPoolConcurrent/512KB-4 148.4µ 151.2µ ~ 0.200
_SubtreeDeserializationWithBufferSizes/16KB-4 649.5µ 627.2µ ~ 0.700
_SubtreeDeserializationWithBufferSizes/32KB-4 589.3µ 631.3µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/64KB-4 590.5µ 641.4µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/128KB-4 593.3µ 633.9µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/512KB-4 590.6µ 597.3µ ~ 0.100
_SubtreeDataDeserializationWithBufferSizes/16KB-4 35.40m 37.74m ~ 0.100
_SubtreeDataDeserializationWithBufferSizes/32KB-4 35.25m 37.54m ~ 0.100
_SubtreeDataDeserializationWithBufferSizes/64KB-4 35.57m 37.23m ~ 0.100
_SubtreeDataDeserializationWithBufferSizes/128KB-4 35.36m 37.16m ~ 0.100
_SubtreeDataDeserializationWithBufferSizes/512KB-4 35.11m 36.10m ~ 0.100
_PooledVsNonPooled/Pooled-4 829.9n 828.8n ~ 0.700
_PooledVsNonPooled/NonPooled-4 8.065µ 7.943µ ~ 0.700
_MemoryFootprint/Current_512KB_32concurrent-4 6.847µ 6.808µ ~ 1.000
_MemoryFootprint/Proposed_32KB_32concurrent-4 9.403µ 9.442µ ~ 0.400
_MemoryFootprint/Alternative_64KB_32concurrent-4 9.049µ 9.308µ ~ 0.100
StoreBlock_Sequential/BelowCSVHeight-4 350.4µ 327.8µ ~ 0.700
StoreBlock_Sequential/AboveCSVHeight-4 348.8µ 330.3µ ~ 0.100
GetUtxoHashes-4 258.1n 264.8n ~ 0.400
GetUtxoHashes_ManyOutputs-4 43.84µ 47.28µ ~ 0.400
_NewMetaDataFromBytes-4 277.9n 228.8n ~ 0.100
_Bytes-4 398.3n 399.6n ~ 0.400
_MetaBytes-4 138.4n 137.5n ~ 0.100

Threshold: >10% with p < 0.05 | Generated: 2026-09-11 14:38 UTC

@icellan
icellan merged commit 5904ceb into bsv-blockchain:release/v0.15 Sep 11, 2026
34 checks passed
@oskarszoon

Copy link
Copy Markdown
Contributor

Follow-up on the reviewed-and-approved state here, from reviewing the identical change on main in #1721. Not asking to reopen this — the test mechanics are correct and I confirmed that independently. One thing in the docs paragraph needs fixing on this branch too, and it matters more here because v0.15.9-beta-1 is already tagged.

docs/topics/services/alert.md says a spend signed for the replacement owner is rejected. It omits that the original stored owner is locked out as well: re-extension supplies the original script so the script check passes, then the store's commitment comparison fails against the new hash. Probed on the PR's own test: UTXO_MISMATCH (74): [Spend] utxo hash mismatch. The output is stranded for both parties and there is no inverse operation in the store API.

The risk is an operator reading that paragraph as "the coin doesn't move yet", running a court-ordered confiscation, and permanently burning the output. Recovery — re-freeze and reassign back to the original hash — is undocumented and diverges by backend: SQL's freeze ignores the hash (sql/alert_system.go:96), Aerospike's matches it (aerospike/alert_system.go:74-76).

Two other sentences in the same paragraph:

  • "The reassignment smoke test covers both behaviors" holds for SQL only. The test sets no UTXOStoreType, so it runs on sqlite, and the maturity assertion depends on ReAssignedUtxoSpendableAfterBlocks = 5 — SQL honours it, Aerospike ignores it (aerospike/alert_system.go passes the hardcoded constant from Interface.go). Production Aerospike has no coverage of either behaviour and could not run this test against a 1000-block gate.
  • "Reassignment to the stored owner still enforces the maturity delay" describes a case no caller produces. services/alert/node.go always derives a new P2PKH from the confiscation input's pubkey, so new != old by construction. The surviving success path in the test is a self-reassignment reachable only via the operator RPC. Every production path is broken; only a no-op still works.

There is no tracking issue for the regression — gh issue list --search reassign and gh search issues ... ReAssignUTXO / ... confiscation return nothing. Given this branch ships, the release body wants a ### Notes entry naming that issue, in the same style v0.15.9-beta-1 already uses for #1701. Five other docs still advertise the feature working, and services/alert/node.go still reports these to the alert network as Processed with a comment that is now false.

Full detail on #1721.

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