Skip to content

allow flexible boarding scripts in client-lib#966

Open
bitcoin-coder-bob wants to merge 4 commits intomasterfrom
bob/flexible-boarding-scripts
Open

allow flexible boarding scripts in client-lib#966
bitcoin-coder-bob wants to merge 4 commits intomasterfrom
bob/flexible-boarding-scripts

Conversation

@bitcoin-coder-bob
Copy link
Copy Markdown
Collaborator

@bitcoin-coder-bob bitcoin-coder-bob commented Mar 13, 2026

closes #885

  • Adds NewBoardingAddress(ctx, vtxoScript) to ArkClient and WalletService interfaces, allowing users to register custom boarding scripts (not just the default single-key script)
  • Custom boarding descriptors are persisted in the wallet store (both file and in-memory) with deduplication
  • GetAddresses now returns custom boarding addresses alongside the default one
  • Fixes addInputs in unroll.go to derive vtxoScript from each individual UTXO's tapscripts instead of from a single freshly generated address (the old // TODO works only with single-key wallet)

Summary by CodeRabbit

  • New Features

    • Create custom boarding addresses from user-provided scripts.
    • Persist and retrieve custom boarding addresses across sessions with deduplication.
    • Wallet API exposes creation of boarding addresses and validates scripts before use.
  • Tests

    • New tests cover creating, persisting, and deduplicating boarding addresses and per-input script handling in transactions.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 13, 2026

Walkthrough

Adds support for creating and persisting custom boarding addresses: new NewBoardingAddress methods on client, service, and wallet layers; per-UTXO vtxo script parsing in unroll; store support and tests for boarding descriptors and deduplication.

Changes

Cohort / File(s) Summary
Interfaces
pkg/client-lib/ark_sdk.go, pkg/client-lib/wallet/wallet.go
Added NewBoardingAddress(ctx context.Context, vtxoScript script.VtxoScript) (*types.Address, error) to ArkClient and WalletService.
Service
pkg/client-lib/funding.go
Added (*service).NewBoardingAddress that performs safeCheck() and validates vtxoScript before delegating to wallet.
Wallet implementation
pkg/client-lib/wallet/singlekey/bitcoin_wallet.go
Added NewBoardingAddress to derive P2TR address from vtxoScript, encode tapscripts, conditionally persist a boarding descriptor, and return address; GetAddresses now includes persisted boarding descriptors.
Store interface & types
pkg/client-lib/wallet/singlekey/store/store.go
Added exported BoardingDescriptor type and WalletStore methods AddBoardingDescriptor / GetBoardingDescriptors.
Store implementations
pkg/client-lib/wallet/singlekey/store/file/store.go, pkg/client-lib/wallet/singlekey/store/inmemory/store.go
Implemented boarding descriptor persistence and retrieval with deduplication; file store writes state JSON and uses 0600 permissions; in-memory store holds a cloned slice.
Unrolling inputs
pkg/client-lib/unroll.go
Changed addInputs to parse each UTXO's own tapscripts per-UTXO (removed single-wallet script assumption) and improved error wrapping.
Tests
pkg/client-lib/unroll_test.go, pkg/client-lib/wallet/singlekey/store/store_test.go, pkg/client-lib/wallet/wallet_test.go
Added tests for per-UTXO script handling, boarding descriptor persistence/deduplication, and NewBoardingAddress behavior.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant Client as ArkClient
    participant Service as FundingService
    participant Wallet as BitcoinWallet
    participant Store as WalletStore
    participant Script as ScriptValidator

    User->>Client: NewBoardingAddress(ctx, vtxoScript)
    Client->>Service: NewBoardingAddress(ctx, vtxoScript)
    Service->>Service: safeCheck()
    Service->>Script: Validate(vtxoScript, signerPubKey, delay)
    Script-->>Service: validation result
    Service->>Wallet: NewBoardingAddress(ctx, vtxoScript)
    Wallet->>Script: TapTree/Parse vtxoScript
    Script-->>Wallet: taproot key, tapscripts
    Wallet->>Wallet: Build P2TR address
    Wallet->>Store: AddBoardingDescriptor({address, tapscripts})
    Store-->>Wallet: persisted/ack
    Wallet-->>Service: *types.Address
    Service-->>Client: *types.Address
    Client-->>User: address with tapscripts
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • Client lib improvements #956: Modifies client-lib wallet and ArkClient interfaces with similar address handling and boarding descriptor management.

