Skip to content

fix: document reassignment lockout and repair re-extension CI tests - #1721

Merged
icellan merged 3 commits into
bsv-blockchain:mainfrom
icellan:fix/reextension-ci-tests
Sep 11, 2026
Merged

fix: document reassignment lockout and repair re-extension CI tests#1721
icellan merged 3 commits into
bsv-blockchain:mainfrom
icellan:fix/reextension-ci-tests

Conversation

@icellan

@icellan icellan commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Mandatory transaction re-extension exposed three test assumptions: concurrent propagation submissions shared mutable transactions, the zero-value invalid-script fixture inherited a minimum fee, and reassignment relied on a replacement locking script that was never stored.

The propagation fixture now decodes a transaction for each concurrent submission, and the invalid-script control explicitly disables the minimum fee and rebuilds the verifier. The renamed SQLite reassignment test pairs public-ingress rejection with exact error checks through a real validator over the same store. It pins script failure for the replacement owner and commitment mismatch for the original owner after maturity, checks that rejected spends leave outputs unspent, and retains self-reassignment only as a positive maturity control. It mines one extra block and waits for both UTXOs to mature before testing those rejection reasons.

The alert, RPC, store, miner and glossary docs now warn that a changed-owner reassignment strands the output for both owners. The documentation distinguishes SQLite coverage from Aerospike, which currently ignores the configurable maturity delay, and links the restoration/recovery issue. Misleading API, settings and daemon comments are corrected. Transaction-validation and store behavior are unchanged.

Validation:

  • Renamed SQLite reassignment smoke test, three runs with the race detector and testtxmetacache.
  • Settings package with the race detector; go vet and staticcheck for the affected test/settings packages.
  • Repository Go lint, Markdown lint, whitespace and conflict checks.
  • The original propagation/validator fixture changes passed the full PR CI run before this review update.

Notes

Ownership-changing UTXO reassignment is currently unsafe after mandatory transaction re-extension: both the original and replacement owners are unable to spend the reassigned output, even after maturity. The alert/confiscation and RPC paths can report successful processing despite that lockout. Do not use ownership-changing reassignment; restoration and a validated recovery procedure are tracked in #1725.

This note is for releases containing the re-extension security merge. The existing v0.15.9-beta-1 tag points to 9272b80c9259d00fd51eb8f66ee658a46adb940a, before that merge, so its release notes have not been marked as affected.

Copilot AI lite review requested due to automatic review settings September 11, 2026 14:20
@github-actions

github-actions Bot commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

🤖 Claude Code Review

Status: Complete

Current Review:
No new issues found. This is a documentation-accuracy + test-repair PR and the claims hold up against the code:

  • Verified the reassign RPC docs match the implementation: ReassignCmd takes OldTxID/OldVout/OldUTXOHash/NewUTXOHash positionally and handleReassign returns nil, nil (JSON null) on success — the old docs (txid/vout/destination → true) were wrong and are now corrected.
  • Confirmed the SQL-vs-Aerospike maturity claim: sql/alert_system.go honors tSettings.UtxoStore.ReAssignedUtxoSpendableAfterBlocks, while aerospike/alert_system.go uses the fixed ReAssignedUtxoSpendableAfterBlocks constant (1000).
  • Cross-file alert.md#24-utxo-reassignment anchor and relative link depths are correct from each doc's location.
  • Test changes are sound: the propagation and reassign tests decode a fresh transaction per submission (avoiding the shared-mutable-tx race that re-extension exposed), and the reassign test pins exact rejection reasons (not spendable until, OP_EQUALVERIFY, UTXO_MISMATCH) while asserting rejected spends leave outputs unspent. The assert import remains used elsewhere in the propagation test.

History:

  • ✅ Resolved: sqlite now listed in the test_daemon.go UTXOStoreType comment (prior Copilot note).
  • ✅ Resolved: rpc.md Function Overview and process flow rewritten to the hash-based contract (prior Copilot note).
  • ✅ Resolved: utxo_reference.md updated to match the new ReAssignUTXO contract (prior Copilot note).

Comment thread docs/topics/services/alert.md Outdated
Mandatory transaction re-extension therefore still retrieves the original script
from the UTXO store. A spend signed only for the replacement owner is rejected even
after maturity; supplying the replacement script in extended transaction bytes
cannot authorize it. Supporting a different owner requires an authoritative source

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.

[Major] The documentation and tests here accurately capture a real behavioral regression, not just a pre-existing gap. Before mandatory re-extension, the old reassign_test.go asserted the opposite: after maturity, the new owner (Charles) could spend the reassigned UTXO. That was the point of the alert-service confiscation flow — reassign a frozen UTXO to a new authority who then spends it after the maturity delay.

With re-extension unconditionally reloading the stored (original) locking script, reassignment-to-a-different-owner is now effectively inert: the confiscated output becomes permanently unspendable by the intended new owner. Documenting it as a "current limitation" is the right call for a test-fix PR, and the same-owner path still exercises the maturity gate — good.

Non-blocking, but worth confirming: is there a tracking issue for restoring new-owner reassignment (the authoritative replacement-script source mentioned here)? Otherwise a security/confiscation feature is silently non-functional with only a docs note recording it.

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.

