Skip to content

Enforce nLockTime/sequence transaction finality at submit#181

Open
sirdeggen wants to merge 5 commits into
mainfrom
claude-feat/locktime
Open

Enforce nLockTime/sequence transaction finality at submit#181
sirdeggen wants to merge 5 commits into
mainfrom
claude-feat/locktime

Conversation

@sirdeggen

@sirdeggen sirdeggen commented May 29, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Transaction validation previously had no finality check — spv.Verify covers only fees, merkle paths, and script execution.
  • Adds a finality gate to ValidateTransaction mirroring teranode util.IsTransactionFinal (consensus rule TNJ-13).

Changes

  • validator/finality.go (new): IsFinal + validLockTime mirror teranode util.IsTransactionFinal / util.ValidLockTime. A tx is final when every input sequence is 0xffffffff, nLockTime == 0, or nLockTime is satisfied (< 500000000 → block height; >= → timestamp/MTP). checkFinality sources height-based locks from the chain tip (next-block height) and time-based locks from the wall clock; chain-tip read failures accept rather than block submission.
  • validator/validator.go: call checkFinality in ValidateTransaction between policy and spv.Verify; map ErrTxNotFinalStatusMalformed. Added nil-safe chainTip/now fields with SetChainTip/SetNowFunc.
  • app/app.go: wire the store (GetActiveTipBlockHeight) as the chain-tip source.

Test plan

  • go test ./validator/ — 14 finality tests pass (locktime-zero, all-inputs-final, height/time pass+fail, mixed sequences, threshold edge cases either side of 500000000, nil-tip skip, tip-error accept)
  • go build ./..., go vet ./validator/ ./app/ clean

Closes ARC issue: bitcoin-sv/arc#145

Add a finality check to ValidateTransaction mirroring teranode
util.IsTransactionFinal (consensus rule TNJ-13): a tx is final when all
input sequence numbers are 0xffffffff, nLockTime is 0, or nLockTime is
satisfied against the chain height / median-time-past. Non-final txs are
rejected with StatusMalformed. Height-based locks read the chain tip from
the store (next-block height); time-based locks use the wall clock.
Chain-tip read failures accept rather than block submission.
@sirdeggen sirdeggen requested a review from mrz1836 as a code owner May 29, 2026 01:24
@github-actions github-actions Bot added the size/L Large change (201–500 lines) label May 29, 2026
@sirdeggen sirdeggen requested review from galt-tr and shruggr and removed request for mrz1836 May 29, 2026 03:43
gosec G115 flagged the uint64->uint32 height and int64->uint32 time
conversions in checkFinality; replace with saturating clamp helpers.
Annotate the chain-tip read-failure path with nolint:nilerr — returning
nil there is intentional graceful degradation.

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

This PR adds submit-time transaction finality validation for nLockTime/sequence handling before SPV verification, integrating chain-tip height data from the application store.

Changes:

  • Adds finality helpers and tests for height/time lock behavior.
  • Calls finality validation from ValidateTransaction and maps non-final transactions to malformed status.
  • Wires the store into the validator as the chain-height source during app bootstrap.

Reviewed changes

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

File Description
validator/finality.go Adds finality evaluation logic and chain-tip interface.
validator/finality_test.go Adds coverage for finality edge cases.
validator/validator.go Invokes finality checks during transaction validation.
app/app.go Configures the validator with the store-backed chain-tip source.

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

Comment thread validator/validator.go
Comment thread validator/finality.go
Comment on lines +103 to +109
// Time-based locktime: wall clock is >= median-time-past, so a lock that
// has elapsed against now() has certainly elapsed against MTP.
now := v.now
if now == nil {
now = func() int64 { return time.Now().Unix() }
}
if validLockTime(tx.LockTime, 0, clampI64ToU32(now())) {

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.

@sirdeggen thoughts?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah this definitely needs to be fixed, the MTP can be calculated with header data only I believe that's possible via chaintracks.

galt-tr and others added 3 commits May 31, 2026 20:24
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/L Large change (201–500 lines)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants