Skip to content
Open
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
7 changes: 6 additions & 1 deletion sdks/js/packages/spark-sdk/src/services/leaf-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1554,7 +1554,12 @@ export default class LeafManager {
}
nodeIds.push(node.id);
} else {
validNodes.push(node);
if (doesTxnNeedRenewed(nodeSequence)) {
nodesToRenewNodeTxn.push(node);
nodeIds.push(node.id);
} else {
validNodes.push(node);
}
}
} catch (err) {
// Skip this node — don't let one malformed leaf abort the entire batch.
Expand Down
19 changes: 18 additions & 1 deletion spark/so/chain/watch_chain.go
Original file line number Diff line number Diff line change
Expand Up @@ -820,7 +820,24 @@ func handleBlock(
}

logger.Sugar().Infof("Started processing coop exits at block height %d", blockHeight)
// TODO: expire pending coop exits after some time so this doesn't become too large
// Expire pending (unconfirmed) coop exits older than the configured threshold.
// This prevents the set from growing unbounded when transactions are never confirmed
// (e.g. evicted from mempool due to low fee or RBF replacement).
expiryDays := int(knobs.GetKnobsService(ctx).GetValue(knobs.KnobWatchChainCoopExitPendingExpiryDays, knobs.CoopExitPendingExpiryDays))
expiryThreshold := time.Now().AddDate(0, 0, -expiryDays)
expiredCount, err := dbClient.CooperativeExit.Delete().
Where(
cooperativeexit.ConfirmationHeightIsNil(),
cooperativeexit.CreateTimeLT(expiryThreshold),
).
Exec(ctx)
if err != nil {
return fmt.Errorf("failed to expire stale pending coop exits: %w", err)
}
if expiredCount > 0 {
logger.Sugar().Infof("Expired %d stale pending coop exit(s) older than %d days", expiredCount, expiryDays)
}

if knobs.GetKnobsService(ctx).GetValue(knobs.KnobWatchChainTweakKeysForCoopExitDelayEnabled, 0) > 0 {
// Build lists of both normal and reversed TxIDs to handle both endianness
confirmedTxIDs := make([]st.TxID, 0, len(confirmedTxHashSet)*2)
Expand Down
5 changes: 5 additions & 0 deletions spark/so/knobs/knobs.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,13 +81,18 @@ const (
KnobWatchChainMarkExitingNodesEnabled = "spark.so.watch_chain.mark_exiting_nodes.enabled"
KnobWatchChainTweakKeysForCoopExitDelayEnabled = "spark.so.watch_chain.tweak_keys_for_coop_exit_delay.enabled"
KnobWatchChainCoopExitKeyTweakRequiredConfirmations = "spark.so.watch_chain.coop_exit_key_tweak_required_confirmations"
KnobWatchChainCoopExitPendingExpiryDays = "spark.so.watch_chain.coop_exit_pending_expiry_days"

// CoopExitConfirmationThreshold is the default required L1 confirmation
// count for both the watch-chain key-tweak advance and the receiver-claim
// finalization guard. Both call sites read
// KnobWatchChainCoopExitKeyTweakRequiredConfirmations with this value as
// the fallback so they can never disagree when the knob is unset.
CoopExitConfirmationThreshold = 6
// CoopExitPendingExpiryDays is the default number of days after which a
// pending (unconfirmed) cooperative exit is considered stale and deleted.
// 14 days matches the default Bitcoin Core mempool expiry (DEFAULT_MEMPOOL_EXPIRY).
CoopExitPendingExpiryDays = 14

// Tokens
KnobTokenTransactionV3Enabled = "spark.so.tokens.token_transaction_v3_enabled"
Expand Down
31 changes: 7 additions & 24 deletions spark/so/utils/btc_address_network.go
Original file line number Diff line number Diff line change
@@ -1,34 +1,17 @@
package utils

import (
"strings"

"github.com/btcsuite/btcd/btcutil"
"github.com/lightsparkdev/spark/common/btcnetwork"
)

// IsBitcoinAddressForNetwork checks if the given Bitcoin address matches the expected prefix for the specified network.
// It uses simple prefix matching for common address types (legacy, P2SH, SegWit, Taproot) for each Bitcoin network.
// TODO: Investigate using btcutil for this instead of using our own.
// IsBitcoinAddressForNetwork checks if the given Bitcoin address is valid for the specified network.
// It uses btcutil.DecodeAddress for proper address validation including checksum verification.
func IsBitcoinAddressForNetwork(address string, network btcnetwork.Network) bool {
switch network {
case btcnetwork.Mainnet:
return hasAnyPrefix(address, "bc1", "3", "1")
case btcnetwork.Regtest:
return hasAnyPrefix(address, "bcrt", "2", "m", "n")
case btcnetwork.Testnet:
return hasAnyPrefix(address, "tb1", "2", "m", "n")
case btcnetwork.Signet:
return hasAnyPrefix(address, "tb1", "sb1", "2", "m", "n")
default:
params, err := network.Params()
if err != nil {
return false
}
}

func hasAnyPrefix(address string, prefixes ...string) bool {
for _, prefix := range prefixes {
if strings.HasPrefix(address, prefix) {
return true
}
}
return false
_, err = btcutil.DecodeAddress(address, params)
return err == nil
}