Skip to content

fix: expire stale pending cooperative exits after configurable threshold - #130

Open
vwinee21 wants to merge 2301 commits into
buildonspark:mainfrom
vwinee21:fix/expire-pending-coop-exits
Open

fix: expire stale pending cooperative exits after configurable threshold#130
vwinee21 wants to merge 2301 commits into
buildonspark:mainfrom
vwinee21:fix/expire-pending-coop-exits

Conversation

@vwinee21

Copy link
Copy Markdown

Problem

Pending cooperative exits (ConfirmationHeightIsNil()) accumulate indefinitely if the L1 transaction is never confirmed — for example, when evicted from the Bitcoin mempool due to low fees or replaced via RBF. This was noted in a TODO comment in watch_chain.go.

Solution

Add expiry logic that runs each block processing cycle. Pending coop exits older than KnobWatchChainCoopExitPendingExpiryDays (default: 14 days) are deleted.

Why 14 days? This matches Bitcoin Core's DEFAULT_MEMPOOL_EXPIRY — by this point, any unconfirmed transaction would have been dropped from the mempool, so the pending record serves no purpose.

Changes

  • so/knobs/knobs.go — add KnobWatchChainCoopExitPendingExpiryDays knob + CoopExitPendingExpiryDays = 14 default constant
  • so/chain/watch_chain.go — replace TODO with expiry logic; logs count at Info level when non-zero

Behavior

  • Threshold is runtime-configurable via the knob system (no code deploy needed)
  • Expiry only affects ConfirmationHeightIsNil() entries — confirmed exits are untouched
  • Non-zero expiry count logged for observability

benwgold and others added 30 commits April 16, 2026 18:01
GitOrigin-RevId: d15831b7011bd07b2fbd045f7f1993dac953cb7c
GitOrigin-RevId: 79c6619c2bb73e8b97c5b17ea8c45e8175ff5999
GitOrigin-RevId: 5271e6a3d2d56b84a6597b67b174c1fbafbf8522
…pc v1.6.0 and fix test

GitOrigin-RevId: 01f86b8fd6be2d43d85bb2166392c002f64cf2dd
GitOrigin-RevId: e8005ade085e83c627dc3ee47cd59936dc257b0c
GitOrigin-RevId: 300d0f1a54ce9b64e8aa0f5f4522a724a348d578
## Summary

Add Apache-2.0 license field to spark-token-primitives Cargo.toml.

---------

Co-authored-by: Lightspark Eng <engineering@lightspark.com>
GitOrigin-RevId: 609745441e48c8f1908437a7c304955478919329
## Summary

Internal.

GitOrigin-RevId: f73777b9c118669f43106fdd6816a7fc796ae889
GitOrigin-RevId: 7c47b61b874129e28f4310844c7adb6f17077c31
…nsfer test

