Skip to content

OOR client 1/4: package primitives + validation baseline - #77

Merged
bhandras merged 7 commits into
mainfrom
oor-client-split-1
Feb 11, 2026
Merged

OOR client 1/4: package primitives + validation baseline#77
bhandras merged 7 commits into
mainfrom
oor-client-split-1

Conversation

@bhandras

@bhandras bhandras commented Jan 12, 2026

Copy link
Copy Markdown
Member

Context

Companion client stack for OOR epic: lightninglabs/darepo#89.

This split introduces protocol primitives and validators that all later OOR FSM
and durability work depends on.

Scope

  • Add canonical OOR submit/finalize package primitives.
  • Add validation rules for OOR package shape and semantics.
  • Add checkpoint helper script primitives used by OOR tx construction.
  • Include baseline cleanup needed by this stack.

Included

  • lib/tx/oor primitives + validators.
  • Checkpoint helper scripts.
  • Unit coverage for package validation.

Not Included

  • FSM orchestration.
  • Actor runtime integration.
  • Snapshot/retry behavior.

Testing

  • Unit tests for submit/finalize validators and primitives.

Stack

@gemini-code-assist

Copy link
Copy Markdown

Summary of Changes

Hello @bhandras, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request lays the groundwork for Off-chain Out-of-band Relay (OOR) transactions by implementing core building blocks. It focuses on creating and validating the structure of these specialized transactions using Partially Signed Bitcoin Transactions (PSBTs), ensuring they adhere to specific ordering rules and contain necessary metadata. The changes also involve refactoring existing transaction-related code to improve modularity and prepare for future extensions of the OOR protocol.

Highlights

  • New OOR Transaction Primitives and Builders: Introduced foundational components for Off-chain Out-of-band Relay (OOR) transactions, including PSBT primitives, builders, and validation logic for both checkpoint and Ark transactions.
  • Modularization of Ark Transaction Logic: Split existing arktx and checkpoint primitives and their PSBT encodings into distinct, more modular packages, enhancing code organization and maintainability.
  • Comprehensive Validation and Test Coverage: Added extensive validation functions for canonical Ark transactions and PSBTs, ensuring correct ordering and structure. New test cases were also introduced to cover the functionality of these new primitives and builders.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request introduces primitives, builders, and validation for Off-chain to On-chain Refresh (OOR) transactions. It also refactors arktx and checkpoint primitives and their PSBT encodings, and extends test coverage. The changes are well-structured, with clear separation of concerns between transaction building, canonicalization, and validation. The introduction of package-level structs for SubmitPackage and FinalizePackage with JSON serialization is a good step towards durable and transportable transaction bundles.

I've found a critical issue in the tap tree decoding logic that would prevent scripts from being correctly decoded, and a couple of medium-severity issues related to efficiency and robustness. My detailed comments are below.