Resolved: the PR now links issue 1725 throughout, which explicitly tracks restoration, recovery, backend parity and operator guidance for ownership-changing reassignment. The previously-silent non-functional confiscation feature is now recorded with a tracking issue. Verified the docs claims against code — SQL honors the configurable maturity delay (sql/alert_system.go:172-176) while Aerospike hardcodes the constant (aerospike/alert_system.go:250), and neither store persists the replacement locking script.

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

The reviewed changes address the affected test assumptions with no unresolved blocking issues.

Pull request overview

Updates tests and documentation for mandatory transaction re-extension and reassignment behavior without changing production code.

Changes:

  • Isolates transactions in concurrent propagation tests.
  • Makes validator fee fixtures explicit.
  • Expands reassignment coverage and documents script limitations.
File summaries
File Description
test/e2e/daemon/ready/reassign_test.go Tests maturity and stored-owner validation paths.
services/validator/utxo_commitment_ambiguity_test.go Makes the unspendable control fee-independent.
services/propagation/Server_test.go Prevents shared transaction mutation during concurrency testing.
docs/topics/services/alert.md Documents reassignment behavior and 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.

@github-actions

github-actions Bot commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Benchmark Comparison Report

Baseline: main (unknown)

Current: PR-1721 (a354df8)

Summary

  • Regressions: 0
  • Improvements: 0
  • Unchanged: 139
  • Significance level: p < 0.05
