Change representation of taproot trees & Internal fixes (#384)

* migrate descriptors --> tapscripts

* fix covenantless

* dynamic boarding exit delay

* remove duplicates in tree and bitcointree

* agnostic signatures validation

* revert GetInfo change

* renaming VtxoScript var

* Agnostic script server (#6)

* Hotfix: Prevent ZMQ-based bitcoin wallet to panic  (#383)

* Hotfix bct embedded wallet w/ ZMQ

* Fixes

* Rename vtxo is_oor to is_pending (#385)

* Rename vtxo is_oor > is_pending

* Clean swaggers

* Revert changes to client and sdk

* descriptor in oneof

* support CHECKSIG_ADD in MultisigClosure

* use right witness size in OOR tx fee estimation

* Revert changes

---------

Co-authored-by: Pietralberto Mazza <18440657+altafan@users.noreply.github.com>
This commit is contained in:
Louis Singer
2024-11-20 18:51:03 +01:00
committed by GitHub
parent 403a82e25e
commit 06dd01ecb1
44 changed files with 2470 additions and 1718 deletions

View File

@@ -651,6 +651,9 @@
}, },
"descriptor": { "descriptor": {
"type": "string" "type": "string"
},
"tapscripts": {
"$ref": "#/definitions/v1Tapscripts"
} }
} }
}, },
@@ -748,6 +751,9 @@
}, },
"descriptor": { "descriptor": {
"type": "string" "type": "string"
},
"tapscripts": {
"$ref": "#/definitions/v1Tapscripts"
} }
} }
}, },
@@ -824,7 +830,7 @@
"title": "VTXO outpoint signed with script's secret key" "title": "VTXO outpoint signed with script's secret key"
} }
}, },
"description": "This message is used to prove to the ASP that the user controls the vtxo without revealing the whole VTXO descriptor." "description": "This message is used to prove to the ASP that the user controls the vtxo without revealing the whole VTXO taproot tree."
}, },
"v1PingResponse": { "v1PingResponse": {
"type": "object" "type": "object"
@@ -1137,6 +1143,17 @@
"v1SubmitTreeSignaturesResponse": { "v1SubmitTreeSignaturesResponse": {
"type": "object" "type": "object"
}, },
"v1Tapscripts": {
"type": "object",
"properties": {
"scripts": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"v1Tree": { "v1Tree": {
"type": "object", "type": "object",
"properties": { "properties": {

View File

@@ -133,7 +133,10 @@ message GetBoardingAddressRequest {
} }
message GetBoardingAddressResponse { message GetBoardingAddressResponse {
string address = 1; string address = 1;
string descriptor = 2; oneof taproot_tree {
string descriptor = 2;
Tapscripts tapscripts = 3;
}
} }
/* In-Round Payment API messages */ /* In-Round Payment API messages */
@@ -298,7 +301,10 @@ message Outpoint {
message Input { message Input {
Outpoint outpoint = 1; Outpoint outpoint = 1;
string descriptor = 2; oneof taproot_tree {
string descriptor = 2;
Tapscripts tapscripts = 3;
}
} }
message Output { message Output {
@@ -355,7 +361,7 @@ message RedeemTransaction {
repeated Vtxo spendable_vtxos = 3; repeated Vtxo spendable_vtxos = 3;
} }
// This message is used to prove to the ASP that the user controls the vtxo without revealing the whole VTXO descriptor. // This message is used to prove to the ASP that the user controls the vtxo without revealing the whole VTXO taproot tree.
message OwnershipProof { message OwnershipProof {
string control_block = 1; string control_block = 1;
string script = 2; string script = 2;
@@ -379,3 +385,7 @@ message DeleteNostrRecipientRequest {
} }
message DeleteNostrRecipientResponse {} message DeleteNostrRecipientResponse {}
message Tapscripts {
repeated string scripts = 1;
}

File diff suppressed because it is too large Load Diff

View File

@@ -282,17 +282,19 @@ func createRootNode(
func createAggregatedKeyWithSweep( func createAggregatedKeyWithSweep(
cosigners []*secp256k1.PublicKey, aspPubkey *secp256k1.PublicKey, roundLifetime int64, cosigners []*secp256k1.PublicKey, aspPubkey *secp256k1.PublicKey, roundLifetime int64,
) (*musig2.AggregateKey, *psbt.TaprootTapLeafScript, error) { ) (*musig2.AggregateKey, *psbt.TaprootTapLeafScript, error) {
sweepClosure := &CSVSigClosure{ sweepClosure := &tree.CSVSigClosure{
Pubkey: aspPubkey, MultisigClosure: tree.MultisigClosure{PubKeys: []*secp256k1.PublicKey{aspPubkey}},
Seconds: uint(roundLifetime), Seconds: uint(roundLifetime),
} }
sweepLeaf, err := sweepClosure.Leaf() sweepScript, err := sweepClosure.Script()
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
tapTree := txscript.AssembleTaprootScriptTree(*sweepLeaf) sweepLeaf := txscript.NewBaseTapLeaf(sweepScript)
tapTree := txscript.AssembleTaprootScriptTree(sweepLeaf)
tapTreeRoot := tapTree.RootNode.TapHash() tapTreeRoot := tapTree.RootNode.TapHash()
aggregatedKey, err := AggregateKeys( aggregatedKey, err := AggregateKeys(

View File

@@ -1,7 +1,6 @@
package bitcointree_test package bitcointree_test
import ( import (
"encoding/hex"
"encoding/json" "encoding/json"
"os" "os"
"testing" "testing"
@@ -49,7 +48,7 @@ func TestRoundTripSignTree(t *testing.T) {
) )
require.NoError(t, err) require.NoError(t, err)
tree, err := bitcointree.CraftCongestionTree( vtxoTree, err := bitcointree.CraftCongestionTree(
&wire.OutPoint{ &wire.OutPoint{
Hash: *testTxid, Hash: *testTxid,
Index: 0, Index: 0,
@@ -62,20 +61,21 @@ func TestRoundTripSignTree(t *testing.T) {
) )
require.NoError(t, err) require.NoError(t, err)
sweepClosure := bitcointree.CSVSigClosure{ sweepClosure := &tree.CSVSigClosure{
Pubkey: asp.PubKey(), MultisigClosure: tree.MultisigClosure{PubKeys: []*secp256k1.PublicKey{asp.PubKey()}},
Seconds: lifetime, Seconds: uint(lifetime),
} }
sweepTapLeaf, err := sweepClosure.Leaf() sweepScript, err := sweepClosure.Script()
require.NoError(t, err) require.NoError(t, err)
sweepTapTree := txscript.AssembleTaprootScriptTree(*sweepTapLeaf) sweepTapLeaf := txscript.NewBaseTapLeaf(sweepScript)
sweepTapTree := txscript.AssembleTaprootScriptTree(sweepTapLeaf)
root := sweepTapTree.RootNode.TapHash() root := sweepTapTree.RootNode.TapHash()
aspCoordinator, err := bitcointree.NewTreeCoordinatorSession( aspCoordinator, err := bitcointree.NewTreeCoordinatorSession(
sharedOutputAmount, sharedOutputAmount,
tree, vtxoTree,
root.CloneBytes(), root.CloneBytes(),
cosignerPubKeys, cosignerPubKeys,
) )
@@ -84,7 +84,7 @@ func TestRoundTripSignTree(t *testing.T) {
// Create signer sessions for all cosigners // Create signer sessions for all cosigners
signerSessions := make([]bitcointree.SignerSession, 20) signerSessions := make([]bitcointree.SignerSession, 20)
for i, cosigner := range cosigners { for i, cosigner := range cosigners {
signerSessions[i] = bitcointree.NewTreeSignerSession(cosigner, sharedOutputAmount, tree, root.CloneBytes()) signerSessions[i] = bitcointree.NewTreeSignerSession(cosigner, sharedOutputAmount, vtxoTree, root.CloneBytes())
} }
// Get nonces from all signers // Get nonces from all signers
@@ -136,24 +136,6 @@ type receiverFixture struct {
Pubkey string `json:"pubkey"` Pubkey string `json:"pubkey"`
} }
func (r receiverFixture) toVtxoScript(asp *secp256k1.PublicKey) bitcointree.VtxoScript {
bytesKey, err := hex.DecodeString(r.Pubkey)
if err != nil {
panic(err)
}
pubkey, err := secp256k1.ParsePubKey(bytesKey)
if err != nil {
panic(err)
}
return &bitcointree.DefaultVtxoScript{
Owner: pubkey,
Asp: asp,
ExitDelay: exitDelay,
}
}
func castReceivers(receivers []receiverFixture) []tree.VtxoLeaf { func castReceivers(receivers []receiverFixture) []tree.VtxoLeaf {
receiversOut := make([]tree.VtxoLeaf, 0, len(receivers)) receiversOut := make([]tree.VtxoLeaf, 0, len(receivers))
for _, r := range receivers { for _, r := range receivers {

View File

@@ -1,203 +0,0 @@
package bitcointree
import (
"bytes"
"fmt"
"github.com/ark-network/ark/common"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/txscript"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
)
type Closure interface {
Leaf() (*txscript.TapLeaf, error)
Decode(script []byte) (bool, error)
}
type CSVSigClosure struct {
Pubkey *secp256k1.PublicKey
Seconds uint
}
type MultisigClosure struct {
Pubkey *secp256k1.PublicKey
AspPubkey *secp256k1.PublicKey
}
func DecodeClosure(script []byte) (Closure, error) {
var closure Closure
closure = &CSVSigClosure{}
if valid, err := closure.Decode(script); err == nil && valid {
return closure, nil
}
closure = &MultisigClosure{}
if valid, err := closure.Decode(script); err == nil && valid {
return closure, nil
}
return nil, fmt.Errorf("invalid closure script")
}
func (f *MultisigClosure) Leaf() (*txscript.TapLeaf, error) {
aspKeyBytes := schnorr.SerializePubKey(f.AspPubkey)
userKeyBytes := schnorr.SerializePubKey(f.Pubkey)
script, err := txscript.NewScriptBuilder().AddData(aspKeyBytes).
AddOp(txscript.OP_CHECKSIGVERIFY).AddData(userKeyBytes).
AddOp(txscript.OP_CHECKSIG).Script()
if err != nil {
return nil, err
}
tapLeaf := txscript.NewBaseTapLeaf(script)
return &tapLeaf, nil
}
func (f *MultisigClosure) Decode(script []byte) (bool, error) {
valid, aspPubKey, err := decodeChecksigScript(script)
if err != nil {
return false, err
}
if !valid {
return false, nil
}
valid, pubkey, err := decodeChecksigScript(script[33:])
if err != nil {
return false, err
}
if !valid {
return false, nil
}
f.Pubkey = pubkey
f.AspPubkey = aspPubKey
rebuilt, err := f.Leaf()
if err != nil {
return false, err
}
if !bytes.Equal(rebuilt.Script, script) {
return false, nil
}
return true, nil
}
func (d *CSVSigClosure) Leaf() (*txscript.TapLeaf, error) {
script, err := encodeCsvWithChecksigScript(d.Pubkey, d.Seconds)
if err != nil {
return nil, err
}
tapLeaf := txscript.NewBaseTapLeaf(script)
return &tapLeaf, nil
}
func (d *CSVSigClosure) Decode(script []byte) (bool, error) {
csvIndex := bytes.Index(
script, []byte{txscript.OP_CHECKSEQUENCEVERIFY, txscript.OP_DROP},
)
if csvIndex == -1 || csvIndex == 0 {
return false, nil
}
sequence := script[:csvIndex]
if len(sequence) > 1 {
sequence = sequence[1:]
}
seconds, err := common.BIP68DecodeSequence(sequence)
if err != nil {
return false, err
}
checksigScript := script[csvIndex+2:]
valid, pubkey, err := decodeChecksigScript(checksigScript)
if err != nil {
return false, err
}
if !valid {
return false, nil
}
rebuilt, err := encodeCsvWithChecksigScript(pubkey, seconds)
if err != nil {
return false, err
}
if !bytes.Equal(rebuilt, script) {
return false, nil
}
d.Pubkey = pubkey
d.Seconds = seconds
return valid, nil
}
func decodeChecksigScript(script []byte) (bool, *secp256k1.PublicKey, error) {
data32Index := bytes.Index(script, []byte{txscript.OP_DATA_32})
if data32Index == -1 {
return false, nil, nil
}
key := script[data32Index+1 : data32Index+33]
if len(key) != 32 {
return false, nil, nil
}
pubkey, err := schnorr.ParsePubKey(key)
if err != nil {
return false, nil, err
}
return true, pubkey, nil
}
// checkSequenceVerifyScript without checksig
func encodeCsvScript(seconds uint) ([]byte, error) {
sequence, err := common.BIP68Sequence(seconds)
if err != nil {
return nil, err
}
return txscript.NewScriptBuilder().
AddInt64(int64(sequence)).
AddOps([]byte{
txscript.OP_CHECKSEQUENCEVERIFY,
txscript.OP_DROP,
}).
Script()
}
// checkSequenceVerifyScript + checksig
func encodeCsvWithChecksigScript(
pubkey *secp256k1.PublicKey, seconds uint,
) ([]byte, error) {
script, err := encodeChecksigScript(pubkey)
if err != nil {
return nil, err
}
csvScript, err := encodeCsvScript(seconds)
if err != nil {
return nil, err
}
return append(csvScript, script...), nil
}
func encodeChecksigScript(pubkey *secp256k1.PublicKey) ([]byte, error) {
key := schnorr.SerializePubKey(pubkey)
return txscript.NewScriptBuilder().AddData(key).
AddOp(txscript.OP_CHECKSIG).Script()
}

View File

@@ -1,30 +0,0 @@
package bitcointree_test
import (
"testing"
"github.com/ark-network/ark/common/bitcointree"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/stretchr/testify/require"
)
func TestRoundTripCSV(t *testing.T) {
seckey, err := secp256k1.GeneratePrivateKey()
require.NoError(t, err)
csvSig := &bitcointree.CSVSigClosure{
Pubkey: seckey.PubKey(),
Seconds: 1024,
}
leaf, err := csvSig.Leaf()
require.NoError(t, err)
var cl bitcointree.CSVSigClosure
valid, err := cl.Decode(leaf.Script)
require.NoError(t, err)
require.True(t, valid)
require.Equal(t, csvSig.Seconds, cl.Seconds)
}

View File

@@ -72,7 +72,7 @@ func UnspendableKey() *secp256k1.PublicKey {
// - every control block and taproot output scripts // - every control block and taproot output scripts
// - input and output amounts // - input and output amounts
func ValidateCongestionTree( func ValidateCongestionTree(
tree tree.CongestionTree, poolTx string, aspPublicKey *secp256k1.PublicKey, roundLifetime int64, vtxoTree tree.CongestionTree, poolTx string, aspPublicKey *secp256k1.PublicKey, roundLifetime int64,
) error { ) error {
poolTransaction, err := psbt.NewFromRawBytes(strings.NewReader(poolTx), true) poolTransaction, err := psbt.NewFromRawBytes(strings.NewReader(poolTx), true)
if err != nil { if err != nil {
@@ -85,17 +85,17 @@ func ValidateCongestionTree(
poolTxAmount := poolTransaction.UnsignedTx.TxOut[sharedOutputIndex].Value poolTxAmount := poolTransaction.UnsignedTx.TxOut[sharedOutputIndex].Value
nbNodes := tree.NumberOfNodes() nbNodes := vtxoTree.NumberOfNodes()
if nbNodes == 0 { if nbNodes == 0 {
return ErrEmptyTree return ErrEmptyTree
} }
if len(tree[0]) != 1 { if len(vtxoTree[0]) != 1 {
return ErrInvalidRootLevel return ErrInvalidRootLevel
} }
// check that root input is connected to the pool tx // check that root input is connected to the pool tx
rootPsetB64 := tree[0][0].Tx rootPsetB64 := vtxoTree[0][0].Tx
rootPset, err := psbt.NewFromRawBytes(strings.NewReader(rootPsetB64), true) rootPset, err := psbt.NewFromRawBytes(strings.NewReader(rootPsetB64), true)
if err != nil { if err != nil {
return fmt.Errorf("invalid root transaction: %w", err) return fmt.Errorf("invalid root transaction: %w", err)
@@ -120,28 +120,29 @@ func ValidateCongestionTree(
return ErrInvalidAmount return ErrInvalidAmount
} }
if len(tree.Leaves()) == 0 { if len(vtxoTree.Leaves()) == 0 {
return ErrNoLeaves return ErrNoLeaves
} }
sweepClosure := &CSVSigClosure{ sweepClosure := &tree.CSVSigClosure{
Seconds: uint(roundLifetime), MultisigClosure: tree.MultisigClosure{PubKeys: []*secp256k1.PublicKey{aspPublicKey}},
Pubkey: aspPublicKey, Seconds: uint(roundLifetime),
} }
sweepLeaf, err := sweepClosure.Leaf() sweepScript, err := sweepClosure.Script()
if err != nil { if err != nil {
return err return err
} }
tapTree := txscript.AssembleTaprootScriptTree(*sweepLeaf) sweepLeaf := txscript.NewBaseTapLeaf(sweepScript)
tapTree := txscript.AssembleTaprootScriptTree(sweepLeaf)
root := tapTree.RootNode.TapHash() root := tapTree.RootNode.TapHash()
// iterates over all the nodes of the tree // iterates over all the nodes of the tree
for _, level := range tree { for _, level := range vtxoTree {
for _, node := range level { for _, node := range level {
if err := validateNodeTransaction( if err := validateNodeTransaction(
node, tree, root.CloneBytes(), node, vtxoTree, root.CloneBytes(),
); err != nil { ); err != nil {
return err return err
} }

View File

@@ -1,94 +1,52 @@
package bitcointree package bitcointree
import ( import (
"encoding/hex"
"fmt" "fmt"
"github.com/ark-network/ark/common" "github.com/ark-network/ark/common"
"github.com/ark-network/ark/common/descriptor" "github.com/ark-network/ark/common/tree"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/txscript"
"github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/decred/dcrd/dcrec/secp256k1/v4"
) )
type VtxoScript common.VtxoScript[bitcoinTapTree] type VtxoScript common.VtxoScript[bitcoinTapTree, *tree.MultisigClosure, *tree.CSVSigClosure]
func ParseVtxoScript(desc string) (VtxoScript, error) { func ParseVtxoScript(scripts []string) (VtxoScript, error) {
types := []VtxoScript{ types := []VtxoScript{
&DefaultVtxoScript{}, &TapscriptsVtxoScript{},
} }
for _, v := range types { for _, v := range types {
if err := v.FromDescriptor(desc); err == nil { if err := v.Decode(scripts); err == nil {
return v, nil return v, nil
} }
} }
return nil, fmt.Errorf("invalid vtxo descriptor: %s", desc) return nil, fmt.Errorf("invalid vtxo scripts: %s", scripts)
} }
/* func NewDefaultVtxoScript(owner, asp *secp256k1.PublicKey, exitDelay uint) VtxoScript {
* DefaultVtxoScript is the default implementation of VTXO with 2 closures base := tree.NewDefaultVtxoScript(owner, asp, exitDelay)
* - Owner and ASP (forfeit)
* - Owner after t (unilateral exit) return &TapscriptsVtxoScript{*base}
*/
type DefaultVtxoScript struct {
Owner *secp256k1.PublicKey
Asp *secp256k1.PublicKey
ExitDelay uint
} }
func (v *DefaultVtxoScript) ToDescriptor() string { type TapscriptsVtxoScript struct {
owner := hex.EncodeToString(schnorr.SerializePubKey(v.Owner)) tree.TapscriptsVtxoScript
return fmt.Sprintf(
descriptor.DefaultVtxoDescriptorTemplate,
hex.EncodeToString(UnspendableKey().SerializeCompressed()),
owner,
hex.EncodeToString(schnorr.SerializePubKey(v.Asp)),
v.ExitDelay,
owner,
)
} }
func (v *DefaultVtxoScript) FromDescriptor(desc string) error { func (v *TapscriptsVtxoScript) TapTree() (*secp256k1.PublicKey, bitcoinTapTree, error) {
owner, asp, exitDelay, err := descriptor.ParseDefaultVtxoDescriptor(desc) leaves := make([]txscript.TapLeaf, len(v.Closures))
if err != nil { for i, closure := range v.Closures {
return err script, err := closure.Script()
if err != nil {
return nil, bitcoinTapTree{}, fmt.Errorf("failed to get script for closure %d: %w", i, err)
}
leaves[i] = txscript.NewBaseTapLeaf(script)
} }
v.Owner = owner tapTree := txscript.AssembleTaprootScriptTree(leaves...)
v.Asp = asp
v.ExitDelay = exitDelay
return nil
}
func (v *DefaultVtxoScript) TapTree() (*secp256k1.PublicKey, bitcoinTapTree, error) {
redeemClosure := &CSVSigClosure{
Pubkey: v.Owner,
Seconds: v.ExitDelay,
}
redeemLeaf, err := redeemClosure.Leaf()
if err != nil {
return nil, bitcoinTapTree{}, err
}
forfeitClosure := &MultisigClosure{
Pubkey: v.Owner,
AspPubkey: v.Asp,
}
forfeitLeaf, err := forfeitClosure.Leaf()
if err != nil {
return nil, bitcoinTapTree{}, err
}
tapTree := txscript.AssembleTaprootScriptTree(
*redeemLeaf, *forfeitLeaf,
)
root := tapTree.RootNode.TapHash() root := tapTree.RootNode.TapHash()
taprootKey := txscript.ComputeTaprootOutputKey( taprootKey := txscript.ComputeTaprootOutputKey(
UnspendableKey(), UnspendableKey(),

View File

@@ -1,43 +0,0 @@
package bitcointree_test
import (
"encoding/hex"
"fmt"
"testing"
"github.com/ark-network/ark/common/bitcointree"
"github.com/ark-network/ark/common/descriptor"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/stretchr/testify/require"
)
func TestParseDescriptor(t *testing.T) {
aspKey, err := secp256k1.GeneratePrivateKey()
require.NoError(t, err)
aliceKey, err := secp256k1.GeneratePrivateKey()
require.NoError(t, err)
aspPubKey := hex.EncodeToString(schnorr.SerializePubKey(aspKey.PubKey()))
alicePubKey := hex.EncodeToString(schnorr.SerializePubKey(aliceKey.PubKey()))
unspendableKey := hex.EncodeToString(bitcointree.UnspendableKey().SerializeCompressed())
defaultScriptDescriptor := fmt.Sprintf(
descriptor.DefaultVtxoDescriptorTemplate,
unspendableKey,
alicePubKey,
aspPubKey,
512,
alicePubKey,
)
vtxo, err := bitcointree.ParseVtxoScript(defaultScriptDescriptor)
require.NoError(t, err)
require.IsType(t, &bitcointree.DefaultVtxoScript{}, vtxo)
require.Equal(t, defaultScriptDescriptor, vtxo.ToDescriptor())
require.Equal(t, alicePubKey, hex.EncodeToString(schnorr.SerializePubKey(vtxo.(*bitcointree.DefaultVtxoScript).Owner)))
require.Equal(t, aspPubKey, hex.EncodeToString(schnorr.SerializePubKey(vtxo.(*bitcointree.DefaultVtxoScript).Asp)))
}

View File

@@ -163,11 +163,11 @@ func (n *node) getWitnessData() (
} }
sweepClosure := &CSVSigClosure{ sweepClosure := &CSVSigClosure{
Pubkey: n.sweepKey, MultisigClosure: MultisigClosure{PubKeys: []*secp256k1.PublicKey{n.sweepKey}},
Seconds: uint(n.roundLifetime), Seconds: uint(n.roundLifetime),
} }
sweepLeaf, err := sweepClosure.Leaf() sweepLeaf, err := sweepClosure.Script()
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@@ -183,13 +183,14 @@ func (n *node) getWitnessData() (
MinRelayFee: n.feeSats, MinRelayFee: n.feeSats,
} }
unrollLeaf, err := unrollClosure.Leaf() unrollScript, err := unrollClosure.Script()
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
branchTaprootTree := taproot.AssembleTaprootScriptTree( branchTaprootTree := taproot.AssembleTaprootScriptTree(
*unrollLeaf, *sweepLeaf, taproot.NewBaseTapElementsLeaf(unrollScript),
taproot.NewBaseTapElementsLeaf(sweepLeaf),
) )
root := branchTaprootTree.RootNode.TapHash() root := branchTaprootTree.RootNode.TapHash()
@@ -224,13 +225,14 @@ func (n *node) getWitnessData() (
RightAmount: rightAmount, RightAmount: rightAmount,
} }
unrollLeaf, err := unrollClosure.Leaf() unrollLeaf, err := unrollClosure.Script()
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
branchTaprootTree := taproot.AssembleTaprootScriptTree( branchTaprootTree := taproot.AssembleTaprootScriptTree(
*unrollLeaf, *sweepLeaf, taproot.NewBaseTapElementsLeaf(unrollLeaf),
taproot.NewBaseTapElementsLeaf(sweepLeaf),
) )
root := branchTaprootTree.RootNode.TapHash() root := branchTaprootTree.RootNode.TapHash()

View File

@@ -9,8 +9,8 @@ import (
"github.com/ark-network/ark/common" "github.com/ark-network/ark/common"
"github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/vulpemventures/go-elements/taproot"
) )
const ( const (
@@ -21,108 +21,333 @@ const (
OP_SUB64 = 0xd8 OP_SUB64 = 0xd8
) )
type MultisigType int
const (
MultisigTypeChecksig MultisigType = iota
MultisigTypeChecksigAdd
)
type Closure interface { type Closure interface {
Leaf() (*taproot.TapElementsLeaf, error) Script() ([]byte, error)
Decode(script []byte) (bool, error) Decode(script []byte) (bool, error)
// WitnessSize returns the size of the witness excluding the script and control block
WitnessSize() int
Witness(controlBlock []byte, signatures map[string][]byte) (wire.TxWitness, error)
} }
// UnrollClosure is liquid-only tapscript letting to enforce
// unrollable UTXO without musig.
type UnrollClosure struct { type UnrollClosure struct {
LeftKey, RightKey *secp256k1.PublicKey LeftKey, RightKey *secp256k1.PublicKey
LeftAmount, RightAmount uint64 LeftAmount, RightAmount uint64
MinRelayFee uint64 MinRelayFee uint64
} }
// MultisigClosure is a closure that contains a list of public keys and a
// CHECKSIG for each key. The witness size is 64 bytes per key, admitting the
// sighash type is SIGHASH_DEFAULT.
type MultisigClosure struct {
PubKeys []*secp256k1.PublicKey
Type MultisigType
}
// CSVSigClosure is a closure that contains a list of public keys and a
// CHECKSEQUENCEVERIFY + DROP. The witness size is 64 bytes per key, admitting
// the sighash type is SIGHASH_DEFAULT.
type CSVSigClosure struct { type CSVSigClosure struct {
Pubkey *secp256k1.PublicKey MultisigClosure
Seconds uint Seconds uint
} }
type MultisigClosure struct {
Pubkey *secp256k1.PublicKey
AspPubkey *secp256k1.PublicKey
}
func DecodeClosure(script []byte) (Closure, error) { func DecodeClosure(script []byte) (Closure, error) {
var closure Closure types := []Closure{
&CSVSigClosure{},
closure = &UnrollClosure{} &MultisigClosure{},
if valid, err := closure.Decode(script); err == nil && valid { &UnrollClosure{},
return closure, nil
} }
closure = &CSVSigClosure{} for _, closure := range types {
if valid, err := closure.Decode(script); err == nil && valid { if valid, err := closure.Decode(script); err == nil && valid {
return closure, nil return closure, nil
} }
closure = &MultisigClosure{}
if valid, err := closure.Decode(script); err == nil && valid {
return closure, nil
} }
return nil, fmt.Errorf("invalid closure script %s", hex.EncodeToString(script)) return nil, fmt.Errorf("invalid closure script %s", hex.EncodeToString(script))
} }
func (f *MultisigClosure) Leaf() (*taproot.TapElementsLeaf, error) { func (f *MultisigClosure) WitnessSize() int {
aspKeyBytes := schnorr.SerializePubKey(f.AspPubkey) return 64 * len(f.PubKeys)
userKeyBytes := schnorr.SerializePubKey(f.Pubkey) }
script, err := txscript.NewScriptBuilder().AddData(aspKeyBytes). func (f *MultisigClosure) Script() ([]byte, error) {
AddOp(txscript.OP_CHECKSIGVERIFY).AddData(userKeyBytes). scriptBuilder := txscript.NewScriptBuilder()
AddOp(txscript.OP_CHECKSIG).Script()
if err != nil { switch f.Type {
return nil, err case MultisigTypeChecksig:
for i, pubkey := range f.PubKeys {
scriptBuilder.AddData(schnorr.SerializePubKey(pubkey))
if i == len(f.PubKeys)-1 {
scriptBuilder.AddOp(txscript.OP_CHECKSIG)
continue
}
scriptBuilder.AddOp(txscript.OP_CHECKSIGVERIFY)
}
case MultisigTypeChecksigAdd:
for i, pubkey := range f.PubKeys {
scriptBuilder.AddData(schnorr.SerializePubKey(pubkey))
if i == 0 {
scriptBuilder.AddOp(txscript.OP_CHECKSIG)
continue
}
scriptBuilder.AddOp(txscript.OP_CHECKSIGADD)
}
scriptBuilder.AddInt64(int64(len(f.PubKeys)))
scriptBuilder.AddOp(txscript.OP_EQUAL)
} }
tapLeaf := taproot.NewBaseTapElementsLeaf(script) return scriptBuilder.Script()
return &tapLeaf, nil
} }
func (f *MultisigClosure) Decode(script []byte) (bool, error) { func (f *MultisigClosure) Decode(script []byte) (bool, error) {
valid, aspPubKey, err := decodeChecksigScript(script) if len(script) == 0 {
if err != nil { return false, fmt.Errorf("empty script")
return false, err
} }
if !valid { valid, err := f.decodeChecksig(script)
if err != nil {
return false, fmt.Errorf("failed to decode checksig: %w", err)
}
if valid {
return true, nil
}
valid, err = f.decodeChecksigAdd(script)
if err != nil {
return false, fmt.Errorf("failed to decode checksigadd: %w", err)
}
if valid {
return valid, nil
}
return false, nil
}
func (f *MultisigClosure) decodeChecksigAdd(script []byte) (bool, error) {
pubkeys := make([]*secp256k1.PublicKey, 0)
// Keep track of position in script
pos := 0
for pos < len(script) {
// Check for 33-byte data push (32 bytes for pubkey + 1 byte for OP_DATA)
if pos+33 > len(script) {
break
}
// Verify we have a 32-byte data push
if script[pos] != txscript.OP_DATA_32 {
return false, nil
}
// Parse the public key
pubkey, err := schnorr.ParsePubKey(script[pos+1 : pos+33])
if err != nil {
return false, err
}
pubkeys = append(pubkeys, pubkey)
pos += 33
// Check if we've reached the end
if pos >= len(script) {
return false, nil
}
// Check for CHECKSIG or CHECKSIGADD pattern
if len(pubkeys) == 1 && script[pos] == txscript.OP_CHECKSIG {
pos++
} else if len(pubkeys) > 1 && script[pos] == txscript.OP_CHECKSIGADD {
pos++
} else {
return false, nil
}
}
lastOp := script[len(script)-1]
if lastOp != txscript.OP_EQUAL {
return false, nil return false, nil
} }
valid, pubkey, err := decodeChecksigScript(script[33:]) // Verify we found at least one public key
if err != nil { if len(pubkeys) == 0 {
return false, err
}
if !valid {
return false, nil return false, nil
} }
f.Pubkey = pubkey f.PubKeys = pubkeys
f.AspPubkey = aspPubKey f.Type = MultisigTypeChecksigAdd
rebuilt, err := f.Leaf() // Verify the script matches what we would generate
rebuilt, err := f.Script()
if err != nil { if err != nil {
f.PubKeys = nil
f.Type = 0
return false, err return false, err
} }
if !bytes.Equal(rebuilt.Script, script) { if !bytes.Equal(rebuilt, script) {
f.PubKeys = nil
f.Type = 0
return false, nil return false, nil
} }
return true, nil return true, nil
} }
func (d *CSVSigClosure) Leaf() (*taproot.TapElementsLeaf, error) { func (f *MultisigClosure) decodeChecksig(script []byte) (bool, error) {
script, err := encodeCsvWithChecksigScript(d.Pubkey, d.Seconds) pubkeys := make([]*secp256k1.PublicKey, 0)
// Keep track of position in script
pos := 0
for pos < len(script) {
// Check for 33-byte data push (32 bytes for pubkey + 1 byte for OP_DATA)
if pos+33 > len(script) {
return false, nil
}
// Verify we have a 32-byte data push
if script[pos] != txscript.OP_DATA_32 {
return false, nil
}
// Parse the public key
pubkey, err := schnorr.ParsePubKey(script[pos+1 : pos+33])
if err != nil {
return false, err
}
pubkeys = append(pubkeys, pubkey)
pos += 33
// Check if we've reached the end
if pos >= len(script) {
return false, nil
}
// Next byte should be either CHECKSIG (last key) or CHECKSIGVERIFY
if script[pos] == txscript.OP_CHECKSIG {
// This should be the last operation
if pos != len(script)-1 {
return false, nil
}
break
} else if script[pos] == txscript.OP_CHECKSIGVERIFY {
pos++
continue
} else {
return false, nil
}
}
// Verify we found at least one public key
if len(pubkeys) == 0 {
return false, nil
}
f.PubKeys = pubkeys
f.Type = MultisigTypeChecksig
// Verify the script matches what we would generate
rebuilt, err := f.Script()
if err != nil {
f.PubKeys = nil
f.Type = 0
return false, err
}
if !bytes.Equal(rebuilt, script) {
f.PubKeys = nil
f.Type = 0
return false, nil
}
return true, nil
}
func (f *MultisigClosure) Witness(controlBlock []byte, signatures map[string][]byte) (wire.TxWitness, error) {
// Create witness stack with capacity for all signatures plus script and control block
witness := make(wire.TxWitness, 0, len(f.PubKeys)+2)
// Add signatures in the reverse order as public keys
for i := len(f.PubKeys) - 1; i >= 0; i-- {
pubKey := f.PubKeys[i]
sig, ok := signatures[hex.EncodeToString(schnorr.SerializePubKey(pubKey))]
if !ok {
return nil, fmt.Errorf("missing signature for public key %x", schnorr.SerializePubKey(pubKey))
}
witness = append(witness, sig)
}
// Get script
script, err := f.Script()
if err != nil {
return nil, fmt.Errorf("failed to generate script: %w", err)
}
// Add script and control block
witness = append(witness, script)
witness = append(witness, controlBlock)
return witness, nil
}
func (f *CSVSigClosure) Witness(controlBlock []byte, signatures map[string][]byte) (wire.TxWitness, error) {
multisigWitness, err := f.MultisigClosure.Witness(controlBlock, signatures)
if err != nil { if err != nil {
return nil, err return nil, err
} }
tapLeaf := taproot.NewBaseTapElementsLeaf(script) script, err := f.Script()
return &tapLeaf, nil if err != nil {
return nil, fmt.Errorf("failed to generate script: %w", err)
}
// replace script with csv script
multisigWitness[len(multisigWitness)-2] = script
return multisigWitness, nil
}
func (f *CSVSigClosure) WitnessSize() int {
return f.MultisigClosure.WitnessSize()
}
func (d *CSVSigClosure) Script() ([]byte, error) {
csvScript, err := txscript.NewScriptBuilder().
AddInt64(int64(d.Seconds)).
AddOps([]byte{
txscript.OP_CHECKSEQUENCEVERIFY,
txscript.OP_DROP,
}).
Script()
if err != nil {
return nil, err
}
multisigScript, err := d.MultisigClosure.Script()
if err != nil {
return nil, err
}
return append(csvScript, multisigScript...), nil
} }
func (d *CSVSigClosure) Decode(script []byte) (bool, error) { func (d *CSVSigClosure) Decode(script []byte) (bool, error) {
if len(script) == 0 {
return false, fmt.Errorf("empty script")
}
csvIndex := bytes.Index( csvIndex := bytes.Index(
script, []byte{txscript.OP_CHECKSEQUENCEVERIFY, txscript.OP_DROP}, script, []byte{txscript.OP_CHECKSEQUENCEVERIFY, txscript.OP_DROP},
) )
@@ -140,8 +365,8 @@ func (d *CSVSigClosure) Decode(script []byte) (bool, error) {
return false, err return false, err
} }
checksigScript := script[csvIndex+2:] multisigClosure := &MultisigClosure{}
valid, pubkey, err := decodeChecksigScript(checksigScript) valid, err := multisigClosure.Decode(script[csvIndex+2:])
if err != nil { if err != nil {
return false, err return false, err
} }
@@ -150,26 +375,21 @@ func (d *CSVSigClosure) Decode(script []byte) (bool, error) {
return false, nil return false, nil
} }
rebuilt, err := encodeCsvWithChecksigScript(pubkey, seconds)
if err != nil {
return false, err
}
if !bytes.Equal(rebuilt, script) {
return false, nil
}
d.Pubkey = pubkey
d.Seconds = seconds d.Seconds = seconds
d.MultisigClosure = *multisigClosure
return valid, nil return valid, nil
} }
func (c *UnrollClosure) WitnessSize() int {
return 0
}
func (c *UnrollClosure) isOneChild() bool { func (c *UnrollClosure) isOneChild() bool {
return c.RightKey == nil && c.MinRelayFee > 0 return c.RightKey == nil && c.MinRelayFee > 0
} }
func (c *UnrollClosure) Leaf() (*taproot.TapElementsLeaf, error) { func (c *UnrollClosure) Script() ([]byte, error) {
if c.LeftKey == nil { if c.LeftKey == nil {
return nil, fmt.Errorf("left key is required") return nil, fmt.Errorf("left key is required")
} }
@@ -178,8 +398,7 @@ func (c *UnrollClosure) Leaf() (*taproot.TapElementsLeaf, error) {
branchScript := encodeOneChildIntrospectionScript( branchScript := encodeOneChildIntrospectionScript(
txscript.OP_0, schnorr.SerializePubKey(c.LeftKey), c.MinRelayFee, txscript.OP_0, schnorr.SerializePubKey(c.LeftKey), c.MinRelayFee,
) )
leaf := taproot.NewBaseTapElementsLeaf(branchScript) return branchScript, nil
return &leaf, nil
} }
if c.LeftAmount == 0 { if c.LeftAmount == 0 {
@@ -205,8 +424,7 @@ func (c *UnrollClosure) Leaf() (*taproot.TapElementsLeaf, error) {
) )
branchScript = append(branchScript, nextScriptRight...) branchScript = append(branchScript, nextScriptRight...)
leaf := taproot.NewBaseTapElementsLeaf(branchScript) return branchScript, nil
return &leaf, nil
} }
func (c *UnrollClosure) Decode(script []byte) (valid bool, err error) { func (c *UnrollClosure) Decode(script []byte) (valid bool, err error) {
@@ -227,12 +445,12 @@ func (c *UnrollClosure) Decode(script []byte) (valid bool, err error) {
c.LeftKey = pubkey c.LeftKey = pubkey
c.MinRelayFee = minrelayfee c.MinRelayFee = minrelayfee
rebuilt, err := c.Leaf() rebuilt, err := c.Script()
if err != nil { if err != nil {
return false, err return false, err
} }
if !bytes.Equal(rebuilt.Script, script) { if !bytes.Equal(rebuilt, script) {
return false, nil return false, nil
} }
@@ -272,12 +490,12 @@ func (c *UnrollClosure) Decode(script []byte) (valid bool, err error) {
c.RightAmount = rightAmount c.RightAmount = rightAmount
c.RightKey = rightKey c.RightKey = rightKey
rebuilt, err := c.Leaf() rebuilt, err := c.Script()
if err != nil { if err != nil {
return false, err return false, err
} }
if !bytes.Equal(rebuilt.Script, script) { if !bytes.Equal(rebuilt, script) {
return false, nil return false, nil
} }
@@ -351,64 +569,6 @@ func decodeOneChildIntrospectionScript(
return true, pubkey, minrelayfee, nil return true, pubkey, minrelayfee, nil
} }
func decodeChecksigScript(script []byte) (bool, *secp256k1.PublicKey, error) {
data32Index := bytes.Index(script, []byte{txscript.OP_DATA_32})
if data32Index == -1 {
return false, nil, nil
}
key := script[data32Index+1 : data32Index+33]
if len(key) != 32 {
return false, nil, nil
}
pubkey, err := schnorr.ParsePubKey(key)
if err != nil {
return false, nil, err
}
return true, pubkey, nil
}
// checkSequenceVerifyScript without checksig
func encodeCsvScript(seconds uint) ([]byte, error) {
sequence, err := common.BIP68Sequence(seconds)
if err != nil {
return nil, err
}
return txscript.NewScriptBuilder().
AddInt64(int64(sequence)).
AddOps([]byte{
txscript.OP_CHECKSEQUENCEVERIFY,
txscript.OP_DROP,
}).
Script()
}
// checkSequenceVerifyScript + checksig
func encodeCsvWithChecksigScript(
pubkey *secp256k1.PublicKey, seconds uint,
) ([]byte, error) {
script, err := encodeChecksigScript(pubkey)
if err != nil {
return nil, err
}
csvScript, err := encodeCsvScript(seconds)
if err != nil {
return nil, err
}
return append(csvScript, script...), nil
}
func encodeChecksigScript(pubkey *secp256k1.PublicKey) ([]byte, error) {
key := schnorr.SerializePubKey(pubkey)
return txscript.NewScriptBuilder().AddData(key).
AddOp(txscript.OP_CHECKSIG).Script()
}
// encodeIntrospectionScript returns an introspection script that checks the // encodeIntrospectionScript returns an introspection script that checks the
// script and the amount of the output at the given index verify will add an // script and the amount of the output at the given index verify will add an
// OP_EQUALVERIFY at the end of the script, otherwise it will add an OP_EQUAL // OP_EQUALVERIFY at the end of the script, otherwise it will add an OP_EQUAL
@@ -496,3 +656,13 @@ func encodeOneChildIntrospectionScript(
return script return script
} }
func (c *UnrollClosure) Witness(controlBlock []byte, _ map[string][]byte) (wire.TxWitness, error) {
script, err := c.Script()
if err != nil {
return nil, fmt.Errorf("failed to generate script: %w", err)
}
// UnrollClosure only needs script and control block
return wire.TxWitness{script, controlBlock}, nil
}

471
common/tree/script_test.go Normal file
View File

@@ -0,0 +1,471 @@
package tree_test
import (
"encoding/hex"
"testing"
"github.com/ark-network/ark/common/tree"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/txscript"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/stretchr/testify/require"
)
func TestRoundTripCSV(t *testing.T) {
seckey, err := secp256k1.GeneratePrivateKey()
require.NoError(t, err)
csvSig := &tree.CSVSigClosure{
MultisigClosure: tree.MultisigClosure{
PubKeys: []*secp256k1.PublicKey{seckey.PubKey()},
},
Seconds: 1024,
}
leaf, err := csvSig.Script()
require.NoError(t, err)
var cl tree.CSVSigClosure
valid, err := cl.Decode(leaf)
require.NoError(t, err)
require.True(t, valid)
require.Equal(t, csvSig.Seconds, cl.Seconds)
}
func TestMultisigClosure(t *testing.T) {
// Generate some test keys
privKey1, err := secp256k1.GeneratePrivateKey()
require.NoError(t, err)
pubKey1 := privKey1.PubKey()
privKey2, err := secp256k1.GeneratePrivateKey()
require.NoError(t, err)
pubKey2 := privKey2.PubKey()
t.Run("valid 2-of-2 multisig", func(t *testing.T) {
closure := &tree.MultisigClosure{
PubKeys: []*secp256k1.PublicKey{pubKey1, pubKey2},
}
// Generate script
script, err := closure.Script()
require.NoError(t, err)
// Test decoding
decodedClosure := &tree.MultisigClosure{}
valid, err := decodedClosure.Decode(script)
require.NoError(t, err)
require.True(t, valid)
require.Equal(t, 2, len(decodedClosure.PubKeys))
// Compare serialized pubkeys
require.Equal(t,
schnorr.SerializePubKey(pubKey1),
schnorr.SerializePubKey(decodedClosure.PubKeys[0]),
)
require.Equal(t,
schnorr.SerializePubKey(pubKey2),
schnorr.SerializePubKey(decodedClosure.PubKeys[1]),
)
})
t.Run("valid single key multisig", func(t *testing.T) {
closure := &tree.MultisigClosure{
PubKeys: []*secp256k1.PublicKey{pubKey1},
}
script, err := closure.Script()
require.NoError(t, err)
decodedClosure := &tree.MultisigClosure{}
valid, err := decodedClosure.Decode(script)
require.NoError(t, err)
require.True(t, valid)
require.Equal(t, 1, len(decodedClosure.PubKeys))
// Compare serialized pubkey
require.Equal(t,
schnorr.SerializePubKey(pubKey1),
schnorr.SerializePubKey(decodedClosure.PubKeys[0]),
)
})
t.Run("invalid empty script", func(t *testing.T) {
closure := &tree.MultisigClosure{}
valid, err := closure.Decode([]byte{})
require.Error(t, err)
require.False(t, valid)
})
t.Run("invalid script - wrong data push", func(t *testing.T) {
script := []byte{
txscript.OP_DATA_33, // Wrong size for schnorr pubkey
}
closure := &tree.MultisigClosure{}
valid, err := closure.Decode(script)
require.NoError(t, err)
require.False(t, valid)
})
t.Run("invalid script - missing checksig", func(t *testing.T) {
pubKeyBytes := schnorr.SerializePubKey(pubKey1)
script := []byte{txscript.OP_DATA_32}
script = append(script, pubKeyBytes...)
// Missing OP_CHECKSIG
closure := &tree.MultisigClosure{}
valid, err := closure.Decode(script)
require.NoError(t, err)
require.False(t, valid)
})
t.Run("invalid script - extra data after checksig", func(t *testing.T) {
pubKeyBytes := schnorr.SerializePubKey(pubKey1)
script := []byte{txscript.OP_DATA_32}
script = append(script, pubKeyBytes...)
script = append(script, txscript.OP_CHECKSIG)
script = append(script, 0x00) // Extra unwanted byte
closure := &tree.MultisigClosure{}
valid, err := closure.Decode(script)
require.NoError(t, err)
require.False(t, valid)
})
t.Run("witness size", func(t *testing.T) {
closure := &tree.MultisigClosure{
PubKeys: []*secp256k1.PublicKey{pubKey1, pubKey2},
}
require.Equal(t, 128, closure.WitnessSize()) // 64 * 2 bytes
})
t.Run("valid 12-of-12 multisig", func(t *testing.T) {
// Generate 12 keys
pubKeys := make([]*secp256k1.PublicKey, 12)
for i := 0; i < 12; i++ {
privKey, err := secp256k1.GeneratePrivateKey()
require.NoError(t, err)
pubKeys[i] = privKey.PubKey()
}
closure := &tree.MultisigClosure{
PubKeys: pubKeys,
}
// Generate script
script, err := closure.Script()
require.NoError(t, err)
// Test decoding
decodedClosure := &tree.MultisigClosure{}
valid, err := decodedClosure.Decode(script)
require.NoError(t, err)
require.True(t, valid)
require.Equal(t, 12, len(decodedClosure.PubKeys))
// Compare all serialized pubkeys
for i := 0; i < 12; i++ {
require.Equal(t,
schnorr.SerializePubKey(pubKeys[i]),
schnorr.SerializePubKey(decodedClosure.PubKeys[i]),
)
}
// Verify witness size is correct for 12 signatures
require.Equal(t, 64*12, closure.WitnessSize())
})
}
func TestCSVSigClosure(t *testing.T) {
// Generate test keys
privKey1, err := secp256k1.GeneratePrivateKey()
require.NoError(t, err)
pubKey1 := privKey1.PubKey()
privKey2, err := secp256k1.GeneratePrivateKey()
require.NoError(t, err)
pubKey2 := privKey2.PubKey()
t.Run("valid single key CSV", func(t *testing.T) {
csvSig := &tree.CSVSigClosure{
MultisigClosure: tree.MultisigClosure{
PubKeys: []*secp256k1.PublicKey{pubKey1},
},
Seconds: 1024,
}
script, err := csvSig.Script()
require.NoError(t, err)
decodedCSV := &tree.CSVSigClosure{}
valid, err := decodedCSV.Decode(script)
require.NoError(t, err)
require.True(t, valid)
require.Equal(t, uint32(1024), uint32(decodedCSV.Seconds))
require.Equal(t, 1, len(decodedCSV.PubKeys))
require.Equal(t,
schnorr.SerializePubKey(pubKey1),
schnorr.SerializePubKey(decodedCSV.PubKeys[0]),
)
})
t.Run("valid 2-of-2 CSV", func(t *testing.T) {
csvSig := &tree.CSVSigClosure{
MultisigClosure: tree.MultisigClosure{
PubKeys: []*secp256k1.PublicKey{pubKey1, pubKey2},
},
Seconds: 2016, // ~2 weeks
}
script, err := csvSig.Script()
require.NoError(t, err)
decodedCSV := &tree.CSVSigClosure{}
valid, err := decodedCSV.Decode(script)
require.NoError(t, err)
require.True(t, valid)
require.Equal(t, uint32(2016), uint32(decodedCSV.Seconds))
require.Equal(t, 2, len(decodedCSV.PubKeys))
require.Equal(t,
schnorr.SerializePubKey(pubKey1),
schnorr.SerializePubKey(decodedCSV.PubKeys[0]),
)
require.Equal(t,
schnorr.SerializePubKey(pubKey2),
schnorr.SerializePubKey(decodedCSV.PubKeys[1]),
)
})
t.Run("invalid empty script", func(t *testing.T) {
csvSig := &tree.CSVSigClosure{}
valid, err := csvSig.Decode([]byte{})
require.Error(t, err)
require.False(t, valid)
})
t.Run("invalid CSV value", func(t *testing.T) {
// Create a script with invalid CSV value
pubKeyBytes := schnorr.SerializePubKey(pubKey1)
script := []byte{txscript.OP_DATA_32}
script = append(script, pubKeyBytes...)
script = append(script, txscript.OP_CHECKSIG)
script = append(script, 0xFF) // Invalid CSV value
csvSig := &tree.CSVSigClosure{}
valid, err := csvSig.Decode(script)
require.NoError(t, err)
require.False(t, valid)
})
t.Run("witness size", func(t *testing.T) {
csvSig := &tree.CSVSigClosure{
MultisigClosure: tree.MultisigClosure{
PubKeys: []*secp256k1.PublicKey{pubKey1, pubKey2},
},
Seconds: 1024,
}
// Should be same as multisig witness size (64 bytes per signature)
require.Equal(t, 128, csvSig.WitnessSize())
})
t.Run("max timelock", func(t *testing.T) {
csvSig := &tree.CSVSigClosure{
MultisigClosure: tree.MultisigClosure{
PubKeys: []*secp256k1.PublicKey{pubKey1},
},
Seconds: 65535, // Maximum allowed value
}
script, err := csvSig.Script()
require.NoError(t, err)
decodedCSV := &tree.CSVSigClosure{}
valid, err := decodedCSV.Decode(script)
require.NoError(t, err)
require.True(t, valid)
require.Equal(t, uint32(65535), uint32(decodedCSV.Seconds))
})
}
func TestMultisigClosureWitness(t *testing.T) {
// Generate test keys
priv1, err := secp256k1.GeneratePrivateKey()
require.NoError(t, err)
pub1 := priv1.PubKey()
priv2, err := secp256k1.GeneratePrivateKey()
require.NoError(t, err)
pub2 := priv2.PubKey()
// Mock control block
controlBlock := []byte("control block")
testCases := []struct {
name string
closure *tree.MultisigClosure
signatures map[string][]byte
expectError bool
}{
{
name: "single signature success",
closure: &tree.MultisigClosure{
PubKeys: []*secp256k1.PublicKey{pub1},
},
signatures: map[string][]byte{
hex.EncodeToString(schnorr.SerializePubKey(pub1)): []byte("signature1"),
},
expectError: false,
},
{
name: "multiple signatures success",
closure: &tree.MultisigClosure{
PubKeys: []*secp256k1.PublicKey{pub1, pub2},
},
signatures: map[string][]byte{
hex.EncodeToString(schnorr.SerializePubKey(pub1)): []byte("signature1"),
hex.EncodeToString(schnorr.SerializePubKey(pub2)): []byte("signature2"),
},
expectError: false,
},
{
name: "missing signature",
closure: &tree.MultisigClosure{
PubKeys: []*secp256k1.PublicKey{pub1, pub2},
},
signatures: map[string][]byte{
hex.EncodeToString(schnorr.SerializePubKey(pub1)): []byte("signature1"),
},
expectError: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
witness, err := tc.closure.Witness(controlBlock, tc.signatures)
if tc.expectError {
require.Error(t, err)
return
}
require.NoError(t, err)
// Total witness stack should be: signatures + script + control block
expectedLen := len(tc.closure.PubKeys) + 2
require.Equal(t, expectedLen, len(witness))
// Verify signatures are in correct order (reverse order of pubkeys)
for i := len(tc.closure.PubKeys) - 1; i >= 0; i-- {
expectedSig := tc.signatures[hex.EncodeToString(schnorr.SerializePubKey(tc.closure.PubKeys[i]))]
witnessIndex := len(witness) - 3 - i
require.Equal(t, expectedSig, witness[:len(witness)-2][witnessIndex])
}
// Verify script is present
script, err := tc.closure.Script()
require.NoError(t, err)
require.Equal(t, script, witness[len(witness)-2])
// Verify control block is last
require.Equal(t, controlBlock, witness[len(witness)-1])
})
}
}
func TestUnrollClosureWitness(t *testing.T) {
closure := &tree.UnrollClosure{
LeftKey: secp256k1.NewPublicKey(new(secp256k1.FieldVal), new(secp256k1.FieldVal)),
RightKey: secp256k1.NewPublicKey(new(secp256k1.FieldVal), new(secp256k1.FieldVal)),
LeftAmount: 1000,
RightAmount: 2000,
}
controlBlock := []byte("control block")
witness, err := closure.Witness(controlBlock, nil)
require.NoError(t, err)
// Should contain script and control block
require.Equal(t, 2, len(witness))
// Verify script is first
script, err := closure.Script()
require.NoError(t, err)
require.Equal(t, script, witness[0])
// Verify control block is last
require.Equal(t, controlBlock, witness[1])
}
func TestCSVSigClosureWitness(t *testing.T) {
// Generate test keys
priv1, err := secp256k1.GeneratePrivateKey()
require.NoError(t, err)
pub1 := priv1.PubKey()
// Create test signature
testSig := []byte("signature1")
signatures := map[string][]byte{
hex.EncodeToString(schnorr.SerializePubKey(pub1)): testSig,
}
controlBlock := []byte("control block")
closure := &tree.CSVSigClosure{
MultisigClosure: tree.MultisigClosure{
PubKeys: []*secp256k1.PublicKey{pub1},
},
Seconds: 144,
}
witness, err := closure.Witness(controlBlock, signatures)
require.NoError(t, err)
// Should contain: signature + script + control block
require.Equal(t, 3, len(witness))
require.Equal(t, testSig, witness[0])
script, err := closure.Script()
require.NoError(t, err)
require.Equal(t, script, witness[1])
require.Equal(t, controlBlock, witness[2])
// Test missing signature
_, err = closure.Witness(controlBlock, nil)
require.Error(t, err)
}
func TestDecodeChecksigAdd(t *testing.T) {
// Generate some test public keys
pubKey1, err := secp256k1.GeneratePrivateKey()
require.NoError(t, err)
pubKey2, err := secp256k1.GeneratePrivateKey()
require.NoError(t, err)
pubKey3, err := secp256k1.GeneratePrivateKey()
require.NoError(t, err)
pubKeys := []*secp256k1.PublicKey{pubKey1.PubKey(), pubKey2.PubKey(), pubKey3.PubKey()}
// Create a script for 3-of-3 multisig using CHECKSIGADD
scriptBuilder := txscript.NewScriptBuilder().
AddData(schnorr.SerializePubKey(pubKeys[0])).
AddOp(txscript.OP_CHECKSIG).
AddData(schnorr.SerializePubKey(pubKeys[1])).
AddOp(txscript.OP_CHECKSIGADD).
AddData(schnorr.SerializePubKey(pubKeys[2])).
AddOp(txscript.OP_CHECKSIGADD).
AddInt64(3).
AddOp(txscript.OP_EQUAL)
script, err := scriptBuilder.Script()
require.NoError(t, err, "failed to build script")
// Decode the script
multisigClosure := &tree.MultisigClosure{}
valid, err := multisigClosure.Decode(script)
require.NoError(t, err, "failed to decode script")
require.True(t, valid, "script should be valid")
require.Equal(t, tree.MultisigTypeChecksigAdd, multisigClosure.Type, "expected MultisigTypeChecksigAdd")
require.Equal(t, 3, len(multisigClosure.PubKeys), "expected 3 public keys")
}

View File

@@ -238,8 +238,8 @@ func validateNodeTransaction(
switch c := closure.(type) { switch c := closure.(type) {
case *CSVSigClosure: case *CSVSigClosure:
isASP := bytes.Equal( isASP := len(c.MultisigClosure.PubKeys) == 1 && bytes.Equal(
schnorr.SerializePubKey(c.Pubkey), schnorr.SerializePubKey(c.MultisigClosure.PubKeys[0]),
schnorr.SerializePubKey(expectedPublicKeyASP), schnorr.SerializePubKey(expectedPublicKeyASP),
) )
isSweepDelay := int64(c.Seconds) == expectedSequence isSweepDelay := int64(c.Seconds) == expectedSequence

View File

@@ -1,89 +1,159 @@
package tree package tree
import ( import (
"bytes"
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"math"
"github.com/ark-network/ark/common" "github.com/ark-network/ark/common"
"github.com/ark-network/ark/common/descriptor"
"github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/vulpemventures/go-elements/taproot" "github.com/vulpemventures/go-elements/taproot"
) )
type VtxoScript common.VtxoScript[elementsTapTree] var (
ErrNoExitLeaf = fmt.Errorf("no exit leaf")
)
func ParseVtxoScript(desc string) (VtxoScript, error) { type VtxoScript common.VtxoScript[elementsTapTree, *MultisigClosure, *CSVSigClosure]
v := &DefaultVtxoScript{}
// TODO add other type func ParseVtxoScript(scripts []string) (VtxoScript, error) {
err := v.FromDescriptor(desc) v := &TapscriptsVtxoScript{}
err := v.Decode(scripts)
return v, err return v, err
} }
/* func NewDefaultVtxoScript(owner, asp *secp256k1.PublicKey, exitDelay uint) *TapscriptsVtxoScript {
* DefaultVtxoScript is the default implementation of VTXO with 2 closures return &TapscriptsVtxoScript{
* - Owner and ASP (forfeit) []Closure{
* - Owner after t (unilateral exit) &CSVSigClosure{
*/ MultisigClosure: MultisigClosure{PubKeys: []*secp256k1.PublicKey{owner}},
type DefaultVtxoScript struct { Seconds: exitDelay,
Owner *secp256k1.PublicKey },
Asp *secp256k1.PublicKey &MultisigClosure{PubKeys: []*secp256k1.PublicKey{owner, asp}},
ExitDelay uint },
}
func (v *DefaultVtxoScript) ToDescriptor() string {
owner := hex.EncodeToString(schnorr.SerializePubKey(v.Owner))
return fmt.Sprintf(
descriptor.DefaultVtxoDescriptorTemplate,
hex.EncodeToString(UnspendableKey().SerializeCompressed()),
owner,
hex.EncodeToString(schnorr.SerializePubKey(v.Asp)),
v.ExitDelay,
owner,
)
}
func (v *DefaultVtxoScript) FromDescriptor(desc string) error {
owner, asp, exitDelay, err := descriptor.ParseDefaultVtxoDescriptor(desc)
if err != nil {
return err
} }
}
v.Owner = owner // TapscriptsVtxoScript represents a taproot script that contains a list of tapscript leaves
v.Asp = asp // the key-path is always unspendable
v.ExitDelay = exitDelay type TapscriptsVtxoScript struct {
Closures []Closure
}
func (v *TapscriptsVtxoScript) Encode() ([]string, error) {
encoded := make([]string, 0)
for _, closure := range v.Closures {
script, err := closure.Script()
if err != nil {
return nil, err
}
encoded = append(encoded, hex.EncodeToString(script))
}
return encoded, nil
}
func (v *TapscriptsVtxoScript) Decode(scripts []string) error {
v.Closures = make([]Closure, 0, len(scripts))
for _, script := range scripts {
scriptBytes, err := hex.DecodeString(script)
if err != nil {
return err
}
closure, err := DecodeClosure(scriptBytes)
if err != nil {
return err
}
v.Closures = append(v.Closures, closure)
}
return nil return nil
} }
func (v *DefaultVtxoScript) TapTree() (*secp256k1.PublicKey, elementsTapTree, error) { func (v *TapscriptsVtxoScript) Validate(asp *secp256k1.PublicKey, minExitDelay uint) error {
redeemClosure := &CSVSigClosure{ aspXonly := schnorr.SerializePubKey(asp)
Pubkey: v.Owner, for _, forfeit := range v.ForfeitClosures() {
Seconds: v.ExitDelay, // must contain asp pubkey
found := false
for _, pubkey := range forfeit.PubKeys {
if bytes.Equal(schnorr.SerializePubKey(pubkey), aspXonly) {
found = true
break
}
}
if !found {
return fmt.Errorf("invalid forfeit closure, ASP pubkey not found")
}
} }
redeemLeaf, err := redeemClosure.Leaf() smallestExit, err := v.SmallestExitDelay()
if err != nil { if err != nil {
return nil, elementsTapTree{}, err if err == ErrNoExitLeaf {
return nil
}
return err
} }
forfeitClosure := &MultisigClosure{ if smallestExit < minExitDelay {
Pubkey: v.Owner, return fmt.Errorf("exit delay is too short")
AspPubkey: v.Asp,
} }
forfeitLeaf, err := forfeitClosure.Leaf() return nil
if err != nil { }
return nil, elementsTapTree{}, err
func (v *TapscriptsVtxoScript) SmallestExitDelay() (uint, error) {
smallest := uint(math.MaxUint32)
for _, closure := range v.Closures {
if csvClosure, ok := closure.(*CSVSigClosure); ok {
if csvClosure.Seconds < smallest {
smallest = csvClosure.Seconds
}
}
} }
tapTree := taproot.AssembleTaprootScriptTree( if smallest == math.MaxUint32 {
*redeemLeaf, *forfeitLeaf, return 0, ErrNoExitLeaf
) }
return smallest, nil
}
func (v *TapscriptsVtxoScript) ForfeitClosures() []*MultisigClosure {
forfeits := make([]*MultisigClosure, 0)
for _, closure := range v.Closures {
if multisigClosure, ok := closure.(*MultisigClosure); ok {
forfeits = append(forfeits, multisigClosure)
}
}
return forfeits
}
func (v *TapscriptsVtxoScript) ExitClosures() []*CSVSigClosure {
exits := make([]*CSVSigClosure, 0)
for _, closure := range v.Closures {
if csvClosure, ok := closure.(*CSVSigClosure); ok {
exits = append(exits, csvClosure)
}
}
return exits
}
func (v *TapscriptsVtxoScript) TapTree() (*secp256k1.PublicKey, elementsTapTree, error) {
leaves := make([]taproot.TapElementsLeaf, 0, len(v.Closures))
for _, closure := range v.Closures {
leaf, err := closure.Script()
if err != nil {
return nil, elementsTapTree{}, err
}
leaves = append(leaves, taproot.NewBaseTapElementsLeaf(leaf))
}
tapTree := taproot.AssembleTaprootScriptTree(leaves...)
root := tapTree.RootNode.TapHash() root := tapTree.RootNode.TapHash()
taprootKey := taproot.ComputeTaprootOutputKey(UnspendableKey(), root[:]) taprootKey := taproot.ComputeTaprootOutputKey(UnspendableKey(), root[:])
return taprootKey, elementsTapTree{tapTree}, nil return taprootKey, elementsTapTree{tapTree}, nil
@@ -111,9 +181,15 @@ func (b elementsTapTree) GetTaprootMerkleProof(leafhash chainhash.Hash) (*common
return nil, err return nil, err
} }
closure, err := DecodeClosure(proof.Script)
if err != nil {
return nil, err
}
return &common.TaprootMerkleProof{ return &common.TaprootMerkleProof{
ControlBlock: controlBlockBytes, ControlBlock: controlBlockBytes,
Script: proof.Script, Script: proof.Script,
WitnessSize: closure.WitnessSize(),
}, nil }, nil
} }

View File

@@ -14,6 +14,7 @@ var (
type TaprootMerkleProof struct { type TaprootMerkleProof struct {
ControlBlock []byte ControlBlock []byte
Script []byte Script []byte
WitnessSize int
} }
// TaprootTree is an interface wrapping the methods needed to spend a vtxo taproot contract // TaprootTree is an interface wrapping the methods needed to spend a vtxo taproot contract
@@ -30,14 +31,15 @@ It may also contain others closures implementing specific use cases.
VtxoScript abstracts the taproot complexity behind vtxo contracts. VtxoScript abstracts the taproot complexity behind vtxo contracts.
it is compiled, transferred and parsed using descriptor string. it is compiled, transferred and parsed using descriptor string.
default vtxo script = tr(_,{ and(pk(USER), pk(ASP)), and(older(T), pk(USER)) })
reversible vtxo script = tr(_,{ { and(pk(SENDER), pk(ASP)), and(older(T), pk(SENDER)) }, { and(pk(RECEIVER), pk(ASP) } })
*/ */
type VtxoScript[T TaprootTree] interface { type VtxoScript[T TaprootTree, F interface{}, E interface{}] interface {
Validate(server *secp256k1.PublicKey, minExitDelay uint) error
TapTree() (taprootKey *secp256k1.PublicKey, taprootScriptTree T, err error) TapTree() (taprootKey *secp256k1.PublicKey, taprootScriptTree T, err error)
ToDescriptor() string Encode() ([]string, error)
FromDescriptor(descriptor string) error Decode(scripts []string) error
SmallestExitDelay() (uint, error)
ForfeitClosures() []F
ExitClosures() []E
} }
// BiggestLeafMerkleProof returns the leaf with the biggest witness size (for fee estimation) // BiggestLeafMerkleProof returns the leaf with the biggest witness size (for fee estimation)

View File

@@ -89,7 +89,7 @@ func (o Outpoint) Equals(other Outpoint) bool {
type Input struct { type Input struct {
Outpoint Outpoint
Descriptor string Tapscripts []string
} }
type AsyncPaymentInput struct { type AsyncPaymentInput struct {
@@ -129,9 +129,9 @@ func (v Vtxo) Address(asp *secp256k1.PublicKey, net common.Network) (string, err
return a.Encode() return a.Encode()
} }
type DescriptorVtxo struct { type TapscriptsVtxo struct {
Vtxo Vtxo
Descriptor string Tapscripts []string
} }
type Output struct { type Output struct {

View File

@@ -144,7 +144,11 @@ func toProtoInput(i client.Input) *arkv1.Input {
Txid: i.Txid, Txid: i.Txid,
Vout: i.VOut, Vout: i.VOut,
}, },
Descriptor_: i.Descriptor, TaprootTree: &arkv1.Input_Tapscripts{
Tapscripts: &arkv1.Tapscripts{
Scripts: i.Tapscripts,
},
},
} }
} }

View File

@@ -118,7 +118,9 @@ func (a *restClient) RegisterInputsForNextRound(
Txid: i.Txid, Txid: i.Txid,
Vout: int64(i.VOut), Vout: int64(i.VOut),
}, },
Descriptor: i.Descriptor, Tapscripts: &models.V1Tapscripts{
Scripts: i.Tapscripts,
},
}) })
} }
body := &models.V1RegisterInputsForNextRoundRequest{ body := &models.V1RegisterInputsForNextRoundRequest{
@@ -402,7 +404,9 @@ func (a *restClient) CreatePayment(
Txid: i.Input.Txid, Txid: i.Input.Txid,
Vout: int64(i.VOut), Vout: int64(i.VOut),
}, },
Descriptor: i.Input.Descriptor, Tapscripts: &models.V1Tapscripts{
Scripts: i.Input.Tapscripts,
},
}, },
ForfeitLeafHash: i.ForfeitLeafHash.String(), ForfeitLeafHash: i.ForfeitLeafHash.String(),
}) })

View File

@@ -8,6 +8,7 @@ package models
import ( import (
"context" "context"
"github.com/go-openapi/errors"
"github.com/go-openapi/strfmt" "github.com/go-openapi/strfmt"
"github.com/go-openapi/swag" "github.com/go-openapi/swag"
) )
@@ -22,15 +23,76 @@ type V1GetBoardingAddressResponse struct {
// descriptor // descriptor
Descriptor string `json:"descriptor,omitempty"` Descriptor string `json:"descriptor,omitempty"`
// tapscripts
Tapscripts *V1Tapscripts `json:"tapscripts,omitempty"`
} }
// Validate validates this v1 get boarding address response // Validate validates this v1 get boarding address response
func (m *V1GetBoardingAddressResponse) Validate(formats strfmt.Registry) error { func (m *V1GetBoardingAddressResponse) Validate(formats strfmt.Registry) error {
var res []error
if err := m.validateTapscripts(formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil return nil
} }
// ContextValidate validates this v1 get boarding address response based on context it is used func (m *V1GetBoardingAddressResponse) validateTapscripts(formats strfmt.Registry) error {
if swag.IsZero(m.Tapscripts) { // not required
return nil
}
if m.Tapscripts != nil {
if err := m.Tapscripts.Validate(formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("tapscripts")
} else if ce, ok := err.(*errors.CompositeError); ok {
return ce.ValidateName("tapscripts")
}
return err
}
}
return nil
}
// ContextValidate validate this v1 get boarding address response based on the context it is used
func (m *V1GetBoardingAddressResponse) ContextValidate(ctx context.Context, formats strfmt.Registry) error { func (m *V1GetBoardingAddressResponse) ContextValidate(ctx context.Context, formats strfmt.Registry) error {
var res []error
if err := m.contextValidateTapscripts(ctx, formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
func (m *V1GetBoardingAddressResponse) contextValidateTapscripts(ctx context.Context, formats strfmt.Registry) error {
if m.Tapscripts != nil {
if swag.IsZero(m.Tapscripts) { // not required
return nil
}
if err := m.Tapscripts.ContextValidate(ctx, formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("tapscripts")
} else if ce, ok := err.(*errors.CompositeError); ok {
return ce.ValidateName("tapscripts")
}
return err
}
}
return nil return nil
} }

View File

@@ -23,6 +23,9 @@ type V1Input struct {
// outpoint // outpoint
Outpoint *V1Outpoint `json:"outpoint,omitempty"` Outpoint *V1Outpoint `json:"outpoint,omitempty"`
// tapscripts
Tapscripts *V1Tapscripts `json:"tapscripts,omitempty"`
} }
// Validate validates this v1 input // Validate validates this v1 input
@@ -33,6 +36,10 @@ func (m *V1Input) Validate(formats strfmt.Registry) error {
res = append(res, err) res = append(res, err)
} }
if err := m.validateTapscripts(formats); err != nil {
res = append(res, err)
}
if len(res) > 0 { if len(res) > 0 {
return errors.CompositeValidationError(res...) return errors.CompositeValidationError(res...)
} }
@@ -58,6 +65,25 @@ func (m *V1Input) validateOutpoint(formats strfmt.Registry) error {
return nil return nil
} }
func (m *V1Input) validateTapscripts(formats strfmt.Registry) error {
if swag.IsZero(m.Tapscripts) { // not required
return nil
}
if m.Tapscripts != nil {
if err := m.Tapscripts.Validate(formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("tapscripts")
} else if ce, ok := err.(*errors.CompositeError); ok {
return ce.ValidateName("tapscripts")
}
return err
}
}
return nil
}
// ContextValidate validate this v1 input based on the context it is used // ContextValidate validate this v1 input based on the context it is used
func (m *V1Input) ContextValidate(ctx context.Context, formats strfmt.Registry) error { func (m *V1Input) ContextValidate(ctx context.Context, formats strfmt.Registry) error {
var res []error var res []error
@@ -66,6 +92,10 @@ func (m *V1Input) ContextValidate(ctx context.Context, formats strfmt.Registry)
res = append(res, err) res = append(res, err)
} }
if err := m.contextValidateTapscripts(ctx, formats); err != nil {
res = append(res, err)
}
if len(res) > 0 { if len(res) > 0 {
return errors.CompositeValidationError(res...) return errors.CompositeValidationError(res...)
} }
@@ -93,6 +123,27 @@ func (m *V1Input) contextValidateOutpoint(ctx context.Context, formats strfmt.Re
return nil return nil
} }
func (m *V1Input) contextValidateTapscripts(ctx context.Context, formats strfmt.Registry) error {
if m.Tapscripts != nil {
if swag.IsZero(m.Tapscripts) { // not required
return nil
}
if err := m.Tapscripts.ContextValidate(ctx, formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("tapscripts")
} else if ce, ok := err.(*errors.CompositeError); ok {
return ce.ValidateName("tapscripts")
}
return err
}
}
return nil
}
// MarshalBinary interface implementation // MarshalBinary interface implementation
func (m *V1Input) MarshalBinary() ([]byte, error) { func (m *V1Input) MarshalBinary() ([]byte, error) {
if m == nil { if m == nil {

View File

@@ -12,7 +12,7 @@ import (
"github.com/go-openapi/swag" "github.com/go-openapi/swag"
) )
// V1OwnershipProof This message is used to prove to the ASP that the user controls the vtxo without revealing the whole VTXO descriptor. // V1OwnershipProof This message is used to prove to the ASP that the user controls the vtxo without revealing the whole VTXO taproot tree.
// //
// swagger:model v1OwnershipProof // swagger:model v1OwnershipProof
type V1OwnershipProof struct { type V1OwnershipProof struct {

View File

@@ -0,0 +1,50 @@
// Code generated by go-swagger; DO NOT EDIT.
package models
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"context"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
)
// V1Tapscripts v1 tapscripts
//
// swagger:model v1Tapscripts
type V1Tapscripts struct {
// scripts
Scripts []string `json:"scripts"`
}
// Validate validates this v1 tapscripts
func (m *V1Tapscripts) Validate(formats strfmt.Registry) error {
return nil
}
// ContextValidate validates this v1 tapscripts based on context it is used
func (m *V1Tapscripts) ContextValidate(ctx context.Context, formats strfmt.Registry) error {
return nil
}
// MarshalBinary interface implementation
func (m *V1Tapscripts) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
return swag.WriteJSON(m)
}
// UnmarshalBinary interface implementation
func (m *V1Tapscripts) UnmarshalBinary(b []byte) error {
var res V1Tapscripts
if err := swag.ReadJSON(b, &res); err != nil {
return err
}
*m = res
return nil
}

View File

@@ -22,7 +22,6 @@ import (
"github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcwallet/waddrmgr" "github.com/btcsuite/btcwallet/waddrmgr"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/lightningnetwork/lnd/lnwallet/chainfee" "github.com/lightningnetwork/lnd/lnwallet/chainfee"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/vulpemventures/go-elements/address" "github.com/vulpemventures/go-elements/address"
@@ -518,7 +517,7 @@ func (a *covenantArkClient) CollaborativeRedeem(
}, },
} }
vtxos := make([]client.DescriptorVtxo, 0) vtxos := make([]client.TapscriptsVtxo, 0)
spendableVtxos, err := a.getVtxos(ctx, false, nil) spendableVtxos, err := a.getVtxos(ctx, false, nil)
if err != nil { if err != nil {
return "", err return "", err
@@ -532,9 +531,9 @@ func (a *covenantArkClient) CollaborativeRedeem(
} }
if vtxoAddr == offchainAddr.Address { if vtxoAddr == offchainAddr.Address {
vtxos = append(vtxos, client.DescriptorVtxo{ vtxos = append(vtxos, client.TapscriptsVtxo{
Vtxo: v, Vtxo: v,
Descriptor: offchainAddr.Descriptor, Tapscripts: offchainAddr.Tapscripts,
}) })
} }
} }
@@ -572,7 +571,7 @@ func (a *covenantArkClient) CollaborativeRedeem(
Txid: coin.Txid, Txid: coin.Txid,
VOut: coin.VOut, VOut: coin.VOut,
}, },
Descriptor: coin.Descriptor, Tapscripts: coin.Tapscripts,
}) })
} }
for _, coin := range selectedBoardingUtxos { for _, coin := range selectedBoardingUtxos {
@@ -581,7 +580,7 @@ func (a *covenantArkClient) CollaborativeRedeem(
Txid: coin.Txid, Txid: coin.Txid,
VOut: coin.VOut, VOut: coin.VOut,
}, },
Descriptor: coin.Descriptor, Tapscripts: coin.Tapscripts,
}) })
} }
@@ -661,7 +660,7 @@ func (a *covenantArkClient) getAllBoardingUtxos(ctx context.Context) ([]types.Ut
VOut: uint32(i), VOut: uint32(i),
Amount: vout.Amount, Amount: vout.Amount,
CreatedAt: createdAt, CreatedAt: createdAt,
Descriptor: addr.Descriptor, Tapscripts: addr.Tapscripts,
}) })
} }
} }
@@ -680,17 +679,14 @@ func (a *covenantArkClient) getClaimableBoardingUtxos(ctx context.Context, opts
claimable := make([]types.Utxo, 0) claimable := make([]types.Utxo, 0)
for _, addr := range boardingAddrs { for _, addr := range boardingAddrs {
boardingScript, err := tree.ParseVtxoScript(addr.Descriptor) boardingScript, err := tree.ParseVtxoScript(addr.Tapscripts)
if err != nil { if err != nil {
return nil, err return nil, err
} }
var boardingTimeout uint boardingTimeout, err := boardingScript.SmallestExitDelay()
if err != nil {
if defaultVtxo, ok := boardingScript.(*tree.DefaultVtxoScript); ok { return nil, err
boardingTimeout = defaultVtxo.ExitDelay
} else {
return nil, fmt.Errorf("unsupported boarding descriptor: %s", addr.Descriptor)
} }
boardingUtxos, err := a.explorer.GetUtxos(addr.Address) boardingUtxos, err := a.explorer.GetUtxos(addr.Address)
@@ -719,7 +715,7 @@ func (a *covenantArkClient) getClaimableBoardingUtxos(ctx context.Context, opts
} }
} }
u := utxo.ToUtxo(boardingTimeout, addr.Descriptor) u := utxo.ToUtxo(boardingTimeout, addr.Tapscripts)
if u.SpendableAt.Before(now) { if u.SpendableAt.Before(now) {
continue continue
} }
@@ -929,7 +925,7 @@ func (a *covenantArkClient) sendOffchain(
return "", fmt.Errorf("no offchain addresses found") return "", fmt.Errorf("no offchain addresses found")
} }
vtxos := make([]client.DescriptorVtxo, 0) vtxos := make([]client.TapscriptsVtxo, 0)
spendableVtxos, err := a.getVtxos(ctx, withExpiryCoinselect, nil) spendableVtxos, err := a.getVtxos(ctx, withExpiryCoinselect, nil)
if err != nil { if err != nil {
@@ -944,9 +940,9 @@ func (a *covenantArkClient) sendOffchain(
} }
if vtxoAddr == offchainAddr.Address { if vtxoAddr == offchainAddr.Address {
vtxos = append(vtxos, client.DescriptorVtxo{ vtxos = append(vtxos, client.TapscriptsVtxo{
Vtxo: v, Vtxo: v,
Descriptor: offchainAddr.Descriptor, Tapscripts: offchainAddr.Tapscripts,
}) })
} }
} }
@@ -958,7 +954,7 @@ func (a *covenantArkClient) sendOffchain(
} }
var selectedBoardingCoins []types.Utxo var selectedBoardingCoins []types.Utxo
var selectedCoins []client.DescriptorVtxo var selectedCoins []client.TapscriptsVtxo
var changeAmount uint64 var changeAmount uint64
// if no receivers, self send all selected coins // if no receivers, self send all selected coins
@@ -1009,7 +1005,7 @@ func (a *covenantArkClient) sendOffchain(
Txid: coin.Txid, Txid: coin.Txid,
VOut: coin.VOut, VOut: coin.VOut,
}, },
Descriptor: coin.Descriptor, Tapscripts: coin.Tapscripts,
}) })
} }
for _, coin := range selectedBoardingCoins { for _, coin := range selectedBoardingCoins {
@@ -1018,7 +1014,7 @@ func (a *covenantArkClient) sendOffchain(
Txid: coin.Txid, Txid: coin.Txid,
VOut: coin.VOut, VOut: coin.VOut,
}, },
Descriptor: coin.Descriptor, Tapscripts: coin.Tapscripts,
}) })
} }
@@ -1057,19 +1053,33 @@ func (a *covenantArkClient) addInputs(
return err return err
} }
vtxoScript, err := tree.ParseVtxoScript(offchain.Descriptor) vtxoScript, err := tree.ParseVtxoScript(offchain.Tapscripts)
if err != nil { if err != nil {
return err return err
} }
var userPubkey, aspPubkey *secp256k1.PublicKey forfeitClosure := vtxoScript.ForfeitClosures()[0]
switch s := vtxoScript.(type) { forfeitScript, err := forfeitClosure.Script()
case *tree.DefaultVtxoScript: if err != nil {
userPubkey = s.Owner return err
aspPubkey = s.Asp }
default:
return fmt.Errorf("unsupported vtxo script: %T", s) forfeitLeaf := taproot.NewBaseTapElementsLeaf(forfeitScript)
_, taprootTree, err := vtxoScript.TapTree()
if err != nil {
return err
}
leafProof, err := taprootTree.GetTaprootMerkleProof(forfeitLeaf.TapHash())
if err != nil {
return err
}
controlBlock, err := taproot.ParseControlBlock(leafProof.Script)
if err != nil {
return err
} }
for _, utxo := range utxos { for _, utxo := range utxos {
@@ -1088,37 +1098,6 @@ func (a *covenantArkClient) addInputs(
return err return err
} }
vtxoScript := &tree.DefaultVtxoScript{
Owner: userPubkey,
Asp: aspPubkey,
ExitDelay: utxo.Delay,
}
forfeitClosure := &tree.MultisigClosure{
Pubkey: userPubkey,
AspPubkey: aspPubkey,
}
forfeitLeaf, err := forfeitClosure.Leaf()
if err != nil {
return err
}
_, taprootTree, err := vtxoScript.TapTree()
if err != nil {
return err
}
leafProof, err := taprootTree.GetTaprootMerkleProof(forfeitLeaf.TapHash())
if err != nil {
return err
}
controlBlock, err := taproot.ParseControlBlock(leafProof.Script)
if err != nil {
return err
}
inputIndex := len(updater.Pset.Inputs) - 1 inputIndex := len(updater.Pset.Inputs) - 1
if err := updater.AddInTapLeafScript( if err := updater.AddInTapLeafScript(
@@ -1138,7 +1117,7 @@ func (a *covenantArkClient) addInputs(
func (a *covenantArkClient) handleRoundStream( func (a *covenantArkClient) handleRoundStream(
ctx context.Context, ctx context.Context,
paymentID string, paymentID string,
vtxosToSign []client.DescriptorVtxo, vtxosToSign []client.TapscriptsVtxo,
boardingUtxos []types.Utxo, boardingUtxos []types.Utxo,
receivers []client.Output, receivers []client.Output,
) (string, error) { ) (string, error) {
@@ -1202,7 +1181,7 @@ func (a *covenantArkClient) handleRoundStream(
func (a *covenantArkClient) handleRoundFinalization( func (a *covenantArkClient) handleRoundFinalization(
ctx context.Context, ctx context.Context,
event client.RoundFinalizationEvent, event client.RoundFinalizationEvent,
vtxos []client.DescriptorVtxo, vtxos []client.TapscriptsVtxo,
boardingUtxos []types.Utxo, boardingUtxos []types.Utxo,
receivers []client.Output, receivers []client.Output,
) (signedForfeits []string, signedRoundTx string, err error) { ) (signedForfeits []string, signedRoundTx string, err error) {
@@ -1233,28 +1212,20 @@ func (a *covenantArkClient) handleRoundFinalization(
} }
for _, boardingUtxo := range boardingUtxos { for _, boardingUtxo := range boardingUtxos {
boardingVtxoScript, err := tree.ParseVtxoScript(boardingUtxo.Descriptor) boardingVtxoScript, err := tree.ParseVtxoScript(boardingUtxo.Tapscripts)
if err != nil { if err != nil {
return nil, "", err return nil, "", err
} }
var forfeitClosure tree.Closure forfeitClosure := boardingVtxoScript.ForfeitClosures()[0]
switch s := boardingVtxoScript.(type) { forfeitScript, err := forfeitClosure.Script()
case *tree.DefaultVtxoScript:
forfeitClosure = &tree.MultisigClosure{
Pubkey: s.Owner,
AspPubkey: a.AspPubkey,
}
default:
return nil, "", fmt.Errorf("unsupported boarding descriptor: %s", boardingUtxo.Descriptor)
}
forfeitLeaf, err := forfeitClosure.Leaf()
if err != nil { if err != nil {
return nil, "", err return nil, "", err
} }
forfeitLeaf := taproot.NewBaseTapElementsLeaf(forfeitScript)
_, taprootTree, err := boardingVtxoScript.TapTree() _, taprootTree, err := boardingVtxoScript.TapTree()
if err != nil { if err != nil {
return nil, "", err return nil, "", err
@@ -1430,7 +1401,7 @@ func (a *covenantArkClient) validateOffChainReceiver(
func (a *covenantArkClient) createAndSignForfeits( func (a *covenantArkClient) createAndSignForfeits(
ctx context.Context, ctx context.Context,
vtxosToSign []client.DescriptorVtxo, vtxosToSign []client.TapscriptsVtxo,
connectors []string, connectors []string,
feeRate chainfee.SatPerKVByte, feeRate chainfee.SatPerKVByte,
) ([]string, error) { ) ([]string, error) {
@@ -1452,7 +1423,7 @@ func (a *covenantArkClient) createAndSignForfeits(
} }
for _, vtxo := range vtxosToSign { for _, vtxo := range vtxosToSign {
vtxoScript, err := tree.ParseVtxoScript(vtxo.Descriptor) vtxoScript, err := tree.ParseVtxoScript(vtxo.Tapscripts)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -1472,25 +1443,15 @@ func (a *covenantArkClient) createAndSignForfeits(
TxIndex: vtxo.VOut, TxIndex: vtxo.VOut,
} }
var forfeitClosure tree.Closure forfeitClosure := vtxoScript.ForfeitClosures()[0]
var witnessSize int
switch s := vtxoScript.(type) { forfeitScript, err := forfeitClosure.Script()
case *tree.DefaultVtxoScript:
forfeitClosure = &tree.MultisigClosure{
Pubkey: s.Owner,
AspPubkey: a.AspPubkey,
}
witnessSize = 64 * 2
default:
return nil, fmt.Errorf("unsupported vtxo script: %T", s)
}
forfeitLeaf, err := forfeitClosure.Leaf()
if err != nil { if err != nil {
return nil, err return nil, err
} }
forfeitLeaf := taproot.NewBaseTapElementsLeaf(forfeitScript)
leafProof, err := vtxoTapTree.GetTaprootMerkleProof(forfeitLeaf.TapHash()) leafProof, err := vtxoTapTree.GetTaprootMerkleProof(forfeitLeaf.TapHash())
if err != nil { if err != nil {
return nil, err return nil, err
@@ -1512,7 +1473,7 @@ func (a *covenantArkClient) createAndSignForfeits(
RevealedScript: leafProof.Script, RevealedScript: leafProof.Script,
ControlBlock: &ctrlBlock.ControlBlock, ControlBlock: &ctrlBlock.ControlBlock,
}, },
witnessSize, forfeitClosure.WitnessSize(),
txscript.WitnessV0PubKeyHashTy, txscript.WitnessV0PubKeyHashTy,
) )
if err != nil { if err != nil {
@@ -1567,19 +1528,14 @@ func (a *covenantArkClient) coinSelectOnchain(
fetchedUtxos := make([]types.Utxo, 0) fetchedUtxos := make([]types.Utxo, 0)
for _, addr := range boardingAddrs { for _, addr := range boardingAddrs {
boardingDescriptor := addr.Descriptor boardingScript, err := tree.ParseVtxoScript(addr.Tapscripts)
boardingScript, err := tree.ParseVtxoScript(boardingDescriptor)
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
} }
var boardingTimeout uint boardingTimeout, err := boardingScript.SmallestExitDelay()
if err != nil {
if defaultVtxo, ok := boardingScript.(*tree.DefaultVtxoScript); ok { return nil, 0, err
boardingTimeout = defaultVtxo.ExitDelay
} else {
return nil, 0, fmt.Errorf("unsupported boarding descriptor: %s", boardingDescriptor)
} }
utxos, err := a.explorer.GetUtxos(addr.Address) utxos, err := a.explorer.GetUtxos(addr.Address)
@@ -1588,7 +1544,7 @@ func (a *covenantArkClient) coinSelectOnchain(
} }
for _, utxo := range utxos { for _, utxo := range utxos {
u := utxo.ToUtxo(boardingTimeout, addr.Descriptor) u := utxo.ToUtxo(boardingTimeout, addr.Tapscripts)
if u.SpendableAt.Before(now) { if u.SpendableAt.Before(now) {
fetchedUtxos = append(fetchedUtxos, u) fetchedUtxos = append(fetchedUtxos, u)
} }
@@ -1624,7 +1580,7 @@ func (a *covenantArkClient) coinSelectOnchain(
} }
for _, utxo := range utxos { for _, utxo := range utxos {
u := utxo.ToUtxo(uint(a.UnilateralExitDelay), addr.Descriptor) u := utxo.ToUtxo(uint(a.UnilateralExitDelay), addr.Tapscripts)
if u.SpendableAt.Before(now) { if u.SpendableAt.Before(now) {
fetchedUtxos = append(fetchedUtxos, u) fetchedUtxos = append(fetchedUtxos, u)
} }

View File

@@ -857,7 +857,7 @@ func (a *covenantlessArkClient) CollaborativeRedeem(
}, },
} }
vtxos := make([]client.DescriptorVtxo, 0) vtxos := make([]client.TapscriptsVtxo, 0)
spendableVtxos, err := a.getVtxos(ctx, nil) spendableVtxos, err := a.getVtxos(ctx, nil)
if err != nil { if err != nil {
return "", err return "", err
@@ -871,9 +871,9 @@ func (a *covenantlessArkClient) CollaborativeRedeem(
} }
if vtxoAddr == offchainAddr.Address { if vtxoAddr == offchainAddr.Address {
vtxos = append(vtxos, client.DescriptorVtxo{ vtxos = append(vtxos, client.TapscriptsVtxo{
Vtxo: v, Vtxo: v,
Descriptor: offchainAddr.Descriptor, Tapscripts: offchainAddr.Tapscripts,
}) })
} }
} }
@@ -911,7 +911,7 @@ func (a *covenantlessArkClient) CollaborativeRedeem(
Txid: coin.Txid, Txid: coin.Txid,
VOut: coin.VOut, VOut: coin.VOut,
}, },
Descriptor: coin.Descriptor, Tapscripts: coin.Tapscripts,
}) })
} }
for _, coin := range selectedBoardingCoins { for _, coin := range selectedBoardingCoins {
@@ -920,7 +920,7 @@ func (a *covenantlessArkClient) CollaborativeRedeem(
Txid: coin.Txid, Txid: coin.Txid,
VOut: coin.VOut, VOut: coin.VOut,
}, },
Descriptor: coin.Descriptor, Tapscripts: coin.Tapscripts,
}) })
} }
@@ -1004,7 +1004,7 @@ func (a *covenantlessArkClient) SendAsync(
sumOfReceivers += receiver.Amount() sumOfReceivers += receiver.Amount()
} }
vtxos := make([]client.DescriptorVtxo, 0) vtxos := make([]client.TapscriptsVtxo, 0)
opts := &CoinSelectOptions{ opts := &CoinSelectOptions{
WithExpirySorting: withExpiryCoinselect, WithExpirySorting: withExpiryCoinselect,
} }
@@ -1021,9 +1021,9 @@ func (a *covenantlessArkClient) SendAsync(
} }
if vtxoAddr == offchainAddr.Address { if vtxoAddr == offchainAddr.Address {
vtxos = append(vtxos, client.DescriptorVtxo{ vtxos = append(vtxos, client.TapscriptsVtxo{
Vtxo: v, Vtxo: v,
Descriptor: offchainAddr.Descriptor, Tapscripts: offchainAddr.Tapscripts,
}) })
} }
} }
@@ -1048,35 +1048,27 @@ func (a *covenantlessArkClient) SendAsync(
inputs := make([]client.AsyncPaymentInput, 0, len(selectedCoins)) inputs := make([]client.AsyncPaymentInput, 0, len(selectedCoins))
for _, coin := range selectedCoins { for _, coin := range selectedCoins {
vtxoScript, err := bitcointree.ParseVtxoScript(coin.Descriptor) vtxoScript, err := bitcointree.ParseVtxoScript(coin.Tapscripts)
if err != nil { if err != nil {
return "", err return "", err
} }
var forfeitClosure bitcointree.Closure forfeitClosure := vtxoScript.ForfeitClosures()[0]
switch s := vtxoScript.(type) { forfeitScript, err := forfeitClosure.Script()
case *bitcointree.DefaultVtxoScript:
forfeitClosure = &bitcointree.MultisigClosure{
Pubkey: s.Owner,
AspPubkey: s.Asp,
}
default:
return "", fmt.Errorf("unsupported vtxo script: %T", s)
}
forfeitLeaf, err := forfeitClosure.Leaf()
if err != nil { if err != nil {
return "", err return "", err
} }
forfeitLeaf := txscript.NewBaseTapLeaf(forfeitScript)
inputs = append(inputs, client.AsyncPaymentInput{ inputs = append(inputs, client.AsyncPaymentInput{
Input: client.Input{ Input: client.Input{
Outpoint: client.Outpoint{ Outpoint: client.Outpoint{
Txid: coin.Txid, Txid: coin.Txid,
VOut: coin.VOut, VOut: coin.VOut,
}, },
Descriptor: coin.Descriptor, Tapscripts: coin.Tapscripts,
}, },
ForfeitLeafHash: forfeitLeaf.TapHash(), ForfeitLeafHash: forfeitLeaf.TapHash(),
}) })
@@ -1158,7 +1150,7 @@ func (a *covenantlessArkClient) SetNostrNotificationRecipient(ctx context.Contex
return err return err
} }
descriptorVtxos := make([]client.DescriptorVtxo, 0) descriptorVtxos := make([]client.TapscriptsVtxo, 0)
for _, offchainAddr := range offchainAddrs { for _, offchainAddr := range offchainAddrs {
for _, vtxo := range spendableVtxos { for _, vtxo := range spendableVtxos {
vtxoAddr, err := vtxo.Address(a.AspPubkey, a.Network) vtxoAddr, err := vtxo.Address(a.AspPubkey, a.Network)
@@ -1167,9 +1159,9 @@ func (a *covenantlessArkClient) SetNostrNotificationRecipient(ctx context.Contex
} }
if vtxoAddr == offchainAddr.Address { if vtxoAddr == offchainAddr.Address {
descriptorVtxos = append(descriptorVtxos, client.DescriptorVtxo{ descriptorVtxos = append(descriptorVtxos, client.TapscriptsVtxo{
Vtxo: vtxo, Vtxo: vtxo,
Descriptor: offchainAddr.Descriptor, Tapscripts: offchainAddr.Tapscripts,
}) })
} }
} }
@@ -1187,35 +1179,24 @@ func (a *covenantlessArkClient) SetNostrNotificationRecipient(ctx context.Contex
} }
// validate the vtxo script type // validate the vtxo script type
vtxoScript, err := bitcointree.ParseVtxoScript(v.Descriptor) vtxoScript, err := bitcointree.ParseVtxoScript(v.Tapscripts)
if err != nil { if err != nil {
return err return err
} }
var forfeitClosure bitcointree.Closure forfeitClosure := vtxoScript.ForfeitClosures()[0]
var signingPubkey string
if defaultVtxoScript, ok := vtxoScript.(*bitcointree.DefaultVtxoScript); ok {
forfeitClosure = &bitcointree.MultisigClosure{
Pubkey: defaultVtxoScript.Owner,
AspPubkey: defaultVtxoScript.Asp,
}
signingPubkey = hex.EncodeToString(schnorr.SerializePubKey(defaultVtxoScript.Owner))
} else {
return fmt.Errorf("unsupported vtxo script: %T", vtxoScript)
}
_, tapTree, err := vtxoScript.TapTree() _, tapTree, err := vtxoScript.TapTree()
if err != nil { if err != nil {
return err return err
} }
forfeitLeaf, err := forfeitClosure.Leaf() forfeitScript, err := forfeitClosure.Script()
if err != nil { if err != nil {
return err return err
} }
forfeitLeaf := txscript.NewBaseTapLeaf(forfeitScript)
merkleProof, err := tapTree.GetTaprootMerkleProof(forfeitLeaf.TapHash()) merkleProof, err := tapTree.GetTaprootMerkleProof(forfeitLeaf.TapHash())
if err != nil { if err != nil {
return err return err
@@ -1236,7 +1217,7 @@ func (a *covenantlessArkClient) SetNostrNotificationRecipient(ctx context.Contex
outpointBytes := append(txhash[:], voutBytes...) outpointBytes := append(txhash[:], voutBytes...)
sigMsg := sha256.Sum256(outpointBytes) sigMsg := sha256.Sum256(outpointBytes)
sig, err := a.wallet.SignMessage(ctx, sigMsg[:], signingPubkey) sig, err := a.wallet.SignMessage(ctx, sigMsg[:])
if err != nil { if err != nil {
return err return err
} }
@@ -1434,7 +1415,7 @@ func (a *covenantlessArkClient) sendOffchain(
return "", fmt.Errorf("no offchain addresses found") return "", fmt.Errorf("no offchain addresses found")
} }
vtxos := make([]client.DescriptorVtxo, 0) vtxos := make([]client.TapscriptsVtxo, 0)
opts := &CoinSelectOptions{ opts := &CoinSelectOptions{
WithExpirySorting: withExpiryCoinselect} WithExpirySorting: withExpiryCoinselect}
spendableVtxos, err := a.getVtxos(ctx, opts) spendableVtxos, err := a.getVtxos(ctx, opts)
@@ -1450,9 +1431,9 @@ func (a *covenantlessArkClient) sendOffchain(
} }
if vtxoAddr == offchainAddr.Address { if vtxoAddr == offchainAddr.Address {
vtxos = append(vtxos, client.DescriptorVtxo{ vtxos = append(vtxos, client.TapscriptsVtxo{
Vtxo: v, Vtxo: v,
Descriptor: offchainAddr.Descriptor, Tapscripts: offchainAddr.Tapscripts,
}) })
} }
} }
@@ -1464,7 +1445,7 @@ func (a *covenantlessArkClient) sendOffchain(
} }
var selectedBoardingCoins []types.Utxo var selectedBoardingCoins []types.Utxo
var selectedCoins []client.DescriptorVtxo var selectedCoins []client.TapscriptsVtxo
var changeAmount uint64 var changeAmount uint64
// if no receivers, self send all selected coins // if no receivers, self send all selected coins
@@ -1513,7 +1494,7 @@ func (a *covenantlessArkClient) sendOffchain(
Txid: coin.Txid, Txid: coin.Txid,
VOut: coin.VOut, VOut: coin.VOut,
}, },
Descriptor: coin.Descriptor, Tapscripts: coin.Tapscripts,
}) })
} }
for _, boardingUtxo := range selectedBoardingCoins { for _, boardingUtxo := range selectedBoardingCoins {
@@ -1522,7 +1503,7 @@ func (a *covenantlessArkClient) sendOffchain(
Txid: boardingUtxo.Txid, Txid: boardingUtxo.Txid,
VOut: boardingUtxo.VOut, VOut: boardingUtxo.VOut,
}, },
Descriptor: boardingUtxo.Descriptor, Tapscripts: boardingUtxo.Tapscripts,
}) })
} }
@@ -1567,21 +1548,11 @@ func (a *covenantlessArkClient) addInputs(
return err return err
} }
vtxoScript, err := tree.ParseVtxoScript(offchain.Descriptor) vtxoScript, err := bitcointree.ParseVtxoScript(offchain.Tapscripts)
if err != nil { if err != nil {
return err return err
} }
var userPubkey, aspPubkey *secp256k1.PublicKey
switch s := vtxoScript.(type) {
case *tree.DefaultVtxoScript:
userPubkey = s.Owner
aspPubkey = s.Asp
default:
return fmt.Errorf("unsupported vtxo script: %T", s)
}
for _, utxo := range utxos { for _, utxo := range utxos {
previousHash, err := chainhash.NewHashFromStr(utxo.Txid) previousHash, err := chainhash.NewHashFromStr(utxo.Txid)
if err != nil { if err != nil {
@@ -1601,18 +1572,14 @@ func (a *covenantlessArkClient) addInputs(
Sequence: sequence, Sequence: sequence,
}) })
vtxoScript := &bitcointree.DefaultVtxoScript{ exitClosures := vtxoScript.ExitClosures()
Owner: userPubkey, if len(exitClosures) <= 0 {
Asp: aspPubkey, return fmt.Errorf("no exit closures found")
ExitDelay: utxo.Delay,
} }
exitClosure := &bitcointree.CSVSigClosure{ exitClosure := exitClosures[0]
Pubkey: userPubkey,
Seconds: uint(utxo.Delay),
}
exitLeaf, err := exitClosure.Leaf() exitScript, err := exitClosure.Script()
if err != nil { if err != nil {
return err return err
} }
@@ -1622,6 +1589,7 @@ func (a *covenantlessArkClient) addInputs(
return err return err
} }
exitLeaf := txscript.NewBaseTapLeaf(exitScript)
leafProof, err := taprootTree.GetTaprootMerkleProof(exitLeaf.TapHash()) leafProof, err := taprootTree.GetTaprootMerkleProof(exitLeaf.TapHash())
if err != nil { if err != nil {
return fmt.Errorf("failed to get taproot merkle proof: %s", err) return fmt.Errorf("failed to get taproot merkle proof: %s", err)
@@ -1644,7 +1612,7 @@ func (a *covenantlessArkClient) addInputs(
func (a *covenantlessArkClient) handleRoundStream( func (a *covenantlessArkClient) handleRoundStream(
ctx context.Context, ctx context.Context,
paymentID string, paymentID string,
vtxosToSign []client.DescriptorVtxo, vtxosToSign []client.TapscriptsVtxo,
boardingUtxos []types.Utxo, boardingUtxos []types.Utxo,
receivers []client.Output, receivers []client.Output,
roundEphemeralKey *secp256k1.PrivateKey, roundEphemeralKey *secp256k1.PrivateKey,
@@ -1763,12 +1731,12 @@ func (a *covenantlessArkClient) handleRoundStream(
func (a *covenantlessArkClient) handleRoundSigningStarted( func (a *covenantlessArkClient) handleRoundSigningStarted(
ctx context.Context, ephemeralKey *secp256k1.PrivateKey, event client.RoundSigningStartedEvent, ctx context.Context, ephemeralKey *secp256k1.PrivateKey, event client.RoundSigningStartedEvent,
) (signerSession bitcointree.SignerSession, err error) { ) (signerSession bitcointree.SignerSession, err error) {
sweepClosure := bitcointree.CSVSigClosure{ sweepClosure := tree.CSVSigClosure{
Pubkey: a.AspPubkey, MultisigClosure: tree.MultisigClosure{PubKeys: []*secp256k1.PublicKey{a.AspPubkey}},
Seconds: uint(a.RoundLifetime), Seconds: uint(a.RoundLifetime),
} }
sweepTapLeaf, err := sweepClosure.Leaf() script, err := sweepClosure.Script()
if err != nil { if err != nil {
return return
} }
@@ -1781,7 +1749,8 @@ func (a *covenantlessArkClient) handleRoundSigningStarted(
sharedOutput := roundTx.UnsignedTx.TxOut[0] sharedOutput := roundTx.UnsignedTx.TxOut[0]
sharedOutputValue := sharedOutput.Value sharedOutputValue := sharedOutput.Value
sweepTapTree := txscript.AssembleTaprootScriptTree(*sweepTapLeaf) sweepTapLeaf := txscript.NewBaseTapLeaf(script)
sweepTapTree := txscript.AssembleTaprootScriptTree(sweepTapLeaf)
root := sweepTapTree.RootNode.TapHash() root := sweepTapTree.RootNode.TapHash()
signerSession = bitcointree.NewTreeSignerSession( signerSession = bitcointree.NewTreeSignerSession(
@@ -1838,7 +1807,7 @@ func (a *covenantlessArkClient) handleRoundSigningNoncesGenerated(
func (a *covenantlessArkClient) handleRoundFinalization( func (a *covenantlessArkClient) handleRoundFinalization(
ctx context.Context, ctx context.Context,
event client.RoundFinalizationEvent, event client.RoundFinalizationEvent,
vtxos []client.DescriptorVtxo, vtxos []client.TapscriptsVtxo,
boardingUtxos []types.Utxo, boardingUtxos []types.Utxo,
receivers []client.Output, receivers []client.Output,
) ([]string, string, error) { ) ([]string, string, error) {
@@ -1870,27 +1839,20 @@ func (a *covenantlessArkClient) handleRoundFinalization(
} }
for _, boardingUtxo := range boardingUtxos { for _, boardingUtxo := range boardingUtxos {
boardingVtxoScript, err := bitcointree.ParseVtxoScript(boardingUtxo.Descriptor) boardingVtxoScript, err := bitcointree.ParseVtxoScript(boardingUtxo.Tapscripts)
if err != nil { if err != nil {
return nil, "", err return nil, "", err
} }
var myPubkey *secp256k1.PublicKey
switch v := boardingVtxoScript.(type) {
case *bitcointree.DefaultVtxoScript:
myPubkey = v.Owner
default:
return nil, "", fmt.Errorf("unsupported boarding descriptor: %s", boardingUtxo.Descriptor)
}
// add tapscript leaf // add tapscript leaf
forfeitClosure := &bitcointree.MultisigClosure{ forfeitClosures := boardingVtxoScript.ForfeitClosures()
Pubkey: myPubkey, if len(forfeitClosures) <= 0 {
AspPubkey: a.AspPubkey, return nil, "", fmt.Errorf("no forfeit closures found")
} }
forfeitLeaf, err := forfeitClosure.Leaf() forfeitClosure := forfeitClosures[0]
forfeitScript, err := forfeitClosure.Script()
if err != nil { if err != nil {
return nil, "", err return nil, "", err
} }
@@ -1900,6 +1862,7 @@ func (a *covenantlessArkClient) handleRoundFinalization(
return nil, "", err return nil, "", err
} }
forfeitLeaf := txscript.NewBaseTapLeaf(forfeitScript)
forfeitProof, err := taprootTree.GetTaprootMerkleProof(forfeitLeaf.TapHash()) forfeitProof, err := taprootTree.GetTaprootMerkleProof(forfeitLeaf.TapHash())
if err != nil { if err != nil {
return nil, "", fmt.Errorf("failed to get taproot merkle proof for boarding utxo: %s", err) return nil, "", fmt.Errorf("failed to get taproot merkle proof for boarding utxo: %s", err)
@@ -2070,7 +2033,7 @@ func (a *covenantlessArkClient) validateOffChainReceiver(
func (a *covenantlessArkClient) createAndSignForfeits( func (a *covenantlessArkClient) createAndSignForfeits(
ctx context.Context, ctx context.Context,
vtxosToSign []client.DescriptorVtxo, vtxosToSign []client.TapscriptsVtxo,
connectors []string, connectors []string,
feeRate chainfee.SatPerKVByte, feeRate chainfee.SatPerKVByte,
) ([]string, error) { ) ([]string, error) {
@@ -2102,7 +2065,7 @@ func (a *covenantlessArkClient) createAndSignForfeits(
} }
for _, vtxo := range vtxosToSign { for _, vtxo := range vtxosToSign {
vtxoScript, err := bitcointree.ParseVtxoScript(vtxo.Descriptor) vtxoScript, err := bitcointree.ParseVtxoScript(vtxo.Tapscripts)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -2127,25 +2090,19 @@ func (a *covenantlessArkClient) createAndSignForfeits(
Index: vtxo.VOut, Index: vtxo.VOut,
} }
var forfeitClosure bitcointree.Closure forfeitClosures := vtxoScript.ForfeitClosures()
var witnessSize int if len(forfeitClosures) <= 0 {
return nil, fmt.Errorf("no forfeit closures found")
switch v := vtxoScript.(type) {
case *bitcointree.DefaultVtxoScript:
forfeitClosure = &bitcointree.MultisigClosure{
Pubkey: v.Owner,
AspPubkey: a.AspPubkey,
}
witnessSize = 64 * 2
default:
return nil, fmt.Errorf("unsupported vtxo script: %T", vtxoScript)
} }
forfeitLeaf, err := forfeitClosure.Leaf() forfeitClosure := forfeitClosures[0]
forfeitScript, err := forfeitClosure.Script()
if err != nil { if err != nil {
return nil, err return nil, err
} }
forfeitLeaf := txscript.NewBaseTapLeaf(forfeitScript)
leafProof, err := vtxoTapTree.GetTaprootMerkleProof(forfeitLeaf.TapHash()) leafProof, err := vtxoTapTree.GetTaprootMerkleProof(forfeitLeaf.TapHash())
if err != nil { if err != nil {
return nil, err return nil, err
@@ -2168,7 +2125,7 @@ func (a *covenantlessArkClient) createAndSignForfeits(
RevealedScript: leafProof.Script, RevealedScript: leafProof.Script,
ControlBlock: ctrlBlock, ControlBlock: ctrlBlock,
}, },
witnessSize, forfeitClosure.WitnessSize(),
parsedScript.Class(), parsedScript.Class(),
) )
if err != nil { if err != nil {
@@ -2221,25 +2178,23 @@ func (a *covenantlessArkClient) coinSelectOnchain(
fetchedUtxos := make([]types.Utxo, 0) fetchedUtxos := make([]types.Utxo, 0)
for _, addr := range boardingAddrs { for _, addr := range boardingAddrs {
boardingScript, err := bitcointree.ParseVtxoScript(addr.Descriptor) boardingScript, err := bitcointree.ParseVtxoScript(addr.Tapscripts)
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
} }
var boardingTimeout uint boardingTimeout, err := boardingScript.SmallestExitDelay()
if err != nil {
if defaultVtxo, ok := boardingScript.(*bitcointree.DefaultVtxoScript); ok { return nil, 0, err
boardingTimeout = defaultVtxo.ExitDelay
} else {
return nil, 0, fmt.Errorf("unsupported boarding descriptor: %s", addr.Descriptor)
} }
utxos, err := a.explorer.GetUtxos(addr.Address) utxos, err := a.explorer.GetUtxos(addr.Address)
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
} }
for _, utxo := range utxos { for _, utxo := range utxos {
u := utxo.ToUtxo(boardingTimeout, addr.Descriptor) u := utxo.ToUtxo(boardingTimeout, addr.Tapscripts)
if u.SpendableAt.Before(now) { if u.SpendableAt.Before(now) {
fetchedUtxos = append(fetchedUtxos, u) fetchedUtxos = append(fetchedUtxos, u)
} }
@@ -2275,7 +2230,7 @@ func (a *covenantlessArkClient) coinSelectOnchain(
} }
for _, utxo := range utxos { for _, utxo := range utxos {
u := utxo.ToUtxo(uint(a.UnilateralExitDelay), addr.Descriptor) u := utxo.ToUtxo(uint(a.UnilateralExitDelay), addr.Tapscripts)
if u.SpendableAt.Before(now) { if u.SpendableAt.Before(now) {
fetchedUtxos = append(fetchedUtxos, u) fetchedUtxos = append(fetchedUtxos, u)
} }
@@ -2407,7 +2362,7 @@ func (a *covenantlessArkClient) getAllBoardingUtxos(
VOut: uint32(i), VOut: uint32(i),
Amount: vout.Amount, Amount: vout.Amount,
CreatedAt: createdAt, CreatedAt: createdAt,
Descriptor: addr.Descriptor, Tapscripts: addr.Tapscripts,
Spent: spent, Spent: spent,
}) })
} }
@@ -2427,17 +2382,14 @@ func (a *covenantlessArkClient) getClaimableBoardingUtxos(ctx context.Context, o
claimable := make([]types.Utxo, 0) claimable := make([]types.Utxo, 0)
for _, addr := range boardingAddrs { for _, addr := range boardingAddrs {
boardingScript, err := bitcointree.ParseVtxoScript(addr.Descriptor) boardingScript, err := bitcointree.ParseVtxoScript(addr.Tapscripts)
if err != nil { if err != nil {
return nil, err return nil, err
} }
var boardingTimeout uint boardingTimeout, err := boardingScript.SmallestExitDelay()
if err != nil {
if defaultVtxo, ok := boardingScript.(*bitcointree.DefaultVtxoScript); ok { return nil, err
boardingTimeout = defaultVtxo.ExitDelay
} else {
return nil, fmt.Errorf("unsupported boarding descriptor: %s", addr.Descriptor)
} }
boardingUtxos, err := a.explorer.GetUtxos(addr.Address) boardingUtxos, err := a.explorer.GetUtxos(addr.Address)
@@ -2466,7 +2418,7 @@ func (a *covenantlessArkClient) getClaimableBoardingUtxos(ctx context.Context, o
} }
} }
u := utxo.ToUtxo(boardingTimeout, addr.Descriptor) u := utxo.ToUtxo(boardingTimeout, addr.Tapscripts)
if u.SpendableAt.Before(now) { if u.SpendableAt.Before(now) {
continue continue
} }

View File

@@ -53,8 +53,8 @@ type SpentStatus struct {
SpentBy string `json:"txid,omitempty"` SpentBy string `json:"txid,omitempty"`
} }
func (e ExplorerUtxo) ToUtxo(delay uint, descriptor string) types.Utxo { func (e ExplorerUtxo) ToUtxo(delay uint, tapscripts []string) types.Utxo {
return newUtxo(e, delay, descriptor) return newUtxo(e, delay, tapscripts)
} }
type Explorer interface { type Explorer interface {
@@ -415,7 +415,7 @@ func parseBitcoinTx(txStr string) (string, string, error) {
return txhex, txid, nil return txhex, txid, nil
} }
func newUtxo(explorerUtxo ExplorerUtxo, delay uint, descriptor string) types.Utxo { func newUtxo(explorerUtxo ExplorerUtxo, delay uint, tapscripts []string) types.Utxo {
utxoTime := explorerUtxo.Status.Blocktime utxoTime := explorerUtxo.Status.Blocktime
createdAt := time.Unix(utxoTime, 0) createdAt := time.Unix(utxoTime, 0)
if utxoTime == 0 { if utxoTime == 0 {
@@ -431,6 +431,6 @@ func newUtxo(explorerUtxo ExplorerUtxo, delay uint, descriptor string) types.Utx
Delay: delay, Delay: delay,
SpendableAt: time.Unix(utxoTime, 0).Add(time.Duration(delay) * time.Second), SpendableAt: time.Unix(utxoTime, 0).Add(time.Duration(delay) * time.Second),
CreatedAt: createdAt, CreatedAt: createdAt,
Descriptor: descriptor, Tapscripts: tapscripts,
} }
} }

View File

@@ -24,12 +24,12 @@ import (
func CoinSelect( func CoinSelect(
boardingUtxos []types.Utxo, boardingUtxos []types.Utxo,
vtxos []client.DescriptorVtxo, vtxos []client.TapscriptsVtxo,
amount, amount,
dust uint64, dust uint64,
sortByExpirationTime bool, sortByExpirationTime bool,
) ([]types.Utxo, []client.DescriptorVtxo, uint64, error) { ) ([]types.Utxo, []client.TapscriptsVtxo, uint64, error) {
selected, notSelected := make([]client.DescriptorVtxo, 0), make([]client.DescriptorVtxo, 0) selected, notSelected := make([]client.TapscriptsVtxo, 0), make([]client.TapscriptsVtxo, 0)
selectedBoarding, notSelectedBoarding := make([]types.Utxo, 0), make([]types.Utxo, 0) selectedBoarding, notSelectedBoarding := make([]types.Utxo, 0), make([]types.Utxo, 0)
selectedAmount := uint64(0) selectedAmount := uint64(0)

View File

@@ -7,7 +7,6 @@ import (
"strings" "strings"
"time" "time"
"github.com/ark-network/ark/common/bitcointree"
"github.com/ark-network/ark/common/tree" "github.com/ark-network/ark/common/tree"
"github.com/ark-network/ark/pkg/client-sdk/client" "github.com/ark-network/ark/pkg/client-sdk/client"
"github.com/ark-network/ark/pkg/client-sdk/explorer" "github.com/ark-network/ark/pkg/client-sdk/explorer"
@@ -172,7 +171,7 @@ func findCovenantlessSweepClosure(
var seconds uint var seconds uint
var sweepClosure *txscript.TapLeaf var sweepClosure *txscript.TapLeaf
for _, tapLeaf := range tx.Inputs[0].TaprootLeafScript { for _, tapLeaf := range tx.Inputs[0].TaprootLeafScript {
closure := &bitcointree.CSVSigClosure{} closure := &tree.CSVSigClosure{}
valid, err := closure.Decode(tapLeaf.Script) valid, err := closure.Decode(tapLeaf.Script)
if err != nil { if err != nil {
continue continue

View File

@@ -113,7 +113,7 @@ type Utxo struct {
Delay uint Delay uint
SpendableAt time.Time SpendableAt time.Time
CreatedAt time.Time CreatedAt time.Time
Descriptor string Tapscripts []string
Spent bool Spent bool
} }

View File

@@ -9,6 +9,7 @@ import (
"github.com/ark-network/ark/common" "github.com/ark-network/ark/common"
"github.com/ark-network/ark/common/bitcointree" "github.com/ark-network/ark/common/bitcointree"
"github.com/ark-network/ark/common/tree"
"github.com/ark-network/ark/pkg/client-sdk/explorer" "github.com/ark-network/ark/pkg/client-sdk/explorer"
"github.com/ark-network/ark/pkg/client-sdk/internal/utils" "github.com/ark-network/ark/pkg/client-sdk/internal/utils"
"github.com/ark-network/ark/pkg/client-sdk/types" "github.com/ark-network/ark/pkg/client-sdk/types"
@@ -43,7 +44,7 @@ func NewBitcoinWallet(
func (w *bitcoinWallet) GetAddresses( func (w *bitcoinWallet) GetAddresses(
ctx context.Context, ctx context.Context,
) ([]wallet.DescriptorAddress, []wallet.DescriptorAddress, []wallet.DescriptorAddress, error) { ) ([]wallet.TapscriptsAddress, []wallet.TapscriptsAddress, []wallet.TapscriptsAddress, error) {
offchainAddr, boardingAddr, err := w.getAddress(ctx) offchainAddr, boardingAddr, err := w.getAddress(ctx)
if err != nil { if err != nil {
return nil, nil, nil, err return nil, nil, nil, err
@@ -69,21 +70,21 @@ func (w *bitcoinWallet) GetAddresses(
return nil, nil, nil, err return nil, nil, nil, err
} }
offchainAddrs := []wallet.DescriptorAddress{ offchainAddrs := []wallet.TapscriptsAddress{
{ {
Descriptor: offchainAddr.Descriptor, Tapscripts: offchainAddr.Tapscripts,
Address: encodedOffchainAddr, Address: encodedOffchainAddr,
}, },
} }
boardingAddrs := []wallet.DescriptorAddress{ boardingAddrs := []wallet.TapscriptsAddress{
{ {
Descriptor: boardingAddr.Descriptor, Tapscripts: boardingAddr.Tapscripts,
Address: boardingAddr.Address, Address: boardingAddr.Address,
}, },
} }
redemptionAddrs := []wallet.DescriptorAddress{ redemptionAddrs := []wallet.TapscriptsAddress{
{ {
Descriptor: offchainAddr.Descriptor, Tapscripts: offchainAddr.Tapscripts,
Address: redemptionAddr.EncodeAddress(), Address: redemptionAddr.EncodeAddress(),
}, },
} }
@@ -92,7 +93,7 @@ func (w *bitcoinWallet) GetAddresses(
func (w *bitcoinWallet) NewAddress( func (w *bitcoinWallet) NewAddress(
ctx context.Context, _ bool, ctx context.Context, _ bool,
) (*wallet.DescriptorAddress, *wallet.DescriptorAddress, error) { ) (*wallet.TapscriptsAddress, *wallet.TapscriptsAddress, error) {
offchainAddr, boardingAddr, err := w.getAddress(ctx) offchainAddr, boardingAddr, err := w.getAddress(ctx)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
@@ -103,34 +104,34 @@ func (w *bitcoinWallet) NewAddress(
return nil, nil, err return nil, nil, err
} }
return &wallet.DescriptorAddress{ return &wallet.TapscriptsAddress{
Descriptor: offchainAddr.Descriptor, Tapscripts: offchainAddr.Tapscripts,
Address: encodedOffchainAddr, Address: encodedOffchainAddr,
}, boardingAddr, nil }, boardingAddr, nil
} }
func (w *bitcoinWallet) NewAddresses( func (w *bitcoinWallet) NewAddresses(
ctx context.Context, _ bool, num int, ctx context.Context, _ bool, num int,
) ([]wallet.DescriptorAddress, []wallet.DescriptorAddress, error) { ) ([]wallet.TapscriptsAddress, []wallet.TapscriptsAddress, error) {
offchainAddr, boardingAddr, err := w.getAddress(ctx) offchainAddr, boardingAddr, err := w.getAddress(ctx)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
offchainAddrs := make([]wallet.DescriptorAddress, 0, num) offchainAddrs := make([]wallet.TapscriptsAddress, 0, num)
boardingAddrs := make([]wallet.DescriptorAddress, 0, num) boardingAddrs := make([]wallet.TapscriptsAddress, 0, num)
for i := 0; i < num; i++ { for i := 0; i < num; i++ {
encodedOffchainAddr, err := offchainAddr.Address.Encode() encodedOffchainAddr, err := offchainAddr.Address.Encode()
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
offchainAddrs = append(offchainAddrs, wallet.DescriptorAddress{ offchainAddrs = append(offchainAddrs, wallet.TapscriptsAddress{
Descriptor: offchainAddr.Descriptor, Tapscripts: offchainAddr.Tapscripts,
Address: encodedOffchainAddr, Address: encodedOffchainAddr,
}) })
boardingAddrs = append(boardingAddrs, wallet.DescriptorAddress{ boardingAddrs = append(boardingAddrs, wallet.TapscriptsAddress{
Descriptor: boardingAddr.Descriptor, Tapscripts: boardingAddr.Tapscripts,
Address: boardingAddr.Address, Address: boardingAddr.Address,
}) })
} }
@@ -192,12 +193,12 @@ func (s *bitcoinWallet) SignTransaction(
) )
txsighashes := txscript.NewTxSigHashes(updater.Upsbt.UnsignedTx, prevoutFetcher) txsighashes := txscript.NewTxSigHashes(updater.Upsbt.UnsignedTx, prevoutFetcher)
myPubkey := schnorr.SerializePubKey(s.walletData.Pubkey)
for i, input := range ptx.Inputs { for i, input := range ptx.Inputs {
if len(input.TaprootLeafScript) > 0 { if len(input.TaprootLeafScript) > 0 {
pubkey := s.walletData.Pubkey
for _, leaf := range input.TaprootLeafScript { for _, leaf := range input.TaprootLeafScript {
closure, err := bitcointree.DecodeClosure(leaf.Script) closure, err := tree.DecodeClosure(leaf.Script)
if err != nil { if err != nil {
return "", err return "", err
} }
@@ -205,10 +206,20 @@ func (s *bitcoinWallet) SignTransaction(
sign := false sign := false
switch c := closure.(type) { switch c := closure.(type) {
case *bitcointree.CSVSigClosure: case *tree.CSVSigClosure:
sign = bytes.Equal(c.Pubkey.SerializeCompressed()[1:], pubkey.SerializeCompressed()[1:]) for _, key := range c.MultisigClosure.PubKeys {
case *bitcointree.MultisigClosure: if bytes.Equal(schnorr.SerializePubKey(key), myPubkey) {
sign = bytes.Equal(c.Pubkey.SerializeCompressed()[1:], pubkey.SerializeCompressed()[1:]) sign = true
break
}
}
case *tree.MultisigClosure:
for _, key := range c.PubKeys {
if bytes.Equal(schnorr.SerializePubKey(key), myPubkey) {
sign = true
break
}
}
} }
if sign { if sign {
@@ -235,7 +246,7 @@ func (s *bitcoinWallet) SignTransaction(
return "", err return "", err
} }
if !sig.Verify(preimage, pubkey) { if !sig.Verify(preimage, s.walletData.Pubkey) {
return "", fmt.Errorf("signature verification failed") return "", fmt.Errorf("signature verification failed")
} }
@@ -244,7 +255,7 @@ func (s *bitcoinWallet) SignTransaction(
} }
updater.Upsbt.Inputs[i].TaprootScriptSpendSig = append(updater.Upsbt.Inputs[i].TaprootScriptSpendSig, &psbt.TaprootScriptSpendSig{ updater.Upsbt.Inputs[i].TaprootScriptSpendSig = append(updater.Upsbt.Inputs[i].TaprootScriptSpendSig, &psbt.TaprootScriptSpendSig{
XOnlyPubKey: schnorr.SerializePubKey(pubkey), XOnlyPubKey: myPubkey,
LeafHash: hash.CloneBytes(), LeafHash: hash.CloneBytes(),
Signature: sig.Serialize(), Signature: sig.Serialize(),
SigHash: txscript.SigHashDefault, SigHash: txscript.SigHashDefault,
@@ -259,17 +270,12 @@ func (s *bitcoinWallet) SignTransaction(
} }
func (w *bitcoinWallet) SignMessage( func (w *bitcoinWallet) SignMessage(
ctx context.Context, message []byte, pubkey string, ctx context.Context, message []byte,
) (string, error) { ) (string, error) {
if w.IsLocked() { if w.IsLocked() {
return "", fmt.Errorf("wallet is locked") return "", fmt.Errorf("wallet is locked")
} }
walletPubkeyHex := hex.EncodeToString(schnorr.SerializePubKey(w.walletData.Pubkey))
if walletPubkeyHex != pubkey {
return "", fmt.Errorf("pubkey mismatch, cannot sign message")
}
sig, err := schnorr.Sign(w.privateKey, message) sig, err := schnorr.Sign(w.privateKey, message)
if err != nil { if err != nil {
return "", err return "", err
@@ -278,14 +284,16 @@ func (w *bitcoinWallet) SignMessage(
return hex.EncodeToString(sig.Serialize()), nil return hex.EncodeToString(sig.Serialize()), nil
} }
type addressWithTapscripts struct {
Address common.Address
Tapscripts []string
}
func (w *bitcoinWallet) getAddress( func (w *bitcoinWallet) getAddress(
ctx context.Context, ctx context.Context,
) ( ) (
*struct { *addressWithTapscripts,
Address common.Address *wallet.TapscriptsAddress,
Descriptor string
},
*wallet.DescriptorAddress,
error, error,
) { ) {
if w.walletData == nil { if w.walletData == nil {
@@ -299,11 +307,11 @@ func (w *bitcoinWallet) getAddress(
netParams := utils.ToBitcoinNetwork(data.Network) netParams := utils.ToBitcoinNetwork(data.Network)
defaultVtxoScript := &bitcointree.DefaultVtxoScript{ defaultVtxoScript := bitcointree.NewDefaultVtxoScript(
Asp: data.AspPubkey, w.walletData.Pubkey,
Owner: w.walletData.Pubkey, data.AspPubkey,
ExitDelay: uint(data.UnilateralExitDelay), uint(data.UnilateralExitDelay),
} )
vtxoTapKey, _, err := defaultVtxoScript.TapTree() vtxoTapKey, _, err := defaultVtxoScript.TapTree()
if err != nil { if err != nil {
@@ -316,16 +324,12 @@ func (w *bitcoinWallet) getAddress(
VtxoTapKey: vtxoTapKey, VtxoTapKey: vtxoTapKey,
} }
myPubkeyStr := hex.EncodeToString(schnorr.SerializePubKey(w.walletData.Pubkey)) boardingVtxoScript := bitcointree.NewDefaultVtxoScript(
descriptorStr := strings.ReplaceAll( w.walletData.Pubkey,
data.BoardingDescriptorTemplate, "USER", myPubkeyStr, data.AspPubkey,
uint(data.UnilateralExitDelay*2),
) )
boardingVtxoScript, err := bitcointree.ParseVtxoScript(descriptorStr)
if err != nil {
return nil, nil, err
}
boardingTapKey, _, err := boardingVtxoScript.TapTree() boardingTapKey, _, err := boardingVtxoScript.TapTree()
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
@@ -339,14 +343,22 @@ func (w *bitcoinWallet) getAddress(
return nil, nil, err return nil, nil, err
} }
return &struct { tapscripts, err := defaultVtxoScript.Encode()
Address common.Address if err != nil {
Descriptor string return nil, nil, err
}{ }
*offchainAddress, defaultVtxoScript.ToDescriptor(),
boardingTapscripts, err := boardingVtxoScript.Encode()
if err != nil {
return nil, nil, err
}
return &addressWithTapscripts{
Address: *offchainAddress,
Tapscripts: tapscripts,
}, },
&wallet.DescriptorAddress{ &wallet.TapscriptsAddress{
Descriptor: descriptorStr, Tapscripts: boardingTapscripts,
Address: boardingAddr.EncodeAddress(), Address: boardingAddr.EncodeAddress(),
}, },
nil nil

View File

@@ -5,7 +5,6 @@ import (
"context" "context"
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"strings"
"github.com/ark-network/ark/common" "github.com/ark-network/ark/common"
"github.com/ark-network/ark/common/tree" "github.com/ark-network/ark/common/tree"
@@ -44,7 +43,7 @@ func NewLiquidWallet(
func (w *liquidWallet) GetAddresses( func (w *liquidWallet) GetAddresses(
ctx context.Context, ctx context.Context,
) ([]wallet.DescriptorAddress, []wallet.DescriptorAddress, []wallet.DescriptorAddress, error) { ) ([]wallet.TapscriptsAddress, []wallet.TapscriptsAddress, []wallet.TapscriptsAddress, error) {
offchainAddr, boardingAddr, err := w.getAddress(ctx) offchainAddr, boardingAddr, err := w.getAddress(ctx)
if err != nil { if err != nil {
return nil, nil, nil, err return nil, nil, nil, err
@@ -72,22 +71,22 @@ func (w *liquidWallet) GetAddresses(
return nil, nil, nil, err return nil, nil, nil, err
} }
offchainAddrs := []wallet.DescriptorAddress{ offchainAddrs := []wallet.TapscriptsAddress{
{ {
Descriptor: offchainAddr.Descriptor, Tapscripts: offchainAddr.Tapscripts,
Address: encodedOffchainAddr, Address: encodedOffchainAddr,
}, },
} }
boardingAddrs := []wallet.DescriptorAddress{ boardingAddrs := []wallet.TapscriptsAddress{
{ {
Descriptor: boardingAddr.Descriptor, Tapscripts: boardingAddr.Tapscripts,
Address: boardingAddr.Address, Address: boardingAddr.Address,
}, },
} }
redemptionAddrs := []wallet.DescriptorAddress{ redemptionAddrs := []wallet.TapscriptsAddress{
{ {
Descriptor: offchainAddr.Descriptor, Tapscripts: offchainAddr.Tapscripts,
Address: redemptionAddr, Address: redemptionAddr,
}, },
} }
@@ -97,7 +96,7 @@ func (w *liquidWallet) GetAddresses(
func (w *liquidWallet) NewAddress( func (w *liquidWallet) NewAddress(
ctx context.Context, _ bool, ctx context.Context, _ bool,
) (*wallet.DescriptorAddress, *wallet.DescriptorAddress, error) { ) (*wallet.TapscriptsAddress, *wallet.TapscriptsAddress, error) {
offchainAddr, boardingAddr, err := w.getAddress(ctx) offchainAddr, boardingAddr, err := w.getAddress(ctx)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
@@ -108,37 +107,37 @@ func (w *liquidWallet) NewAddress(
return nil, nil, err return nil, nil, err
} }
return &wallet.DescriptorAddress{ return &wallet.TapscriptsAddress{
Descriptor: offchainAddr.Descriptor, Tapscripts: offchainAddr.Tapscripts,
Address: encodedOffchainAddr, Address: encodedOffchainAddr,
}, &wallet.DescriptorAddress{ }, &wallet.TapscriptsAddress{
Descriptor: boardingAddr.Descriptor, Tapscripts: boardingAddr.Tapscripts,
Address: boardingAddr.Address, Address: boardingAddr.Address,
}, nil }, nil
} }
func (w *liquidWallet) NewAddresses( func (w *liquidWallet) NewAddresses(
ctx context.Context, _ bool, num int, ctx context.Context, _ bool, num int,
) ([]wallet.DescriptorAddress, []wallet.DescriptorAddress, error) { ) ([]wallet.TapscriptsAddress, []wallet.TapscriptsAddress, error) {
offchainAddr, boardingAddr, err := w.getAddress(ctx) offchainAddr, boardingAddr, err := w.getAddress(ctx)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
offchainAddrs := make([]wallet.DescriptorAddress, 0, num) offchainAddrs := make([]wallet.TapscriptsAddress, 0, num)
boardingAddrs := make([]wallet.DescriptorAddress, 0, num) boardingAddrs := make([]wallet.TapscriptsAddress, 0, num)
for i := 0; i < num; i++ { for i := 0; i < num; i++ {
encodedOffchainAddr, err := offchainAddr.Address.Encode() encodedOffchainAddr, err := offchainAddr.Address.Encode()
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
offchainAddrs = append(offchainAddrs, wallet.DescriptorAddress{ offchainAddrs = append(offchainAddrs, wallet.TapscriptsAddress{
Descriptor: offchainAddr.Descriptor, Tapscripts: offchainAddr.Tapscripts,
Address: encodedOffchainAddr, Address: encodedOffchainAddr,
}) })
boardingAddrs = append(boardingAddrs, wallet.DescriptorAddress{ boardingAddrs = append(boardingAddrs, wallet.TapscriptsAddress{
Descriptor: boardingAddr.Descriptor, Tapscripts: boardingAddr.Tapscripts,
Address: boardingAddr.Address, Address: boardingAddr.Address,
}) })
} }
@@ -212,7 +211,7 @@ func (s *liquidWallet) SignTransaction(
prevoutsAssets = append(prevoutsAssets, input.WitnessUtxo.Asset) prevoutsAssets = append(prevoutsAssets, input.WitnessUtxo.Asset)
} }
serializedPubKey := s.walletData.Pubkey.SerializeCompressed() myPubkey := schnorr.SerializePubKey(s.walletData.Pubkey)
for i, input := range pset.Inputs { for i, input := range pset.Inputs {
if len(input.TapLeafScript) > 0 { if len(input.TapLeafScript) > 0 {
@@ -230,9 +229,19 @@ func (s *liquidWallet) SignTransaction(
sign := false sign := false
switch c := closure.(type) { switch c := closure.(type) {
case *tree.CSVSigClosure: case *tree.CSVSigClosure:
sign = bytes.Equal(c.Pubkey.SerializeCompressed()[1:], serializedPubKey[1:]) for _, key := range c.MultisigClosure.PubKeys {
if bytes.Equal(schnorr.SerializePubKey(key), myPubkey) {
sign = true
break
}
}
case *tree.MultisigClosure: case *tree.MultisigClosure:
sign = bytes.Equal(c.Pubkey.SerializeCompressed()[1:], serializedPubKey[1:]) for _, key := range c.PubKeys {
if bytes.Equal(schnorr.SerializePubKey(key), myPubkey) {
sign = true
break
}
}
} }
if sign { if sign {
@@ -288,17 +297,12 @@ func (s *liquidWallet) SignTransaction(
} }
func (w *liquidWallet) SignMessage( func (w *liquidWallet) SignMessage(
ctx context.Context, message []byte, pubkey string, ctx context.Context, message []byte,
) (string, error) { ) (string, error) {
if w.IsLocked() { if w.IsLocked() {
return "", fmt.Errorf("wallet is locked") return "", fmt.Errorf("wallet is locked")
} }
walletPubkeyHex := hex.EncodeToString(schnorr.SerializePubKey(w.walletData.Pubkey))
if walletPubkeyHex != pubkey {
return "", fmt.Errorf("pubkey mismatch, cannot sign message")
}
sig, err := schnorr.Sign(w.privateKey, message) sig, err := schnorr.Sign(w.privateKey, message)
if err != nil { if err != nil {
return "", err return "", err
@@ -310,11 +314,8 @@ func (w *liquidWallet) SignMessage(
func (w *liquidWallet) getAddress( func (w *liquidWallet) getAddress(
ctx context.Context, ctx context.Context,
) ( ) (
*struct { *addressWithTapscripts,
Address common.Address *wallet.TapscriptsAddress,
Descriptor string
},
*wallet.DescriptorAddress,
error, error,
) { ) {
if w.walletData == nil { if w.walletData == nil {
@@ -328,11 +329,11 @@ func (w *liquidWallet) getAddress(
liquidNet := utils.ToElementsNetwork(data.Network) liquidNet := utils.ToElementsNetwork(data.Network)
vtxoScript := &tree.DefaultVtxoScript{ vtxoScript := tree.NewDefaultVtxoScript(
Owner: w.walletData.Pubkey, w.walletData.Pubkey,
Asp: data.AspPubkey, data.AspPubkey,
ExitDelay: uint(data.UnilateralExitDelay), uint(data.UnilateralExitDelay),
} )
vtxoTapKey, _, err := vtxoScript.TapTree() vtxoTapKey, _, err := vtxoScript.TapTree()
if err != nil { if err != nil {
@@ -345,22 +346,18 @@ func (w *liquidWallet) getAddress(
VtxoTapKey: vtxoTapKey, VtxoTapKey: vtxoTapKey,
} }
myPubkeyStr := hex.EncodeToString(schnorr.SerializePubKey(w.walletData.Pubkey)) boardingVtxoScript := tree.NewDefaultVtxoScript(
descriptorStr := strings.ReplaceAll( w.walletData.Pubkey,
data.BoardingDescriptorTemplate, "USER", myPubkeyStr, data.AspPubkey,
uint(data.UnilateralExitDelay*2),
) )
onboardingScript, err := tree.ParseVtxoScript(descriptorStr) boardingTapKey, _, err := boardingVtxoScript.TapTree()
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
tapKey, _, err := onboardingScript.TapTree() p2tr, err := payment.FromTweakedKey(boardingTapKey, &liquidNet, nil)
if err != nil {
return nil, nil, err
}
p2tr, err := payment.FromTweakedKey(tapKey, &liquidNet, nil)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@@ -370,14 +367,21 @@ func (w *liquidWallet) getAddress(
return nil, nil, err return nil, nil, err
} }
return &struct { tapscripts, err := vtxoScript.Encode()
Address common.Address if err != nil {
Descriptor string return nil, nil, err
}{ }
boardingTapscripts, err := boardingVtxoScript.Encode()
if err != nil {
return nil, nil, err
}
return &addressWithTapscripts{
Address: *offchainAddr, Address: *offchainAddr,
Descriptor: vtxoScript.ToDescriptor(), Tapscripts: tapscripts,
}, &wallet.DescriptorAddress{ }, &wallet.TapscriptsAddress{
Descriptor: descriptorStr, Tapscripts: boardingTapscripts,
Address: boardingAddr, Address: boardingAddr,
}, nil }, nil
} }

View File

@@ -10,8 +10,8 @@ const (
SingleKeyWallet = "singlekey" SingleKeyWallet = "singlekey"
) )
type DescriptorAddress struct { type TapscriptsAddress struct {
Descriptor string Tapscripts []string
Address string Address string
} }
@@ -25,18 +25,18 @@ type WalletService interface {
IsLocked() bool IsLocked() bool
GetAddresses( GetAddresses(
ctx context.Context, ctx context.Context,
) (offchainAddresses, boardingAddresses, redemptionAddresses []DescriptorAddress, err error) ) (offchainAddresses, boardingAddresses, redemptionAddresses []TapscriptsAddress, err error)
NewAddress( NewAddress(
ctx context.Context, change bool, ctx context.Context, change bool,
) (offchainAddr, onchainAddr *DescriptorAddress, err error) ) (offchainAddr, onchainAddr *TapscriptsAddress, err error)
NewAddresses( NewAddresses(
ctx context.Context, change bool, num int, ctx context.Context, change bool, num int,
) (offchainAddresses, onchainAddresses []DescriptorAddress, err error) ) (offchainAddresses, onchainAddresses []TapscriptsAddress, err error)
SignTransaction( SignTransaction(
ctx context.Context, explorerSvc explorer.Explorer, tx string, ctx context.Context, explorerSvc explorer.Explorer, tx string,
) (signedTx string, err error) ) (signedTx string, err error)
SignMessage( SignMessage(
ctx context.Context, message []byte, pubkey string, ctx context.Context, message []byte,
) (signature string, err error) ) (signature string, err error)
Dump(ctx context.Context) (seed string, err error) Dump(ctx context.Context) (seed string, err error)
} }

View File

@@ -9,7 +9,6 @@ import (
"time" "time"
"github.com/ark-network/ark/common" "github.com/ark-network/ark/common"
"github.com/ark-network/ark/common/descriptor"
"github.com/ark-network/ark/common/note" "github.com/ark-network/ark/common/note"
"github.com/ark-network/ark/common/tree" "github.com/ark-network/ark/common/tree"
"github.com/ark-network/ark/server/internal/core/domain" "github.com/ark-network/ark/server/internal/core/domain"
@@ -149,29 +148,30 @@ func (s *covenantService) Stop() {
close(s.eventsCh) close(s.eventsCh)
} }
func (s *covenantService) GetBoardingAddress(ctx context.Context, userPubkey *secp256k1.PublicKey) (string, string, error) { func (s *covenantService) GetBoardingAddress(ctx context.Context, userPubkey *secp256k1.PublicKey) (string, []string, error) {
vtxoScript := &tree.DefaultVtxoScript{ vtxoScript := tree.NewDefaultVtxoScript(userPubkey, s.pubkey, uint(s.boardingExitDelay))
Asp: s.pubkey,
Owner: userPubkey,
ExitDelay: uint(s.boardingExitDelay),
}
tapKey, _, err := vtxoScript.TapTree() tapKey, _, err := vtxoScript.TapTree()
if err != nil { if err != nil {
return "", "", fmt.Errorf("failed to get taproot key: %s", err) return "", nil, fmt.Errorf("failed to get taproot key: %s", err)
} }
p2tr, err := payment.FromTweakedKey(tapKey, s.onchainNetwork(), nil) p2tr, err := payment.FromTweakedKey(tapKey, s.onchainNetwork(), nil)
if err != nil { if err != nil {
return "", "", err return "", nil, err
} }
addr, err := p2tr.TaprootAddress() addr, err := p2tr.TaprootAddress()
if err != nil { if err != nil {
return "", "", err return "", nil, err
} }
return addr, vtxoScript.ToDescriptor(), nil scripts, err := vtxoScript.Encode()
if err != nil {
return "", nil, err
}
return addr, scripts, nil
} }
func (s *covenantService) SpendNotes(_ context.Context, _ []note.Note) (string, error) { func (s *covenantService) SpendNotes(_ context.Context, _ []note.Note) (string, error) {
@@ -211,8 +211,18 @@ func (s *covenantService) SpendVtxos(ctx context.Context, inputs []ports.Input)
return "", fmt.Errorf("tx %s not confirmed", input.Txid) return "", fmt.Errorf("tx %s not confirmed", input.Txid)
} }
vtxoScript, err := tree.ParseVtxoScript(input.Tapscripts)
if err != nil {
return "", fmt.Errorf("failed to parse boarding descriptor: %s", err)
}
exitDelay, err := vtxoScript.SmallestExitDelay()
if err != nil {
return "", fmt.Errorf("failed to get exit delay: %s", err)
}
// if the exit path is available, forbid registering the boarding utxo // if the exit path is available, forbid registering the boarding utxo
if blocktime+int64(s.boardingExitDelay) < now { if blocktime+int64(exitDelay) < now {
return "", fmt.Errorf("tx %s expired", input.Txid) return "", fmt.Errorf("tx %s expired", input.Txid)
} }
@@ -242,7 +252,7 @@ func (s *covenantService) SpendVtxos(ctx context.Context, inputs []ports.Input)
return "", fmt.Errorf("input %s:%d already swept", vtxo.Txid, vtxo.VOut) return "", fmt.Errorf("input %s:%d already swept", vtxo.Txid, vtxo.VOut)
} }
vtxoScript, err := tree.ParseVtxoScript(input.Descriptor) vtxoScript, err := tree.ParseVtxoScript(input.Tapscripts)
if err != nil { if err != nil {
return "", fmt.Errorf("failed to parse boarding descriptor: %s", err) return "", fmt.Errorf("failed to parse boarding descriptor: %s", err)
} }
@@ -290,7 +300,7 @@ func (s *covenantService) newBoardingInput(tx *transaction.Transaction, input po
return nil, fmt.Errorf("failed to parse value: %s", err) return nil, fmt.Errorf("failed to parse value: %s", err)
} }
boardingScript, err := tree.ParseVtxoScript(input.Descriptor) boardingScript, err := tree.ParseVtxoScript(input.Tapscripts)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to parse boarding descriptor: %s", err) return nil, fmt.Errorf("failed to parse boarding descriptor: %s", err)
} }
@@ -309,16 +319,8 @@ func (s *covenantService) newBoardingInput(tx *transaction.Transaction, input po
return nil, fmt.Errorf("descriptor does not match script in transaction output") return nil, fmt.Errorf("descriptor does not match script in transaction output")
} }
if defaultVtxoScript, ok := boardingScript.(*tree.DefaultVtxoScript); ok { if err := boardingScript.Validate(s.pubkey, uint(s.unilateralExitDelay)); err != nil {
if !bytes.Equal(schnorr.SerializePubKey(defaultVtxoScript.Asp), schnorr.SerializePubKey(s.pubkey)) { return nil, err
return nil, fmt.Errorf("invalid boarding descriptor, ASP mismatch")
}
if defaultVtxoScript.ExitDelay != uint(s.boardingExitDelay) {
return nil, fmt.Errorf("invalid boarding descriptor, timeout mismatch")
}
} else {
return nil, fmt.Errorf("only default vtxo script is supported for boarding")
} }
return &ports.BoardingInput{ return &ports.BoardingInput{
@@ -430,15 +432,7 @@ func (s *covenantService) GetInfo(ctx context.Context) (*ServiceInfo, error) {
RoundInterval: s.roundInterval, RoundInterval: s.roundInterval,
Network: s.network.Name, Network: s.network.Name,
Dust: dust, Dust: dust,
BoardingDescriptorTemplate: fmt.Sprintf( ForfeitAddress: forfeitAddress,
descriptor.DefaultVtxoDescriptorTemplate,
hex.EncodeToString(tree.UnspendableKey().SerializeCompressed()),
"USER",
hex.EncodeToString(schnorr.SerializePubKey(s.pubkey)),
s.boardingExitDelay,
"USER",
),
ForfeitAddress: forfeitAddress,
}, nil }, nil
} }
@@ -852,6 +846,7 @@ func (s *covenantService) reactToFraud(ctx context.Context, vtxo domain.Vtxo, mu
forfeitTxid, err := s.wallet.BroadcastTransaction(ctx, forfeitTxHex) forfeitTxid, err := s.wallet.BroadcastTransaction(ctx, forfeitTxHex)
if err != nil { if err != nil {
log.Debug(forfeitTxHex)
return fmt.Errorf("failed to broadcast forfeit tx: %s", err) return fmt.Errorf("failed to broadcast forfeit tx: %s", err)
} }

View File

@@ -11,7 +11,6 @@ import (
"github.com/ark-network/ark/common" "github.com/ark-network/ark/common"
"github.com/ark-network/ark/common/bitcointree" "github.com/ark-network/ark/common/bitcointree"
"github.com/ark-network/ark/common/descriptor"
"github.com/ark-network/ark/common/note" "github.com/ark-network/ark/common/note"
"github.com/ark-network/ark/common/tree" "github.com/ark-network/ark/common/tree"
"github.com/ark-network/ark/server/internal/core/domain" "github.com/ark-network/ark/server/internal/core/domain"
@@ -259,7 +258,7 @@ func (s *covenantlessService) CompleteAsyncPayment(
} }
// verify the tapscript signatures // verify the tapscript signatures
if valid, _, err := s.builder.VerifyTapscriptPartialSigs(tx); err != nil || !valid { if valid, err := s.builder.VerifyTapscriptPartialSigs(tx); err != nil || !valid {
return fmt.Errorf("invalid tx signature: %s", err) return fmt.Errorf("invalid tx signature: %s", err)
} }
} }
@@ -329,12 +328,12 @@ func (s *covenantlessService) CreateAsyncPayment(
ctx context.Context, inputs []AsyncPaymentInput, receivers []domain.Receiver, ctx context.Context, inputs []AsyncPaymentInput, receivers []domain.Receiver,
) (string, error) { ) (string, error) {
vtxosKeys := make([]domain.VtxoKey, 0, len(inputs)) vtxosKeys := make([]domain.VtxoKey, 0, len(inputs))
descriptors := make(map[domain.VtxoKey]string) scripts := make(map[domain.VtxoKey][]string)
forfeitLeaves := make(map[domain.VtxoKey]chainhash.Hash) forfeitLeaves := make(map[domain.VtxoKey]chainhash.Hash)
for _, in := range inputs { for _, in := range inputs {
vtxosKeys = append(vtxosKeys, in.VtxoKey) vtxosKeys = append(vtxosKeys, in.VtxoKey)
descriptors[in.VtxoKey] = in.Descriptor scripts[in.VtxoKey] = in.Tapscripts
forfeitLeaves[in.VtxoKey] = in.ForfeitLeafHash forfeitLeaves[in.VtxoKey] = in.ForfeitLeafHash
} }
@@ -373,7 +372,7 @@ func (s *covenantlessService) CreateAsyncPayment(
} }
redeemTx, err := s.builder.BuildAsyncPaymentTransactions( redeemTx, err := s.builder.BuildAsyncPaymentTransactions(
vtxosInputs, descriptors, forfeitLeaves, receivers, vtxosInputs, scripts, forfeitLeaves, receivers,
) )
if err != nil { if err != nil {
return "", fmt.Errorf("failed to build async payment txs: %s", err) return "", fmt.Errorf("failed to build async payment txs: %s", err)
@@ -395,26 +394,29 @@ func (s *covenantlessService) CreateAsyncPayment(
func (s *covenantlessService) GetBoardingAddress( func (s *covenantlessService) GetBoardingAddress(
ctx context.Context, userPubkey *secp256k1.PublicKey, ctx context.Context, userPubkey *secp256k1.PublicKey,
) (address string, descriptor string, err error) { ) (address string, scripts []string, err error) {
vtxoScript := &bitcointree.DefaultVtxoScript{ vtxoScript := bitcointree.NewDefaultVtxoScript(s.pubkey, userPubkey, uint(s.boardingExitDelay))
Asp: s.pubkey,
Owner: userPubkey,
ExitDelay: uint(s.boardingExitDelay),
}
tapKey, _, err := vtxoScript.TapTree() tapKey, _, err := vtxoScript.TapTree()
if err != nil { if err != nil {
return "", "", fmt.Errorf("failed to get taproot key: %s", err) return "", nil, fmt.Errorf("failed to get taproot key: %s", err)
} }
addr, err := btcutil.NewAddressTaproot( addr, err := btcutil.NewAddressTaproot(
schnorr.SerializePubKey(tapKey), s.chainParams(), schnorr.SerializePubKey(tapKey), s.chainParams(),
) )
if err != nil { if err != nil {
return "", "", fmt.Errorf("failed to get address: %s", err) return "", nil, fmt.Errorf("failed to get address: %s", err)
} }
return addr.EncodeAddress(), vtxoScript.ToDescriptor(), nil scripts, err = vtxoScript.Encode()
if err != nil {
return "", nil, fmt.Errorf("failed to encode vtxo script: %s", err)
}
address = addr.EncodeAddress()
return
} }
func (s *covenantlessService) SpendNotes(ctx context.Context, notes []note.Note) (string, error) { func (s *covenantlessService) SpendNotes(ctx context.Context, notes []note.Note) (string, error) {
@@ -489,8 +491,18 @@ func (s *covenantlessService) SpendVtxos(ctx context.Context, inputs []ports.Inp
return "", fmt.Errorf("tx %s not confirmed", input.Txid) return "", fmt.Errorf("tx %s not confirmed", input.Txid)
} }
vtxoScript, err := bitcointree.ParseVtxoScript(input.Tapscripts)
if err != nil {
return "", fmt.Errorf("failed to parse boarding descriptor: %s", err)
}
exitDelay, err := vtxoScript.SmallestExitDelay()
if err != nil {
return "", fmt.Errorf("failed to get exit delay: %s", err)
}
// if the exit path is available, forbid registering the boarding utxo // if the exit path is available, forbid registering the boarding utxo
if blocktime+int64(s.boardingExitDelay) < now { if blocktime+int64(exitDelay) < now {
return "", fmt.Errorf("tx %s expired", input.Txid) return "", fmt.Errorf("tx %s expired", input.Txid)
} }
@@ -520,7 +532,7 @@ func (s *covenantlessService) SpendVtxos(ctx context.Context, inputs []ports.Inp
return "", fmt.Errorf("input %s:%d already swept", vtxo.Txid, vtxo.VOut) return "", fmt.Errorf("input %s:%d already swept", vtxo.Txid, vtxo.VOut)
} }
vtxoScript, err := bitcointree.ParseVtxoScript(input.Descriptor) vtxoScript, err := bitcointree.ParseVtxoScript(input.Tapscripts)
if err != nil { if err != nil {
return "", fmt.Errorf("failed to parse boarding descriptor: %s", err) return "", fmt.Errorf("failed to parse boarding descriptor: %s", err)
} }
@@ -559,7 +571,7 @@ func (s *covenantlessService) newBoardingInput(tx wire.MsgTx, input ports.Input)
output := tx.TxOut[input.VtxoKey.VOut] output := tx.TxOut[input.VtxoKey.VOut]
boardingScript, err := bitcointree.ParseVtxoScript(input.Descriptor) boardingScript, err := bitcointree.ParseVtxoScript(input.Tapscripts)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to parse boarding descriptor: %s", err) return nil, fmt.Errorf("failed to parse boarding descriptor: %s", err)
} }
@@ -578,16 +590,8 @@ func (s *covenantlessService) newBoardingInput(tx wire.MsgTx, input ports.Input)
return nil, fmt.Errorf("descriptor does not match script in transaction output") return nil, fmt.Errorf("descriptor does not match script in transaction output")
} }
if defaultVtxoScript, ok := boardingScript.(*bitcointree.DefaultVtxoScript); ok { if err := boardingScript.Validate(s.pubkey, uint(s.unilateralExitDelay)); err != nil {
if !bytes.Equal(schnorr.SerializePubKey(defaultVtxoScript.Asp), schnorr.SerializePubKey(s.pubkey)) { return nil, err
return nil, fmt.Errorf("invalid boarding descriptor, ASP mismatch")
}
if defaultVtxoScript.ExitDelay != uint(s.boardingExitDelay) {
return nil, fmt.Errorf("invalid boarding descriptor, timeout mismatch")
}
} else {
return nil, fmt.Errorf("only default vtxo script is supported for boarding")
} }
return &ports.BoardingInput{ return &ports.BoardingInput{
@@ -691,15 +695,7 @@ func (s *covenantlessService) GetInfo(ctx context.Context) (*ServiceInfo, error)
RoundInterval: s.roundInterval, RoundInterval: s.roundInterval,
Network: s.network.Name, Network: s.network.Name,
Dust: dust, Dust: dust,
BoardingDescriptorTemplate: fmt.Sprintf( ForfeitAddress: forfeitAddr,
descriptor.DefaultVtxoDescriptorTemplate,
hex.EncodeToString(bitcointree.UnspendableKey().SerializeCompressed()),
"USER",
hex.EncodeToString(schnorr.SerializePubKey(s.pubkey)),
s.boardingExitDelay,
"USER",
),
ForfeitAddress: forfeitAddr,
}, nil }, nil
} }
@@ -921,7 +917,7 @@ func (s *covenantlessService) startFinalization() {
cosigners = append(cosigners, ephemeralKey.PubKey()) cosigners = append(cosigners, ephemeralKey.PubKey())
unsignedRoundTx, tree, connectorAddress, connectors, err := s.builder.BuildRoundTx( unsignedRoundTx, vtxoTree, connectorAddress, connectors, err := s.builder.BuildRoundTx(
s.pubkey, s.pubkey,
payments, payments,
boardingInputs, boardingInputs,
@@ -937,7 +933,7 @@ func (s *covenantlessService) startFinalization() {
s.forfeitTxs.init(connectors, payments) s.forfeitTxs.init(connectors, payments)
if len(tree) > 0 { if len(vtxoTree) > 0 {
log.Debugf("signing congestion tree for round %s", round.Id) log.Debugf("signing congestion tree for round %s", round.Id)
signingSession := newMusigSigningSession(len(cosigners)) signingSession := newMusigSigningSession(len(cosigners))
@@ -947,14 +943,14 @@ func (s *covenantlessService) startFinalization() {
s.currentRound.UnsignedTx = unsignedRoundTx s.currentRound.UnsignedTx = unsignedRoundTx
// send back the unsigned tree & all cosigners pubkeys // send back the unsigned tree & all cosigners pubkeys
s.propagateRoundSigningStartedEvent(tree, cosigners) s.propagateRoundSigningStartedEvent(vtxoTree, cosigners)
sweepClosure := bitcointree.CSVSigClosure{ sweepClosure := tree.CSVSigClosure{
Pubkey: s.pubkey, MultisigClosure: tree.MultisigClosure{PubKeys: []*secp256k1.PublicKey{s.pubkey}},
Seconds: uint(s.roundLifetime), Seconds: uint(s.roundLifetime),
} }
sweepTapLeaf, err := sweepClosure.Leaf() sweepScript, err := sweepClosure.Script()
if err != nil { if err != nil {
return return
} }
@@ -968,10 +964,11 @@ func (s *covenantlessService) startFinalization() {
sharedOutputAmount := unsignedPsbt.UnsignedTx.TxOut[0].Value sharedOutputAmount := unsignedPsbt.UnsignedTx.TxOut[0].Value
sweepTapTree := txscript.AssembleTaprootScriptTree(*sweepTapLeaf) sweepLeaf := txscript.NewBaseTapLeaf(sweepScript)
sweepTapTree := txscript.AssembleTaprootScriptTree(sweepLeaf)
root := sweepTapTree.RootNode.TapHash() root := sweepTapTree.RootNode.TapHash()
coordinator, err := bitcointree.NewTreeCoordinatorSession(sharedOutputAmount, tree, root.CloneBytes(), cosigners) coordinator, err := bitcointree.NewTreeCoordinatorSession(sharedOutputAmount, vtxoTree, root.CloneBytes(), cosigners)
if err != nil { if err != nil {
round.Fail(fmt.Errorf("failed to create tree coordinator: %s", err)) round.Fail(fmt.Errorf("failed to create tree coordinator: %s", err))
log.WithError(err).Warn("failed to create tree coordinator") log.WithError(err).Warn("failed to create tree coordinator")
@@ -979,7 +976,7 @@ func (s *covenantlessService) startFinalization() {
} }
aspSignerSession := bitcointree.NewTreeSignerSession( aspSignerSession := bitcointree.NewTreeSignerSession(
ephemeralKey, sharedOutputAmount, tree, root.CloneBytes(), ephemeralKey, sharedOutputAmount, vtxoTree, root.CloneBytes(),
) )
nonces, err := aspSignerSession.GetNonces() nonces, err := aspSignerSession.GetNonces()
@@ -1085,11 +1082,11 @@ func (s *covenantlessService) startFinalization() {
log.Debugf("congestion tree signed for round %s", round.Id) log.Debugf("congestion tree signed for round %s", round.Id)
tree = signedTree vtxoTree = signedTree
} }
if _, err := round.StartFinalization( if _, err := round.StartFinalization(
connectorAddress, connectors, tree, unsignedRoundTx, connectorAddress, connectors, vtxoTree, unsignedRoundTx,
); err != nil { ); err != nil {
round.Fail(fmt.Errorf("failed to start finalization: %s", err)) round.Fail(fmt.Errorf("failed to start finalization: %s", err))
log.WithError(err).Warn("failed to start finalization") log.WithError(err).Warn("failed to start finalization")

View File

@@ -24,7 +24,7 @@ type OwnershipProof struct {
func (p OwnershipProof) validate(vtxo domain.Vtxo) error { func (p OwnershipProof) validate(vtxo domain.Vtxo) error {
// verify revealed script and extract user public key // verify revealed script and extract user public key
pubkey, err := decodeForfeitClosure(p.Script) pubkeys, err := decodeForfeitClosure(p.Script)
if err != nil { if err != nil {
return err return err
} }
@@ -49,24 +49,32 @@ func (p OwnershipProof) validate(vtxo domain.Vtxo) error {
outpointBytes := append(txhash[:], voutBytes...) outpointBytes := append(txhash[:], voutBytes...)
sigMsg := sha256.Sum256(outpointBytes) sigMsg := sha256.Sum256(outpointBytes)
if !p.Signature.Verify(sigMsg[:], pubkey) { valid := false
for _, pubkey := range pubkeys {
if p.Signature.Verify(sigMsg[:], pubkey) {
valid = true
break
}
}
if !valid {
return fmt.Errorf("invalid signature") return fmt.Errorf("invalid signature")
} }
return nil return nil
} }
func decodeForfeitClosure(script []byte) (*secp256k1.PublicKey, error) { func decodeForfeitClosure(script []byte) ([]*secp256k1.PublicKey, error) {
var covenantLessForfeitClosure bitcointree.MultisigClosure var forfeit tree.MultisigClosure
if valid, err := covenantLessForfeitClosure.Decode(script); err == nil && valid { valid, err := forfeit.Decode(script)
return covenantLessForfeitClosure.Pubkey, nil if err != nil {
return nil, err
} }
var covenantForfeitClosure tree.CSVSigClosure if !valid {
if valid, err := covenantForfeitClosure.Decode(script); err == nil && valid { return nil, fmt.Errorf("invalid forfeit closure script")
return covenantForfeitClosure.Pubkey, nil
} }
return nil, fmt.Errorf("invalid forfeit closure script") return forfeit.PubKeys, nil
} }

View File

@@ -45,7 +45,7 @@ type Service interface {
) error ) error
GetBoardingAddress( GetBoardingAddress(
ctx context.Context, userPubkey *secp256k1.PublicKey, ctx context.Context, userPubkey *secp256k1.PublicKey,
) (address string, descriptor string, err error) ) (address string, scripts []string, err error)
// Tree signing methods // Tree signing methods
RegisterCosignerPubkey(ctx context.Context, paymentId string, ephemeralPublicKey string) error RegisterCosignerPubkey(ctx context.Context, paymentId string, ephemeralPublicKey string) error
RegisterCosignerNonces( RegisterCosignerNonces(
@@ -62,14 +62,13 @@ type Service interface {
} }
type ServiceInfo struct { type ServiceInfo struct {
PubKey string PubKey string
RoundLifetime int64 RoundLifetime int64
UnilateralExitDelay int64 UnilateralExitDelay int64
RoundInterval int64 RoundInterval int64
Network string Network string
Dust uint64 Dust uint64
BoardingDescriptorTemplate string ForfeitAddress string
ForfeitAddress string
} }
type WalletStatus struct { type WalletStatus struct {

View File

@@ -18,7 +18,7 @@ type SweepInput interface {
type Input struct { type Input struct {
domain.VtxoKey domain.VtxoKey
Descriptor string Tapscripts []string
} }
type BoardingInput struct { type BoardingInput struct {
@@ -49,12 +49,12 @@ type TxBuilder interface {
BuildSweepTx(inputs []SweepInput) (signedSweepTx string, err error) BuildSweepTx(inputs []SweepInput) (signedSweepTx string, err error)
GetSweepInput(node tree.Node) (lifetime int64, sweepInput SweepInput, err error) GetSweepInput(node tree.Node) (lifetime int64, sweepInput SweepInput, err error)
FinalizeAndExtract(tx string) (txhex string, err error) FinalizeAndExtract(tx string) (txhex string, err error)
VerifyTapscriptPartialSigs(tx string) (valid bool, txid string, err error) VerifyTapscriptPartialSigs(tx string) (valid bool, err error)
// FindLeaves returns all the leaves txs that are reachable from the given outpoint // FindLeaves returns all the leaves txs that are reachable from the given outpoint
FindLeaves(congestionTree tree.CongestionTree, fromtxid string, vout uint32) (leaves []tree.Node, err error) FindLeaves(congestionTree tree.CongestionTree, fromtxid string, vout uint32) (leaves []tree.Node, err error)
BuildAsyncPaymentTransactions( BuildAsyncPaymentTransactions(
vtxosToSpend []domain.Vtxo, vtxosToSpend []domain.Vtxo,
descriptors map[domain.VtxoKey]string, scripts map[domain.VtxoKey][]string,
forfeitsLeaves map[domain.VtxoKey]chainhash.Hash, forfeitsLeaves map[domain.VtxoKey]chainhash.Hash,
receivers []domain.Receiver, receivers []domain.Receiver,
) (string, error) ) (string, error)

View File

@@ -3,6 +3,7 @@ package txbuilder
import ( import (
"bytes" "bytes"
"context" "context"
"encoding/hex"
"fmt" "fmt"
"math" "math"
@@ -11,6 +12,7 @@ import (
"github.com/ark-network/ark/server/internal/core/domain" "github.com/ark-network/ark/server/internal/core/domain"
"github.com/ark-network/ark/server/internal/core/ports" "github.com/ark-network/ark/server/internal/core/ports"
"github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/btcutil/psbt"
"github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcwallet/waddrmgr" "github.com/btcsuite/btcwallet/waddrmgr"
@@ -122,7 +124,7 @@ func (b *txBuilder) VerifyForfeitTxs(vtxos []domain.Vtxo, connectors []string, f
return nil, fmt.Errorf("invalid forfeit tx, expect 2 inputs, got %d", len(pset.Inputs)) return nil, fmt.Errorf("invalid forfeit tx, expect 2 inputs, got %d", len(pset.Inputs))
} }
valid, _, err := b.verifyTapscriptPartialSigs(pset) valid, err := b.verifyTapscriptPartialSigs(pset)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -462,40 +464,66 @@ func (b *txBuilder) GetSweepInput(node tree.Node) (lifetime int64, sweepInput po
return lifetime, sweepInput, nil return lifetime, sweepInput, nil
} }
func (b *txBuilder) VerifyTapscriptPartialSigs(tx string) (bool, string, error) { func (b *txBuilder) VerifyTapscriptPartialSigs(tx string) (bool, error) {
pset, err := psetv2.NewPsetFromBase64(tx) pset, err := psetv2.NewPsetFromBase64(tx)
if err != nil { if err != nil {
return false, "", err return false, err
} }
return b.verifyTapscriptPartialSigs(pset) return b.verifyTapscriptPartialSigs(pset)
} }
func (b *txBuilder) verifyTapscriptPartialSigs(pset *psetv2.Pset) (bool, string, error) { func (b *txBuilder) verifyTapscriptPartialSigs(pset *psetv2.Pset) (bool, error) {
utx, _ := pset.UnsignedTx() utx, _ := pset.UnsignedTx()
txid := utx.TxHash().String() txid := utx.TxHash().String()
aspPublicKey, err := b.wallet.GetPubkey(context.Background())
if err != nil {
return false, err
}
for index, input := range pset.Inputs { for index, input := range pset.Inputs {
if len(input.TapLeafScript) == 0 { if len(input.TapLeafScript) == 0 {
continue continue
} }
if input.WitnessUtxo == nil { if input.WitnessUtxo == nil {
return false, txid, fmt.Errorf("missing witness utxo for input %d, cannot verify signature", index) return false, fmt.Errorf("missing witness utxo for input %d, cannot verify signature", index)
} }
// verify taproot leaf script // verify taproot leaf script
tapLeaf := input.TapLeafScript[0] tapLeaf := input.TapLeafScript[0]
closure, err := tree.DecodeClosure(tapLeaf.Script)
if err != nil {
return false, err
}
keys := make(map[string]bool)
switch c := closure.(type) {
case *tree.MultisigClosure:
for _, key := range c.PubKeys {
keys[hex.EncodeToString(schnorr.SerializePubKey(key))] = false
}
case *tree.CSVSigClosure:
for _, key := range c.PubKeys {
keys[hex.EncodeToString(schnorr.SerializePubKey(key))] = false
}
}
// we don't need to check if ASP signed
keys[hex.EncodeToString(schnorr.SerializePubKey(aspPublicKey))] = true
rootHash := tapLeaf.ControlBlock.RootHash(tapLeaf.Script) rootHash := tapLeaf.ControlBlock.RootHash(tapLeaf.Script)
tapKeyFromControlBlock := taproot.ComputeTaprootOutputKey(tree.UnspendableKey(), rootHash[:]) tapKeyFromControlBlock := taproot.ComputeTaprootOutputKey(tree.UnspendableKey(), rootHash[:])
pkscript, err := common.P2TRScript(tapKeyFromControlBlock) pkscript, err := common.P2TRScript(tapKeyFromControlBlock)
if err != nil { if err != nil {
return false, txid, err return false, err
} }
if !bytes.Equal(pkscript, input.WitnessUtxo.Script) { if !bytes.Equal(pkscript, input.WitnessUtxo.Script) {
return false, txid, fmt.Errorf("invalid control block for input %d", index) return false, fmt.Errorf("invalid control block for input %d", index)
} }
leafHash := taproot.NewBaseTapElementsLeaf(tapLeaf.Script).TapHash() leafHash := taproot.NewBaseTapElementsLeaf(tapLeaf.Script).TapHash()
@@ -506,41 +534,93 @@ func (b *txBuilder) verifyTapscriptPartialSigs(pset *psetv2.Pset) (bool, string,
&leafHash, &leafHash,
) )
if err != nil { if err != nil {
return false, txid, err return false, err
} }
for _, tapScriptSig := range input.TapScriptSig { for _, tapScriptSig := range input.TapScriptSig {
sig, err := schnorr.ParseSignature(tapScriptSig.Signature) sig, err := schnorr.ParseSignature(tapScriptSig.Signature)
if err != nil { if err != nil {
return false, txid, err return false, err
} }
pubkey, err := schnorr.ParsePubKey(tapScriptSig.PubKey) pubkey, err := schnorr.ParsePubKey(tapScriptSig.PubKey)
if err != nil { if err != nil {
return false, txid, err return false, err
} }
if !sig.Verify(preimage, pubkey) { if !sig.Verify(preimage, pubkey) {
return false, txid, fmt.Errorf("invalid signature for tx %s", txid) return false, fmt.Errorf("invalid signature for tx %s", txid)
} }
keys[hex.EncodeToString(schnorr.SerializePubKey(pubkey))] = true
}
missingSigs := 0
for key := range keys {
if !keys[key] {
missingSigs++
}
}
if missingSigs > 0 {
return false, fmt.Errorf("missing %d signatures", missingSigs)
} }
} }
return true, txid, nil return true, nil
} }
func (b *txBuilder) FinalizeAndExtract(tx string) (string, error) { func (b *txBuilder) FinalizeAndExtract(tx string) (string, error) {
p, err := psetv2.NewPsetFromBase64(tx) ptx, err := psetv2.NewPsetFromBase64(tx)
if err != nil { if err != nil {
return "", err return "", err
} }
if err := psetv2.FinalizeAll(p); err != nil { for i, in := range ptx.Inputs {
return "", err if in.WitnessUtxo == nil {
return "", fmt.Errorf("missing witness utxo, cannot finalize tx")
}
if len(in.TapLeafScript) > 0 {
tapLeaf := in.TapLeafScript[0]
closure, err := tree.DecodeClosure(tapLeaf.Script)
if err != nil {
return "", err
}
signatures := make(map[string][]byte)
for _, sig := range in.TapScriptSig {
signatures[hex.EncodeToString(sig.PubKey)] = sig.Signature
}
controlBlock, err := tapLeaf.ControlBlock.ToBytes()
if err != nil {
return "", err
}
witness, err := closure.Witness(controlBlock, signatures)
if err != nil {
return "", err
}
var witnessBuf bytes.Buffer
if err := psbt.WriteTxWitness(&witnessBuf, witness); err != nil {
return "", err
}
ptx.Inputs[i].FinalScriptWitness = witnessBuf.Bytes()
continue
}
if err := psetv2.Finalize(ptx, i); err != nil {
return "", fmt.Errorf("failed to finalize signed pset: %s", err)
}
} }
// extract the forfeit tx // extract the forfeit tx
extracted, err := psetv2.Extract(p) extracted, err := psetv2.Extract(ptx)
if err != nil { if err != nil {
return "", err return "", err
} }
@@ -591,7 +671,7 @@ func (b *txBuilder) FindLeaves(
func (b *txBuilder) BuildAsyncPaymentTransactions( func (b *txBuilder) BuildAsyncPaymentTransactions(
_ []domain.Vtxo, _ []domain.Vtxo,
_ map[domain.VtxoKey]string, _ map[domain.VtxoKey][]string,
_ map[domain.VtxoKey]chainhash.Hash, _ map[domain.VtxoKey]chainhash.Hash,
_ []domain.Receiver, _ []domain.Receiver,
) (string, error) { ) (string, error) {
@@ -728,7 +808,7 @@ func (b *txBuilder) createPoolTx(
return nil, fmt.Errorf("failed to convert value to bytes: %s", err) return nil, fmt.Errorf("failed to convert value to bytes: %s", err)
} }
boardingVtxoScript, err := tree.ParseVtxoScript(in.Descriptor) boardingVtxoScript, err := tree.ParseVtxoScript(in.Tapscripts)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -23,6 +23,7 @@ import (
"github.com/btcsuite/btcwallet/waddrmgr" "github.com/btcsuite/btcwallet/waddrmgr"
"github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/lntypes"
) )
type txBuilder struct { type txBuilder struct {
@@ -47,47 +48,74 @@ func (b *txBuilder) GetTxID(tx string) (string, error) {
return ptx.UnsignedTx.TxHash().String(), nil return ptx.UnsignedTx.TxHash().String(), nil
} }
func (b *txBuilder) VerifyTapscriptPartialSigs(tx string) (bool, string, error) { func (b *txBuilder) VerifyTapscriptPartialSigs(tx string) (bool, error) {
ptx, err := psbt.NewFromRawBytes(strings.NewReader(tx), true) ptx, err := psbt.NewFromRawBytes(strings.NewReader(tx), true)
if err != nil { if err != nil {
return false, "", err return false, err
} }
return b.verifyTapscriptPartialSigs(ptx) return b.verifyTapscriptPartialSigs(ptx)
} }
func (b *txBuilder) verifyTapscriptPartialSigs(ptx *psbt.Packet) (bool, string, error) { func (b *txBuilder) verifyTapscriptPartialSigs(ptx *psbt.Packet) (bool, error) {
txid := ptx.UnsignedTx.TxID() txid := ptx.UnsignedTx.TxID()
aspPublicKey, err := b.wallet.GetPubkey(context.Background())
if err != nil {
return false, err
}
for index, input := range ptx.Inputs { for index, input := range ptx.Inputs {
if len(input.TaprootLeafScript) == 0 { if len(input.TaprootLeafScript) == 0 {
continue continue
} }
if input.WitnessUtxo == nil { if input.WitnessUtxo == nil {
return false, txid, fmt.Errorf("missing witness utxo for input %d, cannot verify signature", index) return false, fmt.Errorf("missing witness utxo for input %d, cannot verify signature", index)
} }
// verify taproot leaf script // verify taproot leaf script
tapLeaf := input.TaprootLeafScript[0] tapLeaf := input.TaprootLeafScript[0]
closure, err := tree.DecodeClosure(tapLeaf.Script)
if err != nil {
return false, err
}
keys := make(map[string]bool)
switch c := closure.(type) {
case *tree.MultisigClosure:
for _, key := range c.PubKeys {
keys[hex.EncodeToString(schnorr.SerializePubKey(key))] = false
}
case *tree.CSVSigClosure:
for _, key := range c.PubKeys {
keys[hex.EncodeToString(schnorr.SerializePubKey(key))] = false
}
}
// we don't need to check if ASP signed
keys[hex.EncodeToString(schnorr.SerializePubKey(aspPublicKey))] = true
if len(tapLeaf.ControlBlock) == 0 { if len(tapLeaf.ControlBlock) == 0 {
return false, txid, fmt.Errorf("missing control block for input %d", index) return false, fmt.Errorf("missing control block for input %d", index)
} }
controlBlock, err := txscript.ParseControlBlock(tapLeaf.ControlBlock) controlBlock, err := txscript.ParseControlBlock(tapLeaf.ControlBlock)
if err != nil { if err != nil {
return false, txid, err return false, err
} }
rootHash := controlBlock.RootHash(tapLeaf.Script) rootHash := controlBlock.RootHash(tapLeaf.Script)
tapKeyFromControlBlock := txscript.ComputeTaprootOutputKey(bitcointree.UnspendableKey(), rootHash[:]) tapKeyFromControlBlock := txscript.ComputeTaprootOutputKey(bitcointree.UnspendableKey(), rootHash[:])
pkscript, err := common.P2TRScript(tapKeyFromControlBlock) pkscript, err := common.P2TRScript(tapKeyFromControlBlock)
if err != nil { if err != nil {
return false, txid, err return false, err
} }
if !bytes.Equal(pkscript, input.WitnessUtxo.PkScript) { if !bytes.Equal(pkscript, input.WitnessUtxo.PkScript) {
return false, txid, fmt.Errorf("invalid control block for input %d", index) return false, fmt.Errorf("invalid control block for input %d", index)
} }
preimage, err := b.getTaprootPreimage( preimage, err := b.getTaprootPreimage(
@@ -96,27 +124,40 @@ func (b *txBuilder) verifyTapscriptPartialSigs(ptx *psbt.Packet) (bool, string,
tapLeaf.Script, tapLeaf.Script,
) )
if err != nil { if err != nil {
return false, txid, err return false, err
} }
for _, tapScriptSig := range input.TaprootScriptSpendSig { for _, tapScriptSig := range input.TaprootScriptSpendSig {
sig, err := schnorr.ParseSignature(tapScriptSig.Signature) sig, err := schnorr.ParseSignature(tapScriptSig.Signature)
if err != nil { if err != nil {
return false, txid, err return false, err
} }
pubkey, err := schnorr.ParsePubKey(tapScriptSig.XOnlyPubKey) pubkey, err := schnorr.ParsePubKey(tapScriptSig.XOnlyPubKey)
if err != nil { if err != nil {
return false, txid, err return false, err
} }
if !sig.Verify(preimage, pubkey) { if !sig.Verify(preimage, pubkey) {
return false, txid, fmt.Errorf("invalid signature for tx %s", txid) return false, fmt.Errorf("invalid signature for tx %s", txid)
} }
keys[hex.EncodeToString(schnorr.SerializePubKey(pubkey))] = true
}
missingSigs := 0
for key := range keys {
if !keys[key] {
missingSigs++
}
}
if missingSigs > 0 {
return false, fmt.Errorf("missing %d signatures", missingSigs)
} }
} }
return true, txid, nil return true, nil
} }
func (b *txBuilder) FinalizeAndExtract(tx string) (string, error) { func (b *txBuilder) FinalizeAndExtract(tx string) (string, error) {
@@ -128,47 +169,30 @@ func (b *txBuilder) FinalizeAndExtract(tx string) (string, error) {
for i, in := range ptx.Inputs { for i, in := range ptx.Inputs {
isTaproot := txscript.IsPayToTaproot(in.WitnessUtxo.PkScript) isTaproot := txscript.IsPayToTaproot(in.WitnessUtxo.PkScript)
if isTaproot && len(in.TaprootLeafScript) > 0 { if isTaproot && len(in.TaprootLeafScript) > 0 {
closure, err := bitcointree.DecodeClosure(in.TaprootLeafScript[0].Script) closure, err := tree.DecodeClosure(in.TaprootLeafScript[0].Script)
if err != nil { if err != nil {
return "", err return "", err
} }
witness := make(wire.TxWitness, 4) signatures := make(map[string][]byte)
castClosure, isTaprootMultisig := closure.(*bitcointree.MultisigClosure) for _, sig := range in.TaprootScriptSpendSig {
if isTaprootMultisig { signatures[hex.EncodeToString(sig.XOnlyPubKey)] = sig.Signature
ownerPubkey := schnorr.SerializePubKey(castClosure.Pubkey)
aspKey := schnorr.SerializePubKey(castClosure.AspPubkey)
for _, sig := range in.TaprootScriptSpendSig {
if bytes.Equal(sig.XOnlyPubKey, ownerPubkey) {
witness[0] = sig.Signature
}
if bytes.Equal(sig.XOnlyPubKey, aspKey) {
witness[1] = sig.Signature
}
}
witness[2] = in.TaprootLeafScript[0].Script
witness[3] = in.TaprootLeafScript[0].ControlBlock
for idw, w := range witness {
if w == nil {
return "", fmt.Errorf("missing witness element %d, cannot finalize taproot mutlisig input %d", idw, i)
}
}
var witnessBuf bytes.Buffer
if err := psbt.WriteTxWitness(&witnessBuf, witness); err != nil {
return "", err
}
ptx.Inputs[i].FinalScriptWitness = witnessBuf.Bytes()
continue
} }
witness, err := closure.Witness(in.TaprootLeafScript[0].ControlBlock, signatures)
if err != nil {
return "", err
}
var witnessBuf bytes.Buffer
if err := psbt.WriteTxWitness(&witnessBuf, witness); err != nil {
return "", err
}
ptx.Inputs[i].FinalScriptWitness = witnessBuf.Bytes()
continue
} }
if err := psbt.Finalize(ptx, i); err != nil { if err := psbt.Finalize(ptx, i); err != nil {
@@ -265,7 +289,7 @@ func (b *txBuilder) VerifyForfeitTxs(vtxos []domain.Vtxo, connectors []string, f
return nil, fmt.Errorf("invalid forfeit tx, expect 2 inputs, got %d", len(ptx.Inputs)) return nil, fmt.Errorf("invalid forfeit tx, expect 2 inputs, got %d", len(ptx.Inputs))
} }
valid, _, err := b.verifyTapscriptPartialSigs(ptx) valid, err := b.verifyTapscriptPartialSigs(ptx)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -623,7 +647,7 @@ func (b *txBuilder) FindLeaves(congestionTree tree.CongestionTree, fromtxid stri
func (b *txBuilder) BuildAsyncPaymentTransactions( func (b *txBuilder) BuildAsyncPaymentTransactions(
vtxos []domain.Vtxo, vtxos []domain.Vtxo,
descriptors map[domain.VtxoKey]string, scripts map[domain.VtxoKey][]string,
forfeitsLeaves map[domain.VtxoKey]chainhash.Hash, forfeitsLeaves map[domain.VtxoKey]chainhash.Hash,
receivers []domain.Receiver, receivers []domain.Receiver,
) (string, error) { ) (string, error) {
@@ -638,9 +662,9 @@ func (b *txBuilder) BuildAsyncPaymentTransactions(
redeemTxWeightEstimator := &input.TxWeightEstimator{} redeemTxWeightEstimator := &input.TxWeightEstimator{}
for index, vtxo := range vtxos { for index, vtxo := range vtxos {
desc, ok := descriptors[vtxo.VtxoKey] vtxoTapscripts, ok := scripts[vtxo.VtxoKey]
if !ok { if !ok {
return "", fmt.Errorf("missing descriptor for vtxo %s", vtxo.VtxoKey) return "", fmt.Errorf("missing scripts for vtxo %s", vtxo.VtxoKey)
} }
forfeitLeafHash, ok := forfeitsLeaves[vtxo.VtxoKey] forfeitLeafHash, ok := forfeitsLeaves[vtxo.VtxoKey]
@@ -662,7 +686,7 @@ func (b *txBuilder) BuildAsyncPaymentTransactions(
Index: vtxo.VOut, Index: vtxo.VOut,
} }
vtxoScript, err := bitcointree.ParseVtxoScript(desc) vtxoScript, err := bitcointree.ParseVtxoScript(vtxoTapscripts)
if err != nil { if err != nil {
return "", err return "", err
} }
@@ -698,7 +722,12 @@ func (b *txBuilder) BuildAsyncPaymentTransactions(
return "", err return "", err
} }
redeemTxWeightEstimator.AddTapscriptInput(64*2+40, &waddrmgr.Tapscript{ closure, err := tree.DecodeClosure(leafProof.Script)
if err != nil {
return "", err
}
redeemTxWeightEstimator.AddTapscriptInput(lntypes.WeightUnit(closure.WitnessSize()), &waddrmgr.Tapscript{
RevealedScript: leafProof.Script, RevealedScript: leafProof.Script,
ControlBlock: ctrlBlock, ControlBlock: ctrlBlock,
}) })
@@ -940,7 +969,7 @@ func (b *txBuilder) createRoundTx(
}) })
nSequences = append(nSequences, wire.MaxTxInSequenceNum) nSequences = append(nSequences, wire.MaxTxInSequenceNum)
boardingVtxoScript, err := bitcointree.ParseVtxoScript(boardingInput.Descriptor) boardingVtxoScript, err := bitcointree.ParseVtxoScript(boardingInput.Tapscripts)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -1322,7 +1351,7 @@ func castToOutpoints(inputs []ports.TxInput) []ports.TxOutpoint {
func extractSweepLeaf(input psbt.PInput) (sweepLeaf *psbt.TaprootTapLeafScript, internalKey *secp256k1.PublicKey, lifetime int64, err error) { func extractSweepLeaf(input psbt.PInput) (sweepLeaf *psbt.TaprootTapLeafScript, internalKey *secp256k1.PublicKey, lifetime int64, err error) {
for _, leaf := range input.TaprootLeafScript { for _, leaf := range input.TaprootLeafScript {
closure := &bitcointree.CSVSigClosure{} closure := &tree.CSVSigClosure{}
valid, err := closure.Decode(leaf.Script) valid, err := closure.Decode(leaf.Script)
if err != nil { if err != nil {
return nil, nil, 0, err return nil, nil, 0, err

View File

@@ -5,7 +5,7 @@ import (
"fmt" "fmt"
"github.com/ark-network/ark/common" "github.com/ark-network/ark/common"
"github.com/ark-network/ark/common/bitcointree" "github.com/ark-network/ark/common/tree"
"github.com/ark-network/ark/server/internal/core/ports" "github.com/ark-network/ark/server/internal/core/ports"
"github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/btcutil"
@@ -27,7 +27,7 @@ func sweepTransaction(
Index: input.GetIndex(), Index: input.GetIndex(),
}) })
sweepClosure := bitcointree.CSVSigClosure{} sweepClosure := tree.CSVSigClosure{}
valid, err := sweepClosure.Decode(input.GetLeafScript()) valid, err := sweepClosure.Decode(input.GetLeafScript())
if err != nil { if err != nil {
return nil, err return nil, err

View File

@@ -10,7 +10,7 @@ import (
"time" "time"
"github.com/ark-network/ark/common" "github.com/ark-network/ark/common"
"github.com/ark-network/ark/common/bitcointree" "github.com/ark-network/ark/common/tree"
"github.com/ark-network/ark/server/internal/core/domain" "github.com/ark-network/ark/server/internal/core/domain"
"github.com/ark-network/ark/server/internal/core/ports" "github.com/ark-network/ark/server/internal/core/ports"
"github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/btcsuite/btcd/btcec/v2/schnorr"
@@ -745,46 +745,29 @@ func (s *service) SignTransaction(ctx context.Context, partialTx string, extract
for i, in := range ptx.Inputs { for i, in := range ptx.Inputs {
isTaproot := txscript.IsPayToTaproot(in.WitnessUtxo.PkScript) isTaproot := txscript.IsPayToTaproot(in.WitnessUtxo.PkScript)
if isTaproot && len(in.TaprootLeafScript) > 0 { if isTaproot && len(in.TaprootLeafScript) > 0 {
closure, err := bitcointree.DecodeClosure(in.TaprootLeafScript[0].Script) closure, err := tree.DecodeClosure(in.TaprootLeafScript[0].Script)
if err != nil { if err != nil {
return "", err return "", err
} }
witness := make(wire.TxWitness, 4) signatures := make(map[string][]byte)
castClosure, isTaprootMultisig := closure.(*bitcointree.MultisigClosure) for _, sig := range in.TaprootScriptSpendSig {
if isTaprootMultisig { signatures[hex.EncodeToString(sig.XOnlyPubKey)] = sig.Signature
ownerPubkey := schnorr.SerializePubKey(castClosure.Pubkey)
aspKey := schnorr.SerializePubKey(castClosure.AspPubkey)
for _, sig := range in.TaprootScriptSpendSig {
if bytes.Equal(sig.XOnlyPubKey, ownerPubkey) {
witness[0] = sig.Signature
}
if bytes.Equal(sig.XOnlyPubKey, aspKey) {
witness[1] = sig.Signature
}
}
witness[2] = in.TaprootLeafScript[0].Script
witness[3] = in.TaprootLeafScript[0].ControlBlock
for idw, w := range witness {
if w == nil {
return "", fmt.Errorf("missing witness element %d, cannot finalize taproot mutlisig input %d", idw, i)
}
}
var witnessBuf bytes.Buffer
if err := psbt.WriteTxWitness(&witnessBuf, witness); err != nil {
return "", err
}
ptx.Inputs[i].FinalScriptWitness = witnessBuf.Bytes()
continue
} }
witness, err := closure.Witness(in.TaprootLeafScript[0].ControlBlock, signatures)
if err != nil {
return "", err
}
var witnessBuf bytes.Buffer
if err := psbt.WriteTxWitness(&witnessBuf, witness); err != nil {
return "", err
}
ptx.Inputs[i].FinalScriptWitness = witnessBuf.Bytes()
continue
} }
if err := psbt.Finalize(ptx, i); err != nil { if err := psbt.Finalize(ptx, i); err != nil {

View File

@@ -12,7 +12,6 @@ import (
pb "github.com/ark-network/ark/api-spec/protobuf/gen/ocean/v1" pb "github.com/ark-network/ark/api-spec/protobuf/gen/ocean/v1"
"github.com/ark-network/ark/common/tree" "github.com/ark-network/ark/common/tree"
"github.com/ark-network/ark/server/internal/core/ports" "github.com/ark-network/ark/server/internal/core/ports"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/btcutil/psbt" "github.com/btcsuite/btcd/btcutil/psbt"
"github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/txscript"
@@ -58,43 +57,29 @@ func (s *service) SignTransaction(
return "", err return "", err
} }
switch c := closure.(type) { signatures := make(map[string][]byte)
case *tree.MultisigClosure:
asp := schnorr.SerializePubKey(c.AspPubkey)
owner := schnorr.SerializePubKey(c.Pubkey)
witness := make([][]byte, 4) for _, sig := range in.TapScriptSig {
for _, sig := range in.TapScriptSig { signatures[hex.EncodeToString(sig.PubKey)] = sig.Signature
if bytes.Equal(sig.PubKey, owner) {
witness[0] = sig.Signature
continue
}
if bytes.Equal(sig.PubKey, asp) {
witness[1] = sig.Signature
}
}
witness[2] = tapLeaf.Script
controlBlock, err := tapLeaf.ControlBlock.ToBytes()
if err != nil {
return "", err
}
witness[3] = controlBlock
var witnessBuf bytes.Buffer
if err := psbt.WriteTxWitness(&witnessBuf, witness); err != nil {
return "", err
}
ptx.Inputs[i].FinalScriptWitness = witnessBuf.Bytes()
continue
default:
return "", fmt.Errorf("unexpected closure type %T", c)
} }
controlBlock, err := tapLeaf.ControlBlock.ToBytes()
if err != nil {
return "", err
}
witness, err := closure.Witness(controlBlock, signatures)
if err != nil {
return "", err
}
var witnessBuf bytes.Buffer
if err := psbt.WriteTxWitness(&witnessBuf, witness); err != nil {
return "", err
}
ptx.Inputs[i].FinalScriptWitness = witnessBuf.Bytes()
continue
} }
if err := psetv2.Finalize(ptx, i); err != nil { if err := psetv2.Finalize(ptx, i); err != nil {

View File

@@ -3,9 +3,12 @@ package handlers
import ( import (
"context" "context"
"encoding/hex" "encoding/hex"
"fmt"
"sync" "sync"
arkv1 "github.com/ark-network/ark/api-spec/protobuf/gen/ark/v1" arkv1 "github.com/ark-network/ark/api-spec/protobuf/gen/ark/v1"
"github.com/ark-network/ark/common/bitcointree"
"github.com/ark-network/ark/common/descriptor"
"github.com/ark-network/ark/server/internal/core/application" "github.com/ark-network/ark/server/internal/core/application"
"github.com/ark-network/ark/server/internal/core/domain" "github.com/ark-network/ark/server/internal/core/domain"
"github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/decred/dcrd/dcrec/secp256k1/v4"
@@ -43,6 +46,15 @@ func (h *handler) GetInfo(
return nil, err return nil, err
} }
desc := fmt.Sprintf(
descriptor.DefaultVtxoDescriptorTemplate,
hex.EncodeToString(bitcointree.UnspendableKey().SerializeCompressed()),
"USER",
info.PubKey,
info.UnilateralExitDelay,
info.PubKey,
)
return &arkv1.GetInfoResponse{ return &arkv1.GetInfoResponse{
Pubkey: info.PubKey, Pubkey: info.PubKey,
RoundLifetime: info.RoundLifetime, RoundLifetime: info.RoundLifetime,
@@ -50,8 +62,9 @@ func (h *handler) GetInfo(
RoundInterval: info.RoundInterval, RoundInterval: info.RoundInterval,
Network: info.Network, Network: info.Network,
Dust: int64(info.Dust), Dust: int64(info.Dust),
BoardingDescriptorTemplate: info.BoardingDescriptorTemplate,
ForfeitAddress: info.ForfeitAddress, ForfeitAddress: info.ForfeitAddress,
BoardingDescriptorTemplate: desc,
VtxoDescriptorTemplates: []string{desc},
}, nil }, nil
} }
@@ -73,14 +86,18 @@ func (h *handler) GetBoardingAddress(
return nil, status.Error(codes.InvalidArgument, "invalid pubkey (parse error)") return nil, status.Error(codes.InvalidArgument, "invalid pubkey (parse error)")
} }
addr, descriptor, err := h.svc.GetBoardingAddress(ctx, userPubkey) addr, tapscripts, err := h.svc.GetBoardingAddress(ctx, userPubkey)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &arkv1.GetBoardingAddressResponse{ return &arkv1.GetBoardingAddressResponse{
Address: addr, Address: addr,
Descriptor_: descriptor, TaprootTree: &arkv1.GetBoardingAddressResponse_Tapscripts{
Tapscripts: &arkv1.Tapscripts{
Scripts: tapscripts,
},
},
}, nil }, nil
} }

View File

@@ -43,7 +43,7 @@ func parseAsyncPaymentInputs(ins []*arkv1.AsyncPaymentInput) ([]application.Asyn
Txid: input.GetInput().GetOutpoint().GetTxid(), Txid: input.GetInput().GetOutpoint().GetTxid(),
VOut: input.GetInput().GetOutpoint().GetVout(), VOut: input.GetInput().GetOutpoint().GetVout(),
}, },
Descriptor: input.GetInput().GetDescriptor_(), Tapscripts: input.GetInput().GetTapscripts().GetScripts(),
}, },
ForfeitLeafHash: *forfeitLeafHash, ForfeitLeafHash: *forfeitLeafHash,
}) })
@@ -82,7 +82,7 @@ func parseInputs(ins []*arkv1.Input) ([]ports.Input, error) {
Txid: input.GetOutpoint().GetTxid(), Txid: input.GetOutpoint().GetTxid(),
VOut: input.GetOutpoint().GetVout(), VOut: input.GetOutpoint().GetVout(),
}, },
Descriptor: input.GetDescriptor_(), Tapscripts: input.GetTapscripts().GetScripts(),
}) })
} }