Comment thread lib/tx/checkpoint/taptree.go Outdated
Comment on lines +198 to +202
if t, ok := parsedTypes[typeTapLeafScript]; ok {
if t == nil {
leaf.Script = script
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

critical

There appears to be a bug in the logic for decoding the tapleaf script. The condition if t == nil will likely always be false for a known TLV type like typeTapLeafScript. When tlv.Stream.DecodeWithParsedTypes finds a known type, it returns a non-nil tlv.Record in the parsedTypes map. A nil value is used to indicate an unknown TLV type was encountered. Because typeTapLeafScript is a known type in the decoder stream, t will not be nil, and leaf.Script will never be assigned. This will result in decoded leaves having nil scripts.

The correct approach, as seen in btcwallet, is to just check for the presence of the key in the map. The script variable is populated by the DecodeWithParsedTypes call via the pointer passed to MakePrimitiveRecord.

if _, ok := parsedTypes[typeTapLeafScript]; ok {
	leaf.Script = script
}

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.

Fixed in split-1: tapleaf decode now sets leaf.Script whenever the tapleaf-script type is present, so decode no longer depends on the map entry value being nil.

Comment thread lib/tx/checkpoint/build.go Outdated
Comment on lines +72 to +89
tapscript, err := scripts.CheckpointTapScript(
policy, in.OwnerLeafScript,
)
if err != nil {
return nil, err
}

encodedTapTree, err := EncodeTapTree(tapLeafScripts(tapscript.Leaves))
if err != nil {
return nil, err
}

checkpointPkScript, err := scripts.CheckpointPkScript(
policy, in.OwnerLeafScript,
)
if err != nil {
return nil, err
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The BuildPSBT function currently calls scripts.CheckpointTapScript and then scripts.CheckpointPkScript. However, scripts.CheckpointPkScript internally calls scripts.CheckpointTapScript again, leading to redundant computation.

You can optimize this by computing the checkpointPkScript directly from the tapscript variable that is already available. This avoids the second call to CheckpointTapScript.

tapscript, err := scripts.CheckpointTapScript(
	policy, in.OwnerLeafScript,
)
if err != nil {
	return nil, err
}

encodedTapTree, err := EncodeTapTree(tapLeafScripts(tapscript.Leaves))
if err != nil {
	return nil, err
}

tapKey, err := tapscript.TaprootKey()
if err != nil {
	return nil, fmt.Errorf("unable to compute taproot key: %w", err)
}

checkpointPkScript, err := txscript.PayToTaprootScript(tapKey)
if err != nil {
	return nil, fmt.Errorf("unable to create p2tr script: %w", err)
}

@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 in latest push: simplified this path by deriving taproot key from tapscript once and building the output script directly (PayToTaprootScript), removing the redundant recomputation path.

Comment thread lib/tx/oor/taptree.go
Comment on lines +71 to +85
func GetTapTreePSBTInput(input psbt.PInput) ([]byte, error) {
for _, u := range input.Unknowns {
if bytes.Equal(u.Key, TapTreePSBTKey) {
if len(u.Value) == 0 {
return nil, fmt.Errorf(
"tap tree value is empty",
)
}

return u.Value, nil
}
}

return nil, fmt.Errorf("tap tree not found")
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The GetTapTreePSBTInput function returns the value of the first taptree key it finds in the PSBT input's unknown fields. If multiple taptree keys are present, it will silently ignore the subsequent ones. This could lead to ambiguity if a PSBT is malformed with multiple taptree entries.

To make this more robust, the function should ensure that there is exactly one taptree key and return an error if multiple are found.

func GetTapTreePSBTInput(input psbt.PInput) ([]byte, error) {
	var tapTreeValue []byte
	var found bool

	for _, u := range input.Unknowns {
		if bytes.Equal(u.Key, TapTreePSBTKey) {
			if found {
				return nil, fmt.Errorf("multiple tap tree " +
					"entries found")
			}

			if len(u.Value) == 0 {
				return nil, fmt.Errorf(
					"tap tree value is empty",
				)
			}

			tapTreeValue = u.Value
			found = true
		}
	}

	if !found {
		return nil, fmt.Errorf("tap tree not found")
	}

	return tapTreeValue, nil
}

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.

Addressed: GetTapTreePSBTInput now rejects duplicate tap tree entries instead of accepting first-match.

@bhandras
bhandras marked this pull request as draft January 12, 2026 18:25
@bhandras bhandras changed the title OOR tx primitives + builders OOR[1/3] tx primitives + builders Jan 12, 2026
@bhandras bhandras changed the title OOR[1/3] tx primitives + builders OOR client split 1: primitives + validation Jan 13, 2026
@Roasbeef
Roasbeef force-pushed the vtxo-actor-manager branch 2 times, most recently from d4fd6c0 to 7d7f9bf Compare January 15, 2026 01:00
@bhandras
bhandras force-pushed the oor-client-split-1 branch 2 times, most recently from f6e2018 to 5af35c8 Compare January 18, 2026 08:52
@bhandras
bhandras changed the base branch from vtxo-actor-manager to main January 18, 2026 08:52
@bhandras
bhandras marked this pull request as ready for review January 18, 2026 09:50
@litbot-9000

Copy link
Copy Markdown
Collaborator

@bhandras, remember to re-request review from reviewers when ready

@ellemouton ellemouton left a comment

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.

nice work! great structure.

Main blocking questions are for the very last commit wrt checkpoint txs. But perhaps those questions will be answered as I make my way through the series

Comment thread lib/tx/oor/taptree.go
// We treat this as part of the OOR PSBT profile so client and server
// implementations can deterministically attach, validate, and later use
// the same metadata during finalization.
TapTreePSBTKey = []byte("taptree")

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.

wondering what is a good rule for knowing when to add data to PSBT kv pairs vs when to explicitly communicate it.

For example, for the VTX tree signing, I opted to explicitly communicate co-signers rather than embedding it in the PSBT like was done in arkade.

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.

Rule we’re using: put tx-bound signing material in PSBT; keep session/protocol control data explicit in RPC fields.

Comment thread lib/tx/oor/submit.go
Comment thread lib/scripts/checkpoint_oor.go Outdated
Comment thread lib/scripts/checkpoint_oor.go
Comment on lines +40 to +46
// The checkpoint tree for v0 is a simple two-leaf tree:
//
// - an operator-controlled CSV unroll leaf (operator key + relative
// timelock),
// - an owner-controlled collaborative leaf (provided by the caller as raw
// script).
//

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.

hmm im a bit confused here. in my mind, the checkpoint is "owned" by the operator since the operator is the party who can sweep after the CSV (so that is the operator-controlled CSV leaf path), then : the other leave is a collab multisig between operator and client. ie, it is important that that is the exact script

@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.

Clarified the docs: checkpoint has operator CSV timeout leaf + collaborative operator/owner leaf, and this helper only commits the provided script bytes into the tree. Higher layers are responsible for enforcing that those bytes are the exact expected closure script.

@bhandras
bhandras force-pushed the oor-client-split-1 branch 5 times, most recently from acc8c5c to 2b7d0fa Compare February 6, 2026 12:52
@bhandras bhandras changed the title OOR client split 1: primitives + validation OOR client 1/4: package primitives + validation baseline Feb 6, 2026
@bhandras
bhandras force-pushed the oor-client-split-1 branch 2 times, most recently from 24cbc6e to 9ffa9bb Compare February 6, 2026 15:43
@bhandras
bhandras requested a review from ellemouton February 6, 2026 17:39

@ellemouton ellemouton left a comment

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.

noice!

(cherry picked from commit eadff97)
Add a v0 structural validator for OOR submit packages.

The validator enforces canonical Ark PSBT ordering, ensures each Ark
input spends a provided checkpoint tx output (vout=0), checks that Ark
PSBT witness UTXOs match the referenced checkpoint output, and requires
per-input `taptree` metadata for later finalization.

Unit tests cover a happy path and common failure cases.

(cherry picked from commit 1834eea)
Add a v0 structural validator for OOR finalize packages.

The validator checks that the provided checkpoint PSBT set matches the
Ark tx input checkpoint set (txid:vout=0), and requires each checkpoint
PSBT to include some signature material (final witness/script or taproot
sig fields).

Unit tests cover a happy path and common failure cases.

(cherry picked from commit 9f97816)
Add draft OOR checkpoint script helpers.

This introduces a minimal CheckpointPolicy and helpers to deterministically
construct a two-leaf checkpoint taproot tree (operator CSV unroll leaf plus
caller-provided owner leaf) and derive the corresponding P2TR pkScript.

The implementation lives in new files to minimize overlap with ongoing
closure/vtxo refactors, and is intended to be swapped to closure-based
building later.

Unit tests assert the result is a valid P2TR pkScript and that the tapscript
root hash and output key are computed consistently.

(cherry picked from commit f6e2018)
Address lint failures introduced by rebasing `oor-client-split-1`
onto the latest `main`.

Fix `gocritic`'s append-assign warning in the OOR tap tree
helper, shorten comment/tag lines to satisfy the custom `ll`
80-column rule, and remove the now-unneeded `nolint` directive
in DB store config.
@bhandras
bhandras merged commit 945faf1 into main Feb 11, 2026
16 checks passed
@bhandras
bhandras deleted the oor-client-split-1 branch February 11, 2026 11:36
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.

3 participants