All benchmark results (sec/op)
Benchmark Baseline Current Change p-value
_NewBlockFromBytes-4 1.810µ 1.967µ ~ 1.000
Block_ValidOrderAndBlessed_DiskVsMemory/leaves=1024/memory-4 12.31m 12.11m ~ 0.100
Block_ValidOrderAndBlessed_DiskVsMemory/leaves=1024/disk_1-4 12.93m 12.86m ~ 0.400
Block_ValidOrderAndBlessed_DiskVsMemory/leaves=1024/disk_2-4 12.92m 12.92m ~ 0.700
Block_ValidOrderAndBlessed_DiskVsMemory/leaves=16384/memo... 29.83m 30.51m ~ 0.400
Block_ValidOrderAndBlessed_DiskVsMemory/leaves=16384/disk... 39.23m 39.76m ~ 0.100
Block_ValidOrderAndBlessed_DiskVsMemory/leaves=16384/disk... 38.62m 38.62m ~ 1.000
SplitSyncedParentMap_SetIfNotExists/256_buckets-4 64.27n 64.06n ~ 0.200
SplitSyncedParentMap_SetIfNotExists/16_buckets-4 63.96n 64.10n ~ 0.500
SplitSyncedParentMap_SetIfNotExists/1_bucket-4 64.03n 64.14n ~ 0.600
SplitSyncedParentMap_ConcurrentSetIfNotExists/256_buckets... 30.43n 31.15n ~ 0.700
SplitSyncedParentMap_ConcurrentSetIfNotExists/16_buckets_... 52.68n 53.31n ~ 0.100
SplitSyncedParentMap_ConcurrentSetIfNotExists/1_bucket_pa... 111.7n 119.0n ~ 0.100
MiningCandidate_Stringify_Short-4 245.4n 243.4n ~ 0.200
MiningCandidate_Stringify_Long-4 1.603µ 1.600µ ~ 0.400
MiningSolution_Stringify-4 785.6n 794.4n ~ 1.000
BlockInfo_MarshalJSON-4 1.586µ 1.589µ ~ 0.700
NewFromBytes-4 95.43n 95.01n ~ 0.200
AddTxBatchColumnar_Validation-4 1.629µ 1.585µ ~ 0.100
OffsetValidationLoop-4 420.8n 421.5n ~ 0.400
Mine_EasyDifficulty-4 67.25µ 68.46µ ~ 0.700
Mine_WithAddress-4 7.085µ 7.264µ ~ 0.700
BlockAssembler_AddTx-4 0.02901n 0.02819n ~ 0.700
AddNode-4 11.01 10.94 ~ 0.700
AddNodeWithMap-4 12.06 11.94 ~ 0.400
DirectSubtreeAdd/4_per_subtree-4 58.11n 59.47n ~ 0.100
DirectSubtreeAdd/64_per_subtree-4 30.40n 30.40n ~ 1.000
DirectSubtreeAdd/256_per_subtree-4 29.03n 29.66n ~ 0.100
DirectSubtreeAdd/1024_per_subtree-4 27.85n 28.22n ~ 0.100
DirectSubtreeAdd/2048_per_subtree-4 27.52n 27.78n ~ 0.400
SubtreeProcessorAdd/4_per_subtree-4 238.9n 240.4n ~ 0.100
SubtreeProcessorAdd/64_per_subtree-4 234.5n 234.5n ~ 0.800
SubtreeProcessorAdd/256_per_subtree-4 234.4n 236.8n ~ 1.000
SubtreeProcessorAdd/1024_per_subtree-4 232.1n 228.1n ~ 0.100
SubtreeProcessorAdd/2048_per_subtree-4 230.5n 228.6n ~ 0.100
SubtreeProcessorRotate/4_per_subtree-4 232.8n 230.4n ~ 0.200
SubtreeProcessorRotate/64_per_subtree-4 231.6n 230.5n ~ 0.700
SubtreeProcessorRotate/256_per_subtree-4 230.8n 230.7n ~ 1.000
SubtreeProcessorRotate/1024_per_subtree-4 230.0n 231.7n ~ 0.400
SubtreeNodeAddOnly/4_per_subtree-4 54.48n 55.49n ~ 0.100
SubtreeNodeAddOnly/64_per_subtree-4 34.15n 34.28n ~ 0.600
SubtreeNodeAddOnly/256_per_subtree-4 33.29n 33.45n ~ 0.200
SubtreeNodeAddOnly/1024_per_subtree-4 32.64n 32.80n ~ 0.100
SubtreeCreationOnly/4_per_subtree-4 115.7n 140.1n ~ 0.100
SubtreeCreationOnly/64_per_subtree-4 407.5n 398.5n ~ 0.700
SubtreeCreationOnly/256_per_subtree-4 1.297µ 1.280µ ~ 0.100
SubtreeCreationOnly/1024_per_subtree-4 4.328µ 4.753µ ~ 0.100
SubtreeCreationOnly/2048_per_subtree-4 7.659µ 8.234µ ~ 0.100
SubtreeProcessorOverheadBreakdown/64_per_subtree-4 231.1n 232.5n ~ 0.200
SubtreeProcessorOverheadBreakdown/1024_per_subtree-4 231.0n 234.3n ~ 0.100
ParallelGetAndSetIfNotExists/1k_nodes-4 10.23m 12.64m ~ 0.100
ParallelGetAndSetIfNotExists/10k_nodes-4 13.60m 16.54m ~ 0.100
ParallelGetAndSetIfNotExists/50k_nodes-4 16.85m 20.49m ~ 0.100
ParallelGetAndSetIfNotExists/100k_nodes-4 19.41m 18.22m ~ 0.200
SequentialGetAndSetIfNotExists/1k_nodes-4 10.08m 13.27m ~ 0.100
SequentialGetAndSetIfNotExists/10k_nodes-4 13.99m 17.84m ~ 0.100
SequentialGetAndSetIfNotExists/50k_nodes-4 25.74m 22.97m ~ 0.400
SequentialGetAndSetIfNotExists/100k_nodes-4 30.23m 33.05m ~ 0.700
ProcessOwnBlockSubtreeNodesParallel/1k_nodes-4 12.39m 12.10m ~ 0.700
ProcessOwnBlockSubtreeNodesParallel/10k_nodes-4 14.40m 14.63m ~ 0.700
ProcessOwnBlockSubtreeNodesParallel/100k_nodes-4 19.00m 19.33m ~ 0.100
ProcessOwnBlockSubtreeNodesSequential/1k_nodes-4 14.57m 13.36m ~ 0.700
ProcessOwnBlockSubtreeNodesSequential/10k_nodes-4 15.82m 16.32m ~ 1.000
ProcessOwnBlockSubtreeNodesSequential/100k_nodes-4 54.52m 57.14m ~ 0.100
DiskTxMap_SetIfNotExists-4 4.809µ 4.772µ ~ 1.000
DiskTxMap_SetIfNotExists_Parallel-4 4.181µ 4.504µ ~ 0.100
DiskTxMap_ExistenceOnly-4 302.0n 306.4n ~ 0.700
Queue-4 230.1n 230.4n ~ 0.800
AtomicPointer-4 8.805n 8.799n ~ 1.000
TxMapSetIfNotExists-4 53.98n 53.88n ~ 0.700
TxMapSetIfNotExistsDuplicate-4 54.60n 54.40n ~ 0.100
ChannelSendReceive-4 706.8n 706.4n ~ 1.000
CalcBlockWork-4 473.5n 472.1n ~ 1.000
CalculateWork-4 628.5n 641.6n ~ 0.700
CheckOldBlockIDs/on-chain-prefetch/1000-4 43.29µ 48.67µ ~ 0.700
CheckOldBlockIDs/in-memory-chain-check/1000-4 1.092m 1.079m ~ 0.100
CheckOldBlockIDs/on-chain-prefetch/10000-4 316.4µ 309.4µ ~ 0.100
CheckOldBlockIDs/in-memory-chain-check/10000-4 1.674m 1.634m ~ 0.100
BuildBlockLocatorString_Helpers/Size_10-4 1.182µ 1.150µ ~ 0.100
BuildBlockLocatorString_Helpers/Size_100-4 11.12µ 10.88µ ~ 0.100
BuildBlockLocatorString_Helpers/Size_1000-4 108.4µ 107.7µ ~ 0.400
CatchupWithHeaderCache-4 105.7m 105.6m ~ 0.200
_BufferPoolAllocation/16KB-4 3.957µ 4.102µ ~ 0.400
_BufferPoolAllocation/32KB-4 8.879µ 11.077µ ~ 0.400
_BufferPoolAllocation/64KB-4 17.20µ 17.89µ ~ 0.700
_BufferPoolAllocation/128KB-4 34.33µ 35.25µ ~ 0.200
_BufferPoolAllocation/512KB-4 123.2µ 120.1µ ~ 0.200
_BufferPoolConcurrent/32KB-4 19.64µ 19.25µ ~ 0.200
_BufferPoolConcurrent/64KB-4 31.36µ 29.97µ ~ 0.600
_BufferPoolConcurrent/512KB-4 155.0µ 155.8µ ~ 1.000
_SubtreeDeserializationWithBufferSizes/16KB-4 696.9µ 667.7µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/32KB-4 638.5µ 612.3µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/64KB-4 621.5µ 683.7µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/128KB-4 634.0µ 716.1µ ~ 0.100
_SubtreeDeserializationWithBufferSizes/512KB-4 640.5µ 679.4µ ~ 0.200
_SubtreeDataDeserializationWithBufferSizes/16KB-4 36.29m 36.83m ~ 0.100
_SubtreeDataDeserializationWithBufferSizes/32KB-4 36.75m 36.51m ~ 0.700
_SubtreeDataDeserializationWithBufferSizes/64KB-4 36.38m 36.79m ~ 0.100
_SubtreeDataDeserializationWithBufferSizes/128KB-4 36.00m 36.68m ~ 0.100
_SubtreeDataDeserializationWithBufferSizes/512KB-4 36.34m 37.08m ~ 0.200
_PooledVsNonPooled/Pooled-4 742.1n 739.4n ~ 0.100
_PooledVsNonPooled/NonPooled-4 8.308µ 8.109µ ~ 1.000
_MemoryFootprint/Current_512KB_32concurrent-4 6.702µ 6.935µ ~ 0.100
_MemoryFootprint/Proposed_32KB_32concurrent-4 9.707µ 10.244µ ~ 0.100
_MemoryFootprint/Alternative_64KB_32concurrent-4 9.196µ 10.049µ ~ 0.100
_prepareTxsPerLevel-4 323.3m 323.3m ~ 1.000
_prepareTxsPerLevelOrdered-4 3.613m 4.254m ~ 0.400
_prepareTxsPerLevel_Comparison/Original-4 317.5m 328.2m ~ 0.200
_prepareTxsPerLevel_Comparison/Optimized-4 3.230m 4.521m ~ 0.100
SubtreeSizes/10k_tx_4_per_subtree-4 1.368m 1.316m ~ 0.700
SubtreeSizes/10k_tx_16_per_subtree-4 307.8µ 312.0µ ~ 1.000
SubtreeSizes/10k_tx_64_per_subtree-4 74.07µ 73.60µ ~ 1.000
SubtreeSizes/10k_tx_256_per_subtree-4 18.41µ 18.20µ ~ 0.100
SubtreeSizes/10k_tx_512_per_subtree-4 9.105µ 9.017µ ~ 0.200
SubtreeSizes/10k_tx_1024_per_subtree-4 4.501µ 4.479µ ~ 0.700
SubtreeSizes/10k_tx_2k_per_subtree-4 2.241µ 2.215µ ~ 0.700
BlockSizeScaling/10k_tx_64_per_subtree-4 70.87µ 73.76µ ~ 0.700
BlockSizeScaling/10k_tx_256_per_subtree-4 17.88µ 17.82µ ~ 1.000
BlockSizeScaling/10k_tx_1024_per_subtree-4 4.422µ 4.466µ ~ 0.700
BlockSizeScaling/50k_tx_64_per_subtree-4 376.4µ 390.2µ ~ 0.700
BlockSizeScaling/50k_tx_256_per_subtree-4 89.16µ 91.18µ ~ 0.100
BlockSizeScaling/50k_tx_1024_per_subtree-4 22.22µ 21.76µ ~ 0.200
SubtreeAllocations/small_subtrees_exists_check-4 152.0µ 152.2µ ~ 1.000
SubtreeAllocations/small_subtrees_data_fetch-4 160.6µ 160.6µ ~ 1.000
SubtreeAllocations/small_subtrees_full_validation-4 310.7µ 318.7µ ~ 0.200
SubtreeAllocations/medium_subtrees_exists_check-4 9.070µ 8.932µ ~ 0.400
SubtreeAllocations/medium_subtrees_data_fetch-4 9.795µ 9.434µ ~ 0.200
SubtreeAllocations/medium_subtrees_full_validation-4 17.95µ 17.60µ ~ 0.200
SubtreeAllocations/large_subtrees_exists_check-4 2.160µ 2.161µ ~ 1.000
SubtreeAllocations/large_subtrees_data_fetch-4 2.284µ 2.282µ ~ 1.000
SubtreeAllocations/large_subtrees_full_validation-4 4.458µ 4.446µ ~ 0.500
StoreBlock_Sequential/BelowCSVHeight-4 334.6µ 331.8µ ~ 0.400
StoreBlock_Sequential/AboveCSVHeight-4 337.0µ 332.9µ ~ 0.100
GetUtxoHashes-4 262.9n 259.9n ~ 0.700
GetUtxoHashes_ManyOutputs-4 42.66µ 43.04µ ~ 0.400
MetaBytes-4 88.68n 87.54n ~ 0.100
_NewMetaDataFromBytes-4 278.3n 276.0n ~ 0.700
_Bytes-4 395.5n 390.2n ~ 0.700
_MetaBytes-4 137.3n 136.5n ~ 0.300

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

