Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion baselib/actor/restart.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,9 @@ func PrependRestartMessage(
MessageType: msg.MessageType(),
Payload: payload,
Priority: RestartPriority,
AvailableAt: time.Now(),
// Use epoch so restart delivery never depends on wall-clock skew
// versus a test/fake delivery-store clock.
AvailableAt: time.Unix(0, 0),
MaxAttempts: 1, // Restart message should only be delivered once.
})
}
Expand Down
174 changes: 84 additions & 90 deletions lib/tx/oor/canonical.go → lib/tx/arktx/canonical.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package oor
package arktx

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The existing lib/tx/oor package keeps its public API stable via thin
wrappers and aliases, so server/client call sites (and tests) continue
to work while we migrate incrementally.

just checking (maybe answered later): but think we dont have to keep things backwards compatible rn and can just do big refactors (i know the agents like to do this bw compat thing)

@bhandras bhandras Feb 6, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Updated: transitional OOR canonical wrappers were removed and call sites now use lib/tx/arktx directly, so there is no extra compatibility layer left here.


import (
"bytes"
Expand All @@ -10,6 +10,12 @@ import (
"github.com/lightninglabs/darepo-client/lib/scripts"
)

const (
// TxVersion is the canonical transaction version used for v0 Ark
// transfers. We use v3 to support package relay.
TxVersion = 3
)

// IsAnchorOutput returns true if the output is the v0 Ark anchor output (P2A,
// value 0).
func IsAnchorOutput(out *wire.TxOut) bool {
Expand All @@ -24,21 +30,14 @@ func IsAnchorOutput(out *wire.TxOut) bool {
return bytes.Equal(out.PkScript, scripts.AnchorPkScript)
}

// ValidateCanonicalArkTx validates the canonical ordering rules for an Ark tx
// (as a raw transaction).
//
// The v0 rule set is based on BIP-0069-style sorting:
// - inputs are ordered by previous outpoint (txid, then vout); and
// - non-anchor outputs are ordered lexicographically by raw pkScript bytes,
// with output value used only as a tie-breaker.
//
// Ark txs additionally include exactly one anchor output (P2A, value 0), and
// the anchor output must be the final output.
// ValidateCanonicalTx validates canonical ordering rules for an Ark tx
// (as a raw transaction), including that it contains exactly one anchor output
// and that the anchor output is the last output.
//
// This is a structural validator only. It does not validate signatures, script
// satisfaction, or VTXO ownership. Those checks belong in higher-level
// validators that have access to policy and VTXO set state.
func ValidateCanonicalArkTx(tx *wire.MsgTx) error {
func ValidateCanonicalTx(tx *wire.MsgTx) error {
if tx == nil {
return fmt.Errorf("ark tx must be provided")
}
Expand All @@ -65,97 +64,31 @@ func ValidateCanonicalArkTx(tx *wire.MsgTx) error {
"output")
}

err := validateCanonicalArkOutputs(tx)
err := validateCanonicalOutputs(tx)
if err != nil {
return err
}

return validateCanonicalArkInputs(tx)
return validateCanonicalInputs(tx)
}

// ValidateCanonicalArkPSBT validates canonical ordering for an Ark tx PSBT.
func ValidateCanonicalArkPSBT(pkt *psbt.Packet) error {
// ValidateCanonicalPSBT validates canonical ordering for an Ark tx PSBT.
func ValidateCanonicalPSBT(pkt *psbt.Packet) error {
if pkt == nil || pkt.UnsignedTx == nil {
return fmt.Errorf("ark psbt must include unsigned tx")
}

return ValidateCanonicalArkTx(pkt.UnsignedTx)
return ValidateCanonicalTx(pkt.UnsignedTx)
}

// validateCanonicalArkInputs validates BIP-0069-style ordering of Ark tx
// inputs.
func validateCanonicalArkInputs(tx *wire.MsgTx) error {
for i := 1; i < len(tx.TxIn); i++ {
prev := tx.TxIn[i-1].PreviousOutPoint
cur := tx.TxIn[i].PreviousOutPoint

cmp := bytes.Compare(prev.Hash[:], cur.Hash[:])
switch {
case cmp < 0:
continue

case cmp > 0:
return fmt.Errorf("ark tx inputs are not canonical " +
"(outpoint hash order)")
}

if prev.Index > cur.Index {
return fmt.Errorf("ark tx inputs are not canonical " +
"(outpoint index order)")
}
}

return nil
}

// validateCanonicalArkOutputs validates recipient output ordering rules and
// anchor placement for v0 OOR transfers.
//
// Recipient outputs exclude the anchor output. Their ordering is compatible
// with BIP-0069 output ordering, using raw pkScript bytes as the primary sort
// key.
func validateCanonicalArkOutputs(tx *wire.MsgTx) error {
if len(tx.TxOut) == 0 {
return fmt.Errorf("ark tx has no outputs")
}

recipientOuts := tx.TxOut[:len(tx.TxOut)-1]

for i := 1; i < len(recipientOuts); i++ {
prev := recipientOuts[i-1]
cur := recipientOuts[i]

if IsAnchorOutput(prev) || IsAnchorOutput(cur) {
return fmt.Errorf("anchor output must be last")
}

cmp := bytes.Compare(prev.PkScript, cur.PkScript)
switch {
case cmp < 0:
continue

case cmp > 0:
return fmt.Errorf("ark tx outputs are not canonical " +
"(pkScript order)")
}

if prev.Value > cur.Value {
return fmt.Errorf("ark tx outputs are not canonical " +
"(value tie-break)")
}
}

return nil
}

// CanonicalizeArkTxOrdering sorts the transaction inputs and outputs in-place
// CanonicalizeOrdering sorts the transaction inputs and outputs in-place
// according to the v0 rules.
//
// CanonicalizeArkTxOrdering does not insert or remove outputs. It assumes the
// CanonicalizeOrdering does not insert or remove outputs. It assumes the
// caller has already constructed an Ark tx that includes exactly one anchor
// output of value 0. If the anchor output is missing or invalid, this returns
// an error rather than guessing what to do.
func CanonicalizeArkTxOrdering(tx *wire.MsgTx) error {
func CanonicalizeOrdering(tx *wire.MsgTx) error {
if tx == nil {
return fmt.Errorf("ark tx must be provided")
}
Expand Down Expand Up @@ -194,12 +127,13 @@ func CanonicalizeArkTxOrdering(tx *wire.MsgTx) error {
a := recipientOuts[i]
b := recipientOuts[j]

cmp := bytes.Compare(a.PkScript, b.PkScript)
if cmp != 0 {
return cmp < 0
// We order recipient outputs using BIP69 output ordering
// (amount, then pkScript bytes).
if a.Value != b.Value {
return a.Value < b.Value
}

return a.Value < b.Value
return bytes.Compare(a.PkScript, b.PkScript) < 0
})

recipientOuts = append(recipientOuts, anchorOut)
Expand All @@ -219,3 +153,63 @@ func CanonicalizeArkTxOrdering(tx *wire.MsgTx) error {

return nil
}

// validateCanonicalInputs validates BIP69 ordering of Ark tx inputs.
func validateCanonicalInputs(tx *wire.MsgTx) error {
for i := 1; i < len(tx.TxIn); i++ {
prev := tx.TxIn[i-1].PreviousOutPoint
cur := tx.TxIn[i].PreviousOutPoint

cmp := bytes.Compare(prev.Hash[:], cur.Hash[:])
switch {
case cmp < 0:
continue

case cmp > 0:
return fmt.Errorf("ark tx inputs are not canonical " +
"(outpoint hash order)")
}

if prev.Index > cur.Index {
return fmt.Errorf("ark tx inputs are not canonical " +
"(outpoint index order)")
}
}

return nil
}

// validateCanonicalOutputs validates recipient output ordering rules and anchor
// placement for v0 Ark transfers.
func validateCanonicalOutputs(tx *wire.MsgTx) error {
if len(tx.TxOut) == 0 {
return fmt.Errorf("ark tx has no outputs")
}

recipientOuts := tx.TxOut[:len(tx.TxOut)-1]

for i := 1; i < len(recipientOuts); i++ {
prev := recipientOuts[i-1]
cur := recipientOuts[i]

if IsAnchorOutput(prev) || IsAnchorOutput(cur) {
return fmt.Errorf("anchor output must be last")
}

if prev.Value < cur.Value {
continue
}

if prev.Value > cur.Value {
return fmt.Errorf("ark tx outputs are not canonical " +
"(value order)")
}

if bytes.Compare(prev.PkScript, cur.PkScript) > 0 {
return fmt.Errorf("ark tx outputs are not canonical " +
"(pkScript order)")
}
}

return nil
}
51 changes: 51 additions & 0 deletions lib/tx/arktx/canonical_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package arktx

import (
"testing"

"github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/darepo-client/lib/scripts"
"github.com/stretchr/testify/require"
)

// TestCanonicalizeOrderingSortsAndValidates asserts CanonicalizeOrdering
// produces a transaction that passes ValidateCanonicalTx, even if the input tx
// is not canonical.
func TestCanonicalizeOrderingSortsAndValidates(t *testing.T) {
t.Parallel()

tx := wire.NewMsgTx(TxVersion)
tx.AddTxIn(&wire.TxIn{
PreviousOutPoint: wire.OutPoint{
Hash: [32]byte{2},
Index: 1,
},
})
tx.AddTxIn(&wire.TxIn{
PreviousOutPoint: wire.OutPoint{
Hash: [32]byte{1},
Index: 0,
},
})

// Add outputs in non-canonical order and with anchor not last.
tx.TxOut = append(tx.TxOut,
&wire.TxOut{
Value: 1,
PkScript: []byte{0x52},
},
scripts.AnchorOutput(),
&wire.TxOut{
Value: 2,
PkScript: []byte{0x51},
},
)

err := CanonicalizeOrdering(tx)
require.NoError(t, err)

err = ValidateCanonicalTx(tx)
require.NoError(t, err)

require.True(t, IsAnchorOutput(tx.TxOut[len(tx.TxOut)-1]))
}
9 changes: 9 additions & 0 deletions lib/tx/arktx/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package arktx

// Package arktx contains helpers for constructing and validating Ark
// transactions that represent the virtual-chain step following checkpoints.
//
// Canonical output ordering is critical because multiple subsystems rely on
// byte-identical transaction construction (client retries, server validation,
// and persisted snapshots). This package provides a single, shared definition
// of that canonical ordering.
Loading
Loading