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

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