@icellan
icellan requested a review from oskarszoon September 11, 2026 14:47

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

The test changes are sound and I verified each rather than reading them. The docs paragraph is the problem.

Verified honest, not papering over:

  • utxo_commitment_ambiguity_test.go fixes a genuinely red test. Reverting just the two added lines under SETTINGS_CONTEXT=test: "... TX_POLICY (39): transaction fee is too low -> insufficient-fee" does not contain "Opcode missing or not understood". settings.conf:923-927 sets minminingtxfee = 0.00000001 with .dev/.docker/.teratestnet overridden to 0 but no .test, and make test runs SETTINGS_CONTEXT=test. Only the realSats: 0 fixture needs it. The NewTxValidator rebuild is necessary, not cosmetic — ScriptVerifierGoBDK.go:218-220 snapshots policy into a native object, so mutating settings alone is a silent no-op.
  • Server_test.go fixes a real race, and removes it rather than narrowing it. Base test against current production code under -race on Postgres: 17 WARNING: DATA RACE, writes at Validator.go:1657/:1658. PR head: clean. Production decodes its own tx per submission (Server.go:1058, :693), so this is a test-only defect.
  • ReAssignUTXO(old == new) is supported by both stores, not undefined behaviour — SQL's WHERE ... frozen = true still matches and SET utxo_hash is a no-op write; the Aerospike UDF looks up by old hash and writes the new one as separate arguments.

