-
Notifications
You must be signed in to change notification settings - Fork 9
OOR client 1/4: package primitives + validation baseline #77
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
3442d28
17ff76d
fa57785
1551d23
1344c2c
7a8ef31
3f9fbc0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,103 @@ | ||
| package scripts | ||
|
|
||
| import ( | ||
| "fmt" | ||
|
|
||
| "github.com/btcsuite/btcd/btcec/v2" | ||
| "github.com/btcsuite/btcd/txscript" | ||
| "github.com/btcsuite/btcwallet/waddrmgr" | ||
| "github.com/lightningnetwork/lnd/input" | ||
| ) | ||
|
|
||
| // CheckpointPolicy defines the parameters for constructing an OOR checkpoint | ||
| // taproot tree. | ||
| // | ||
| // This is intentionally interface-first and minimal: it provides enough | ||
| // information to deterministically derive a checkpoint output pkScript, while | ||
| // allowing the underlying closure system to evolve later. | ||
| type CheckpointPolicy struct { | ||
| // OperatorKey is the public key required by the operator-controlled CSV | ||
| // unroll leaf. | ||
| OperatorKey *btcec.PublicKey | ||
|
|
||
| // CSVDelay is the relative timelock enforced by the | ||
| // operator-controlled leaf. | ||
| // | ||
| // This is a raw BIP-68 sequence value interpreted by | ||
| // OP_CHECKSEQUENCEVERIFY. | ||
| CSVDelay uint32 | ||
| } | ||
|
|
||
| // CheckpointTapScript constructs the tapscript for an OOR checkpoint output. | ||
| // | ||
| // The checkpoint tree for v0 is a simple two-leaf tree: | ||
| // | ||
| // - an operator-controlled CSV unroll leaf (operator key + relative | ||
| // timelock), | ||
| // - a collaborative leaf between operator and VTXO owner (provided by the | ||
| // caller as raw script bytes). | ||
| // | ||
| // "Owner" here means the owner of the VTXO being refreshed. The checkpoint | ||
| // output itself is still operator-controlled on the CSV timeout path. | ||
| // | ||
|
Comment on lines
+33
to
+42
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
| // For v0, the checkpoint output always uses the ARK NUMS internal key so there | ||
| // is no key-path spend and all spends go through one of the script leaves. | ||
| // | ||
| // This function does not validate that ownerLeafScript is "a correct Ark | ||
| // closure". That validation belongs in higher layers once the canonical closure | ||
| // system is in place (see the closures PRs). For now, this gives OOR primitives | ||
| // a deterministic way to bind checkpoint scripts. | ||
| func CheckpointTapScript(policy CheckpointPolicy, | ||
| ownerLeafScript []byte) (*waddrmgr.Tapscript, error) { | ||
|
|
||
| switch { | ||
| case policy.OperatorKey == nil: | ||
| return nil, fmt.Errorf("operator key must be provided") | ||
|
|
||
| case len(ownerLeafScript) == 0: | ||
| return nil, fmt.Errorf("owner leaf script must be provided") | ||
| } | ||
|
|
||
| unrollLeaf, err := UnilateralCSVTimeoutTapLeaf( | ||
| policy.OperatorKey, policy.CSVDelay, | ||
| ) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("unable to construct unroll leaf: %w", | ||
| err) | ||
| } | ||
|
|
||
| ownerLeaf := txscript.NewBaseTapLeaf(ownerLeafScript) | ||
|
|
||
| tapscript := input.TapscriptFullTree(&ARKNUMSKey, unrollLeaf, ownerLeaf) | ||
|
|
||
| // Compute and set the root hash since TapscriptFullTree doesn't | ||
| // populate it. | ||
| tree := txscript.AssembleTaprootScriptTree(tapscript.Leaves...) | ||
| rootHash := tree.RootNode.TapHash() | ||
| tapscript.RootHash = rootHash[:] | ||
|
|
||
| return tapscript, nil | ||
| } | ||
|
|
||
| // CheckpointPkScript returns the pkScript for a checkpoint output produced by | ||
| // CheckpointTapScript. | ||
| // | ||
| // The caller should treat this as the canonical way to derive checkpoint output | ||
| // scripts for v0 OOR transfers so both client and server can validate and | ||
| // serialize checkpoint transactions deterministically. | ||
| func CheckpointPkScript(policy CheckpointPolicy, | ||
| ownerLeafScript []byte) ([]byte, error) { | ||
|
|
||
| tapscript, err := CheckpointTapScript(policy, ownerLeafScript) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| tapKey, err := tapscript.TaprootKey() | ||
| if err != nil { | ||
| return nil, fmt.Errorf("unable to compute taproot key: %w", | ||
| err) | ||
| } | ||
|
|
||
| return txscript.PayToTaprootScript(tapKey) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| package scripts | ||
|
|
||
| import ( | ||
| "testing" | ||
|
|
||
| "github.com/btcsuite/btcd/btcec/v2" | ||
| "github.com/btcsuite/btcd/txscript" | ||
| "github.com/stretchr/testify/require" | ||
| ) | ||
|
|
||
| // TestCheckpointPkScriptIsTaproot asserts that the checkpoint helper returns a | ||
| // valid P2TR script and that it binds the expected internal key and tap tree. | ||
| func TestCheckpointPkScriptIsTaproot(t *testing.T) { | ||
| t.Parallel() | ||
|
|
||
| operatorKey, err := btcec.NewPrivateKey() | ||
| require.NoError(t, err) | ||
|
|
||
| policy := CheckpointPolicy{ | ||
| OperatorKey: operatorKey.PubKey(), | ||
| CSVDelay: 10, | ||
| } | ||
|
|
||
| ownerLeafScript := []byte{ | ||
| txscript.OP_1, | ||
| txscript.OP_1, | ||
| txscript.OP_ADD, | ||
| txscript.OP_2, | ||
| txscript.OP_EQUAL, | ||
| } | ||
|
|
||
| tapscript, err := CheckpointTapScript(policy, ownerLeafScript) | ||
| require.NoError(t, err) | ||
| require.NotNil(t, tapscript) | ||
|
|
||
| pkScript, err := CheckpointPkScript(policy, ownerLeafScript) | ||
| require.NoError(t, err) | ||
| require.True(t, txscript.IsPayToTaproot(pkScript)) | ||
|
|
||
| tree := txscript.AssembleTaprootScriptTree(tapscript.Leaves...) | ||
| expectedRoot := tree.RootNode.TapHash() | ||
| require.Equal(t, expectedRoot[:], tapscript.RootHash) | ||
|
|
||
| expectedKey := txscript.ComputeTaprootOutputKey( | ||
| &ARKNUMSKey, tapscript.RootHash, | ||
| ) | ||
|
|
||
| actualKey, err := tapscript.TaprootKey() | ||
| require.NoError(t, err) | ||
| require.Equal(t, expectedKey.SerializeCompressed(), | ||
| actualKey.SerializeCompressed()) | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.