Suggested reviewers

  • sekulicd
  • altafan
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'allow flexible boarding scripts in client-lib' accurately summarizes the main change: enabling flexible/custom boarding scripts in the client library through the new NewBoardingAddress method.
Linked Issues check ✅ Passed The PR fully addresses issue #885 by adding NewBoardingAddress method to allow flexible/custom boarding scripts, persisting custom boarding descriptors, and fixing the addInputs function to derive vtxoScript per-UTXO instead of from a single address.
Out of Scope Changes check ✅ Passed All changes are in-scope: NewBoardingAddress implementation, wallet store extensions for boarding descriptor persistence, GetAddresses modifications to return custom boarding addresses, and unroll.go fixes for per-UTXO script derivation are all directly tied to enabling flexible boarding scripts as required by issue #885.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch bob/flexible-boarding-scripts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@arkanaai
Copy link
Copy Markdown
Contributor

arkanaai bot commented Mar 14, 2026

[Arkana Review — PR #966: allow flexible boarding scripts in client-lib]

Reviewed at: 2026-03-14 14:00 UTC


Summary

Adds NewBoardingAddress(ctx, vtxoScript) to ArkClient and WalletService, backed by persistent storage of custom boarding descriptors in both file and in-memory stores. Also fixes addInputs in unroll.go to derive the vtxoScript from each individual UTXO's tapscripts rather than from a freshly generated offchain address (the old TODO-marked single-key assumption).


What looks good

Script validation at generation time: NewBoardingAddress calls vtxoScript.Validate(a.SignerPubKey, a.BoardingExitDelay, false) before persisting — ensures only protocol-valid scripts can be registered. Correct.

addInputs fix (unroll.go): Moving from wallet.NewAddress to per-UTXO script.ParseVtxoScript(utxo.Tapscripts) is the right fix. The old code would have silently used the wrong script for multi-script wallets, causing malformed unroll PSBTs.

Store deduplication: AddBoardingDescriptor is a no-op for duplicate addresses (tested explicitly). Good — prevents bloat and double-presentation in GetAddresses.

Test coverage: Both in-memory and file store paths tested with add/retrieve/dedup/multi-descriptor cases.


Questions / potential issues

1. No removal / cleanup path

AddBoardingDescriptor is add-only. Once a custom boarding address is registered in the wallet, it persists forever and will be returned from GetAddresses on every call. For addresses that have been spent/swept this could accumulate indefinitely and pollute the address list. Should there be a RemoveBoardingDescriptor or a swept-check during GetAddresses?

2. Script validation uses BoardingExitDelay — is this always correct?

NewBoardingAddress validates vtxoScript against a.BoardingExitDelay. But for unrolled VTXO re-entry (the primary use case of PR #961 which this appears to complement), the correct delay is unilateralExitDelay. If a caller passes a script built with unilateralExitDelay, the validation here will reject it. Is there an intent for this API to also serve the #961 unrolled-rejoin flow, or is it strictly for custom boarding scripts? If both, the validation should accept either delay (or take the expected delay as a parameter).

3. File store concurrent writes

In filestore.AddBoardingDescriptor, the pattern is: read file → unmarshal → append → write file. There is no file lock between read and write. Concurrent calls from multiple goroutines could race and lose descriptors. The existing wallet data writes appear to have the same pattern — is this acceptable because the client-lib is single-threaded in practice, or should a mutex be added?

4. GetAddresses ordering

Custom boarding addresses are appended before the default boarding address (from walletData). This is fine functionally but could confuse callers that expect the default boarding address to be first. Low priority — just worth noting for SDK consumers.


Nits

  • NewBoardingAddress on bitcoinWallet calls configStore.GetData to resolve network params — this could be cached (same pattern is used in NewAddresses). Not a correctness issue.
  • The file store uses raw map[string]any for the JSON roundtrip which requires a manual type assertion for boarding_descriptors. A typed struct for the full file format would be safer long-term.

Cross-repo impact

This adds a new method to the ArkClient interface and WalletService interface. Any third-party implementations of these interfaces (outside the monorepo) will need to add NewBoardingAddress. If there are external implementors, this is a breaking interface change. Worth noting in the PR description.


Verdict

The core functionality is correct and the unroll fix is a genuine bug fix. Questions 1 and 2 are the most important to address — especially the delay mismatch if this is meant to serve the unrolled-VTXO rejoin path.

— Arkana

@bitcoin-coder-bob
Copy link
Copy Markdown
Collaborator Author

@arkanaai answering your questions:

1: I don't think there enough times that people board (and thus calls to NewBoardingAddress to warrant a need for removal.

2: NewBoardingAddress is strictly for new custom boarding scripts is is not involved in the unrolled-VTXO rejoin flow at all. PR #961 handles unrolled re-entry entirely server-side: RegisterIntent tags unrolled VTXOs with isUnrolledVtxo: true, and validateBoardingInput/processBoardingInputs switch to UnilateralExitDelay for those inputs. The client never calls NewBoardingAddress for that path — the VTXOs already have their tapscripts from the original tree. So BoardingExitDelay is the correct floor here. Any script registered through NewBoardingAddress will sit on-chain as a fresh boarding UTXO waiting to be claimed in a round, and the longer delay is needed to prevent the user from racing the server via the exit path.

@arkanaai
Copy link
Copy Markdown
Contributor

arkanaai bot commented Mar 16, 2026

Thanks @bitcoin-coder-bob — that clarifies both points.

On (1): Makes sense to keep it add-only for now given boarding frequency. If the address list ever grows unwieldy in long-lived wallets it'd be easy to add a cleanup pass later.

On (2): Understood — NewBoardingAddress is strictly for fresh boarding scripts, and the unrolled-VTXO rejoin path in PR #961 is entirely server-side. So BoardingExitDelay as the validation floor is correct here, and there's no delay mismatch concern.

That resolves my main blockers. The concurrent file write concern (point 3) is the one remaining nit but if the client-lib is single-goroutine in practice it's not a correctness issue. Good to go from my side.

@bitcoin-coder-bob bitcoin-coder-bob marked this pull request as ready for review March 26, 2026 11:34
@arkanaai
Copy link
Copy Markdown
Contributor

arkanaai bot commented Mar 26, 2026

[Arkana Review — PR #966: allow flexible boarding scripts in client-lib (follow-up)]

Reviewed at: 2026-03-26 11:34 UTC | Head: 46989c8


New since last review

Two commits since my initial review: a merge from master and a test commit (add tests for NewBoardingAddress and addInputs per-utxo fix).

Test coverage assessment

unroll_test.goTestAddInputsPerUtxoScript: Solid regression test. Creates two UTXOs with different owner keys, feeds them through addInputs, and verifies each PSBT input gets its own distinct tapscript and control block. This directly guards against the bug the PR fixes (shared script derived from a fresh address). Good.

wallet_test.goTestNewBoardingAddress: Covers the full lifecycle:

  • Default boarding address baseline
  • Custom address via NewBoardingAddress
  • Deduplication (same script → no-op)
  • Multiple distinct scripts accumulate correctly

Both test files are well-structured. No gaps I can see for the current scope.

Previously raised points — status

# Issue Status
1 No removal/cleanup path for boarding descriptors Addressed by author — add-only is fine given low boarding frequency
2 BoardingExitDelay vs UnilateralExitDelay mismatch concern Addressed by author — NewBoardingAddress is strictly for fresh boarding, not unrolled-VTXO rejoin (#961 handles that server-side)
3 File store concurrent writes (no lock between read/write) Acknowledged as non-issue for single-goroutine client-lib — acceptable
4 GetAddresses ordering (custom before default) Low priority, noted

All main concerns resolved. No new issues introduced by the test commit.

— Arkana

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (1)
pkg/client-lib/unroll.go (1)

555-558: Include outpoint context when tapscript parsing fails.

Returning the raw parse error makes debugging multi-input failures harder; include Txid:VOut in the error.

Proposed patch
 		vtxoScript, err := script.ParseVtxoScript(utxo.Tapscripts)
 		if err != nil {
-			return err
+			return fmt.Errorf("parse vtxo tapscripts for %s:%d: %w", utxo.Txid, utxo.VOut, err)
 		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/client-lib/unroll.go` around lines 555 - 558, The ParseVtxoScript call
returns raw errors without context; modify the error path after vtxoScript, err
:= script.ParseVtxoScript(utxo.Tapscripts) to wrap or return a new error that
includes the outpoint identification (Txid:VOut) from the utxo (e.g., utxo.Txid
and utxo.Vout) so callers can see which input failed; update the return to
include that string alongside the original err (use fmt.Errorf or an
errors.Wrap-style pattern in the same function in unroll.go).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@pkg/client-lib/unroll_test.go`:
- Around line 17-19: The test currently ignores errors from
btcec.NewPrivateKey() for signerKey, ownerKey1, and ownerKey2; update
unroll_test.go so each call checks the returned error and fails the test on
error (e.g., use t.Fatal/t.Fatalf or require.NoError) before using the keys,
ensuring signerKey, ownerKey1 and ownerKey2 are not nil if key generation fails.

In `@pkg/client-lib/wallet/singlekey/bitcoin_wallet.go`:
- Around line 180-227: NewBoardingAddress currently persists any vtxoScript
allowing callers to register a boarding script with an exit delay below the
configured floor; before calling w.walletStore.AddBoardingDescriptor in
bitcoinWallet.NewBoardingAddress, retrieve data.BoardingExitDelay (already
fetched from w.configStore) and validate the script's exit delay (e.g. using the
VtxoScript method that exposes its exit/lock delay) is >=
data.BoardingExitDelay; if it is below the floor return an error (same behavior
as WalletService.NewBoardingAddress or a shared helper), so the check occurs in
this path prior to persisting the descriptor.
- Around line 215-221: The code is persisting the built-in default boarding
descriptor, causing the default boarding address to be duplicated by
GetAddresses; before calling w.walletStore.AddBoardingDescriptor(descriptor) in
the method that builds the BoardingDescriptor, detect if the constructed
descriptor matches the built-in default (compare the Address and/or Tapscripts
against the known default boarding descriptor used by GetAddresses, e.g., the
walletstore's default descriptor or the value returned by a
DefaultBoardingDescriptor helper) and skip calling AddBoardingDescriptor when it
is identical to that default.

In `@pkg/client-lib/wallet/singlekey/store/file/store.go`:
- Around line 146-150: The file write in the wallet store currently marshals
currentData and writes to s.filePath with mode 0755; change the permission to
owner-only (0600) so the wallet state is not world-readable/executable: after
json.Marshal(currentData) use os.WriteFile(s.filePath, jsonString, 0600) (in the
same function handling currentData and s.filePath) to harden state.json
permissions.

In `@pkg/client-lib/wallet/singlekey/store/inmemory/store.go`:
- Around line 35-55: The boarding descriptor Tapscripts slice is shared between
callers and the store; create a helper cloneDescriptor(descriptor
walletstore.BoardingDescriptor) walletstore.BoardingDescriptor that deep-copies
the Tapscripts slice (make and copy the slice) and use it in
AddBoardingDescriptor to append a cloned descriptor and in
GetBoardingDescriptors to return a slice of cloned descriptors (copy each
element via cloneDescriptor) so callers cannot mutate the store's internal
slices via shared slice headers.

---

Nitpick comments:
In `@pkg/client-lib/unroll.go`:
- Around line 555-558: The ParseVtxoScript call returns raw errors without
context; modify the error path after vtxoScript, err :=
script.ParseVtxoScript(utxo.Tapscripts) to wrap or return a new error that
includes the outpoint identification (Txid:VOut) from the utxo (e.g., utxo.Txid
and utxo.Vout) so callers can see which input failed; update the return to
include that string alongside the original err (use fmt.Errorf or an
errors.Wrap-style pattern in the same function in unroll.go).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ada211f3-7ade-4388-adc9-be8585921f03

📥 Commits

Reviewing files that changed from the base of the PR and between 4b4ff48 and 46989c8.

📒 Files selected for processing (11)
  • pkg/client-lib/ark_sdk.go
  • pkg/client-lib/funding.go
  • pkg/client-lib/unroll.go
  • pkg/client-lib/unroll_test.go
  • pkg/client-lib/wallet/singlekey/bitcoin_wallet.go
  • pkg/client-lib/wallet/singlekey/store/file/store.go
  • pkg/client-lib/wallet/singlekey/store/inmemory/store.go
  • pkg/client-lib/wallet/singlekey/store/store.go
  • pkg/client-lib/wallet/singlekey/store/store_test.go
  • pkg/client-lib/wallet/wallet.go
  • pkg/client-lib/wallet/wallet_test.go

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
pkg/client-lib/unroll.go (1)

534-534: Remove the unused context.Context parameter from addInputs.

Line 534 keeps _ context.Context even though the helper no longer uses context. Since this is a private method, you can simplify the signature and both call sites.

Suggested diff
-func (a *service) addInputs(
-	_ context.Context, updater *psbt.Updater, utxos []types.Utxo,
-) error {
+func (a *service) addInputs(
+	updater *psbt.Updater, utxos []types.Utxo,
+) error {
-	if err := a.addInputs(ctx, updater, utxos); err != nil {
+	if err := a.addInputs(updater, utxos); err != nil {
-	if err := a.addInputs(ctx, updater, utxos); err != nil {
+	if err := a.addInputs(updater, utxos); err != nil {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/client-lib/unroll.go` at line 534, The addInputs helper currently has an
unused first parameter _ context.Context; remove the context.Context parameter
from addInputs' signature and update both call sites to stop passing a context.
Locate the private function named addInputs and eliminate the leading context
argument, then find callers that pass a context value (e.g., addInputs(...)) and
adjust them to pass only the remaining parameters (updater *psbt.Updater, utxos
[]types.Utxo), ensuring imports/usages are updated accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@pkg/client-lib/unroll.go`:
- Line 534: The addInputs helper currently has an unused first parameter _
context.Context; remove the context.Context parameter from addInputs' signature
and update both call sites to stop passing a context. Locate the private
function named addInputs and eliminate the leading context argument, then find
callers that pass a context value (e.g., addInputs(...)) and adjust them to pass
only the remaining parameters (updater *psbt.Updater, utxos []types.Utxo),
ensuring imports/usages are updated accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4e0bd895-7b33-40d7-bdc7-47eeb5e44835

📥 Commits

Reviewing files that changed from the base of the PR and between 46989c8 and 0b78ac4.

📒 Files selected for processing (5)
  • pkg/client-lib/unroll.go
  • pkg/client-lib/unroll_test.go
  • pkg/client-lib/wallet/singlekey/bitcoin_wallet.go
  • pkg/client-lib/wallet/singlekey/store/file/store.go
  • pkg/client-lib/wallet/singlekey/store/inmemory/store.go
🚧 Files skipped from review as they are similar to previous changes (3)
  • pkg/client-lib/unroll_test.go
  • pkg/client-lib/wallet/singlekey/bitcoin_wallet.go
  • pkg/client-lib/wallet/singlekey/store/file/store.go

@arkanaai
Copy link
Copy Markdown
Contributor

arkanaai bot commented Mar 26, 2026

🔍 Arkana Review — arkade-os/arkd#966

Flexible boarding scripts in client-lib — good feature, well-structured. Key observations:

Security Analysis

Validation is properly layeredvtxoScript.Validate() is called in both the service layer (funding.go:NewBoardingAddress) and the wallet layer (bitcoin_wallet.go:NewBoardingAddress). This is correct: direct callers of WalletService bypass the service layer, so the wallet-level validation prevents persistence of scripts with invalid exit delays. ✅

Exit delay floor enforcementValidate(a.SignerPubKey, a.BoardingExitDelay, false) correctly enforces that the custom script's exit delay is not below the operator's configured floor. The false parameter (not requiring exact match) is appropriate for boarding scripts where users may want longer delays.

Per-UTXO script derivation fix — The unroll.go change is a critical bugfix. Previously, addInputs derived vtxoScript from a freshly generated address (same script for all UTXOs). Now each UTXO's tapscripts are parsed individually. Without this fix, spending UTXOs with different scripts would produce incorrect PSBT witness data, potentially causing transaction failures or fund loss.

Code Quality

  • DeduplicationAddBoardingDescriptor correctly deduplicates by address in both file and in-memory stores. ✅
  • File permission fix07550600 on wallet store file is a good security hygiene improvement. ✅
  • In-memory store cloningcloneDescriptor() prevents mutation of internal state through shared slice references. ✅
  • Test coverageTestAddInputsPerUtxoScript directly validates the unroll fix by asserting different leaf scripts and control blocks per input. TestBoardingDescriptors covers persistence and deduplication across both store types. ✅

Minor Observations

  1. File store read-modify-writeAddBoardingDescriptor in the file store reads the entire JSON, deserializes, modifies, and writes back. There's no file locking — concurrent calls could race. This matches the existing AddWallet pattern, so it's consistent, but worth noting for future multi-process scenarios.

  2. No upper bound on custom boarding descriptors — A client could accumulate many custom boarding addresses over time. Consider adding a reasonable cap or documenting expected limits.

  3. ctx parameter unused in addInputs — The change to _ context.Context is clean, but worth checking if any downstream callers expect context propagation here.

Overall: solid implementation with correct security boundaries. The unroll fix alone is high-value.

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.

Allow flexible boarding scripts

1 participant