P1 — docs/topics/services/alert.md:139-147 omits that the original owner is locked out too.

The paragraph says a spend signed for the replacement owner is rejected. It does not say the original stored owner is also locked out: re-extension supplies the original script so the script check passes, then the commitment comparison fails against the new hash. Probed: UTXO_MISMATCH (74): [Spend] utxo hash mismatch. The output is stranded for everyone, and there is no inverse operation in the store API.

An operator reading this as "the coin doesn't move yet" runs a court-ordered confiscation and permanently burns the output. That is the sentence to fix before this lands. Recovery — re-freeze and reassign back to the original hash — is undocumented and backend-divergent: SQL's freeze ignores the hash (sql/alert_system.go:96), Aerospike's matches it (aerospike/alert_system.go:74-76).

P1 — two more sentences in the same paragraph are wrong.

  • "The reassignment smoke test covers both behaviors" is true for SQL only. The test sets no UTXOStoreType, so it runs on sqlite, and its maturity assertion depends on ReAssignedUtxoSpendableAfterBlocks = 5 — which SQL honours and Aerospike ignores (aerospike/alert_system.go:250 passes the hardcoded constant from Interface.go:48). Production Aerospike has no coverage of either behaviour and could not run this test anyway, 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:459-492 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 (handlers.go:2814). Every production path is broken; only a no-op still works.

P1 — no tracking issue, and the regression is release-visible.

gh issue list --search reassign, gh search issues ... ReAssignUTXO and ... confiscation all return nothing. This is the confiscation path and v0.15.9-beta-1 is tagged. Five other docs still advertise the feature working — rpc_reference.md:1536-1548, minersHowToInteractWithRPCServer.md:196-204, stores/utxo.md:394-399, alert_reference.md:327, glossary.md:3 — plus settings/utxostore_settings.go:54 and stores/utxo/Interface.go:527-528. And services/alert/node.go:455-492 still reports these to the alert network as Processed, with a comment at :478 that is now false.

The repo already has the pattern for this: v0.15.9-beta-1's release body carries a ### Notes section plus "See #1701 for the remaining non-atomic path". Release notes are generated from PR titles, and the release skill's triage puts test-only PRs under Hold — so a test: title makes this invisible. Suggest opening the issue, adding a ### Notes entry naming it, and retitling to mention the documented limitation.

P2, worth doing while the file is open:

  • TestShouldAllowReassign now asserts rejection before maturity, after maturity, and that the output stays unspent. The name says the opposite of what it tests. TestReassignEnforcesMaturityAndRejectsUnstoredScript or similar. (It is a real pin, to be fair — two assertions would fail if the feature started working again.)
  • All three negative assertions are bare require.Error with no reason pinned. :161's message claims the replacement script was rejected, but newSpend is still immature at that point — maturity is only confirmed at :170-172 — so the rejection is attributable to the gate, not the script, and the assertion survives a script-checking regression. The require.Contains discipline used at utxo_commitment_ambiguity_test.go:167,193 is what made the fee bug above visible; it is absent from the test that most needs it.
  • :165, :172 and :180 sit exactly on the maturity boundary — the gate is strict < and MineAndWait(5) lands on precisely spendableIn. One assertion depended on that before; three do now. A one-block shift flips :172 to IMMATURE and silently changes :173's rejection reason while still passing. Mine +1.
  • :170-172 reads UTXO-store status synchronously with no retry, on that same boundary, while MineAndWait waits on the blockchain store and the UTXO-store height is published asynchronously. New flake surface — and since make test excludes ./test/, this runs only in the retrying smoketest suite, so a flake gets retried green rather than reported.
  • Missing residue check after Bob's early rejection, where the Charles path has one at :175-177.

Two things outside the diff, both confirmed on main: Aerospike ignores utxostore_reassignedUtxoSpendableAfterBlocks despite the setting being documented "DO NOT reduce in production", and daemon/test_daemon.go:114 still claims an empty UTXOStoreType defaults to aerospike when :454 only builds a container when non-empty — which is plausibly what made the "covers both behaviors" sentence seem true.

