mirror of
https://github.com/aljazceru/ark.git
synced 2025-12-17 12:14:21 +01:00
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:
@@ -651,6 +651,9 @@
|
||||
},
|
||||
"descriptor": {
|
||||
"type": "string"
|
||||
},
|
||||
"tapscripts": {
|
||||
"$ref": "#/definitions/v1Tapscripts"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -748,6 +751,9 @@
|
||||
},
|
||||
"descriptor": {
|
||||
"type": "string"
|
||||
},
|
||||
"tapscripts": {
|
||||
"$ref": "#/definitions/v1Tapscripts"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -824,7 +830,7 @@
|
||||
"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": {
|
||||
"type": "object"
|
||||
@@ -1137,6 +1143,17 @@
|
||||
"v1SubmitTreeSignaturesResponse": {
|
||||
"type": "object"
|
||||
},
|
||||
"v1Tapscripts": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"scripts": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1Tree": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -133,7 +133,10 @@ message GetBoardingAddressRequest {
|
||||
}
|
||||
message GetBoardingAddressResponse {
|
||||
string address = 1;
|
||||
string descriptor = 2;
|
||||
oneof taproot_tree {
|
||||
string descriptor = 2;
|
||||
Tapscripts tapscripts = 3;
|
||||
}
|
||||
}
|
||||
|
||||
/* In-Round Payment API messages */
|
||||
@@ -298,7 +301,10 @@ message Outpoint {
|
||||
|
||||
message Input {
|
||||
Outpoint outpoint = 1;
|
||||
string descriptor = 2;
|
||||
oneof taproot_tree {
|
||||
string descriptor = 2;
|
||||
Tapscripts tapscripts = 3;
|
||||
}
|
||||
}
|
||||
|
||||
message Output {
|
||||
@@ -355,7 +361,7 @@ message RedeemTransaction {
|
||||
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 {
|
||||
string control_block = 1;
|
||||
string script = 2;
|
||||
@@ -378,4 +384,8 @@ message DeleteNostrRecipientRequest {
|
||||
repeated SignedVtxoOutpoint vtxos = 1;
|
||||
}
|
||||
|
||||
message DeleteNostrRecipientResponse {}
|
||||
message DeleteNostrRecipientResponse {}
|
||||
|
||||
message Tapscripts {
|
||||
repeated string scripts = 1;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -282,17 +282,19 @@ func createRootNode(
|
||||
func createAggregatedKeyWithSweep(
|
||||
cosigners []*secp256k1.PublicKey, aspPubkey *secp256k1.PublicKey, roundLifetime int64,
|
||||
) (*musig2.AggregateKey, *psbt.TaprootTapLeafScript, error) {
|
||||
sweepClosure := &CSVSigClosure{
|
||||
Pubkey: aspPubkey,
|
||||
Seconds: uint(roundLifetime),
|
||||
sweepClosure := &tree.CSVSigClosure{
|
||||
MultisigClosure: tree.MultisigClosure{PubKeys: []*secp256k1.PublicKey{aspPubkey}},
|
||||
Seconds: uint(roundLifetime),
|
||||
}
|
||||
|
||||
sweepLeaf, err := sweepClosure.Leaf()
|
||||
sweepScript, err := sweepClosure.Script()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
tapTree := txscript.AssembleTaprootScriptTree(*sweepLeaf)
|
||||
sweepLeaf := txscript.NewBaseTapLeaf(sweepScript)
|
||||
|
||||
tapTree := txscript.AssembleTaprootScriptTree(sweepLeaf)
|
||||
tapTreeRoot := tapTree.RootNode.TapHash()
|
||||
|
||||
aggregatedKey, err := AggregateKeys(
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package bitcointree_test
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"testing"
|
||||
@@ -49,7 +48,7 @@ func TestRoundTripSignTree(t *testing.T) {
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
tree, err := bitcointree.CraftCongestionTree(
|
||||
vtxoTree, err := bitcointree.CraftCongestionTree(
|
||||
&wire.OutPoint{
|
||||
Hash: *testTxid,
|
||||
Index: 0,
|
||||
@@ -62,20 +61,21 @@ func TestRoundTripSignTree(t *testing.T) {
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
sweepClosure := bitcointree.CSVSigClosure{
|
||||
Pubkey: asp.PubKey(),
|
||||
Seconds: lifetime,
|
||||
sweepClosure := &tree.CSVSigClosure{
|
||||
MultisigClosure: tree.MultisigClosure{PubKeys: []*secp256k1.PublicKey{asp.PubKey()}},
|
||||
Seconds: uint(lifetime),
|
||||
}
|
||||
|
||||
sweepTapLeaf, err := sweepClosure.Leaf()
|
||||
sweepScript, err := sweepClosure.Script()
|
||||
require.NoError(t, err)
|
||||
|
||||
sweepTapTree := txscript.AssembleTaprootScriptTree(*sweepTapLeaf)
|
||||
sweepTapLeaf := txscript.NewBaseTapLeaf(sweepScript)
|
||||
sweepTapTree := txscript.AssembleTaprootScriptTree(sweepTapLeaf)
|
||||
root := sweepTapTree.RootNode.TapHash()
|
||||
|
||||
aspCoordinator, err := bitcointree.NewTreeCoordinatorSession(
|
||||
sharedOutputAmount,
|
||||
tree,
|
||||
vtxoTree,
|
||||
root.CloneBytes(),
|
||||
cosignerPubKeys,
|
||||
)
|
||||
@@ -84,7 +84,7 @@ func TestRoundTripSignTree(t *testing.T) {
|
||||
// Create signer sessions for all cosigners
|
||||
signerSessions := make([]bitcointree.SignerSession, 20)
|
||||
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
|
||||
@@ -136,24 +136,6 @@ type receiverFixture struct {
|
||||
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 {
|
||||
receiversOut := make([]tree.VtxoLeaf, 0, len(receivers))
|
||||
for _, r := range receivers {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -72,7 +72,7 @@ func UnspendableKey() *secp256k1.PublicKey {
|
||||
// - every control block and taproot output scripts
|
||||
// - input and output amounts
|
||||
func ValidateCongestionTree(
|
||||
tree tree.CongestionTree, poolTx string, aspPublicKey *secp256k1.PublicKey, roundLifetime int64,
|
||||
vtxoTree tree.CongestionTree, poolTx string, aspPublicKey *secp256k1.PublicKey, roundLifetime int64,
|
||||
) error {
|
||||
poolTransaction, err := psbt.NewFromRawBytes(strings.NewReader(poolTx), true)
|
||||
if err != nil {
|
||||
@@ -85,17 +85,17 @@ func ValidateCongestionTree(
|
||||
|
||||
poolTxAmount := poolTransaction.UnsignedTx.TxOut[sharedOutputIndex].Value
|
||||
|
||||
nbNodes := tree.NumberOfNodes()
|
||||
nbNodes := vtxoTree.NumberOfNodes()
|
||||
if nbNodes == 0 {
|
||||
return ErrEmptyTree
|
||||
}
|
||||
|
||||
if len(tree[0]) != 1 {
|
||||
if len(vtxoTree[0]) != 1 {
|
||||
return ErrInvalidRootLevel
|
||||
}
|
||||
|
||||
// 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)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid root transaction: %w", err)
|
||||
@@ -120,28 +120,29 @@ func ValidateCongestionTree(
|
||||
return ErrInvalidAmount
|
||||
}
|
||||
|
||||
if len(tree.Leaves()) == 0 {
|
||||
if len(vtxoTree.Leaves()) == 0 {
|
||||
return ErrNoLeaves
|
||||
}
|
||||
|
||||
sweepClosure := &CSVSigClosure{
|
||||
Seconds: uint(roundLifetime),
|
||||
Pubkey: aspPublicKey,
|
||||
sweepClosure := &tree.CSVSigClosure{
|
||||
MultisigClosure: tree.MultisigClosure{PubKeys: []*secp256k1.PublicKey{aspPublicKey}},
|
||||
Seconds: uint(roundLifetime),
|
||||
}
|
||||
|
||||
sweepLeaf, err := sweepClosure.Leaf()
|
||||
sweepScript, err := sweepClosure.Script()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tapTree := txscript.AssembleTaprootScriptTree(*sweepLeaf)
|
||||
sweepLeaf := txscript.NewBaseTapLeaf(sweepScript)
|
||||
tapTree := txscript.AssembleTaprootScriptTree(sweepLeaf)
|
||||
root := tapTree.RootNode.TapHash()
|
||||
|
||||
// iterates over all the nodes of the tree
|
||||
for _, level := range tree {
|
||||
for _, level := range vtxoTree {
|
||||
for _, node := range level {
|
||||
if err := validateNodeTransaction(
|
||||
node, tree, root.CloneBytes(),
|
||||
node, vtxoTree, root.CloneBytes(),
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1,94 +1,52 @@
|
||||
package bitcointree
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
|
||||
"github.com/ark-network/ark/common"
|
||||
"github.com/ark-network/ark/common/descriptor"
|
||||
"github.com/btcsuite/btcd/btcec/v2/schnorr"
|
||||
"github.com/ark-network/ark/common/tree"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"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{
|
||||
&DefaultVtxoScript{},
|
||||
&TapscriptsVtxoScript{},
|
||||
}
|
||||
|
||||
for _, v := range types {
|
||||
if err := v.FromDescriptor(desc); err == nil {
|
||||
if err := v.Decode(scripts); err == nil {
|
||||
return v, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("invalid vtxo descriptor: %s", desc)
|
||||
return nil, fmt.Errorf("invalid vtxo scripts: %s", scripts)
|
||||
}
|
||||
|
||||
/*
|
||||
* DefaultVtxoScript is the default implementation of VTXO with 2 closures
|
||||
* - Owner and ASP (forfeit)
|
||||
* - Owner after t (unilateral exit)
|
||||
*/
|
||||
type DefaultVtxoScript struct {
|
||||
Owner *secp256k1.PublicKey
|
||||
Asp *secp256k1.PublicKey
|
||||
ExitDelay uint
|
||||
func NewDefaultVtxoScript(owner, asp *secp256k1.PublicKey, exitDelay uint) VtxoScript {
|
||||
base := tree.NewDefaultVtxoScript(owner, asp, exitDelay)
|
||||
|
||||
return &TapscriptsVtxoScript{*base}
|
||||
}
|
||||
|
||||
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,
|
||||
)
|
||||
type TapscriptsVtxoScript struct {
|
||||
tree.TapscriptsVtxoScript
|
||||
}
|
||||
|
||||
func (v *DefaultVtxoScript) FromDescriptor(desc string) error {
|
||||
owner, asp, exitDelay, err := descriptor.ParseDefaultVtxoDescriptor(desc)
|
||||
if err != nil {
|
||||
return err
|
||||
func (v *TapscriptsVtxoScript) TapTree() (*secp256k1.PublicKey, bitcoinTapTree, error) {
|
||||
leaves := make([]txscript.TapLeaf, len(v.Closures))
|
||||
for i, closure := range v.Closures {
|
||||
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
|
||||
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,
|
||||
)
|
||||
|
||||
tapTree := txscript.AssembleTaprootScriptTree(leaves...)
|
||||
root := tapTree.RootNode.TapHash()
|
||||
taprootKey := txscript.ComputeTaprootOutputKey(
|
||||
UnspendableKey(),
|
||||
|
||||
@@ -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)))
|
||||
}
|
||||
@@ -163,11 +163,11 @@ func (n *node) getWitnessData() (
|
||||
}
|
||||
|
||||
sweepClosure := &CSVSigClosure{
|
||||
Pubkey: n.sweepKey,
|
||||
Seconds: uint(n.roundLifetime),
|
||||
MultisigClosure: MultisigClosure{PubKeys: []*secp256k1.PublicKey{n.sweepKey}},
|
||||
Seconds: uint(n.roundLifetime),
|
||||
}
|
||||
|
||||
sweepLeaf, err := sweepClosure.Leaf()
|
||||
sweepLeaf, err := sweepClosure.Script()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
@@ -183,13 +183,14 @@ func (n *node) getWitnessData() (
|
||||
MinRelayFee: n.feeSats,
|
||||
}
|
||||
|
||||
unrollLeaf, err := unrollClosure.Leaf()
|
||||
unrollScript, err := unrollClosure.Script()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
branchTaprootTree := taproot.AssembleTaprootScriptTree(
|
||||
*unrollLeaf, *sweepLeaf,
|
||||
taproot.NewBaseTapElementsLeaf(unrollScript),
|
||||
taproot.NewBaseTapElementsLeaf(sweepLeaf),
|
||||
)
|
||||
root := branchTaprootTree.RootNode.TapHash()
|
||||
|
||||
@@ -224,13 +225,14 @@ func (n *node) getWitnessData() (
|
||||
RightAmount: rightAmount,
|
||||
}
|
||||
|
||||
unrollLeaf, err := unrollClosure.Leaf()
|
||||
unrollLeaf, err := unrollClosure.Script()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
branchTaprootTree := taproot.AssembleTaprootScriptTree(
|
||||
*unrollLeaf, *sweepLeaf,
|
||||
taproot.NewBaseTapElementsLeaf(unrollLeaf),
|
||||
taproot.NewBaseTapElementsLeaf(sweepLeaf),
|
||||
)
|
||||
root := branchTaprootTree.RootNode.TapHash()
|
||||
|
||||
|
||||
@@ -9,8 +9,8 @@ import (
|
||||
"github.com/ark-network/ark/common"
|
||||
"github.com/btcsuite/btcd/btcec/v2/schnorr"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||
"github.com/vulpemventures/go-elements/taproot"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -21,108 +21,333 @@ const (
|
||||
OP_SUB64 = 0xd8
|
||||
)
|
||||
|
||||
type MultisigType int
|
||||
|
||||
const (
|
||||
MultisigTypeChecksig MultisigType = iota
|
||||
MultisigTypeChecksigAdd
|
||||
)
|
||||
|
||||
type Closure interface {
|
||||
Leaf() (*taproot.TapElementsLeaf, error)
|
||||
Script() ([]byte, 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 {
|
||||
LeftKey, RightKey *secp256k1.PublicKey
|
||||
LeftAmount, RightAmount 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 {
|
||||
Pubkey *secp256k1.PublicKey
|
||||
MultisigClosure
|
||||
Seconds uint
|
||||
}
|
||||
|
||||
type MultisigClosure struct {
|
||||
Pubkey *secp256k1.PublicKey
|
||||
AspPubkey *secp256k1.PublicKey
|
||||
}
|
||||
|
||||
func DecodeClosure(script []byte) (Closure, error) {
|
||||
var closure Closure
|
||||
|
||||
closure = &UnrollClosure{}
|
||||
if valid, err := closure.Decode(script); err == nil && valid {
|
||||
return closure, nil
|
||||
types := []Closure{
|
||||
&CSVSigClosure{},
|
||||
&MultisigClosure{},
|
||||
&UnrollClosure{},
|
||||
}
|
||||
|
||||
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
|
||||
for _, closure := range types {
|
||||
if valid, err := closure.Decode(script); err == nil && valid {
|
||||
return closure, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("invalid closure script %s", hex.EncodeToString(script))
|
||||
}
|
||||
|
||||
func (f *MultisigClosure) Leaf() (*taproot.TapElementsLeaf, error) {
|
||||
aspKeyBytes := schnorr.SerializePubKey(f.AspPubkey)
|
||||
userKeyBytes := schnorr.SerializePubKey(f.Pubkey)
|
||||
func (f *MultisigClosure) WitnessSize() int {
|
||||
return 64 * len(f.PubKeys)
|
||||
}
|
||||
|
||||
script, err := txscript.NewScriptBuilder().AddData(aspKeyBytes).
|
||||
AddOp(txscript.OP_CHECKSIGVERIFY).AddData(userKeyBytes).
|
||||
AddOp(txscript.OP_CHECKSIG).Script()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
func (f *MultisigClosure) Script() ([]byte, error) {
|
||||
scriptBuilder := txscript.NewScriptBuilder()
|
||||
|
||||
switch f.Type {
|
||||
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 &tapLeaf, nil
|
||||
return scriptBuilder.Script()
|
||||
}
|
||||
|
||||
func (f *MultisigClosure) Decode(script []byte) (bool, error) {
|
||||
valid, aspPubKey, err := decodeChecksigScript(script)
|
||||
if err != nil {
|
||||
return false, err
|
||||
if len(script) == 0 {
|
||||
return false, fmt.Errorf("empty script")
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
valid, pubkey, err := decodeChecksigScript(script[33:])
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if !valid {
|
||||
// Verify we found at least one public key
|
||||
if len(pubkeys) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
f.Pubkey = pubkey
|
||||
f.AspPubkey = aspPubKey
|
||||
f.PubKeys = pubkeys
|
||||
f.Type = MultisigTypeChecksigAdd
|
||||
|
||||
rebuilt, err := f.Leaf()
|
||||
// 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, script) {
|
||||
if !bytes.Equal(rebuilt, script) {
|
||||
f.PubKeys = nil
|
||||
f.Type = 0
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (d *CSVSigClosure) Leaf() (*taproot.TapElementsLeaf, error) {
|
||||
script, err := encodeCsvWithChecksigScript(d.Pubkey, d.Seconds)
|
||||
func (f *MultisigClosure) decodeChecksig(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) {
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tapLeaf := taproot.NewBaseTapElementsLeaf(script)
|
||||
return &tapLeaf, nil
|
||||
script, err := f.Script()
|
||||
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) {
|
||||
if len(script) == 0 {
|
||||
return false, fmt.Errorf("empty script")
|
||||
}
|
||||
|
||||
csvIndex := bytes.Index(
|
||||
script, []byte{txscript.OP_CHECKSEQUENCEVERIFY, txscript.OP_DROP},
|
||||
)
|
||||
@@ -140,8 +365,8 @@ func (d *CSVSigClosure) Decode(script []byte) (bool, error) {
|
||||
return false, err
|
||||
}
|
||||
|
||||
checksigScript := script[csvIndex+2:]
|
||||
valid, pubkey, err := decodeChecksigScript(checksigScript)
|
||||
multisigClosure := &MultisigClosure{}
|
||||
valid, err := multisigClosure.Decode(script[csvIndex+2:])
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@@ -150,26 +375,21 @@ func (d *CSVSigClosure) Decode(script []byte) (bool, error) {
|
||||
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.MultisigClosure = *multisigClosure
|
||||
|
||||
return valid, nil
|
||||
}
|
||||
|
||||
func (c *UnrollClosure) WitnessSize() int {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (c *UnrollClosure) isOneChild() bool {
|
||||
return c.RightKey == nil && c.MinRelayFee > 0
|
||||
}
|
||||
|
||||
func (c *UnrollClosure) Leaf() (*taproot.TapElementsLeaf, error) {
|
||||
func (c *UnrollClosure) Script() ([]byte, error) {
|
||||
if c.LeftKey == nil {
|
||||
return nil, fmt.Errorf("left key is required")
|
||||
}
|
||||
@@ -178,8 +398,7 @@ func (c *UnrollClosure) Leaf() (*taproot.TapElementsLeaf, error) {
|
||||
branchScript := encodeOneChildIntrospectionScript(
|
||||
txscript.OP_0, schnorr.SerializePubKey(c.LeftKey), c.MinRelayFee,
|
||||
)
|
||||
leaf := taproot.NewBaseTapElementsLeaf(branchScript)
|
||||
return &leaf, nil
|
||||
return branchScript, nil
|
||||
}
|
||||
|
||||
if c.LeftAmount == 0 {
|
||||
@@ -205,8 +424,7 @@ func (c *UnrollClosure) Leaf() (*taproot.TapElementsLeaf, error) {
|
||||
)
|
||||
branchScript = append(branchScript, nextScriptRight...)
|
||||
|
||||
leaf := taproot.NewBaseTapElementsLeaf(branchScript)
|
||||
return &leaf, nil
|
||||
return branchScript, nil
|
||||
}
|
||||
|
||||
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.MinRelayFee = minrelayfee
|
||||
|
||||
rebuilt, err := c.Leaf()
|
||||
rebuilt, err := c.Script()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if !bytes.Equal(rebuilt.Script, script) {
|
||||
if !bytes.Equal(rebuilt, script) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
@@ -272,12 +490,12 @@ func (c *UnrollClosure) Decode(script []byte) (valid bool, err error) {
|
||||
c.RightAmount = rightAmount
|
||||
c.RightKey = rightKey
|
||||
|
||||
rebuilt, err := c.Leaf()
|
||||
rebuilt, err := c.Script()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if !bytes.Equal(rebuilt.Script, script) {
|
||||
if !bytes.Equal(rebuilt, script) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
@@ -351,64 +569,6 @@ func decodeOneChildIntrospectionScript(
|
||||
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
|
||||
// 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
|
||||
@@ -496,3 +656,13 @@ func encodeOneChildIntrospectionScript(
|
||||
|
||||
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
471
common/tree/script_test.go
Normal 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")
|
||||
}
|
||||
@@ -238,8 +238,8 @@ func validateNodeTransaction(
|
||||
|
||||
switch c := closure.(type) {
|
||||
case *CSVSigClosure:
|
||||
isASP := bytes.Equal(
|
||||
schnorr.SerializePubKey(c.Pubkey),
|
||||
isASP := len(c.MultisigClosure.PubKeys) == 1 && bytes.Equal(
|
||||
schnorr.SerializePubKey(c.MultisigClosure.PubKeys[0]),
|
||||
schnorr.SerializePubKey(expectedPublicKeyASP),
|
||||
)
|
||||
isSweepDelay := int64(c.Seconds) == expectedSequence
|
||||
|
||||
@@ -1,89 +1,159 @@
|
||||
package tree
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"math"
|
||||
|
||||
"github.com/ark-network/ark/common"
|
||||
"github.com/ark-network/ark/common/descriptor"
|
||||
"github.com/btcsuite/btcd/btcec/v2/schnorr"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||
"github.com/vulpemventures/go-elements/taproot"
|
||||
)
|
||||
|
||||
type VtxoScript common.VtxoScript[elementsTapTree]
|
||||
var (
|
||||
ErrNoExitLeaf = fmt.Errorf("no exit leaf")
|
||||
)
|
||||
|
||||
func ParseVtxoScript(desc string) (VtxoScript, error) {
|
||||
v := &DefaultVtxoScript{}
|
||||
// TODO add other type
|
||||
err := v.FromDescriptor(desc)
|
||||
type VtxoScript common.VtxoScript[elementsTapTree, *MultisigClosure, *CSVSigClosure]
|
||||
|
||||
func ParseVtxoScript(scripts []string) (VtxoScript, error) {
|
||||
v := &TapscriptsVtxoScript{}
|
||||
|
||||
err := v.Decode(scripts)
|
||||
return v, err
|
||||
}
|
||||
|
||||
/*
|
||||
* DefaultVtxoScript is the default implementation of VTXO with 2 closures
|
||||
* - Owner and ASP (forfeit)
|
||||
* - Owner after t (unilateral exit)
|
||||
*/
|
||||
type DefaultVtxoScript struct {
|
||||
Owner *secp256k1.PublicKey
|
||||
Asp *secp256k1.PublicKey
|
||||
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
|
||||
func NewDefaultVtxoScript(owner, asp *secp256k1.PublicKey, exitDelay uint) *TapscriptsVtxoScript {
|
||||
return &TapscriptsVtxoScript{
|
||||
[]Closure{
|
||||
&CSVSigClosure{
|
||||
MultisigClosure: MultisigClosure{PubKeys: []*secp256k1.PublicKey{owner}},
|
||||
Seconds: exitDelay,
|
||||
},
|
||||
&MultisigClosure{PubKeys: []*secp256k1.PublicKey{owner, asp}},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
v.Owner = owner
|
||||
v.Asp = asp
|
||||
v.ExitDelay = exitDelay
|
||||
// TapscriptsVtxoScript represents a taproot script that contains a list of tapscript leaves
|
||||
// the key-path is always unspendable
|
||||
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
|
||||
}
|
||||
|
||||
func (v *DefaultVtxoScript) TapTree() (*secp256k1.PublicKey, elementsTapTree, error) {
|
||||
redeemClosure := &CSVSigClosure{
|
||||
Pubkey: v.Owner,
|
||||
Seconds: v.ExitDelay,
|
||||
func (v *TapscriptsVtxoScript) Validate(asp *secp256k1.PublicKey, minExitDelay uint) error {
|
||||
aspXonly := schnorr.SerializePubKey(asp)
|
||||
for _, forfeit := range v.ForfeitClosures() {
|
||||
// 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 {
|
||||
return nil, elementsTapTree{}, err
|
||||
if err == ErrNoExitLeaf {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
forfeitClosure := &MultisigClosure{
|
||||
Pubkey: v.Owner,
|
||||
AspPubkey: v.Asp,
|
||||
if smallestExit < minExitDelay {
|
||||
return fmt.Errorf("exit delay is too short")
|
||||
}
|
||||
|
||||
forfeitLeaf, err := forfeitClosure.Leaf()
|
||||
if err != nil {
|
||||
return nil, elementsTapTree{}, err
|
||||
return nil
|
||||
}
|
||||
|
||||
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(
|
||||
*redeemLeaf, *forfeitLeaf,
|
||||
)
|
||||
if smallest == math.MaxUint32 {
|
||||
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()
|
||||
|
||||
taprootKey := taproot.ComputeTaprootOutputKey(UnspendableKey(), root[:])
|
||||
|
||||
return taprootKey, elementsTapTree{tapTree}, nil
|
||||
@@ -111,9 +181,15 @@ func (b elementsTapTree) GetTaprootMerkleProof(leafhash chainhash.Hash) (*common
|
||||
return nil, err
|
||||
}
|
||||
|
||||
closure, err := DecodeClosure(proof.Script)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &common.TaprootMerkleProof{
|
||||
ControlBlock: controlBlockBytes,
|
||||
Script: proof.Script,
|
||||
WitnessSize: closure.WitnessSize(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ var (
|
||||
type TaprootMerkleProof struct {
|
||||
ControlBlock []byte
|
||||
Script []byte
|
||||
WitnessSize int
|
||||
}
|
||||
|
||||
// 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.
|
||||
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)
|
||||
ToDescriptor() string
|
||||
FromDescriptor(descriptor string) error
|
||||
Encode() ([]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)
|
||||
|
||||
@@ -89,7 +89,7 @@ func (o Outpoint) Equals(other Outpoint) bool {
|
||||
|
||||
type Input struct {
|
||||
Outpoint
|
||||
Descriptor string
|
||||
Tapscripts []string
|
||||
}
|
||||
|
||||
type AsyncPaymentInput struct {
|
||||
@@ -129,9 +129,9 @@ func (v Vtxo) Address(asp *secp256k1.PublicKey, net common.Network) (string, err
|
||||
return a.Encode()
|
||||
}
|
||||
|
||||
type DescriptorVtxo struct {
|
||||
type TapscriptsVtxo struct {
|
||||
Vtxo
|
||||
Descriptor string
|
||||
Tapscripts []string
|
||||
}
|
||||
|
||||
type Output struct {
|
||||
|
||||
@@ -144,7 +144,11 @@ func toProtoInput(i client.Input) *arkv1.Input {
|
||||
Txid: i.Txid,
|
||||
Vout: i.VOut,
|
||||
},
|
||||
Descriptor_: i.Descriptor,
|
||||
TaprootTree: &arkv1.Input_Tapscripts{
|
||||
Tapscripts: &arkv1.Tapscripts{
|
||||
Scripts: i.Tapscripts,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -118,7 +118,9 @@ func (a *restClient) RegisterInputsForNextRound(
|
||||
Txid: i.Txid,
|
||||
Vout: int64(i.VOut),
|
||||
},
|
||||
Descriptor: i.Descriptor,
|
||||
Tapscripts: &models.V1Tapscripts{
|
||||
Scripts: i.Tapscripts,
|
||||
},
|
||||
})
|
||||
}
|
||||
body := &models.V1RegisterInputsForNextRoundRequest{
|
||||
@@ -402,7 +404,9 @@ func (a *restClient) CreatePayment(
|
||||
Txid: i.Input.Txid,
|
||||
Vout: int64(i.VOut),
|
||||
},
|
||||
Descriptor: i.Input.Descriptor,
|
||||
Tapscripts: &models.V1Tapscripts{
|
||||
Scripts: i.Input.Tapscripts,
|
||||
},
|
||||
},
|
||||
ForfeitLeafHash: i.ForfeitLeafHash.String(),
|
||||
})
|
||||
|
||||
@@ -8,6 +8,7 @@ package models
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/go-openapi/errors"
|
||||
"github.com/go-openapi/strfmt"
|
||||
"github.com/go-openapi/swag"
|
||||
)
|
||||
@@ -22,15 +23,76 @@ type V1GetBoardingAddressResponse struct {
|
||||
|
||||
// descriptor
|
||||
Descriptor string `json:"descriptor,omitempty"`
|
||||
|
||||
// tapscripts
|
||||
Tapscripts *V1Tapscripts `json:"tapscripts,omitempty"`
|
||||
}
|
||||
|
||||
// Validate validates this v1 get boarding address response
|
||||
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
|
||||
}
|
||||
|
||||
// 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 {
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,9 @@ type V1Input struct {
|
||||
|
||||
// outpoint
|
||||
Outpoint *V1Outpoint `json:"outpoint,omitempty"`
|
||||
|
||||
// tapscripts
|
||||
Tapscripts *V1Tapscripts `json:"tapscripts,omitempty"`
|
||||
}
|
||||
|
||||
// Validate validates this v1 input
|
||||
@@ -33,6 +36,10 @@ func (m *V1Input) Validate(formats strfmt.Registry) error {
|
||||
res = append(res, err)
|
||||
}
|
||||
|
||||
if err := m.validateTapscripts(formats); err != nil {
|
||||
res = append(res, err)
|
||||
}
|
||||
|
||||
if len(res) > 0 {
|
||||
return errors.CompositeValidationError(res...)
|
||||
}
|
||||
@@ -58,6 +65,25 @@ func (m *V1Input) validateOutpoint(formats strfmt.Registry) error {
|
||||
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
|
||||
func (m *V1Input) ContextValidate(ctx context.Context, formats strfmt.Registry) error {
|
||||
var res []error
|
||||
@@ -66,6 +92,10 @@ func (m *V1Input) ContextValidate(ctx context.Context, formats strfmt.Registry)
|
||||
res = append(res, err)
|
||||
}
|
||||
|
||||
if err := m.contextValidateTapscripts(ctx, formats); err != nil {
|
||||
res = append(res, err)
|
||||
}
|
||||
|
||||
if len(res) > 0 {
|
||||
return errors.CompositeValidationError(res...)
|
||||
}
|
||||
@@ -93,6 +123,27 @@ func (m *V1Input) contextValidateOutpoint(ctx context.Context, formats strfmt.Re
|
||||
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
|
||||
func (m *V1Input) MarshalBinary() ([]byte, error) {
|
||||
if m == nil {
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
"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
|
||||
type V1OwnershipProof struct {
|
||||
|
||||
50
pkg/client-sdk/client/rest/service/models/v1_tapscripts.go
Normal file
50
pkg/client-sdk/client/rest/service/models/v1_tapscripts.go
Normal 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
|
||||
}
|
||||
@@ -22,7 +22,6 @@ import (
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/btcsuite/btcwallet/waddrmgr"
|
||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"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)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -532,9 +531,9 @@ func (a *covenantArkClient) CollaborativeRedeem(
|
||||
}
|
||||
|
||||
if vtxoAddr == offchainAddr.Address {
|
||||
vtxos = append(vtxos, client.DescriptorVtxo{
|
||||
vtxos = append(vtxos, client.TapscriptsVtxo{
|
||||
Vtxo: v,
|
||||
Descriptor: offchainAddr.Descriptor,
|
||||
Tapscripts: offchainAddr.Tapscripts,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -572,7 +571,7 @@ func (a *covenantArkClient) CollaborativeRedeem(
|
||||
Txid: coin.Txid,
|
||||
VOut: coin.VOut,
|
||||
},
|
||||
Descriptor: coin.Descriptor,
|
||||
Tapscripts: coin.Tapscripts,
|
||||
})
|
||||
}
|
||||
for _, coin := range selectedBoardingUtxos {
|
||||
@@ -581,7 +580,7 @@ func (a *covenantArkClient) CollaborativeRedeem(
|
||||
Txid: coin.Txid,
|
||||
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),
|
||||
Amount: vout.Amount,
|
||||
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)
|
||||
|
||||
for _, addr := range boardingAddrs {
|
||||
boardingScript, err := tree.ParseVtxoScript(addr.Descriptor)
|
||||
boardingScript, err := tree.ParseVtxoScript(addr.Tapscripts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var boardingTimeout uint
|
||||
|
||||
if defaultVtxo, ok := boardingScript.(*tree.DefaultVtxoScript); ok {
|
||||
boardingTimeout = defaultVtxo.ExitDelay
|
||||
} else {
|
||||
return nil, fmt.Errorf("unsupported boarding descriptor: %s", addr.Descriptor)
|
||||
boardingTimeout, err := boardingScript.SmallestExitDelay()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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) {
|
||||
continue
|
||||
}
|
||||
@@ -929,7 +925,7 @@ func (a *covenantArkClient) sendOffchain(
|
||||
return "", fmt.Errorf("no offchain addresses found")
|
||||
}
|
||||
|
||||
vtxos := make([]client.DescriptorVtxo, 0)
|
||||
vtxos := make([]client.TapscriptsVtxo, 0)
|
||||
|
||||
spendableVtxos, err := a.getVtxos(ctx, withExpiryCoinselect, nil)
|
||||
if err != nil {
|
||||
@@ -944,9 +940,9 @@ func (a *covenantArkClient) sendOffchain(
|
||||
}
|
||||
|
||||
if vtxoAddr == offchainAddr.Address {
|
||||
vtxos = append(vtxos, client.DescriptorVtxo{
|
||||
vtxos = append(vtxos, client.TapscriptsVtxo{
|
||||
Vtxo: v,
|
||||
Descriptor: offchainAddr.Descriptor,
|
||||
Tapscripts: offchainAddr.Tapscripts,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -958,7 +954,7 @@ func (a *covenantArkClient) sendOffchain(
|
||||
}
|
||||
|
||||
var selectedBoardingCoins []types.Utxo
|
||||
var selectedCoins []client.DescriptorVtxo
|
||||
var selectedCoins []client.TapscriptsVtxo
|
||||
var changeAmount uint64
|
||||
|
||||
// if no receivers, self send all selected coins
|
||||
@@ -1009,7 +1005,7 @@ func (a *covenantArkClient) sendOffchain(
|
||||
Txid: coin.Txid,
|
||||
VOut: coin.VOut,
|
||||
},
|
||||
Descriptor: coin.Descriptor,
|
||||
Tapscripts: coin.Tapscripts,
|
||||
})
|
||||
}
|
||||
for _, coin := range selectedBoardingCoins {
|
||||
@@ -1018,7 +1014,7 @@ func (a *covenantArkClient) sendOffchain(
|
||||
Txid: coin.Txid,
|
||||
VOut: coin.VOut,
|
||||
},
|
||||
Descriptor: coin.Descriptor,
|
||||
Tapscripts: coin.Tapscripts,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1057,19 +1053,33 @@ func (a *covenantArkClient) addInputs(
|
||||
return err
|
||||
}
|
||||
|
||||
vtxoScript, err := tree.ParseVtxoScript(offchain.Descriptor)
|
||||
vtxoScript, err := tree.ParseVtxoScript(offchain.Tapscripts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var userPubkey, aspPubkey *secp256k1.PublicKey
|
||||
forfeitClosure := vtxoScript.ForfeitClosures()[0]
|
||||
|
||||
switch s := vtxoScript.(type) {
|
||||
case *tree.DefaultVtxoScript:
|
||||
userPubkey = s.Owner
|
||||
aspPubkey = s.Asp
|
||||
default:
|
||||
return fmt.Errorf("unsupported vtxo script: %T", s)
|
||||
forfeitScript, err := forfeitClosure.Script()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -1088,37 +1098,6 @@ func (a *covenantArkClient) addInputs(
|
||||
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
|
||||
|
||||
if err := updater.AddInTapLeafScript(
|
||||
@@ -1138,7 +1117,7 @@ func (a *covenantArkClient) addInputs(
|
||||
func (a *covenantArkClient) handleRoundStream(
|
||||
ctx context.Context,
|
||||
paymentID string,
|
||||
vtxosToSign []client.DescriptorVtxo,
|
||||
vtxosToSign []client.TapscriptsVtxo,
|
||||
boardingUtxos []types.Utxo,
|
||||
receivers []client.Output,
|
||||
) (string, error) {
|
||||
@@ -1202,7 +1181,7 @@ func (a *covenantArkClient) handleRoundStream(
|
||||
func (a *covenantArkClient) handleRoundFinalization(
|
||||
ctx context.Context,
|
||||
event client.RoundFinalizationEvent,
|
||||
vtxos []client.DescriptorVtxo,
|
||||
vtxos []client.TapscriptsVtxo,
|
||||
boardingUtxos []types.Utxo,
|
||||
receivers []client.Output,
|
||||
) (signedForfeits []string, signedRoundTx string, err error) {
|
||||
@@ -1233,28 +1212,20 @@ func (a *covenantArkClient) handleRoundFinalization(
|
||||
}
|
||||
|
||||
for _, boardingUtxo := range boardingUtxos {
|
||||
boardingVtxoScript, err := tree.ParseVtxoScript(boardingUtxo.Descriptor)
|
||||
boardingVtxoScript, err := tree.ParseVtxoScript(boardingUtxo.Tapscripts)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
var forfeitClosure tree.Closure
|
||||
forfeitClosure := boardingVtxoScript.ForfeitClosures()[0]
|
||||
|
||||
switch s := boardingVtxoScript.(type) {
|
||||
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()
|
||||
forfeitScript, err := forfeitClosure.Script()
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
forfeitLeaf := taproot.NewBaseTapElementsLeaf(forfeitScript)
|
||||
|
||||
_, taprootTree, err := boardingVtxoScript.TapTree()
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
@@ -1430,7 +1401,7 @@ func (a *covenantArkClient) validateOffChainReceiver(
|
||||
|
||||
func (a *covenantArkClient) createAndSignForfeits(
|
||||
ctx context.Context,
|
||||
vtxosToSign []client.DescriptorVtxo,
|
||||
vtxosToSign []client.TapscriptsVtxo,
|
||||
connectors []string,
|
||||
feeRate chainfee.SatPerKVByte,
|
||||
) ([]string, error) {
|
||||
@@ -1452,7 +1423,7 @@ func (a *covenantArkClient) createAndSignForfeits(
|
||||
}
|
||||
|
||||
for _, vtxo := range vtxosToSign {
|
||||
vtxoScript, err := tree.ParseVtxoScript(vtxo.Descriptor)
|
||||
vtxoScript, err := tree.ParseVtxoScript(vtxo.Tapscripts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1472,25 +1443,15 @@ func (a *covenantArkClient) createAndSignForfeits(
|
||||
TxIndex: vtxo.VOut,
|
||||
}
|
||||
|
||||
var forfeitClosure tree.Closure
|
||||
var witnessSize int
|
||||
forfeitClosure := vtxoScript.ForfeitClosures()[0]
|
||||
|
||||
switch s := vtxoScript.(type) {
|
||||
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()
|
||||
forfeitScript, err := forfeitClosure.Script()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
forfeitLeaf := taproot.NewBaseTapElementsLeaf(forfeitScript)
|
||||
|
||||
leafProof, err := vtxoTapTree.GetTaprootMerkleProof(forfeitLeaf.TapHash())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -1512,7 +1473,7 @@ func (a *covenantArkClient) createAndSignForfeits(
|
||||
RevealedScript: leafProof.Script,
|
||||
ControlBlock: &ctrlBlock.ControlBlock,
|
||||
},
|
||||
witnessSize,
|
||||
forfeitClosure.WitnessSize(),
|
||||
txscript.WitnessV0PubKeyHashTy,
|
||||
)
|
||||
if err != nil {
|
||||
@@ -1567,19 +1528,14 @@ func (a *covenantArkClient) coinSelectOnchain(
|
||||
|
||||
fetchedUtxos := make([]types.Utxo, 0)
|
||||
for _, addr := range boardingAddrs {
|
||||
boardingDescriptor := addr.Descriptor
|
||||
|
||||
boardingScript, err := tree.ParseVtxoScript(boardingDescriptor)
|
||||
boardingScript, err := tree.ParseVtxoScript(addr.Tapscripts)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
var boardingTimeout uint
|
||||
|
||||
if defaultVtxo, ok := boardingScript.(*tree.DefaultVtxoScript); ok {
|
||||
boardingTimeout = defaultVtxo.ExitDelay
|
||||
} else {
|
||||
return nil, 0, fmt.Errorf("unsupported boarding descriptor: %s", boardingDescriptor)
|
||||
boardingTimeout, err := boardingScript.SmallestExitDelay()
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
utxos, err := a.explorer.GetUtxos(addr.Address)
|
||||
@@ -1588,7 +1544,7 @@ func (a *covenantArkClient) coinSelectOnchain(
|
||||
}
|
||||
|
||||
for _, utxo := range utxos {
|
||||
u := utxo.ToUtxo(boardingTimeout, addr.Descriptor)
|
||||
u := utxo.ToUtxo(boardingTimeout, addr.Tapscripts)
|
||||
if u.SpendableAt.Before(now) {
|
||||
fetchedUtxos = append(fetchedUtxos, u)
|
||||
}
|
||||
@@ -1624,7 +1580,7 @@ func (a *covenantArkClient) coinSelectOnchain(
|
||||
}
|
||||
|
||||
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) {
|
||||
fetchedUtxos = append(fetchedUtxos, u)
|
||||
}
|
||||
|
||||
@@ -857,7 +857,7 @@ func (a *covenantlessArkClient) CollaborativeRedeem(
|
||||
},
|
||||
}
|
||||
|
||||
vtxos := make([]client.DescriptorVtxo, 0)
|
||||
vtxos := make([]client.TapscriptsVtxo, 0)
|
||||
spendableVtxos, err := a.getVtxos(ctx, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -871,9 +871,9 @@ func (a *covenantlessArkClient) CollaborativeRedeem(
|
||||
}
|
||||
|
||||
if vtxoAddr == offchainAddr.Address {
|
||||
vtxos = append(vtxos, client.DescriptorVtxo{
|
||||
vtxos = append(vtxos, client.TapscriptsVtxo{
|
||||
Vtxo: v,
|
||||
Descriptor: offchainAddr.Descriptor,
|
||||
Tapscripts: offchainAddr.Tapscripts,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -911,7 +911,7 @@ func (a *covenantlessArkClient) CollaborativeRedeem(
|
||||
Txid: coin.Txid,
|
||||
VOut: coin.VOut,
|
||||
},
|
||||
Descriptor: coin.Descriptor,
|
||||
Tapscripts: coin.Tapscripts,
|
||||
})
|
||||
}
|
||||
for _, coin := range selectedBoardingCoins {
|
||||
@@ -920,7 +920,7 @@ func (a *covenantlessArkClient) CollaborativeRedeem(
|
||||
Txid: coin.Txid,
|
||||
VOut: coin.VOut,
|
||||
},
|
||||
Descriptor: coin.Descriptor,
|
||||
Tapscripts: coin.Tapscripts,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1004,7 +1004,7 @@ func (a *covenantlessArkClient) SendAsync(
|
||||
sumOfReceivers += receiver.Amount()
|
||||
}
|
||||
|
||||
vtxos := make([]client.DescriptorVtxo, 0)
|
||||
vtxos := make([]client.TapscriptsVtxo, 0)
|
||||
opts := &CoinSelectOptions{
|
||||
WithExpirySorting: withExpiryCoinselect,
|
||||
}
|
||||
@@ -1021,9 +1021,9 @@ func (a *covenantlessArkClient) SendAsync(
|
||||
}
|
||||
|
||||
if vtxoAddr == offchainAddr.Address {
|
||||
vtxos = append(vtxos, client.DescriptorVtxo{
|
||||
vtxos = append(vtxos, client.TapscriptsVtxo{
|
||||
Vtxo: v,
|
||||
Descriptor: offchainAddr.Descriptor,
|
||||
Tapscripts: offchainAddr.Tapscripts,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1048,35 +1048,27 @@ func (a *covenantlessArkClient) SendAsync(
|
||||
inputs := make([]client.AsyncPaymentInput, 0, len(selectedCoins))
|
||||
|
||||
for _, coin := range selectedCoins {
|
||||
vtxoScript, err := bitcointree.ParseVtxoScript(coin.Descriptor)
|
||||
vtxoScript, err := bitcointree.ParseVtxoScript(coin.Tapscripts)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var forfeitClosure bitcointree.Closure
|
||||
forfeitClosure := vtxoScript.ForfeitClosures()[0]
|
||||
|
||||
switch s := vtxoScript.(type) {
|
||||
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()
|
||||
forfeitScript, err := forfeitClosure.Script()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
forfeitLeaf := txscript.NewBaseTapLeaf(forfeitScript)
|
||||
|
||||
inputs = append(inputs, client.AsyncPaymentInput{
|
||||
Input: client.Input{
|
||||
Outpoint: client.Outpoint{
|
||||
Txid: coin.Txid,
|
||||
VOut: coin.VOut,
|
||||
},
|
||||
Descriptor: coin.Descriptor,
|
||||
Tapscripts: coin.Tapscripts,
|
||||
},
|
||||
ForfeitLeafHash: forfeitLeaf.TapHash(),
|
||||
})
|
||||
@@ -1158,7 +1150,7 @@ func (a *covenantlessArkClient) SetNostrNotificationRecipient(ctx context.Contex
|
||||
return err
|
||||
}
|
||||
|
||||
descriptorVtxos := make([]client.DescriptorVtxo, 0)
|
||||
descriptorVtxos := make([]client.TapscriptsVtxo, 0)
|
||||
for _, offchainAddr := range offchainAddrs {
|
||||
for _, vtxo := range spendableVtxos {
|
||||
vtxoAddr, err := vtxo.Address(a.AspPubkey, a.Network)
|
||||
@@ -1167,9 +1159,9 @@ func (a *covenantlessArkClient) SetNostrNotificationRecipient(ctx context.Contex
|
||||
}
|
||||
|
||||
if vtxoAddr == offchainAddr.Address {
|
||||
descriptorVtxos = append(descriptorVtxos, client.DescriptorVtxo{
|
||||
descriptorVtxos = append(descriptorVtxos, client.TapscriptsVtxo{
|
||||
Vtxo: vtxo,
|
||||
Descriptor: offchainAddr.Descriptor,
|
||||
Tapscripts: offchainAddr.Tapscripts,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1187,35 +1179,24 @@ func (a *covenantlessArkClient) SetNostrNotificationRecipient(ctx context.Contex
|
||||
}
|
||||
|
||||
// validate the vtxo script type
|
||||
vtxoScript, err := bitcointree.ParseVtxoScript(v.Descriptor)
|
||||
vtxoScript, err := bitcointree.ParseVtxoScript(v.Tapscripts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var forfeitClosure bitcointree.Closure
|
||||
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)
|
||||
}
|
||||
forfeitClosure := vtxoScript.ForfeitClosures()[0]
|
||||
|
||||
_, tapTree, err := vtxoScript.TapTree()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
forfeitLeaf, err := forfeitClosure.Leaf()
|
||||
forfeitScript, err := forfeitClosure.Script()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
forfeitLeaf := txscript.NewBaseTapLeaf(forfeitScript)
|
||||
merkleProof, err := tapTree.GetTaprootMerkleProof(forfeitLeaf.TapHash())
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -1236,7 +1217,7 @@ func (a *covenantlessArkClient) SetNostrNotificationRecipient(ctx context.Contex
|
||||
outpointBytes := append(txhash[:], voutBytes...)
|
||||
sigMsg := sha256.Sum256(outpointBytes)
|
||||
|
||||
sig, err := a.wallet.SignMessage(ctx, sigMsg[:], signingPubkey)
|
||||
sig, err := a.wallet.SignMessage(ctx, sigMsg[:])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1434,7 +1415,7 @@ func (a *covenantlessArkClient) sendOffchain(
|
||||
return "", fmt.Errorf("no offchain addresses found")
|
||||
}
|
||||
|
||||
vtxos := make([]client.DescriptorVtxo, 0)
|
||||
vtxos := make([]client.TapscriptsVtxo, 0)
|
||||
opts := &CoinSelectOptions{
|
||||
WithExpirySorting: withExpiryCoinselect}
|
||||
spendableVtxos, err := a.getVtxos(ctx, opts)
|
||||
@@ -1450,9 +1431,9 @@ func (a *covenantlessArkClient) sendOffchain(
|
||||
}
|
||||
|
||||
if vtxoAddr == offchainAddr.Address {
|
||||
vtxos = append(vtxos, client.DescriptorVtxo{
|
||||
vtxos = append(vtxos, client.TapscriptsVtxo{
|
||||
Vtxo: v,
|
||||
Descriptor: offchainAddr.Descriptor,
|
||||
Tapscripts: offchainAddr.Tapscripts,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1464,7 +1445,7 @@ func (a *covenantlessArkClient) sendOffchain(
|
||||
}
|
||||
|
||||
var selectedBoardingCoins []types.Utxo
|
||||
var selectedCoins []client.DescriptorVtxo
|
||||
var selectedCoins []client.TapscriptsVtxo
|
||||
var changeAmount uint64
|
||||
|
||||
// if no receivers, self send all selected coins
|
||||
@@ -1513,7 +1494,7 @@ func (a *covenantlessArkClient) sendOffchain(
|
||||
Txid: coin.Txid,
|
||||
VOut: coin.VOut,
|
||||
},
|
||||
Descriptor: coin.Descriptor,
|
||||
Tapscripts: coin.Tapscripts,
|
||||
})
|
||||
}
|
||||
for _, boardingUtxo := range selectedBoardingCoins {
|
||||
@@ -1522,7 +1503,7 @@ func (a *covenantlessArkClient) sendOffchain(
|
||||
Txid: boardingUtxo.Txid,
|
||||
VOut: boardingUtxo.VOut,
|
||||
},
|
||||
Descriptor: boardingUtxo.Descriptor,
|
||||
Tapscripts: boardingUtxo.Tapscripts,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1567,21 +1548,11 @@ func (a *covenantlessArkClient) addInputs(
|
||||
return err
|
||||
}
|
||||
|
||||
vtxoScript, err := tree.ParseVtxoScript(offchain.Descriptor)
|
||||
vtxoScript, err := bitcointree.ParseVtxoScript(offchain.Tapscripts)
|
||||
if err != nil {
|
||||
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 {
|
||||
previousHash, err := chainhash.NewHashFromStr(utxo.Txid)
|
||||
if err != nil {
|
||||
@@ -1601,18 +1572,14 @@ func (a *covenantlessArkClient) addInputs(
|
||||
Sequence: sequence,
|
||||
})
|
||||
|
||||
vtxoScript := &bitcointree.DefaultVtxoScript{
|
||||
Owner: userPubkey,
|
||||
Asp: aspPubkey,
|
||||
ExitDelay: utxo.Delay,
|
||||
exitClosures := vtxoScript.ExitClosures()
|
||||
if len(exitClosures) <= 0 {
|
||||
return fmt.Errorf("no exit closures found")
|
||||
}
|
||||
|
||||
exitClosure := &bitcointree.CSVSigClosure{
|
||||
Pubkey: userPubkey,
|
||||
Seconds: uint(utxo.Delay),
|
||||
}
|
||||
exitClosure := exitClosures[0]
|
||||
|
||||
exitLeaf, err := exitClosure.Leaf()
|
||||
exitScript, err := exitClosure.Script()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1622,6 +1589,7 @@ func (a *covenantlessArkClient) addInputs(
|
||||
return err
|
||||
}
|
||||
|
||||
exitLeaf := txscript.NewBaseTapLeaf(exitScript)
|
||||
leafProof, err := taprootTree.GetTaprootMerkleProof(exitLeaf.TapHash())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get taproot merkle proof: %s", err)
|
||||
@@ -1644,7 +1612,7 @@ func (a *covenantlessArkClient) addInputs(
|
||||
func (a *covenantlessArkClient) handleRoundStream(
|
||||
ctx context.Context,
|
||||
paymentID string,
|
||||
vtxosToSign []client.DescriptorVtxo,
|
||||
vtxosToSign []client.TapscriptsVtxo,
|
||||
boardingUtxos []types.Utxo,
|
||||
receivers []client.Output,
|
||||
roundEphemeralKey *secp256k1.PrivateKey,
|
||||
@@ -1763,12 +1731,12 @@ func (a *covenantlessArkClient) handleRoundStream(
|
||||
func (a *covenantlessArkClient) handleRoundSigningStarted(
|
||||
ctx context.Context, ephemeralKey *secp256k1.PrivateKey, event client.RoundSigningStartedEvent,
|
||||
) (signerSession bitcointree.SignerSession, err error) {
|
||||
sweepClosure := bitcointree.CSVSigClosure{
|
||||
Pubkey: a.AspPubkey,
|
||||
Seconds: uint(a.RoundLifetime),
|
||||
sweepClosure := tree.CSVSigClosure{
|
||||
MultisigClosure: tree.MultisigClosure{PubKeys: []*secp256k1.PublicKey{a.AspPubkey}},
|
||||
Seconds: uint(a.RoundLifetime),
|
||||
}
|
||||
|
||||
sweepTapLeaf, err := sweepClosure.Leaf()
|
||||
script, err := sweepClosure.Script()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -1781,7 +1749,8 @@ func (a *covenantlessArkClient) handleRoundSigningStarted(
|
||||
sharedOutput := roundTx.UnsignedTx.TxOut[0]
|
||||
sharedOutputValue := sharedOutput.Value
|
||||
|
||||
sweepTapTree := txscript.AssembleTaprootScriptTree(*sweepTapLeaf)
|
||||
sweepTapLeaf := txscript.NewBaseTapLeaf(script)
|
||||
sweepTapTree := txscript.AssembleTaprootScriptTree(sweepTapLeaf)
|
||||
root := sweepTapTree.RootNode.TapHash()
|
||||
|
||||
signerSession = bitcointree.NewTreeSignerSession(
|
||||
@@ -1838,7 +1807,7 @@ func (a *covenantlessArkClient) handleRoundSigningNoncesGenerated(
|
||||
func (a *covenantlessArkClient) handleRoundFinalization(
|
||||
ctx context.Context,
|
||||
event client.RoundFinalizationEvent,
|
||||
vtxos []client.DescriptorVtxo,
|
||||
vtxos []client.TapscriptsVtxo,
|
||||
boardingUtxos []types.Utxo,
|
||||
receivers []client.Output,
|
||||
) ([]string, string, error) {
|
||||
@@ -1870,27 +1839,20 @@ func (a *covenantlessArkClient) handleRoundFinalization(
|
||||
}
|
||||
|
||||
for _, boardingUtxo := range boardingUtxos {
|
||||
boardingVtxoScript, err := bitcointree.ParseVtxoScript(boardingUtxo.Descriptor)
|
||||
boardingVtxoScript, err := bitcointree.ParseVtxoScript(boardingUtxo.Tapscripts)
|
||||
if err != nil {
|
||||
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
|
||||
forfeitClosure := &bitcointree.MultisigClosure{
|
||||
Pubkey: myPubkey,
|
||||
AspPubkey: a.AspPubkey,
|
||||
forfeitClosures := boardingVtxoScript.ForfeitClosures()
|
||||
if len(forfeitClosures) <= 0 {
|
||||
return nil, "", fmt.Errorf("no forfeit closures found")
|
||||
}
|
||||
|
||||
forfeitLeaf, err := forfeitClosure.Leaf()
|
||||
forfeitClosure := forfeitClosures[0]
|
||||
|
||||
forfeitScript, err := forfeitClosure.Script()
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
@@ -1900,6 +1862,7 @@ func (a *covenantlessArkClient) handleRoundFinalization(
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
forfeitLeaf := txscript.NewBaseTapLeaf(forfeitScript)
|
||||
forfeitProof, err := taprootTree.GetTaprootMerkleProof(forfeitLeaf.TapHash())
|
||||
if err != nil {
|
||||
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(
|
||||
ctx context.Context,
|
||||
vtxosToSign []client.DescriptorVtxo,
|
||||
vtxosToSign []client.TapscriptsVtxo,
|
||||
connectors []string,
|
||||
feeRate chainfee.SatPerKVByte,
|
||||
) ([]string, error) {
|
||||
@@ -2102,7 +2065,7 @@ func (a *covenantlessArkClient) createAndSignForfeits(
|
||||
}
|
||||
|
||||
for _, vtxo := range vtxosToSign {
|
||||
vtxoScript, err := bitcointree.ParseVtxoScript(vtxo.Descriptor)
|
||||
vtxoScript, err := bitcointree.ParseVtxoScript(vtxo.Tapscripts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -2127,25 +2090,19 @@ func (a *covenantlessArkClient) createAndSignForfeits(
|
||||
Index: vtxo.VOut,
|
||||
}
|
||||
|
||||
var forfeitClosure bitcointree.Closure
|
||||
var witnessSize int
|
||||
|
||||
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)
|
||||
forfeitClosures := vtxoScript.ForfeitClosures()
|
||||
if len(forfeitClosures) <= 0 {
|
||||
return nil, fmt.Errorf("no forfeit closures found")
|
||||
}
|
||||
|
||||
forfeitLeaf, err := forfeitClosure.Leaf()
|
||||
forfeitClosure := forfeitClosures[0]
|
||||
|
||||
forfeitScript, err := forfeitClosure.Script()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
forfeitLeaf := txscript.NewBaseTapLeaf(forfeitScript)
|
||||
leafProof, err := vtxoTapTree.GetTaprootMerkleProof(forfeitLeaf.TapHash())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -2168,7 +2125,7 @@ func (a *covenantlessArkClient) createAndSignForfeits(
|
||||
RevealedScript: leafProof.Script,
|
||||
ControlBlock: ctrlBlock,
|
||||
},
|
||||
witnessSize,
|
||||
forfeitClosure.WitnessSize(),
|
||||
parsedScript.Class(),
|
||||
)
|
||||
if err != nil {
|
||||
@@ -2221,25 +2178,23 @@ func (a *covenantlessArkClient) coinSelectOnchain(
|
||||
|
||||
fetchedUtxos := make([]types.Utxo, 0)
|
||||
for _, addr := range boardingAddrs {
|
||||
boardingScript, err := bitcointree.ParseVtxoScript(addr.Descriptor)
|
||||
boardingScript, err := bitcointree.ParseVtxoScript(addr.Tapscripts)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
var boardingTimeout uint
|
||||
|
||||
if defaultVtxo, ok := boardingScript.(*bitcointree.DefaultVtxoScript); ok {
|
||||
boardingTimeout = defaultVtxo.ExitDelay
|
||||
} else {
|
||||
return nil, 0, fmt.Errorf("unsupported boarding descriptor: %s", addr.Descriptor)
|
||||
boardingTimeout, err := boardingScript.SmallestExitDelay()
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
utxos, err := a.explorer.GetUtxos(addr.Address)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
for _, utxo := range utxos {
|
||||
u := utxo.ToUtxo(boardingTimeout, addr.Descriptor)
|
||||
u := utxo.ToUtxo(boardingTimeout, addr.Tapscripts)
|
||||
if u.SpendableAt.Before(now) {
|
||||
fetchedUtxos = append(fetchedUtxos, u)
|
||||
}
|
||||
@@ -2275,7 +2230,7 @@ func (a *covenantlessArkClient) coinSelectOnchain(
|
||||
}
|
||||
|
||||
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) {
|
||||
fetchedUtxos = append(fetchedUtxos, u)
|
||||
}
|
||||
@@ -2407,7 +2362,7 @@ func (a *covenantlessArkClient) getAllBoardingUtxos(
|
||||
VOut: uint32(i),
|
||||
Amount: vout.Amount,
|
||||
CreatedAt: createdAt,
|
||||
Descriptor: addr.Descriptor,
|
||||
Tapscripts: addr.Tapscripts,
|
||||
Spent: spent,
|
||||
})
|
||||
}
|
||||
@@ -2427,17 +2382,14 @@ func (a *covenantlessArkClient) getClaimableBoardingUtxos(ctx context.Context, o
|
||||
claimable := make([]types.Utxo, 0)
|
||||
|
||||
for _, addr := range boardingAddrs {
|
||||
boardingScript, err := bitcointree.ParseVtxoScript(addr.Descriptor)
|
||||
boardingScript, err := bitcointree.ParseVtxoScript(addr.Tapscripts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var boardingTimeout uint
|
||||
|
||||
if defaultVtxo, ok := boardingScript.(*bitcointree.DefaultVtxoScript); ok {
|
||||
boardingTimeout = defaultVtxo.ExitDelay
|
||||
} else {
|
||||
return nil, fmt.Errorf("unsupported boarding descriptor: %s", addr.Descriptor)
|
||||
boardingTimeout, err := boardingScript.SmallestExitDelay()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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) {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -53,8 +53,8 @@ type SpentStatus struct {
|
||||
SpentBy string `json:"txid,omitempty"`
|
||||
}
|
||||
|
||||
func (e ExplorerUtxo) ToUtxo(delay uint, descriptor string) types.Utxo {
|
||||
return newUtxo(e, delay, descriptor)
|
||||
func (e ExplorerUtxo) ToUtxo(delay uint, tapscripts []string) types.Utxo {
|
||||
return newUtxo(e, delay, tapscripts)
|
||||
}
|
||||
|
||||
type Explorer interface {
|
||||
@@ -415,7 +415,7 @@ func parseBitcoinTx(txStr string) (string, string, error) {
|
||||
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
|
||||
createdAt := time.Unix(utxoTime, 0)
|
||||
if utxoTime == 0 {
|
||||
@@ -431,6 +431,6 @@ func newUtxo(explorerUtxo ExplorerUtxo, delay uint, descriptor string) types.Utx
|
||||
Delay: delay,
|
||||
SpendableAt: time.Unix(utxoTime, 0).Add(time.Duration(delay) * time.Second),
|
||||
CreatedAt: createdAt,
|
||||
Descriptor: descriptor,
|
||||
Tapscripts: tapscripts,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,12 +24,12 @@ import (
|
||||
|
||||
func CoinSelect(
|
||||
boardingUtxos []types.Utxo,
|
||||
vtxos []client.DescriptorVtxo,
|
||||
vtxos []client.TapscriptsVtxo,
|
||||
amount,
|
||||
dust uint64,
|
||||
sortByExpirationTime bool,
|
||||
) ([]types.Utxo, []client.DescriptorVtxo, uint64, error) {
|
||||
selected, notSelected := make([]client.DescriptorVtxo, 0), make([]client.DescriptorVtxo, 0)
|
||||
) ([]types.Utxo, []client.TapscriptsVtxo, uint64, error) {
|
||||
selected, notSelected := make([]client.TapscriptsVtxo, 0), make([]client.TapscriptsVtxo, 0)
|
||||
selectedBoarding, notSelectedBoarding := make([]types.Utxo, 0), make([]types.Utxo, 0)
|
||||
selectedAmount := uint64(0)
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ark-network/ark/common/bitcointree"
|
||||
"github.com/ark-network/ark/common/tree"
|
||||
"github.com/ark-network/ark/pkg/client-sdk/client"
|
||||
"github.com/ark-network/ark/pkg/client-sdk/explorer"
|
||||
@@ -172,7 +171,7 @@ func findCovenantlessSweepClosure(
|
||||
var seconds uint
|
||||
var sweepClosure *txscript.TapLeaf
|
||||
for _, tapLeaf := range tx.Inputs[0].TaprootLeafScript {
|
||||
closure := &bitcointree.CSVSigClosure{}
|
||||
closure := &tree.CSVSigClosure{}
|
||||
valid, err := closure.Decode(tapLeaf.Script)
|
||||
if err != nil {
|
||||
continue
|
||||
|
||||
@@ -113,7 +113,7 @@ type Utxo struct {
|
||||
Delay uint
|
||||
SpendableAt time.Time
|
||||
CreatedAt time.Time
|
||||
Descriptor string
|
||||
Tapscripts []string
|
||||
Spent bool
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
|
||||
"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/pkg/client-sdk/explorer"
|
||||
"github.com/ark-network/ark/pkg/client-sdk/internal/utils"
|
||||
"github.com/ark-network/ark/pkg/client-sdk/types"
|
||||
@@ -43,7 +44,7 @@ func NewBitcoinWallet(
|
||||
|
||||
func (w *bitcoinWallet) GetAddresses(
|
||||
ctx context.Context,
|
||||
) ([]wallet.DescriptorAddress, []wallet.DescriptorAddress, []wallet.DescriptorAddress, error) {
|
||||
) ([]wallet.TapscriptsAddress, []wallet.TapscriptsAddress, []wallet.TapscriptsAddress, error) {
|
||||
offchainAddr, boardingAddr, err := w.getAddress(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
@@ -69,21 +70,21 @@ func (w *bitcoinWallet) GetAddresses(
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
offchainAddrs := []wallet.DescriptorAddress{
|
||||
offchainAddrs := []wallet.TapscriptsAddress{
|
||||
{
|
||||
Descriptor: offchainAddr.Descriptor,
|
||||
Tapscripts: offchainAddr.Tapscripts,
|
||||
Address: encodedOffchainAddr,
|
||||
},
|
||||
}
|
||||
boardingAddrs := []wallet.DescriptorAddress{
|
||||
boardingAddrs := []wallet.TapscriptsAddress{
|
||||
{
|
||||
Descriptor: boardingAddr.Descriptor,
|
||||
Tapscripts: boardingAddr.Tapscripts,
|
||||
Address: boardingAddr.Address,
|
||||
},
|
||||
}
|
||||
redemptionAddrs := []wallet.DescriptorAddress{
|
||||
redemptionAddrs := []wallet.TapscriptsAddress{
|
||||
{
|
||||
Descriptor: offchainAddr.Descriptor,
|
||||
Tapscripts: offchainAddr.Tapscripts,
|
||||
Address: redemptionAddr.EncodeAddress(),
|
||||
},
|
||||
}
|
||||
@@ -92,7 +93,7 @@ func (w *bitcoinWallet) GetAddresses(
|
||||
|
||||
func (w *bitcoinWallet) NewAddress(
|
||||
ctx context.Context, _ bool,
|
||||
) (*wallet.DescriptorAddress, *wallet.DescriptorAddress, error) {
|
||||
) (*wallet.TapscriptsAddress, *wallet.TapscriptsAddress, error) {
|
||||
offchainAddr, boardingAddr, err := w.getAddress(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
@@ -103,34 +104,34 @@ func (w *bitcoinWallet) NewAddress(
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return &wallet.DescriptorAddress{
|
||||
Descriptor: offchainAddr.Descriptor,
|
||||
return &wallet.TapscriptsAddress{
|
||||
Tapscripts: offchainAddr.Tapscripts,
|
||||
Address: encodedOffchainAddr,
|
||||
}, boardingAddr, nil
|
||||
}
|
||||
|
||||
func (w *bitcoinWallet) NewAddresses(
|
||||
ctx context.Context, _ bool, num int,
|
||||
) ([]wallet.DescriptorAddress, []wallet.DescriptorAddress, error) {
|
||||
) ([]wallet.TapscriptsAddress, []wallet.TapscriptsAddress, error) {
|
||||
offchainAddr, boardingAddr, err := w.getAddress(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
offchainAddrs := make([]wallet.DescriptorAddress, 0, num)
|
||||
boardingAddrs := make([]wallet.DescriptorAddress, 0, num)
|
||||
offchainAddrs := make([]wallet.TapscriptsAddress, 0, num)
|
||||
boardingAddrs := make([]wallet.TapscriptsAddress, 0, num)
|
||||
for i := 0; i < num; i++ {
|
||||
encodedOffchainAddr, err := offchainAddr.Address.Encode()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
offchainAddrs = append(offchainAddrs, wallet.DescriptorAddress{
|
||||
Descriptor: offchainAddr.Descriptor,
|
||||
offchainAddrs = append(offchainAddrs, wallet.TapscriptsAddress{
|
||||
Tapscripts: offchainAddr.Tapscripts,
|
||||
Address: encodedOffchainAddr,
|
||||
})
|
||||
boardingAddrs = append(boardingAddrs, wallet.DescriptorAddress{
|
||||
Descriptor: boardingAddr.Descriptor,
|
||||
boardingAddrs = append(boardingAddrs, wallet.TapscriptsAddress{
|
||||
Tapscripts: boardingAddr.Tapscripts,
|
||||
Address: boardingAddr.Address,
|
||||
})
|
||||
}
|
||||
@@ -192,12 +193,12 @@ func (s *bitcoinWallet) SignTransaction(
|
||||
)
|
||||
|
||||
txsighashes := txscript.NewTxSigHashes(updater.Upsbt.UnsignedTx, prevoutFetcher)
|
||||
myPubkey := schnorr.SerializePubKey(s.walletData.Pubkey)
|
||||
|
||||
for i, input := range ptx.Inputs {
|
||||
if len(input.TaprootLeafScript) > 0 {
|
||||
pubkey := s.walletData.Pubkey
|
||||
for _, leaf := range input.TaprootLeafScript {
|
||||
closure, err := bitcointree.DecodeClosure(leaf.Script)
|
||||
closure, err := tree.DecodeClosure(leaf.Script)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -205,10 +206,20 @@ func (s *bitcoinWallet) SignTransaction(
|
||||
sign := false
|
||||
|
||||
switch c := closure.(type) {
|
||||
case *bitcointree.CSVSigClosure:
|
||||
sign = bytes.Equal(c.Pubkey.SerializeCompressed()[1:], pubkey.SerializeCompressed()[1:])
|
||||
case *bitcointree.MultisigClosure:
|
||||
sign = bytes.Equal(c.Pubkey.SerializeCompressed()[1:], pubkey.SerializeCompressed()[1:])
|
||||
case *tree.CSVSigClosure:
|
||||
for _, key := range c.MultisigClosure.PubKeys {
|
||||
if bytes.Equal(schnorr.SerializePubKey(key), myPubkey) {
|
||||
sign = true
|
||||
break
|
||||
}
|
||||
}
|
||||
case *tree.MultisigClosure:
|
||||
for _, key := range c.PubKeys {
|
||||
if bytes.Equal(schnorr.SerializePubKey(key), myPubkey) {
|
||||
sign = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if sign {
|
||||
@@ -235,7 +246,7 @@ func (s *bitcoinWallet) SignTransaction(
|
||||
return "", err
|
||||
}
|
||||
|
||||
if !sig.Verify(preimage, pubkey) {
|
||||
if !sig.Verify(preimage, s.walletData.Pubkey) {
|
||||
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{
|
||||
XOnlyPubKey: schnorr.SerializePubKey(pubkey),
|
||||
XOnlyPubKey: myPubkey,
|
||||
LeafHash: hash.CloneBytes(),
|
||||
Signature: sig.Serialize(),
|
||||
SigHash: txscript.SigHashDefault,
|
||||
@@ -259,17 +270,12 @@ func (s *bitcoinWallet) SignTransaction(
|
||||
}
|
||||
|
||||
func (w *bitcoinWallet) SignMessage(
|
||||
ctx context.Context, message []byte, pubkey string,
|
||||
ctx context.Context, message []byte,
|
||||
) (string, error) {
|
||||
if w.IsLocked() {
|
||||
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)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -278,14 +284,16 @@ func (w *bitcoinWallet) SignMessage(
|
||||
return hex.EncodeToString(sig.Serialize()), nil
|
||||
}
|
||||
|
||||
type addressWithTapscripts struct {
|
||||
Address common.Address
|
||||
Tapscripts []string
|
||||
}
|
||||
|
||||
func (w *bitcoinWallet) getAddress(
|
||||
ctx context.Context,
|
||||
) (
|
||||
*struct {
|
||||
Address common.Address
|
||||
Descriptor string
|
||||
},
|
||||
*wallet.DescriptorAddress,
|
||||
*addressWithTapscripts,
|
||||
*wallet.TapscriptsAddress,
|
||||
error,
|
||||
) {
|
||||
if w.walletData == nil {
|
||||
@@ -299,11 +307,11 @@ func (w *bitcoinWallet) getAddress(
|
||||
|
||||
netParams := utils.ToBitcoinNetwork(data.Network)
|
||||
|
||||
defaultVtxoScript := &bitcointree.DefaultVtxoScript{
|
||||
Asp: data.AspPubkey,
|
||||
Owner: w.walletData.Pubkey,
|
||||
ExitDelay: uint(data.UnilateralExitDelay),
|
||||
}
|
||||
defaultVtxoScript := bitcointree.NewDefaultVtxoScript(
|
||||
w.walletData.Pubkey,
|
||||
data.AspPubkey,
|
||||
uint(data.UnilateralExitDelay),
|
||||
)
|
||||
|
||||
vtxoTapKey, _, err := defaultVtxoScript.TapTree()
|
||||
if err != nil {
|
||||
@@ -316,16 +324,12 @@ func (w *bitcoinWallet) getAddress(
|
||||
VtxoTapKey: vtxoTapKey,
|
||||
}
|
||||
|
||||
myPubkeyStr := hex.EncodeToString(schnorr.SerializePubKey(w.walletData.Pubkey))
|
||||
descriptorStr := strings.ReplaceAll(
|
||||
data.BoardingDescriptorTemplate, "USER", myPubkeyStr,
|
||||
boardingVtxoScript := bitcointree.NewDefaultVtxoScript(
|
||||
w.walletData.Pubkey,
|
||||
data.AspPubkey,
|
||||
uint(data.UnilateralExitDelay*2),
|
||||
)
|
||||
|
||||
boardingVtxoScript, err := bitcointree.ParseVtxoScript(descriptorStr)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
boardingTapKey, _, err := boardingVtxoScript.TapTree()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
@@ -339,14 +343,22 @@ func (w *bitcoinWallet) getAddress(
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return &struct {
|
||||
Address common.Address
|
||||
Descriptor string
|
||||
}{
|
||||
*offchainAddress, defaultVtxoScript.ToDescriptor(),
|
||||
tapscripts, err := defaultVtxoScript.Encode()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
boardingTapscripts, err := boardingVtxoScript.Encode()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return &addressWithTapscripts{
|
||||
Address: *offchainAddress,
|
||||
Tapscripts: tapscripts,
|
||||
},
|
||||
&wallet.DescriptorAddress{
|
||||
Descriptor: descriptorStr,
|
||||
&wallet.TapscriptsAddress{
|
||||
Tapscripts: boardingTapscripts,
|
||||
Address: boardingAddr.EncodeAddress(),
|
||||
},
|
||||
nil
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/ark-network/ark/common"
|
||||
"github.com/ark-network/ark/common/tree"
|
||||
@@ -44,7 +43,7 @@ func NewLiquidWallet(
|
||||
|
||||
func (w *liquidWallet) GetAddresses(
|
||||
ctx context.Context,
|
||||
) ([]wallet.DescriptorAddress, []wallet.DescriptorAddress, []wallet.DescriptorAddress, error) {
|
||||
) ([]wallet.TapscriptsAddress, []wallet.TapscriptsAddress, []wallet.TapscriptsAddress, error) {
|
||||
offchainAddr, boardingAddr, err := w.getAddress(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
@@ -72,22 +71,22 @@ func (w *liquidWallet) GetAddresses(
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
offchainAddrs := []wallet.DescriptorAddress{
|
||||
offchainAddrs := []wallet.TapscriptsAddress{
|
||||
{
|
||||
Descriptor: offchainAddr.Descriptor,
|
||||
Tapscripts: offchainAddr.Tapscripts,
|
||||
Address: encodedOffchainAddr,
|
||||
},
|
||||
}
|
||||
boardingAddrs := []wallet.DescriptorAddress{
|
||||
boardingAddrs := []wallet.TapscriptsAddress{
|
||||
{
|
||||
Descriptor: boardingAddr.Descriptor,
|
||||
Tapscripts: boardingAddr.Tapscripts,
|
||||
Address: boardingAddr.Address,
|
||||
},
|
||||
}
|
||||
|
||||
redemptionAddrs := []wallet.DescriptorAddress{
|
||||
redemptionAddrs := []wallet.TapscriptsAddress{
|
||||
{
|
||||
Descriptor: offchainAddr.Descriptor,
|
||||
Tapscripts: offchainAddr.Tapscripts,
|
||||
Address: redemptionAddr,
|
||||
},
|
||||
}
|
||||
@@ -97,7 +96,7 @@ func (w *liquidWallet) GetAddresses(
|
||||
|
||||
func (w *liquidWallet) NewAddress(
|
||||
ctx context.Context, _ bool,
|
||||
) (*wallet.DescriptorAddress, *wallet.DescriptorAddress, error) {
|
||||
) (*wallet.TapscriptsAddress, *wallet.TapscriptsAddress, error) {
|
||||
offchainAddr, boardingAddr, err := w.getAddress(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
@@ -108,37 +107,37 @@ func (w *liquidWallet) NewAddress(
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return &wallet.DescriptorAddress{
|
||||
Descriptor: offchainAddr.Descriptor,
|
||||
return &wallet.TapscriptsAddress{
|
||||
Tapscripts: offchainAddr.Tapscripts,
|
||||
Address: encodedOffchainAddr,
|
||||
}, &wallet.DescriptorAddress{
|
||||
Descriptor: boardingAddr.Descriptor,
|
||||
}, &wallet.TapscriptsAddress{
|
||||
Tapscripts: boardingAddr.Tapscripts,
|
||||
Address: boardingAddr.Address,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (w *liquidWallet) NewAddresses(
|
||||
ctx context.Context, _ bool, num int,
|
||||
) ([]wallet.DescriptorAddress, []wallet.DescriptorAddress, error) {
|
||||
) ([]wallet.TapscriptsAddress, []wallet.TapscriptsAddress, error) {
|
||||
offchainAddr, boardingAddr, err := w.getAddress(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
offchainAddrs := make([]wallet.DescriptorAddress, 0, num)
|
||||
boardingAddrs := make([]wallet.DescriptorAddress, 0, num)
|
||||
offchainAddrs := make([]wallet.TapscriptsAddress, 0, num)
|
||||
boardingAddrs := make([]wallet.TapscriptsAddress, 0, num)
|
||||
for i := 0; i < num; i++ {
|
||||
encodedOffchainAddr, err := offchainAddr.Address.Encode()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
offchainAddrs = append(offchainAddrs, wallet.DescriptorAddress{
|
||||
Descriptor: offchainAddr.Descriptor,
|
||||
offchainAddrs = append(offchainAddrs, wallet.TapscriptsAddress{
|
||||
Tapscripts: offchainAddr.Tapscripts,
|
||||
Address: encodedOffchainAddr,
|
||||
})
|
||||
boardingAddrs = append(boardingAddrs, wallet.DescriptorAddress{
|
||||
Descriptor: boardingAddr.Descriptor,
|
||||
boardingAddrs = append(boardingAddrs, wallet.TapscriptsAddress{
|
||||
Tapscripts: boardingAddr.Tapscripts,
|
||||
Address: boardingAddr.Address,
|
||||
})
|
||||
}
|
||||
@@ -212,7 +211,7 @@ func (s *liquidWallet) SignTransaction(
|
||||
prevoutsAssets = append(prevoutsAssets, input.WitnessUtxo.Asset)
|
||||
}
|
||||
|
||||
serializedPubKey := s.walletData.Pubkey.SerializeCompressed()
|
||||
myPubkey := schnorr.SerializePubKey(s.walletData.Pubkey)
|
||||
|
||||
for i, input := range pset.Inputs {
|
||||
if len(input.TapLeafScript) > 0 {
|
||||
@@ -230,9 +229,19 @@ func (s *liquidWallet) SignTransaction(
|
||||
sign := false
|
||||
switch c := closure.(type) {
|
||||
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:
|
||||
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 {
|
||||
@@ -288,17 +297,12 @@ func (s *liquidWallet) SignTransaction(
|
||||
}
|
||||
|
||||
func (w *liquidWallet) SignMessage(
|
||||
ctx context.Context, message []byte, pubkey string,
|
||||
ctx context.Context, message []byte,
|
||||
) (string, error) {
|
||||
if w.IsLocked() {
|
||||
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)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -310,11 +314,8 @@ func (w *liquidWallet) SignMessage(
|
||||
func (w *liquidWallet) getAddress(
|
||||
ctx context.Context,
|
||||
) (
|
||||
*struct {
|
||||
Address common.Address
|
||||
Descriptor string
|
||||
},
|
||||
*wallet.DescriptorAddress,
|
||||
*addressWithTapscripts,
|
||||
*wallet.TapscriptsAddress,
|
||||
error,
|
||||
) {
|
||||
if w.walletData == nil {
|
||||
@@ -328,11 +329,11 @@ func (w *liquidWallet) getAddress(
|
||||
|
||||
liquidNet := utils.ToElementsNetwork(data.Network)
|
||||
|
||||
vtxoScript := &tree.DefaultVtxoScript{
|
||||
Owner: w.walletData.Pubkey,
|
||||
Asp: data.AspPubkey,
|
||||
ExitDelay: uint(data.UnilateralExitDelay),
|
||||
}
|
||||
vtxoScript := tree.NewDefaultVtxoScript(
|
||||
w.walletData.Pubkey,
|
||||
data.AspPubkey,
|
||||
uint(data.UnilateralExitDelay),
|
||||
)
|
||||
|
||||
vtxoTapKey, _, err := vtxoScript.TapTree()
|
||||
if err != nil {
|
||||
@@ -345,22 +346,18 @@ func (w *liquidWallet) getAddress(
|
||||
VtxoTapKey: vtxoTapKey,
|
||||
}
|
||||
|
||||
myPubkeyStr := hex.EncodeToString(schnorr.SerializePubKey(w.walletData.Pubkey))
|
||||
descriptorStr := strings.ReplaceAll(
|
||||
data.BoardingDescriptorTemplate, "USER", myPubkeyStr,
|
||||
boardingVtxoScript := tree.NewDefaultVtxoScript(
|
||||
w.walletData.Pubkey,
|
||||
data.AspPubkey,
|
||||
uint(data.UnilateralExitDelay*2),
|
||||
)
|
||||
|
||||
onboardingScript, err := tree.ParseVtxoScript(descriptorStr)
|
||||
boardingTapKey, _, err := boardingVtxoScript.TapTree()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
tapKey, _, err := onboardingScript.TapTree()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
p2tr, err := payment.FromTweakedKey(tapKey, &liquidNet, nil)
|
||||
p2tr, err := payment.FromTweakedKey(boardingTapKey, &liquidNet, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
@@ -370,14 +367,21 @@ func (w *liquidWallet) getAddress(
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return &struct {
|
||||
Address common.Address
|
||||
Descriptor string
|
||||
}{
|
||||
tapscripts, err := vtxoScript.Encode()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
boardingTapscripts, err := boardingVtxoScript.Encode()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return &addressWithTapscripts{
|
||||
Address: *offchainAddr,
|
||||
Descriptor: vtxoScript.ToDescriptor(),
|
||||
}, &wallet.DescriptorAddress{
|
||||
Descriptor: descriptorStr,
|
||||
Tapscripts: tapscripts,
|
||||
}, &wallet.TapscriptsAddress{
|
||||
Tapscripts: boardingTapscripts,
|
||||
Address: boardingAddr,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -10,8 +10,8 @@ const (
|
||||
SingleKeyWallet = "singlekey"
|
||||
)
|
||||
|
||||
type DescriptorAddress struct {
|
||||
Descriptor string
|
||||
type TapscriptsAddress struct {
|
||||
Tapscripts []string
|
||||
Address string
|
||||
}
|
||||
|
||||
@@ -25,18 +25,18 @@ type WalletService interface {
|
||||
IsLocked() bool
|
||||
GetAddresses(
|
||||
ctx context.Context,
|
||||
) (offchainAddresses, boardingAddresses, redemptionAddresses []DescriptorAddress, err error)
|
||||
) (offchainAddresses, boardingAddresses, redemptionAddresses []TapscriptsAddress, err error)
|
||||
NewAddress(
|
||||
ctx context.Context, change bool,
|
||||
) (offchainAddr, onchainAddr *DescriptorAddress, err error)
|
||||
) (offchainAddr, onchainAddr *TapscriptsAddress, err error)
|
||||
NewAddresses(
|
||||
ctx context.Context, change bool, num int,
|
||||
) (offchainAddresses, onchainAddresses []DescriptorAddress, err error)
|
||||
) (offchainAddresses, onchainAddresses []TapscriptsAddress, err error)
|
||||
SignTransaction(
|
||||
ctx context.Context, explorerSvc explorer.Explorer, tx string,
|
||||
) (signedTx string, err error)
|
||||
SignMessage(
|
||||
ctx context.Context, message []byte, pubkey string,
|
||||
ctx context.Context, message []byte,
|
||||
) (signature string, err error)
|
||||
Dump(ctx context.Context) (seed string, err error)
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"time"
|
||||
|
||||
"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/tree"
|
||||
"github.com/ark-network/ark/server/internal/core/domain"
|
||||
@@ -149,29 +148,30 @@ func (s *covenantService) Stop() {
|
||||
close(s.eventsCh)
|
||||
}
|
||||
|
||||
func (s *covenantService) GetBoardingAddress(ctx context.Context, userPubkey *secp256k1.PublicKey) (string, string, error) {
|
||||
vtxoScript := &tree.DefaultVtxoScript{
|
||||
Asp: s.pubkey,
|
||||
Owner: userPubkey,
|
||||
ExitDelay: uint(s.boardingExitDelay),
|
||||
}
|
||||
func (s *covenantService) GetBoardingAddress(ctx context.Context, userPubkey *secp256k1.PublicKey) (string, []string, error) {
|
||||
vtxoScript := tree.NewDefaultVtxoScript(userPubkey, s.pubkey, uint(s.boardingExitDelay))
|
||||
|
||||
tapKey, _, err := vtxoScript.TapTree()
|
||||
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)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
addr, err := p2tr.TaprootAddress()
|
||||
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) {
|
||||
@@ -211,8 +211,18 @@ func (s *covenantService) SpendVtxos(ctx context.Context, inputs []ports.Input)
|
||||
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 blocktime+int64(s.boardingExitDelay) < now {
|
||||
if blocktime+int64(exitDelay) < now {
|
||||
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)
|
||||
}
|
||||
|
||||
vtxoScript, err := tree.ParseVtxoScript(input.Descriptor)
|
||||
vtxoScript, err := tree.ParseVtxoScript(input.Tapscripts)
|
||||
if err != nil {
|
||||
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)
|
||||
}
|
||||
|
||||
boardingScript, err := tree.ParseVtxoScript(input.Descriptor)
|
||||
boardingScript, err := tree.ParseVtxoScript(input.Tapscripts)
|
||||
if err != nil {
|
||||
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")
|
||||
}
|
||||
|
||||
if defaultVtxoScript, ok := boardingScript.(*tree.DefaultVtxoScript); ok {
|
||||
if !bytes.Equal(schnorr.SerializePubKey(defaultVtxoScript.Asp), schnorr.SerializePubKey(s.pubkey)) {
|
||||
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")
|
||||
if err := boardingScript.Validate(s.pubkey, uint(s.unilateralExitDelay)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &ports.BoardingInput{
|
||||
@@ -430,15 +432,7 @@ func (s *covenantService) GetInfo(ctx context.Context) (*ServiceInfo, error) {
|
||||
RoundInterval: s.roundInterval,
|
||||
Network: s.network.Name,
|
||||
Dust: dust,
|
||||
BoardingDescriptorTemplate: fmt.Sprintf(
|
||||
descriptor.DefaultVtxoDescriptorTemplate,
|
||||
hex.EncodeToString(tree.UnspendableKey().SerializeCompressed()),
|
||||
"USER",
|
||||
hex.EncodeToString(schnorr.SerializePubKey(s.pubkey)),
|
||||
s.boardingExitDelay,
|
||||
"USER",
|
||||
),
|
||||
ForfeitAddress: forfeitAddress,
|
||||
ForfeitAddress: forfeitAddress,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -852,6 +846,7 @@ func (s *covenantService) reactToFraud(ctx context.Context, vtxo domain.Vtxo, mu
|
||||
|
||||
forfeitTxid, err := s.wallet.BroadcastTransaction(ctx, forfeitTxHex)
|
||||
if err != nil {
|
||||
log.Debug(forfeitTxHex)
|
||||
return fmt.Errorf("failed to broadcast forfeit tx: %s", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
|
||||
"github.com/ark-network/ark/common"
|
||||
"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/tree"
|
||||
"github.com/ark-network/ark/server/internal/core/domain"
|
||||
@@ -259,7 +258,7 @@ func (s *covenantlessService) CompleteAsyncPayment(
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
@@ -329,12 +328,12 @@ func (s *covenantlessService) CreateAsyncPayment(
|
||||
ctx context.Context, inputs []AsyncPaymentInput, receivers []domain.Receiver,
|
||||
) (string, error) {
|
||||
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)
|
||||
|
||||
for _, in := range inputs {
|
||||
vtxosKeys = append(vtxosKeys, in.VtxoKey)
|
||||
descriptors[in.VtxoKey] = in.Descriptor
|
||||
scripts[in.VtxoKey] = in.Tapscripts
|
||||
forfeitLeaves[in.VtxoKey] = in.ForfeitLeafHash
|
||||
}
|
||||
|
||||
@@ -373,7 +372,7 @@ func (s *covenantlessService) CreateAsyncPayment(
|
||||
}
|
||||
|
||||
redeemTx, err := s.builder.BuildAsyncPaymentTransactions(
|
||||
vtxosInputs, descriptors, forfeitLeaves, receivers,
|
||||
vtxosInputs, scripts, forfeitLeaves, receivers,
|
||||
)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to build async payment txs: %s", err)
|
||||
@@ -395,26 +394,29 @@ func (s *covenantlessService) CreateAsyncPayment(
|
||||
|
||||
func (s *covenantlessService) GetBoardingAddress(
|
||||
ctx context.Context, userPubkey *secp256k1.PublicKey,
|
||||
) (address string, descriptor string, err error) {
|
||||
vtxoScript := &bitcointree.DefaultVtxoScript{
|
||||
Asp: s.pubkey,
|
||||
Owner: userPubkey,
|
||||
ExitDelay: uint(s.boardingExitDelay),
|
||||
}
|
||||
) (address string, scripts []string, err error) {
|
||||
vtxoScript := bitcointree.NewDefaultVtxoScript(s.pubkey, userPubkey, uint(s.boardingExitDelay))
|
||||
|
||||
tapKey, _, err := vtxoScript.TapTree()
|
||||
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(
|
||||
schnorr.SerializePubKey(tapKey), s.chainParams(),
|
||||
)
|
||||
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) {
|
||||
@@ -489,8 +491,18 @@ func (s *covenantlessService) SpendVtxos(ctx context.Context, inputs []ports.Inp
|
||||
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 blocktime+int64(s.boardingExitDelay) < now {
|
||||
if blocktime+int64(exitDelay) < now {
|
||||
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)
|
||||
}
|
||||
|
||||
vtxoScript, err := bitcointree.ParseVtxoScript(input.Descriptor)
|
||||
vtxoScript, err := bitcointree.ParseVtxoScript(input.Tapscripts)
|
||||
if err != nil {
|
||||
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]
|
||||
|
||||
boardingScript, err := bitcointree.ParseVtxoScript(input.Descriptor)
|
||||
boardingScript, err := bitcointree.ParseVtxoScript(input.Tapscripts)
|
||||
if err != nil {
|
||||
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")
|
||||
}
|
||||
|
||||
if defaultVtxoScript, ok := boardingScript.(*bitcointree.DefaultVtxoScript); ok {
|
||||
if !bytes.Equal(schnorr.SerializePubKey(defaultVtxoScript.Asp), schnorr.SerializePubKey(s.pubkey)) {
|
||||
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")
|
||||
if err := boardingScript.Validate(s.pubkey, uint(s.unilateralExitDelay)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &ports.BoardingInput{
|
||||
@@ -691,15 +695,7 @@ func (s *covenantlessService) GetInfo(ctx context.Context) (*ServiceInfo, error)
|
||||
RoundInterval: s.roundInterval,
|
||||
Network: s.network.Name,
|
||||
Dust: dust,
|
||||
BoardingDescriptorTemplate: fmt.Sprintf(
|
||||
descriptor.DefaultVtxoDescriptorTemplate,
|
||||
hex.EncodeToString(bitcointree.UnspendableKey().SerializeCompressed()),
|
||||
"USER",
|
||||
hex.EncodeToString(schnorr.SerializePubKey(s.pubkey)),
|
||||
s.boardingExitDelay,
|
||||
"USER",
|
||||
),
|
||||
ForfeitAddress: forfeitAddr,
|
||||
ForfeitAddress: forfeitAddr,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -921,7 +917,7 @@ func (s *covenantlessService) startFinalization() {
|
||||
|
||||
cosigners = append(cosigners, ephemeralKey.PubKey())
|
||||
|
||||
unsignedRoundTx, tree, connectorAddress, connectors, err := s.builder.BuildRoundTx(
|
||||
unsignedRoundTx, vtxoTree, connectorAddress, connectors, err := s.builder.BuildRoundTx(
|
||||
s.pubkey,
|
||||
payments,
|
||||
boardingInputs,
|
||||
@@ -937,7 +933,7 @@ func (s *covenantlessService) startFinalization() {
|
||||
|
||||
s.forfeitTxs.init(connectors, payments)
|
||||
|
||||
if len(tree) > 0 {
|
||||
if len(vtxoTree) > 0 {
|
||||
log.Debugf("signing congestion tree for round %s", round.Id)
|
||||
|
||||
signingSession := newMusigSigningSession(len(cosigners))
|
||||
@@ -947,14 +943,14 @@ func (s *covenantlessService) startFinalization() {
|
||||
|
||||
s.currentRound.UnsignedTx = unsignedRoundTx
|
||||
// send back the unsigned tree & all cosigners pubkeys
|
||||
s.propagateRoundSigningStartedEvent(tree, cosigners)
|
||||
s.propagateRoundSigningStartedEvent(vtxoTree, cosigners)
|
||||
|
||||
sweepClosure := bitcointree.CSVSigClosure{
|
||||
Pubkey: s.pubkey,
|
||||
Seconds: uint(s.roundLifetime),
|
||||
sweepClosure := tree.CSVSigClosure{
|
||||
MultisigClosure: tree.MultisigClosure{PubKeys: []*secp256k1.PublicKey{s.pubkey}},
|
||||
Seconds: uint(s.roundLifetime),
|
||||
}
|
||||
|
||||
sweepTapLeaf, err := sweepClosure.Leaf()
|
||||
sweepScript, err := sweepClosure.Script()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -968,10 +964,11 @@ func (s *covenantlessService) startFinalization() {
|
||||
|
||||
sharedOutputAmount := unsignedPsbt.UnsignedTx.TxOut[0].Value
|
||||
|
||||
sweepTapTree := txscript.AssembleTaprootScriptTree(*sweepTapLeaf)
|
||||
sweepLeaf := txscript.NewBaseTapLeaf(sweepScript)
|
||||
sweepTapTree := txscript.AssembleTaprootScriptTree(sweepLeaf)
|
||||
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 {
|
||||
round.Fail(fmt.Errorf("failed to create tree coordinator: %s", err))
|
||||
log.WithError(err).Warn("failed to create tree coordinator")
|
||||
@@ -979,7 +976,7 @@ func (s *covenantlessService) startFinalization() {
|
||||
}
|
||||
|
||||
aspSignerSession := bitcointree.NewTreeSignerSession(
|
||||
ephemeralKey, sharedOutputAmount, tree, root.CloneBytes(),
|
||||
ephemeralKey, sharedOutputAmount, vtxoTree, root.CloneBytes(),
|
||||
)
|
||||
|
||||
nonces, err := aspSignerSession.GetNonces()
|
||||
@@ -1085,11 +1082,11 @@ func (s *covenantlessService) startFinalization() {
|
||||
|
||||
log.Debugf("congestion tree signed for round %s", round.Id)
|
||||
|
||||
tree = signedTree
|
||||
vtxoTree = signedTree
|
||||
}
|
||||
|
||||
if _, err := round.StartFinalization(
|
||||
connectorAddress, connectors, tree, unsignedRoundTx,
|
||||
connectorAddress, connectors, vtxoTree, unsignedRoundTx,
|
||||
); err != nil {
|
||||
round.Fail(fmt.Errorf("failed to start finalization: %s", err))
|
||||
log.WithError(err).Warn("failed to start finalization")
|
||||
|
||||
@@ -24,7 +24,7 @@ type OwnershipProof struct {
|
||||
|
||||
func (p OwnershipProof) validate(vtxo domain.Vtxo) error {
|
||||
// verify revealed script and extract user public key
|
||||
pubkey, err := decodeForfeitClosure(p.Script)
|
||||
pubkeys, err := decodeForfeitClosure(p.Script)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -49,24 +49,32 @@ func (p OwnershipProof) validate(vtxo domain.Vtxo) error {
|
||||
outpointBytes := append(txhash[:], voutBytes...)
|
||||
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 nil
|
||||
}
|
||||
|
||||
func decodeForfeitClosure(script []byte) (*secp256k1.PublicKey, error) {
|
||||
var covenantLessForfeitClosure bitcointree.MultisigClosure
|
||||
func decodeForfeitClosure(script []byte) ([]*secp256k1.PublicKey, error) {
|
||||
var forfeit tree.MultisigClosure
|
||||
|
||||
if valid, err := covenantLessForfeitClosure.Decode(script); err == nil && valid {
|
||||
return covenantLessForfeitClosure.Pubkey, nil
|
||||
valid, err := forfeit.Decode(script)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var covenantForfeitClosure tree.CSVSigClosure
|
||||
if valid, err := covenantForfeitClosure.Decode(script); err == nil && valid {
|
||||
return covenantForfeitClosure.Pubkey, nil
|
||||
if !valid {
|
||||
return nil, fmt.Errorf("invalid forfeit closure script")
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("invalid forfeit closure script")
|
||||
return forfeit.PubKeys, nil
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ type Service interface {
|
||||
) error
|
||||
GetBoardingAddress(
|
||||
ctx context.Context, userPubkey *secp256k1.PublicKey,
|
||||
) (address string, descriptor string, err error)
|
||||
) (address string, scripts []string, err error)
|
||||
// Tree signing methods
|
||||
RegisterCosignerPubkey(ctx context.Context, paymentId string, ephemeralPublicKey string) error
|
||||
RegisterCosignerNonces(
|
||||
@@ -62,14 +62,13 @@ type Service interface {
|
||||
}
|
||||
|
||||
type ServiceInfo struct {
|
||||
PubKey string
|
||||
RoundLifetime int64
|
||||
UnilateralExitDelay int64
|
||||
RoundInterval int64
|
||||
Network string
|
||||
Dust uint64
|
||||
BoardingDescriptorTemplate string
|
||||
ForfeitAddress string
|
||||
PubKey string
|
||||
RoundLifetime int64
|
||||
UnilateralExitDelay int64
|
||||
RoundInterval int64
|
||||
Network string
|
||||
Dust uint64
|
||||
ForfeitAddress string
|
||||
}
|
||||
|
||||
type WalletStatus struct {
|
||||
|
||||
@@ -18,7 +18,7 @@ type SweepInput interface {
|
||||
|
||||
type Input struct {
|
||||
domain.VtxoKey
|
||||
Descriptor string
|
||||
Tapscripts []string
|
||||
}
|
||||
|
||||
type BoardingInput struct {
|
||||
@@ -49,12 +49,12 @@ type TxBuilder interface {
|
||||
BuildSweepTx(inputs []SweepInput) (signedSweepTx string, err error)
|
||||
GetSweepInput(node tree.Node) (lifetime int64, sweepInput SweepInput, 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(congestionTree tree.CongestionTree, fromtxid string, vout uint32) (leaves []tree.Node, err error)
|
||||
BuildAsyncPaymentTransactions(
|
||||
vtxosToSpend []domain.Vtxo,
|
||||
descriptors map[domain.VtxoKey]string,
|
||||
scripts map[domain.VtxoKey][]string,
|
||||
forfeitsLeaves map[domain.VtxoKey]chainhash.Hash,
|
||||
receivers []domain.Receiver,
|
||||
) (string, error)
|
||||
|
||||
@@ -3,6 +3,7 @@ package txbuilder
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"math"
|
||||
|
||||
@@ -11,6 +12,7 @@ import (
|
||||
"github.com/ark-network/ark/server/internal/core/domain"
|
||||
"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/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"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))
|
||||
}
|
||||
|
||||
valid, _, err := b.verifyTapscriptPartialSigs(pset)
|
||||
valid, err := b.verifyTapscriptPartialSigs(pset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -462,40 +464,66 @@ func (b *txBuilder) GetSweepInput(node tree.Node) (lifetime int64, sweepInput po
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
return false, "", err
|
||||
return false, err
|
||||
}
|
||||
|
||||
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()
|
||||
txid := utx.TxHash().String()
|
||||
|
||||
aspPublicKey, err := b.wallet.GetPubkey(context.Background())
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
for index, input := range pset.Inputs {
|
||||
if len(input.TapLeafScript) == 0 {
|
||||
continue
|
||||
}
|
||||
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
|
||||
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)
|
||||
tapKeyFromControlBlock := taproot.ComputeTaprootOutputKey(tree.UnspendableKey(), rootHash[:])
|
||||
|
||||
pkscript, err := common.P2TRScript(tapKeyFromControlBlock)
|
||||
if err != nil {
|
||||
return false, txid, err
|
||||
return false, err
|
||||
}
|
||||
|
||||
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()
|
||||
@@ -506,41 +534,93 @@ func (b *txBuilder) verifyTapscriptPartialSigs(pset *psetv2.Pset) (bool, string,
|
||||
&leafHash,
|
||||
)
|
||||
if err != nil {
|
||||
return false, txid, err
|
||||
return false, err
|
||||
}
|
||||
|
||||
for _, tapScriptSig := range input.TapScriptSig {
|
||||
sig, err := schnorr.ParseSignature(tapScriptSig.Signature)
|
||||
if err != nil {
|
||||
return false, txid, err
|
||||
return false, err
|
||||
}
|
||||
|
||||
pubkey, err := schnorr.ParsePubKey(tapScriptSig.PubKey)
|
||||
if err != nil {
|
||||
return false, txid, err
|
||||
return false, err
|
||||
}
|
||||
|
||||
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) {
|
||||
p, err := psetv2.NewPsetFromBase64(tx)
|
||||
ptx, err := psetv2.NewPsetFromBase64(tx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err := psetv2.FinalizeAll(p); err != nil {
|
||||
return "", err
|
||||
for i, in := range ptx.Inputs {
|
||||
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
|
||||
extracted, err := psetv2.Extract(p)
|
||||
extracted, err := psetv2.Extract(ptx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -591,7 +671,7 @@ func (b *txBuilder) FindLeaves(
|
||||
|
||||
func (b *txBuilder) BuildAsyncPaymentTransactions(
|
||||
_ []domain.Vtxo,
|
||||
_ map[domain.VtxoKey]string,
|
||||
_ map[domain.VtxoKey][]string,
|
||||
_ map[domain.VtxoKey]chainhash.Hash,
|
||||
_ []domain.Receiver,
|
||||
) (string, error) {
|
||||
@@ -728,7 +808,7 @@ func (b *txBuilder) createPoolTx(
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"github.com/btcsuite/btcwallet/waddrmgr"
|
||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||
"github.com/lightningnetwork/lnd/input"
|
||||
"github.com/lightningnetwork/lnd/lntypes"
|
||||
)
|
||||
|
||||
type txBuilder struct {
|
||||
@@ -47,47 +48,74 @@ func (b *txBuilder) GetTxID(tx string) (string, error) {
|
||||
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)
|
||||
if err != nil {
|
||||
return false, "", err
|
||||
return false, err
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
aspPublicKey, err := b.wallet.GetPubkey(context.Background())
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
for index, input := range ptx.Inputs {
|
||||
if len(input.TaprootLeafScript) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
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
|
||||
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 {
|
||||
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)
|
||||
if err != nil {
|
||||
return false, txid, err
|
||||
return false, err
|
||||
}
|
||||
|
||||
rootHash := controlBlock.RootHash(tapLeaf.Script)
|
||||
tapKeyFromControlBlock := txscript.ComputeTaprootOutputKey(bitcointree.UnspendableKey(), rootHash[:])
|
||||
pkscript, err := common.P2TRScript(tapKeyFromControlBlock)
|
||||
if err != nil {
|
||||
return false, txid, err
|
||||
return false, err
|
||||
}
|
||||
|
||||
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(
|
||||
@@ -96,27 +124,40 @@ func (b *txBuilder) verifyTapscriptPartialSigs(ptx *psbt.Packet) (bool, string,
|
||||
tapLeaf.Script,
|
||||
)
|
||||
if err != nil {
|
||||
return false, txid, err
|
||||
return false, err
|
||||
}
|
||||
|
||||
for _, tapScriptSig := range input.TaprootScriptSpendSig {
|
||||
sig, err := schnorr.ParseSignature(tapScriptSig.Signature)
|
||||
if err != nil {
|
||||
return false, txid, err
|
||||
return false, err
|
||||
}
|
||||
|
||||
pubkey, err := schnorr.ParsePubKey(tapScriptSig.XOnlyPubKey)
|
||||
if err != nil {
|
||||
return false, txid, err
|
||||
return false, err
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -128,47 +169,30 @@ func (b *txBuilder) FinalizeAndExtract(tx string) (string, error) {
|
||||
for i, in := range ptx.Inputs {
|
||||
isTaproot := txscript.IsPayToTaproot(in.WitnessUtxo.PkScript)
|
||||
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 {
|
||||
return "", err
|
||||
}
|
||||
|
||||
witness := make(wire.TxWitness, 4)
|
||||
signatures := make(map[string][]byte)
|
||||
|
||||
castClosure, isTaprootMultisig := closure.(*bitcointree.MultisigClosure)
|
||||
if isTaprootMultisig {
|
||||
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
|
||||
for _, sig := range in.TaprootScriptSpendSig {
|
||||
signatures[hex.EncodeToString(sig.XOnlyPubKey)] = sig.Signature
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
valid, _, err := b.verifyTapscriptPartialSigs(ptx)
|
||||
valid, err := b.verifyTapscriptPartialSigs(ptx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -623,7 +647,7 @@ func (b *txBuilder) FindLeaves(congestionTree tree.CongestionTree, fromtxid stri
|
||||
|
||||
func (b *txBuilder) BuildAsyncPaymentTransactions(
|
||||
vtxos []domain.Vtxo,
|
||||
descriptors map[domain.VtxoKey]string,
|
||||
scripts map[domain.VtxoKey][]string,
|
||||
forfeitsLeaves map[domain.VtxoKey]chainhash.Hash,
|
||||
receivers []domain.Receiver,
|
||||
) (string, error) {
|
||||
@@ -638,9 +662,9 @@ func (b *txBuilder) BuildAsyncPaymentTransactions(
|
||||
|
||||
redeemTxWeightEstimator := &input.TxWeightEstimator{}
|
||||
for index, vtxo := range vtxos {
|
||||
desc, ok := descriptors[vtxo.VtxoKey]
|
||||
vtxoTapscripts, ok := scripts[vtxo.VtxoKey]
|
||||
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]
|
||||
@@ -662,7 +686,7 @@ func (b *txBuilder) BuildAsyncPaymentTransactions(
|
||||
Index: vtxo.VOut,
|
||||
}
|
||||
|
||||
vtxoScript, err := bitcointree.ParseVtxoScript(desc)
|
||||
vtxoScript, err := bitcointree.ParseVtxoScript(vtxoTapscripts)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -698,7 +722,12 @@ func (b *txBuilder) BuildAsyncPaymentTransactions(
|
||||
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,
|
||||
ControlBlock: ctrlBlock,
|
||||
})
|
||||
@@ -940,7 +969,7 @@ func (b *txBuilder) createRoundTx(
|
||||
})
|
||||
nSequences = append(nSequences, wire.MaxTxInSequenceNum)
|
||||
|
||||
boardingVtxoScript, err := bitcointree.ParseVtxoScript(boardingInput.Descriptor)
|
||||
boardingVtxoScript, err := bitcointree.ParseVtxoScript(boardingInput.Tapscripts)
|
||||
if err != nil {
|
||||
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) {
|
||||
for _, leaf := range input.TaprootLeafScript {
|
||||
closure := &bitcointree.CSVSigClosure{}
|
||||
closure := &tree.CSVSigClosure{}
|
||||
valid, err := closure.Decode(leaf.Script)
|
||||
if err != nil {
|
||||
return nil, nil, 0, err
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"fmt"
|
||||
|
||||
"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/btcsuite/btcd/btcec/v2/schnorr"
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
@@ -27,7 +27,7 @@ func sweepTransaction(
|
||||
Index: input.GetIndex(),
|
||||
})
|
||||
|
||||
sweepClosure := bitcointree.CSVSigClosure{}
|
||||
sweepClosure := tree.CSVSigClosure{}
|
||||
valid, err := sweepClosure.Decode(input.GetLeafScript())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"time"
|
||||
|
||||
"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/ports"
|
||||
"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 {
|
||||
isTaproot := txscript.IsPayToTaproot(in.WitnessUtxo.PkScript)
|
||||
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 {
|
||||
return "", err
|
||||
}
|
||||
|
||||
witness := make(wire.TxWitness, 4)
|
||||
signatures := make(map[string][]byte)
|
||||
|
||||
castClosure, isTaprootMultisig := closure.(*bitcointree.MultisigClosure)
|
||||
if isTaprootMultisig {
|
||||
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
|
||||
for _, sig := range in.TaprootScriptSpendSig {
|
||||
signatures[hex.EncodeToString(sig.XOnlyPubKey)] = sig.Signature
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
pb "github.com/ark-network/ark/api-spec/protobuf/gen/ocean/v1"
|
||||
"github.com/ark-network/ark/common/tree"
|
||||
"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/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
@@ -58,43 +57,29 @@ func (s *service) SignTransaction(
|
||||
return "", err
|
||||
}
|
||||
|
||||
switch c := closure.(type) {
|
||||
case *tree.MultisigClosure:
|
||||
asp := schnorr.SerializePubKey(c.AspPubkey)
|
||||
owner := schnorr.SerializePubKey(c.Pubkey)
|
||||
signatures := make(map[string][]byte)
|
||||
|
||||
witness := make([][]byte, 4)
|
||||
for _, sig := range in.TapScriptSig {
|
||||
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)
|
||||
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 {
|
||||
|
||||
@@ -3,9 +3,12 @@ package handlers
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
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/domain"
|
||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||
@@ -43,6 +46,15 @@ func (h *handler) GetInfo(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
desc := fmt.Sprintf(
|
||||
descriptor.DefaultVtxoDescriptorTemplate,
|
||||
hex.EncodeToString(bitcointree.UnspendableKey().SerializeCompressed()),
|
||||
"USER",
|
||||
info.PubKey,
|
||||
info.UnilateralExitDelay,
|
||||
info.PubKey,
|
||||
)
|
||||
|
||||
return &arkv1.GetInfoResponse{
|
||||
Pubkey: info.PubKey,
|
||||
RoundLifetime: info.RoundLifetime,
|
||||
@@ -50,8 +62,9 @@ func (h *handler) GetInfo(
|
||||
RoundInterval: info.RoundInterval,
|
||||
Network: info.Network,
|
||||
Dust: int64(info.Dust),
|
||||
BoardingDescriptorTemplate: info.BoardingDescriptorTemplate,
|
||||
ForfeitAddress: info.ForfeitAddress,
|
||||
BoardingDescriptorTemplate: desc,
|
||||
VtxoDescriptorTemplates: []string{desc},
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -73,14 +86,18 @@ func (h *handler) GetBoardingAddress(
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &arkv1.GetBoardingAddressResponse{
|
||||
Address: addr,
|
||||
Descriptor_: descriptor,
|
||||
Address: addr,
|
||||
TaprootTree: &arkv1.GetBoardingAddressResponse_Tapscripts{
|
||||
Tapscripts: &arkv1.Tapscripts{
|
||||
Scripts: tapscripts,
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ func parseAsyncPaymentInputs(ins []*arkv1.AsyncPaymentInput) ([]application.Asyn
|
||||
Txid: input.GetInput().GetOutpoint().GetTxid(),
|
||||
VOut: input.GetInput().GetOutpoint().GetVout(),
|
||||
},
|
||||
Descriptor: input.GetInput().GetDescriptor_(),
|
||||
Tapscripts: input.GetInput().GetTapscripts().GetScripts(),
|
||||
},
|
||||
ForfeitLeafHash: *forfeitLeafHash,
|
||||
})
|
||||
@@ -82,7 +82,7 @@ func parseInputs(ins []*arkv1.Input) ([]ports.Input, error) {
|
||||
Txid: input.GetOutpoint().GetTxid(),
|
||||
VOut: input.GetOutpoint().GetVout(),
|
||||
},
|
||||
Descriptor: input.GetDescriptor_(),
|
||||
Tapscripts: input.GetTapscripts().GetScripts(),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user