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

* migrate descriptors --> tapscripts

* fix covenantless

* dynamic boarding exit delay

* remove duplicates in tree and bitcointree

* agnostic signatures validation

* revert GetInfo change

* renaming VtxoScript var

* Agnostic script server (#6)

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

* Hotfix bct embedded wallet w/ ZMQ

* Fixes

* Rename vtxo is_oor to is_pending (#385)

* Rename vtxo is_oor > is_pending

* Clean swaggers

* Revert changes to client and sdk

* descriptor in oneof

* support CHECKSIG_ADD in MultisigClosure

* use right witness size in OOR tx fee estimation

* Revert changes

---------

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

View File

@@ -651,6 +651,9 @@
},
"descriptor": {
"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": {

View File

@@ -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;
@@ -379,3 +385,7 @@ message DeleteNostrRecipientRequest {
}
message DeleteNostrRecipientResponse {}
message Tapscripts {
repeated string scripts = 1;
}

File diff suppressed because it is too large Load Diff

View File

@@ -282,17 +282,19 @@ func createRootNode(
func createAggregatedKeyWithSweep(
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(

View File

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

View File

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

View File

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

View File

@@ -72,7 +72,7 @@ func UnspendableKey() *secp256k1.PublicKey {
// - every control block and taproot output scripts
// - 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
}

View File

@@ -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(),

View File

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

View File

@@ -163,11 +163,11 @@ func (n *node) getWitnessData() (
}
sweepClosure := &CSVSigClosure{
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()

View File

@@ -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
View File

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

View File

@@ -238,8 +238,8 @@ func validateNodeTransaction(
switch c := closure.(type) {
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

View File

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

View File

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

View File

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

View File

@@ -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,
},
},
}
}

View File

@@ -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(),
})

View File

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

View File

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

View File

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

View File

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

View File

@@ -22,7 +22,6 @@ import (
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/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)
}

View File

@@ -857,7 +857,7 @@ func (a *covenantlessArkClient) CollaborativeRedeem(
},
}
vtxos := make([]client.DescriptorVtxo, 0)
vtxos := make([]client.TapscriptsVtxo, 0)
spendableVtxos, err := a.getVtxos(ctx, nil)
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
}

View File

@@ -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,
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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(),
})
}