@icellan
icellan force-pushed the fix/reextension-ci-tests branch from ac2833f to 117ce1f Compare September 11, 2026 16:03
Copilot AI review requested due to automatic review settings September 11, 2026 16:03
@icellan icellan changed the title test: fix CI fixtures after mandatory transaction re-extension fix: document reassignment lockout and repair re-extension CI tests Sep 11, 2026
@icellan

icellan commented Sep 11, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the requested changes in 117ce1f.

  • The warning now states that ownership-changing reassignment strands the output for both owners, explains the script failure versus commitment mismatch, and warns that successful RPC/alert processing does not establish spendability. The RPC, miner, store, glossary and API/settings descriptions now point out the limitation too.
  • Opened Reassignment after mandatory re-extension strands UTXOs for both owners #1725 for authoritative replacement-script support, backend parity, recovery and operator guidance. The docs explicitly avoid claiming a validated portable recovery procedure.
  • Renamed the smoke test to TestReassignSQLiteEnforcesMaturityAndRejectsUnstoredScript, pinned its SQLite backend, and described self-reassignment solely as a maturity control. Corrected the daemon's default-backend comment and documented Aerospike's fixed delay.
  • Every negative scenario now pairs public-ingress rejection with an exact cause from a real in-process validator over the same store. This is necessary because the public boundary redacts commitment mismatch on main and also redacts the maturity/script causes on release/v0.15. The probe clones each submission so re-extension cannot change later attempts. The test checks the original owner's UTXO_MISMATCH, both Charles attempts' OP_EQUALVERIFY, Bob's early maturity rejection, and the unspent state after rejected spends.
  • Mines one extra block and waits until both UTXOs report mature before checking the post-maturity failures.
  • Retitled this PR and added a Notes section for the next affected release. v0.15.9-beta-1 points to 9272b80c9259d00fd51eb8f66ee658a46adb940a, which predates the release-branch security merge c7682127778565279fe51369194a88e4925dedd6; I have not incorrectly marked that existing tag as affected.

Validation: three race-enabled runs of the renamed smoke test on each branch, race-enabled settings tests, vet/staticcheck, Go/Markdown lint and whitespace/conflict checks. The release follow-up is #1726; its assertions and documentation match main, with one constructor argument adapted for the older validator API and one pre-existing trailing space removed by the required hook. Transaction-validation and store behavior are unchanged.

@icellan
icellan requested a review from oskarszoon September 11, 2026 16:04

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.

🟡 Changes recommended

A moderate test issue and several documentation corrections remain unresolved.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (5)

docs/howto/miners/minersHowToInteractWithRPCServer.md:197

  • The updated command entry still instructs callers to provide a destination address, but the registered ReassignCmd accepts four positional values: old txid, vout, old UTXO hash, and new UTXO hash (services/rpc/bsvjson/chainsvrcmds.go:715-720). The handler also returns nil, not a boolean (services/rpc/handlers.go:2799-2820). Please correct this how-to and its parameter/result details so the new warning is actionable.