GitOrigin-RevId: 055a31f6b7157e19ec4048564a464ce3ad9b49aa
GitOrigin-RevId: 0fca47504f7f0d59cfeb982361364785562c9f1a
GitOrigin-RevId: a1bd7bd05a5a74b06cbf91ab1ffe452684239d29
…ng (#6216)

## Summary

Make `spark-token-primitives` publishable to crates.io by solving the
proto file dependency problem without duplicating files in git.

The crate's `build.rs` references `../../protos` which works in the
monorepo but breaks for published crate consumers who won't have that
directory. The fix:

- `build.rs` checks for a local `protos/` directory first (present in
published tarballs), falling back to `../../protos` (monorepo layout)
- `Cargo.toml` adds `package.include` listing the four required `.proto`
files — Cargo's include list overrides `.gitignore`, so the files get
bundled into the tarball without being committed to git
- `spark-token-primitives/.gitignore` ignores `protos/` so the
pre-publish copy is never accidentally committed
- `publish.sh` copies the required protos, runs `cargo publish`, and
cleans up on exit

## Test Plan

Verified with `cargo package --list --allow-dirty` that the four proto
files appear in the tarball. Verified monorepo build still works via
`cargo build -p spark-token-primitives`.

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Lightspark Eng <engineering@lightspark.com>
GitOrigin-RevId: e1fcf1289ee7be10bffe9521bf9963b9125b07e8
## Summary

Introduced a combined gossip message type that carries both the preimage
and the optional key tweak settlement data in a single message.
This will allow changing preimage swaps to apply both atomically.

GitOrigin-RevId: a19389d6a9977fcef4e74e1909cfac99deb004f9
…#6220)

## Summary

Add the standard crates.io publishing metadata fields to the
`spark-token-primitives` `Cargo.toml` so the crate is correctly indexed
and discoverable.

Without these fields, `cargo publish` succeeds but crates.io shows the
crate with no description, no author, and no searchable keywords.

## Test Plan

N/A — metadata-only change; no functional code modified.

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Lightspark Eng <engineering@lightspark.com>
GitOrigin-RevId: 6ce92f9a11580633b3e992e5a4af98b10f2a6963
## Summary

Running renew leaf tests with both 2PC knobs off and on.

## Test Plan

This is the test

GitOrigin-RevId: 96a755e2b73e78be3f53bcce67dac7446ce09b31
## Summary

Fixed timelock calculation in `getNextTransactionSequence` to ensure proper alignment with `TIME_LOCK_INTERVAL`. The function now normalizes the current timelock by removing any remainder when divided by the interval, preventing potential issues with misaligned timelock values that could affect transaction sequencing.

## Test Plan

N/A

GitOrigin-RevId: 49e0fa5935fb197d10cad347bddfdc0a0a04ebed
## Summary

This refines the Vite example's target and network selection UI.

- always show the Network row so the current network is visible even
when LOCAL is selected
- force LOCAL to use REGTEST and disable other network buttons while
LOCAL is selected, instead of hiding the network selector entirely
- reuse the existing segmented-control styling for the Target and
Network rows
- add a real disabled visual state for buttons so locked networks no
longer look selectable
- remove the debug clientEnv line from the top of the page

## Test Plan

- `yarn workspace @buildonspark/spark-vite-app format`
- `yarn workspace @buildonspark/spark-vite-app build`

GitOrigin-RevId: 785531c96c64aea81408988287fa12123a3f3113
## Summary

Adds a dedicated `Deposit` section to the Vite example app and a
local-only `Fund Locally` flow for regtest development. The new flow
gets a fresh single-use deposit address, funds it through local bitcoind
JSON-RPC, mines a block, then claims the deposit back into the wallet.

This also adds a same-origin `/bitcoin-rpc` Vite proxy so browser
clients can use the local bitcoind RPC without exposing credentials to
frontend code.

## Test Plan

- `yarn workspace @buildonspark/spark-vite-app format`
- `yarn workspace @buildonspark/spark-vite-app build`

GitOrigin-RevId: 376c4d6ebfbeaa8240e95fa6ba720ad2cc26e8a1
## Summary

Add RisingWaveDatabasePath in main as a placeholder

GitOrigin-RevId: b27ec882028c8cbf4163f68f91bcf99aeda99d60
## Summary
- Simplify the Spark Vite example so environment selection happens in
the app UI instead of through multiple start commands.

GitOrigin-RevId: 27ff01ff2cf6b992a8541b5e1eeaebeed16506eb
## Summary

`GetUtxosForIdentity` and `GetUtxosForAddress` both implement their
`exclude_claimed` filter as a `NOT IN` subquery against
`utxo_swaps.utxo`, which is a nullable edge. A single non-cancelled row
with `NULL utxo` makes the subquery's result contain `NULL`; in SQL
three-valued logic, `x NOT IN (…, NULL)` evaluates to `NULL` — not
`TRUE` — for every row, so both handlers return an empty UTXO list for
every identity/address on the affected cluster. All downstream flows
that rely on these RPCs (client-side deposit sync, auto-claim,
static-deposit detection) silently break.

Fix: add `IS NOT NULL` to the subquery predicate in both handlers so
`NULL` rows are excluded from the `NOT IN` comparison.

Observed on dev regtest: a single stale \`utxo_swaps\` row with status
!= \`CANCELLED\` and null \`utxo\` was enough to make every observatory
coop-exit flow time out waiting for its confirmed deposit to appear,
even though the UTXO row existed and was correctly linked to a static
deposit address owned by the querying identity.

## Test Plan

Added two regression subtests, one per handler:
-
\`TestGetUtxosForIdentity/exclude_claimed_ignores_non-cancelled_swaps_with_null_utxo_edge\`
-
\`TestGetUtxosFromAddress/exclude_claimed_ignores_non-cancelled_swaps_with_null_utxo_edge\`

Each creates a non-cancelled \`UtxoSwap\` with no \`utxo\` edge and
asserts the corresponding handler still returns the expected UTXOs. Both
fail without the fix (return 0 UTXOs) and pass with it.

Full \`./so/handler/...\` suite passes locally (2908 tests, 46 skipped,
0 failed); golangci-lint clean.

GitOrigin-RevId: b165dd76ad6f606ce309896c6f59097b85ae8ced
## Summary

Add logs for 2PC engine

## Test Plan

N/A

GitOrigin-RevId: b381be87c9fa53d163eee019c0ad50e92c445fe4
## Summary

isTxBroadcast now resolves the electrs URL internally from the
Network enum and uses BitcoinFaucet (bitcoind RPC) for LOCAL,
matching the pattern already used in spark-wallet.ts. This removes
the electrsUrl and networkProto parameters from
constructUnilateralExitFeeBumpPackages, replacing them with a
single Network parameter.

This eliminates the regtest-mempool service dependency for CI
integration tests.

Contributes to SP-2392.

## Test Plan

CI.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
GitOrigin-RevId: 056eabfd49f561d9baca8bedf32548e42f2d3e6e
## Summary
Fix `spark-sdk`'s Node.js ESM WASM setup so randomness-dependent
bindings work on Node 18 without relying on CommonJS fallbacks or
ambient global mutation.

The generated WASM wrappers were previously split between
`globalThis.crypto` access and legacy `module.require` fallback
behavior. That made the Node 18 ESM path fragile and pushed the package
toward Node-specific ESM workarounds that do not fit the shared
browser/node output graph. This change routes both the Spark FROST and
token-primitives WASM wrappers through the SDK's `getCrypto()`
abstraction, keeps the Node entrypoint deterministic with
`nodeCrypto.webcrypto`, regenerates the patched bindings from source,
and fixes the FROST binding regen script so the browser artifacts are
patched from the correct directory.

## Test Plan
- Regenerated Spark FROST bindings with
`CC_wasm32_unknown_unknown=/opt/homebrew/opt/llvm/bin/clang
AR_wasm32_unknown_unknown=/opt/homebrew/opt/llvm/bin/llvm-ar mise exec
node@20 -- bash ./build-bindings.sh` from `signer/spark-frost-uniffi`
- Regenerated token-primitives bindings with `mise exec node@20 -- bash
./build-bindings.sh` from `signer/spark-token-primitives-uniffi`
- Ran `mise exec node@20 -- yarn workspace @buildonspark/spark-sdk
build`
- Ran `mise exec node@20 -- yarn workspace @buildonspark/spark-sdk
test-cmd src/tests/secret-sharing.test.ts
src/tests/transaction-construction.test.ts`
- Ran Node 18 and Node 20 ESM smoke tests against the built
`spark-wallet.node-*.js` chunk, covering
`SparkFrostNodeJS.splitSecretWithProofs(...)` and
`SparkTokenPrimitivesNodeJS.prepareTokenInvoice(...)`
- Rebuilt the example app and exercised `Test WASM Signing` and
`Generate New` in `sdks/js/examples/spark-vite-app` via Playwright

---------

Co-authored-by: Lightspark Eng <engineering@lightspark.com>
GitOrigin-RevId: 1db42b589bcd868cebcfee89211e09578889da16
## Summary

Add `LICENSE` (Apache-2.0) to the `spark-token-primitives` crate
directory and include it in the published tarball via the `include` list
in `Cargo.toml`.

The crate's `Cargo.toml` already declares `license = "Apache-2.0"`, but
without a `LICENSE` file in the `include` list, `cargo package` does not
bundle the actual license text. crates.io and downstream consumers
expect the license file to be present in the tarball alongside the
declaration.

## Test Plan

`cargo package --manifest-path signer/spark-token-primitives/Cargo.toml
--list` confirms `LICENSE` is included in the packaged files.

GitOrigin-RevId: f995e966352b9a74e1f4923b39ebf15c58027790
## Summary

Removed knob `spark.so.enable_strict_finalize_signature` and hardcoded
the strict path. The knob has been fully on in prod since 2025-12-02.

GitOrigin-RevId: 52453a473a9fa112cd830dfc1a954c7cf5f0aebf
## Summary

Adding a Maintainers file

## Test Plan

N/A

GitOrigin-RevId: d0832460580438a3d7e6651ba715aa4ab6e11b64
## Summary

Run both path for deposit tests

## Test Plan

CI

GitOrigin-RevId: 1c3e86d2077e64d265f5cc4448ff8e2b7f546d2f
## Summary

`spark.proto`, `spark_token.proto`, and `multisig.proto` all import
`validate/validate.proto`, but `publish.sh` was only copying the four
top-level proto files. This caused `cargo publish --dry-run` to fail
with:

```
protoc failed: validate/validate.proto: File not found.
```

**Changes:**
- `publish.sh`: create `protos/validate/` and copy
`validate/validate.proto` alongside the other vendored protos
- `Cargo.toml`: add `protos/validate/validate.proto` to the `include`
list so it's bundled in the crate tarball

## Test Plan

`./publish.sh --dry-run` now completes successfully:
```
Packaged 21 files, 231.7KiB (44.4KiB compressed)
Verifying spark-token-primitives v0.1.0 ...
Finished `dev` profile [unoptimized + debuginfo] target(s) in 2.68s
warning: aborting upload due to dry run
```

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Lightspark Eng <engineering@lightspark.com>
GitOrigin-RevId: c6699b7b4acd24d18d690534da95fd13e2d4c9f5
peterrojs and others added 29 commits May 12, 2026 17:19
…ves (#6471)

## Summary

Wires the `@buildonspark/spark-sdk` React Native install paths to the `spark-token-primitives` UniFFI bindings. Stacked on top of #6458 (which adds the Rust-side build scripts).

**This PR cannot merge until the generated artifacts are committed alongside it.** The Swift/Kotlin sources here import UniFFI-generated types (`SelectedTokenOutput`, `ReceiverTokenOutput`, `TransferBuildRequest`, …) and the podspec vendors `ios/spark_token_primitivesFFI.xcframework`. None of those exist yet — they're produced by `signer/spark-token-primitives-uniffi/build-rn-bindings.sh` (added in #6458). Whoever runs that script needs to commit the generated bindings + native binaries onto this branch before merge.

What's in this PR:
- `ios/SparkTokenPrimitivesModule.{swift,m,h}` — RCT bridge unpacking JS `[String: Any]` into UniFFI-generated structs and calling the 5 functions. Pattern lifted from `SparkFrostModule.swift` (same `arrayToData` / `[Any] as? [Int]` marshalling).
- `android/src/main/java/com/sparktokenprimitives/SparkTokenPrimitivesModule.kt` + `SparkTokenPrimitivesPackage.kt` — Kotlin `@ReactModule` mirror of the iOS bridge.
- `src/token-primitives-bindings/token-primitives-bindings.react-native.ts` — TS layer extending `SparkTokenPrimitivesBase`, marshalling `Uint8Array ↔ number[]` across the RN bridge.
- `src/index.react-native.ts` — adds the missing `setSparkTokenPrimitivesOnce()` call. The RN entry never registered the token-primitives binding, so any prior RN consumer would have hit `getSparkTokenPrimitives()` errors. Node and browser entries already register it.
- `spark-sdk.podspec` — vendors both `spark_frostFFI.xcframework` and `spark_token_primitivesFFI.xcframework`.
- `react-native.config.js` — declares both `SparkFrostPackage` and `SparkTokenPrimitivesPackage` for Android autolinking. RN autolinking only auto-discovers a single ReactPackage per library, so multi-package libraries need an explicit config.
- `package.json` — adds `react-native.config.js` to the `files` array so it ships in the published tarball.

## Test Plan

- [x] `yarn tsdown` builds clean against the new TS binding; `dist/native/index.react-native.js` contains `NativeModules.SparkTokenPrimitivesModule` and the 5 method calls.
- [x] `yarn types` produces the same set of pre-existing TS errors as `main` (logger / sdk-logger issues in unrelated files); no new errors from this PR.
- [x] Prettier-formatted TS.
- [ ] **BLOCKING:** run #6458's `build-rn-bindings.sh` and commit generated artifacts to this branch — without them, `pod install` and Android `gradle build` will fail.
- [ ] iOS simulator smoke test: build a minimal RN app, call `hash_partial_token_transaction` with a known input, assert byte-equal with the Node WASM result.
- [ ] Android emulator smoke test: same parity check.

GitOrigin-RevId: 5f31208d28f51bde14256efd7bf4e000be95db11
… method (#6544)

## Summary

Adds `SparkWallet.cleanup()` as the preferred public cleanup method
while keeping `cleanupConnections()` as a deprecated compatibility
alias. This gives SDK users a clearer public API name without breaking
existing callers.

## Test Plan

- `yarn workspace @buildonspark/spark-sdk test-cmd
src/tests/wrapPublicMethod.test.ts`
- `yarn workspace @buildonspark/spark-sdk types`
- `yarn changeset status --since=origin/main`
- `yarn workspace @buildonspark/spark-sdk build`
- `yarn workspace @buildonspark/issuer-sdk build`
- `yarn workspace @buildonspark/spark-mcp types`
- `yarn workspace @buildonspark/cli types`
- `yarn workspace @buildonspark/interactive-cli types`
- `yarn workspace artillery-engine-spark types`
- `yarn workspace @buildonspark/nodejs-scripts types`
- `yarn workspace @buildonspark/nestjs-app build`
- `git diff --check`

GitOrigin-RevId: 0d2e3993ee4be55918f9c48423ab9a934edd755f
## Summary

`QueryAllTransfers` now detects the sender-only +
outgoing-in-flight-status-subset filter shape — used by SDK callers like
`queryPrimarySwapTransfers`, `queryPendingOutgoingTransfers`, and
`getOwnedBalance` sender path — and dispatches to a specialized SQL
builder that drives
`idx_transfers_outgoing_in_flight_sender_pubkey_time` via column-based
leading equality + top-N pushdown. Behind
`KnobReadMIMODataModelOutgoingInFlight` (default off, 100 in Tiltfile).
Other filter shapes (receiver, SR1, mixed/wider statuses) fall through
to legacy `queryTransfers` unchanged.

## Test Plan

**Prod plan (`spark-rds.sh EXPLAIN` against SSP mainnet, canonical TS1
shape):** planner picks
`idx_transfers_outgoing_in_flight_sender_pubkey_time` cleanly — `Index
Cond: sender_identity_pubkey`, top-N pushdown (LIMIT cost 297 vs
full-scan cost 8196), no `Sort` above LIMIT.

**Dbseed perf scoreboard (warm):**

| Shape | Pubkey | Plan | Warm | Rows |
|---|---|---|---:|---:|
| TS1 full 4-state + types | so0_extreme (~25M edges) |
`transfer_status` + Sort * | 109ms | 100 |
| TS1 full 4-state + types | so0_medpending (~5M edges) |
`transfer_status` + Sort * | 102ms | 40 |
| Single-status subset | so0_extreme | `transfer_status` + Sort * | 12ms
| 53 |

\* The dbseed wipeout is a pre-existing stats artifact unrelated to this
PR — dbseed's `full` profile overstresses the global 4-state set (36K
rows vs ~753 in prod) which tips the planner's cost model. Same plan was
observed before this PR for the equivalent legacy SQL. Prod plan picks
correctly.

**Equivalence (`TestQueryAllTransfers_Equivalence_OutgoingInFlight`):**
5 cases pass against ephemeral Postgres — full 4-state, single status,
2-state subset, status-outside-partial fall-through,
receiver-participant fall-through. Asserts ordered transfer ID identity
+ per-transfer `Status` / `Type` / `Network` / leaf-id set equivalence
between legacy `queryTransfers` and `queryOutgoingInFlight`.

---
Resolves SP-3055
created with claude session 1db49ec5-f137-487b-a181-b0977a560703

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
GitOrigin-RevId: de34dabc893bf127fcd5fa3286329561bfda5fbe
## Summary

Pin the `frost-secp256k1-tr` and `frost-core` dependencies to the
`frost-core/v3.0.0-ls` tag on `lightsparkdev/frost`, replacing the prior
`nested-signing` branch reference. The branch was merged into `main`
upstream and this tag captures the v3.0.0 state for reproducible builds.
Follow-up to #6425.

Affects:
- `signer/Cargo.toml` + `signer/Cargo.lock`
- `sdks/js/packages/spark-frost-bare-addon/Cargo.toml` + `Cargo.lock`

The bare-addon `Cargo.lock` also picks up a stale `ecies` git pin
getting reconciled with the registry version that `signer/Cargo.toml`
already specified (leftover from #6421).

Bare prebuilds will be regenerated by GHA.

## Test Plan

- \`cargo check --tests\` clean for \`spark-frost\`
- \`cargo check\` clean for \`spark-frost-bare-addon\`
- CI

---------

Co-authored-by: Lightspark Eng <engineering@lightspark.com>
GitOrigin-RevId: 3513067824805dc9a1cc882af637272ed4b5552f
…ransaction handler (#6468)

## Summary
Fix error handling bug in token transaction handler where keyshare errors could be silently ignored.

---
🤖 *custodial-hashrate* | [Dashboard](https://zeus.dev.dev.sparkinfra.net/#/instance?id=custodial-hashrate) | [Voice](https://zeus.dev.dev.sparkinfra.net/#/command-center?focus=custodial-hashrate) | [Feedback](https://zeus.dev.dev.sparkinfra.net/feedback)

GitOrigin-RevId: c11ea8be46ed2d056cc3a8027bb70350101b57aa
## Summary

`QueryNodes` already caps owner-based pagination, but the `node_ids`
path accepted an unbounded list. A client could submit a very large
`node_ids` array and force UUID parsing, a large SQL `IN` predicate,
response map allocation, and optional parent-chain loading.

This adds a pre-parse cap of 1000 node IDs for direct `QueryNodes`
lookups and rejects oversized requests before UUID parsing or DB query
construction.

## Test Plan

- `go test ./so/handler -run
'TestQueryNodesRejectsTooManyNodeIDsBeforeParsing$' -count=1`
- `go test ./so/handler -run 'TestQueryNodes' -count=1`

GitOrigin-RevId: 4bcaa7d001ff1b9e9e68a9b5ed8459f170f4389a
## Summary

Make ClaimTransferTweakKeys idempotent for receiver-side states. When
concurrent calls race on the same transfer, the second call now returns
success instead of a confusing error if the transfer has already
advanced past the tweak-keys phase.

GitOrigin-RevId: 95af39f0801f2120841dbe747abbedb04c62b89d
## Summary

Reject malformed `payment_hash` lengths at the `QueryUserSignedRefunds`
handler boundary. The handler now validates that the payment hash is
exactly 32 bytes and returns `InvalidArgument` for any other length.

- Add handler-level validation for `payment_hash` length in
`QueryUserSignedRefunds`
- Add unit test covering malformed hash lengths (0, 1, 31, 33, 64 bytes)
- Add integration test in `so/security_test` for the raw API input path
- Exclude `so/security_test` from unit-test runs (requires a live stack,
like `so/grpc_test`)

## Test Plan

- `go test ./so/handler -run
'TestQueryUserSignedRefunds_RejectsMalformedPaymentHashLength$'
-count=1`
- Integration test in `so/security_test` validates live server rejects
all non-32-byte hashes with `InvalidArgument`

---------

Co-authored-by: Claude <noreply@anthropic.com>
GitOrigin-RevId: 8b5aa12a34f5756c65a91baaca89d5d337d15241
…6473)

## Summary

#6451 pre-loaded `TransferLeaves` with nested `Leaf -> Tree /
SigningKeyshare / Parent` on the two MIMO list builders, collapsing the
per-transfer N+1 in `MarshalProto`. The prod impact was sizable enough
to justify extending the same pattern to the remaining transfer list
endpoints — `GetStuckLightningPayments`, `QueryNodeTransferHistory`, and
`InternalTransferHandler.GetTransfers` — which still fan out ~5N serial
edge queries per page.

`GetStuckLightningPayments` already had a partial pre-load (`Leaf ->
Tree` only); this extends it so `MarshalProto` takes the fully
pre-loaded path instead of falling back to per-leaf lazy loads. The
other two add the chain from scratch.

## Test Plan

Behavior unchanged: pre-loads only affect query-fanout shape, and
`MarshalProto` (post-#6451) already handles both pre-loaded and
lazy-load inputs.

---
Related to SP-2917
created with claude session f5c869ad-9ca1-4950-8a26-4f779bb4ece6

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
GitOrigin-RevId: 71ca76583a35962424d5d9fda1092a1b12aefb28
## Summary

Allow `NODE_EXTRA_CA_CERTS` through the Spark JS SDK Turbo strict
environment so callers can trust a custom local CA when running nested
Turbo tasks.

This is defensive hardening for workflows like webdev's SSP dev-cli
hermetic test, where the GitHub Actions step sets `NODE_EXTRA_CA_CERTS`
but `yarn turbo run test:integration:ssp` otherwise filters it before
the Jest/Node process opens Spark authn connections.

`NODE_EXTRA_CA_CERTS` is included in both `globalEnv` and
`globalPassThroughEnv`, matching the existing local-ingress pattern: it
is visible to strict-mode Turbo tasks and participates in task hashing
when TLS trust input changes.

## Test Plan

- `git diff --check -- sdks/js/turbo.json`
- `yarn turbo run test:integration:ssp --dry=json >
/tmp/spark-turbo-node-extra-ca-dry-v2.json`
- Verified the dry run still uses strict env mode and includes
`NODE_EXTRA_CA_CERTS` in both the specified env list and global
pass-through env allowlist.

GitOrigin-RevId: 1b35fd8e83daa4ec61695cc7943427abadb62f5c
## Summary

Updates the Next.js example app to the latest Next 15 backport release
and keeps `eslint-config-next` on the matching version. This pulls in
the patched React Flight protocol implementation without moving the
example to Next 16.

## Test Plan

- `yarn workspace @buildonspark/spark-sdk build`
- `yarn workspace spark-nextjs-app build`

GitOrigin-RevId: 94f405b245db19128336a6c7b939ed1d35da0b9c
## Summary

`QueryTransfers` caps response page size, but caller-provided
`filter.transfer_ids` was unbounded. A large transfer ID filter forces
UUID parsing and a large SQL `IN` predicate before the normal page-size
cap can help.

This caps direct transfer ID filters at 1000 values and rejects
oversized requests before UUID parsing and query construction.

## Test Plan

- `go test ./so/handler -run
'TestQueryTransfersRejectsTooManyTransferIDsBeforeParsing$' -count=1`
- `go test ./so/handler -run 'TestQueryTransfers' -count=1`

GitOrigin-RevId: 1b7ca30e87e915570c9eef99ef332d93ef3b53da
## Summary

`QueryTokenOutputs` accepted caller-provided network enum values and
passed them directly to `btcnetwork.FromProtoNetwork`. When the network
was `UNSPECIFIED` or an unknown enum, the conversion error was untyped
and surfaced as an internal RPC error.

This validates that `network` is specified up front and wraps unknown
enum conversion failures as malformed client input. The new unit test
covers both `UNSPECIFIED` and an unknown enum value.

## Test Plan

- `go test ./so/handler/tokens -run
'TestQueryTokenOutputsRejectsInvalidNetwork|TestQueryTokenOutputsRejectsNilRequest'
-count=1`\n- `git diff --check`

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
GitOrigin-RevId: c514ea4d4c3b1dfcb8625d8e3aa5fb6cf6bcc4ec
## Summary

Validate token revocation share input references before adding them to
the operator share map.

The preferred `InputTtxoRef` hash/vout format is used only when
`PrevTokenTransactionHash` is exactly 32 bytes. To preserve backwards
compatibility with older operators, a malformed or absent `InputTtxoRef`
still falls back to the legacy `InputTtxoId` UUID when that field is
present. The new validation errors only when there is no usable input
reference at all, when the usable legacy UUID is malformed, or when the
operator/share/secret fields themselves are malformed.

## Test Plan

- `env -u MINIKUBE_IP go test ./so/handler/tokens -run
TestBuildInputOperatorShareMap -count=1 -timeout=300s`
- `env -u MINIKUBE_IP GOFLAGS='-tags=lightspark' go test
./so/handler/tokens -run TestBuildInputOperatorShareMap -count=1
-timeout=300s`
- `git diff --check`
GitOrigin-RevId: ea214cd2ac9e49e418edfb774f5ca58a71cd6239
## Summary

Updates the React Native example apps from React Native CLI 19.0.0 to
the patched 19.1.2 release line. This keeps the examples on the same CLI
major while pulling in the patched Metro development server package.

## Test Plan

- `yarn workspace @buildonspark/spark-react-native-app lint`
- `yarn workspace @buildonspark/issuer-sdk build`
- `yarn workspace @buildonspark/spark-expo-react-native-app build`

GitOrigin-RevId: 2c592501a5590eac8c3e15f7eb7e429087321c02
…fer (#6583)

## Summary

Follow-up to #6473. `GetStuckTransfers` (both legacy and MIMO branches),
`QueryStuckTransfer`, and `QueryLightningSwapTransfer` share the same
N+1 fanout as the endpoints fixed in #6473. The stuck-transfer handlers
additionally walk leaves + per-leaf `SigningKeyshare` explicitly inside
`marshalStuckTransfer`, so the preload chain alone isn't enough —
`.QueryX().All(ctx)` always issues fresh SQL.

Adds the `WithTransferLeaves → WithLeaf → WithTree / WithSigningKeyshare
/ WithParent` chain to all four query sites and refactors
`marshalStuckTransfer` to read from `transfer.Edges.TransferLeaves` /
`leaf.Edges.SigningKeyshare` directly. If a future caller forgets the
preload, the handler now errors instead of silently degrading to N+1.

## Test Plan
- Three new postgres-backed tests assert `SigningKeysharePublicShares`
is populated for an attached leaf via the MIMO path, the legacy path,
and `QueryStuckTransfer` — previously zero coverage on that field.
- `QueryLightningSwapTransfer` goes through `transfer.MarshalProto(ctx)`
directly (no `marshalStuckTransfer`-style edge walk), which #6451
already made preload-aware, so the preload addition is behavior-neutral.

---
Related to SP-2917
created with claude session 456e80ba-a666-4afd-a57b-5b6e657ac434

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
GitOrigin-RevId: 5975a192b09dd9061901dc89976325249733ff31
## Summary

FinalizeNodeSignaturesV2 accepts client-supplied node IDs and signature
bytes. The current creation path returned untyped errors for missing
node IDs and invalid signatures, which surfaced to clients as Internal
errors.

This change classifies malformed node IDs as InvalidArgument, missing
nodes as NotFound, missing required direct signatures as
InvalidArgument, and bad signature verification failures as
FailedPrecondition while preserving existing validation flow. The new
tests cover malformed node IDs, nonexistent node IDs, and invalid
signature bytes against an existing node.

## Test Plan

- `go test ./so/handler -run
'TestFinalizeNodeSignaturesV2RejectsMalformedAndMissingNodeIDs|TestFinalizeNodeSignaturesV2RejectsBadSignature'
-count=1`\n- `go test ./so/handler -run
'TestFinalizeSignatureHandler_ErrorCases|TestFinalizeNodeSignaturesV2RejectsMalformedAndMissingNodeIDs|TestFinalizeNodeSignaturesV2RejectsBadSignature'
-count=1`\n- `git diff --check`
GitOrigin-RevId: b5696eb16163c64b30d96ccf39db5824eeccb903
## Summary

The shared deposit UTXO verifier accepts client-provided UTXO
identifiers. When callers provide a missing UTXO object or a txid with
the wrong length, the helper currently returns plain errors, causing
public RPCs that use it to surface Internal instead of InvalidArgument.

This change classifies nil UTXO inputs as missing-field InvalidArgument
and malformed txid values as malformed-field InvalidArgument in both
verifier variants. Existing NotFound behavior for well-formed but
unknown UTXOs is preserved.

## Test Plan

- `go test ./so/handler -run 'TestVerifiedTargetUtxo/invalid_txid'
-count=1`\n- `git diff --check`
GitOrigin-RevId: aef58a5ce572e7411d9d22f34fcffd1c3ba80c5a
## Summary

InitiateStaticDepositUtxoRefund validates the client-provided refund
transaction after resolving the requested static-deposit UTXO. The
validator returned plain errors for missing, malformed, and structurally
mismatched refund transactions, which surfaced as Internal errors on the
public RPC.

This change classifies missing refund transaction bytes as
InvalidArgument missing field, malformed transaction bytes as
InvalidArgument malformed field, and refund transactions that do not
spend the requested UTXO as InvalidArgument malformed field. The
structural check itself is unchanged.

## Test Plan

- `go test ./so/handler -run
'TestValidateStaticDepositRefundTxRejectsClientRawTxInputs' -count=1`\n-
`git diff --check`

GitOrigin-RevId: 3ac6bd1968c12b7de5ce5005f259d8206b952112
## Summary

Return typed gRPC errors from ProvidePreimage validation instead of
allowing attacker-controlled malformed inputs and missing preimage
requests to surface as Internal errors. Payment hash and preimage length
failures now return InvalidArgument, identity key parsing failures
return InvalidArgument, preimage/hash mismatch returns
FailedPrecondition, and a well-formed but missing preimage request
returns NotFound.

This preserves the existing wrapper context because Spark gRPC errors
are preserved through wrapped error chains.

## Test Plan

- `go test ./so/handler -run
'TestValidatePreimage_InvalidPreimage_Errors|TestProvidePreimageRejectsMalformedIdentityPublicKeyWithInvalidArgument'
-count=1`
- `git diff --check`
GitOrigin-RevId: d10e458765b4616a5c0318f0e84854c15c36533f
## Summary

QueryHTLC accepted unbounded client-provided transfer_id and
payment_hash filter lists. Those values were allocated, parsed, and
passed into SQL predicates before any count limit, so a large request
could consume handler CPU/memory and build oversized IN predicates. This
caps both filters at 500 values and rejects payment hashes that are not
exactly 32 bytes before DB access.

## Test Plan

- `go test ./so/handler -run
'TestQueryHTLCRejectsFilterResourceExhaustionBeforeDB$' -count=1`
- `go test ./so/handler -count=1`

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
GitOrigin-RevId: c20e343b840651b472c62b6dd8f8ee8506bf3396
## Summary

`COUNTER_SWAP_V3` validates that the linked primary transfer exists, is
still cancellable, has enough time left, matches the counter transfer
amount, and has reversed sender/receiver identities. It did not validate
that the counter-transfer leaves are on the same Bitcoin network as the
primary transfer.

That left an SSP-mediated swap able to bind a user primary transfer on
one network to an SSP counter transfer on another network. The amount
and party checks still pass, but the swap no longer represents an equal
exchange of value on the same network.

This change rejects `COUNTER_SWAP_V3` creation when the loaded
counter-transfer leaves resolve to a different network than the linked
primary swap transfer.

## Test Plan

- `go test -tags lightspark ./so/handler -run
TestCreateTransfer_CounterSwapV3_FailsWithMismatchedNetwork -count=1`
- `go test -tags lightspark ./so/handler -run
'TestCreateTransfer_CounterSwapV3_FailsWithMismatched(Network|Amount|Parties)'
-count=1`
- `go test -tags lightspark ./so/handler -count=1`
GitOrigin-RevId: 397b92354c2f65c7b21682781ee9796336123adb
## Summary

FinalizeNodeSignatures and FinalizeNodeSignaturesV2 accepted an
unbounded client-provided node_signatures list. That list is later
parsed into UUID slices/maps, used in SQL predicates, processed one node
at a time, and included in error formatting paths, so an oversized
request can force unnecessary handler allocation, parsing, and DB work.
This caps the request at the existing 1000-leaf processing limit before
ownership checks or DB access.

## Test Plan

- `go test ./so/handler -run
'TestFinalizeSignatureHandler_FinalizeNodeSignaturesRejectsTooManyNodeSignaturesBeforeDB$'
-count=1`
- `go test ./so/handler -count=1`
- `git diff --check`
GitOrigin-RevId: 411709908b0fe647dec6079ad90ff857c1e4e522
## Summary

Updates JS tooling dependencies so transitive security fixes are
resolved through maintained upstream packages:

- Bumps `ts-jest` to resolve `handlebars` 4.7.9.
- Bumps Artillery and dedupes AWS/Azure transitive packages so their XML
parser path resolves to patched `fast-xml-parser` 5.x versions.
- Refreshes protobuf loading/exporter dependencies so `protobufjs`
resolves to patched versions.

## Test Plan

- `yarn workspace artillery-engine-spark build`
- `yarn workspace @buildonspark/spark-mcp test`
- `yarn workspace @buildonspark/issuer-sdk test`
- `yarn workspace @buildonspark/nodejs-scripts test`
- `yarn workspace @buildonspark/spark-sdk test`
- `yarn workspace @buildonspark/nestjs-app build`

GitOrigin-RevId: 4145fc64582b27e366605a60b3f5badc96def500
## Summary

Return typed gRPC errors from transfer query validation paths instead of
allowing malformed client filters to surface as Internal errors.
Malformed participant identity keys, malformed transfer IDs, invalid
transfer statuses, and status filters on pending-only queries now return
InvalidArgument.

This intentionally does not duplicate the existing transfer query
pagination PR; negative limit/offset and nil-filter getter behavior
remain separate.

## Test Plan

- `go test ./so/handler -run
'TestQueryTransfersRejectsMalformedFiltersWithTypedErrors' -count=1`
- `git diff --check`

GitOrigin-RevId: c67007a0fdf8b41e4425f28d236c0f833614eb8c
## Summary
This PR bumps a few dependency versions with reported issues.

GitOrigin-RevId: b39ccde599a3035c7f37e3b8d0caf440b79f111f
## Summary

The shared knob
`spark.so.watch_chain.coop_exit_key_tweak_required_confirmations` is
read by two code paths with different fallback defaults
(`watch_chain.go` defaults to `3`, `checkCoopExitTxBroadcasted` defaults
to `6`). If the knob is unset and the delay path is enabled, watch-chain
advances a coop-exit transfer to `SENDER_KEY_TWEAKED` at 3 confirmations
while the finalization guard would still require 6. The MIMO
`ClaimTransfer` branch and
`InternalTransferHandler.FinalizeTransferReceiver` both skip
`checkCoopExitTxBroadcasted` entirely, so the receiver-finalization side
never enforces the 6-confirmation policy.

This is a non-issue in production today, as all knobs are set to 1.
However, this change is a convergence correction and defense against
future regression.

This change unifies the default by moving
`CoopExitConfirmationThreshold` into the `knobs` package and references
it from both call sites, then adds `checkCoopExitTxBroadcasted` to the
MIMO claim branch and to `FinalizeTransferReceiver` to mirror the
coordinator-side guard added in SP-2961.

## Test Plan
- Existing `TestCoopExitBasic` / `TestCoopExitSingleCall` still pass —
they mine `CoopExitConfirmationThreshold+2` blocks, so the new guard
remains satisfied.
- Follow-up: focused negative-case integration test for the MIMO claim
path at sub-threshold confirmations.

---
Resolves SP-3098
created with claude session 456e80ba-a666-4afd-a57b-5b6e657ac434

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
GitOrigin-RevId: b21d15f89b47ee943a941f40ad999d4a96d48e6b
Replace fragile prefix matching with proper btcutil.DecodeAddress
validation which includes checksum verification.

Resolves TODO in IsBitcoinAddressForNetwork.
Pending coop exits with ConfirmationHeightIsNil() can accumulate
indefinitely if the L1 transaction is never confirmed (e.g. evicted
from mempool due to low fee or replaced via RBF).

Add expiry logic that runs each block cycle and deletes pending coop
exits older than KnobWatchChainCoopExitPendingExpiryDays (default: 14
days). The 14-day default matches Bitcoin Core's DEFAULT_MEMPOOL_EXPIRY
so entries are cleaned up around the time Bitcoin Core would drop them.

Expiry count is logged at Info level when non-zero for observability.
The threshold is runtime-configurable via the knob system without a
code deploy.
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.