6. `reassign`: Updates a frozen UTXO's commitment for a destination address
    - **Known regression:** do not reassign to a different owner. A successful result can leave the output unspendable by both owners, even after maturity. See the [reassignment limitation](../../topics/services/alert.md#24-utxo-reassignment) and [issue 1725](https://github.com/bsv-blockchain/teranode/issues/1725).

docs/references/services/rpc_reference.md:1543

  • This new section repeats the wrong RPC contract: ReassignCmd requires OldTxID, OldVout, OldUTXOHash, and NewUTXOHash, while handleReassign returns nil on success (services/rpc/bsvjson/chainsvrcmds.go:715-720, services/rpc/handlers.go:2799-2820). The documented destination address, three-argument example, and boolean result will cause callers to send invalid requests; please update them to the actual hash-based API.
**Known regression:** do not reassign to a different owner. The RPC can return
`true` while leaving the output unspendable by both owners, even after maturity.
See the [reassignment limitation](../../topics/services/alert.md#24-utxo-reassignment)
and [issue 1725](https://github.com/bsv-blockchain/teranode/issues/1725).

docs/topics/services/alert.md:147

  • The Alert Service feature summary at lines 33-35 still says it can reassign UTXOs to another address, while this section now says ownership-changing reassignment must not be used. Update that summary too so the page's top-level feature list does not contradict the operator warning.
**Known regression — do not reassign to a different owner:** mandatory transaction
re-extension makes an output with a changed commitment unspendable by both owners,
even after maturity. `ReAssignUTXO` updates the commitment, freeze state and maturity
height but does not persist the replacement locking script. The new owner's spend
fails against the original stored script. The original owner's signature passes
that script, but its spend fails with `UTXO_MISMATCH` against the changed commitment.
Waiting for more blocks does not restore spendability.

docs/topics/stores/utxo.md:395

  • The store overview above still advertises ReAssignUTXO as reassigning a UTXO to a different owner (line 49). That contradicts this new warning and leaves the unsupported ownership-changing path presented as a normal feature; update the overview bullet to describe the commitment-only behavior and restriction.
3. **Reassigning UTXOs**: Updates the commitment of a frozen output.
    - **Known regression:** changing the owner leaves the output unspendable by both owners after mandatory re-extension, even after maturity. Do not use ownership-changing reassignment. See the [reassignment limitation](../services/alert.md#24-utxo-reassignment) and [issue 1725](https://github.com/bsv-blockchain/teranode/issues/1725).

test/e2e/daemon/ready/reassign_test.go:60

  • The bytes are captured after ProcessTransaction has already run. The validator unconditionally re-extends the same *bt.Tx in place (services/validator/Validator.go:959-970, 1639-1643), so line 60 serializes the stored Bob script rather than the submitted Charles script; the later retry also reuses that mutated object. This lets the test pass without exercising rejection of a replacement script supplied in the original extended bytes. Save tx.ExtendedBytes() before ingress and run ingress, probe, and retry from fresh decodes.
		require.Error(t, td.PropagationClient.ProcessTransaction(td.Ctx, tx))
		// Validation re-extends in place. Preserve the submitted extended
		// fields so a later attempt still supplies the same replacement script.
		probe, err := bt.NewTxFromBytes(tx.ExtendedBytes())
  • Files reviewed: 14/14 changed files
  • Comments generated: 3
  • Review effort level: Lite

Comment thread daemon/test_daemon.go Outdated
Comment thread docs/topics/services/rpc.md Outdated
Comment thread stores/utxo/Interface.go
Copilot AI review requested due to automatic review settings September 11, 2026 16:17
@icellan

icellan commented Sep 11, 2026

Copy link
Copy Markdown
Contributor Author

Addressed Copilot's three inline comments and the additional findings in its review summary in a21b8ae; the same updates are on #1726.

  • Added SQLite to the supported test-backend values.
  • Corrected the RPC overview, four positional hash-based arguments, null result, example, process flow and miner how-to. Removed the obsolete address-based diagram from the command section.
  • Synchronized the public UTXO API reference and corrected the alert/store feature summaries so they no longer advertise working ownership changes.
  • Snapshot extended bytes before ingress and decode separate transactions for ingress and the in-process probe. The original fixture remains untouched for retries. The client already serializes requests rather than passing its pointer to the server, so the reported mutation was not occurring across ingress; the new structure makes this ownership explicit anyway.

Validation: the reassignment smoke test passed three runs with the race detector on each branch; vet, staticcheck, Go/Markdown lint, whitespace/conflict checks and JSON example validation passed. Transaction-validation and store behavior remain unchanged.

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.

🔵 Needs a closer look

Four documentation comments leave contradictory reassignment behavior or API guidance unresolved.

Review details

Suppressed comments (4)

docs/references/services/alert_reference.md:325

  • This update still leaves the identifier list below saying reassignment takes a New Address (line 333), but the corrected API accepts a replacement UTXO hash and no destination address. Please update that stale bullet as well; otherwise this reference contradicts the API contract documented here.
**Known regression:** an applied ownership-changing reassignment can leave the
output unspendable by both owners, even after maturity. Do not treat processing
status as proof of spendability. See the
[reassignment limitation](../../topics/services/alert.md#24-utxo-reassignment) and
[issue 1725](https://github.com/bsv-blockchain/teranode/issues/1725).

docs/topics/services/alert.md:153

  • The new warning is followed by an unchanged flow that still describes ownership-changing reassignment as a normal successful procedure: it creates a new locking script, updates the database for the new assignment, and adds successful calls to processed without explaining that the script is not stored and the output remains unspendable. Please update/remove the diagram and steps or label them explicitly as the current unsafe processing path; otherwise operators can follow a procedure this page says not to use.
This affects ownership-changing alert/confiscation and RPC reassignment. A successful
RPC result or alert processing response does not guarantee that the output can be
spent. There is no documented, validated recovery procedure across backends: SQL
freezing ignores the supplied commitment, whereas Aerospike checks it, so operators
must not assume that re-freezing and restoring the original hash is portable.

docs/topics/services/rpc.md:76

  • Restricted is not an implemented access restriction: reassign remains in rpcLimited (services/rpc/Server.go:287-340), and the authentication reference still lists it as available to limited users (docs/references/services/rpc_reference.md:418-420). This status therefore conflicts with the actual RPC contract; keep it supported and describe the unsafe ownership-changing behavior in the description, or change the authorization and the auth documentation together.
| reassign                  | Restricted | Updates a frozen UTXO commitment; ownership changes currently strand the output |

stores/utxo/Interface.go:531

  • The interface comment is now accurate, but the exported backend implementations still have contradictory godoc: stores/utxo/sql/alert_system.go:147-150 says reassignment assigns a new transaction output and becomes spendable after the delay, and the Aerospike method still describes a new transaction output. Update those implementation comments too, otherwise readers of the concrete store APIs can still believe ownership-changing reassignment is spendable after maturity.
	// ReAssignUTXO updates a frozen UTXO's commitment and maturity gate.
	// It does not persist a replacement locking script. Changing the owner
	// currently strands the output for both owners even after maturity; see
	// https://github.com/bsv-blockchain/teranode/issues/1725.
	// SQL honors the configured delay; Aerospike currently uses the fixed constant.
  • Files reviewed: 15/15 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

@sonarqubecloud

Copy link
Copy Markdown

@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 a21b8ae39 (base d5fc44a65 = current main, 15 files +223/−79). Every item from the last round is fixed, and two went past the ask — the title change and the ### Notes block. Nothing here blocks merge.

Verified rather than taken on trust:

  • The original-owner lockout is now stated and pinned. Corrupting the expected cause under -overlay fails with the real error — UTXO_ERROR (79): error in sql spend (batched mode) -> UTXO_MISMATCH (74): [Spend] utxo hash mismatch — so requireRejected(originalOwnerSpendingTx, "UTXO_MISMATCH") is load-bearing, not decorative.
  • Every claim in the new alert.md paragraph was checked against the store code, the Lua UDF and the error allowlist. All of them hold, including the two that carry the most weight: SQL freezing ignoring the supplied commitment where Aerospike checks it (sql/alert_system.go:59-64,96 vs teranode.lua:733), and Aerospike ignoring utxostore_reassignedUtxoSpendableAfterBlocks (aerospike/alert_system.go:250). Nothing is overstated in the other direction.
  • The four nils to validator.New are the three Kafka producers plus the block-assembly client. All nil-guarded, and the block-assembly one is unreachable because AddTXToBlockAssembly:false short-circuits it at :1233 — which also suppresses WithLocked(true), so the probe can't leave a 2PC-locked record. No goroutine starts on this configuration, Prometheus is behind a sync.Once, -race is clean (0 races, ok 8.902s). The struct-tag edits have exactly one consumer (settings/export.go:139-142, raw Tag.Get) and no golden file.
  • MineAndWait(+1) is margin in the right direction — +0 also passes, so it isn't masking an off-by-one.

Three one-line test fixes I'd still take, in priority order:

  • reassign_test.go:65 — the probe passes td.UtxoStore.GetBlockHeight() (tip) while ingress passes 0, which resolves to blockState.Height + 1 (propagation/Server.go:1429Validator.go:888-889). That height feeds the maturity gate at sql/sql.go:2763, so the probe is systematically one block stricter than the path it exists to explain. Set testReassignedUtxoSpendableAfter = 1 and ingress accepts and permanently spends the output at a height where the probe still says "not spendable until": require.Error then fails with a misdirecting message and every later assertion runs against a UTXO set the test already destroyed. Passing 0 gives an identical PASS.
  • reassign_test.go:62 — the helper proves the cause on the probe but leaves ingress as a bare require.Error. Propagation has two non-verdict failure arms (Server.go:1158-1165/:1203-1210 "server at capacity", Server.go:1399-1401 blob-store StorageError) that satisfy it while the probe independently returns the expected cause. Measured at the client, 3 of the 4 call sites already get the exact cause there — only UTXO_MISMATCH is redacted, which is ERR_UTXO_MISMATCH being absent from publicCauseCodes (errors/errors.go:773-812) while ERR_UTXO_FROZEN and ERR_TX_INVALID are present. So the redaction comment is right, but the probe is needed for one assertion, not four. Capture the ingress error and assert on it for the other three.
  • reassign_test.go:193require.Eventually discards err, status and which spend failed, and GetSpend reports a commitment mismatch as an error rather than a status (sql/sql.go:3545-3547). A future ReAssignUTXO regression would surface as a 30-second "both outputs must mature in the UTXO store" with the cause never printed. require.EventuallyWithT.

Doc surfaces still advertising the feature — my earlier list was short, so these are new asks, not repeats:

  • docs/topics/services/rpc.md:76 — table still reads reassign | Supported | Reassigns ownership … to a new Bitcoin address.
  • docs/topics/services/rpc.md:1053-1104 — a full ownership-transfer how-to, unchanged, directly under the new warning.
  • docs/topics/services/img/plantuml/rpc/rpc-reassign.puml:92-96, rendered at rpc.md:1068Effects: - UTXO ownership changed to new address - Original script replaced with new script. The diagram asserts exactly what the warning retracts. (alert_reassign_utxo.svg is fine — it's a control-flow diagram and makes no spendability claim.)
  • Lower priority: alert.md:33-35 feature list; sql/alert_system.go:147-149 and aerospike/alert_system.go:215-223, where only the interface godoc was updated; and stores/utxo/tests/tests.go:276,316-318, a conformance test still asserting different-owner reassignment "should succeed".

Separately, and more important than anything left in this PR: #1722 merged at 14:44:59Z with the old wording. release/v0.15 currently carries the paragraph that omits the original-owner lockout and still says "the reassignment smoke test covers both behaviors" and "reassignment to the stored owner still enforces the maturity delay". Verified against the branch. That's the version that ships, and it needs its own backport of the corrected paragraph, the Interface.go and settings comment fixes, and the strengthened test. The comment I left on #1722 landed after the merge, so nothing is gated on it.

One nit and one correction. Nit: alert.md:141-143 is slightly imprecise on cause — re-extension produces the new owner's lockout; the original owner's UTXO_MISMATCH predates it and is the intended effect of confiscation. Correction to my own last review: I flagged alert_reassign_utxo.svg as suspect and it isn't — rpc-reassign.puml is the wrong diagram.

@icellan
icellan merged commit c9afd9e into bsv-blockchain:main Sep 11, 2026
38 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