Add reversible policy to pending vtxos (#311)

* [server] descriptor-based vtxo script

* [server] fix unit tests

* [sdk] descriptor based vtxo

* empty config check & version flag support

* fix: empty config check & version flag support (#309)

* fix

* [sdk] several fixes

* [sdk][server] several fixes

* [common][sdk] add reversible VtxoScript type, use it in async payment

* [common] improve parser

* [common] fix reversible vtxo parser

* [sdk] remove logs

* fix forfeit map

* remove debug log

* [sdk] do not allow reversible vtxo script in case of self-transfer

* remove signing pubkey

* remove signer public key, craft forfeit txs client side

* go work sync

* fix linter errors

* rename MakeForfeitTxs to BuildForfeitTxs

* fix conflicts

* fix tests

* comment VtxoScript type

* revert ROUND_INTERVAL value

---------

Co-authored-by: Pietralberto Mazza <18440657+altafan@users.noreply.github.com>
Co-authored-by: sekulicd <sekula87@gmail.com>
This commit is contained in:
Louis Singer
2024-09-19 10:01:33 +02:00
committed by GitHub
parent 7a83f9957e
commit 10ef0dbffa
82 changed files with 3440 additions and 2612 deletions

View File

@@ -7,6 +7,7 @@ import (
"github.com/ark-network/ark/common/bitcointree"
"github.com/ark-network/ark/common/tree"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
)
const (
@@ -37,7 +38,7 @@ type ASPClient interface {
ctx context.Context, signedForfeitTxs []string, signedRoundTx string,
) error
CreatePayment(
ctx context.Context, inputs []VtxoKey, outputs []Output,
ctx context.Context, inputs []Input, outputs []Output,
) (string, []string, error)
CompletePayment(
ctx context.Context, signedRedeemTx string, signedUnconditionalForfeitTxs []string,
@@ -67,40 +68,19 @@ type RoundEventChannel struct {
Err error
}
type Input interface {
GetTxID() string
GetVOut() uint32
GetDescriptor() string
}
type VtxoKey struct {
type Outpoint struct {
Txid string
VOut uint32
}
func (k VtxoKey) GetTxID() string {
return k.Txid
}
func (k VtxoKey) GetVOut() uint32 {
return k.VOut
}
func (k VtxoKey) GetDescriptor() string {
return ""
}
type BoardingInput struct {
VtxoKey
type Input struct {
Outpoint
Descriptor string
}
func (k BoardingInput) GetDescriptor() string {
return k.Descriptor
}
type Vtxo struct {
VtxoKey
Outpoint
Descriptor string
Amount uint64
RoundTxid string
ExpiresAt *time.Time
@@ -111,8 +91,9 @@ type Vtxo struct {
}
type Output struct {
Address string
Amount uint64
Address string // onchain output address
Descriptor string // offchain vtxo descriptor
Amount uint64
}
type RoundStage int
@@ -152,11 +133,11 @@ type Round struct {
}
type RoundFinalizationEvent struct {
ID string
Tx string
ForfeitTxs []string
Tree tree.CongestionTree
Connectors []string
ID string
Tx string
Tree tree.CongestionTree
Connectors []string
MinRelayFeeRate chainfee.SatPerKVByte
}
func (e RoundFinalizationEvent) isRoundEvent() {}

View File

@@ -14,6 +14,7 @@ import (
"github.com/ark-network/ark/pkg/client-sdk/client"
"github.com/ark-network/ark/pkg/client-sdk/internal/utils"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/credentials/insecure"
@@ -202,15 +203,10 @@ func (a *grpcClient) FinalizePayment(
}
func (a *grpcClient) CreatePayment(
ctx context.Context, inputs []client.VtxoKey, outputs []client.Output,
ctx context.Context, inputs []client.Input, outputs []client.Output,
) (string, []string, error) {
insCast := make([]client.Input, 0, len(inputs))
for _, in := range inputs {
insCast = append(insCast, in)
}
req := &arkv1.CreatePaymentRequest{
Inputs: ins(insCast).toProto(),
Inputs: ins(inputs).toProto(),
Outputs: outs(outputs).toProto(),
}
resp, err := a.svc.CreatePayment(ctx, req)
@@ -323,9 +319,20 @@ func (a *grpcClient) SendTreeSignatures(
type out client.Output
func (o out) toProto() *arkv1.Output {
if len(o.Address) > 0 {
return &arkv1.Output{
Destination: &arkv1.Output_Address{
Address: o.Address,
},
Amount: o.Amount,
}
}
return &arkv1.Output{
Address: o.Address,
Amount: o.Amount,
Destination: &arkv1.Output_Descriptor_{
Descriptor_: o.Descriptor,
},
Amount: o.Amount,
}
}
@@ -362,11 +369,11 @@ func (e event) toRoundEvent() (client.RoundEvent, error) {
if ee := e.GetRoundFinalization(); ee != nil {
tree := treeFromProto{ee.GetCongestionTree()}.parse()
return client.RoundFinalizationEvent{
ID: ee.GetId(),
Tx: ee.GetPoolTx(),
ForfeitTxs: ee.GetForfeitTxs(),
Tree: tree,
Connectors: ee.GetConnectors(),
ID: ee.GetId(),
Tx: ee.GetPoolTx(),
Tree: tree,
Connectors: ee.GetConnectors(),
MinRelayFeeRate: chainfee.SatPerKVByte(ee.MinRelayFeeRate),
}, nil
}
@@ -430,17 +437,18 @@ func (v vtxo) toVtxo() client.Vtxo {
uncondForfeitTxs = v.GetPendingData().GetUnconditionalForfeitTxs()
}
return client.Vtxo{
VtxoKey: client.VtxoKey{
Txid: v.GetOutpoint().GetVtxoInput().GetTxid(),
VOut: v.GetOutpoint().GetVtxoInput().GetVout(),
Outpoint: client.Outpoint{
Txid: v.GetOutpoint().GetTxid(),
VOut: v.GetOutpoint().GetVout(),
},
Amount: v.GetReceiver().GetAmount(),
Amount: v.GetAmount(),
RoundTxid: v.GetPoolTxid(),
ExpiresAt: expiresAt,
Pending: v.GetPending(),
RedeemTx: redeemTx,
UnconditionalForfeitTxs: uncondForfeitTxs,
SpentBy: v.GetSpentBy(),
Descriptor: v.GetDescriptor_(),
}
}
@@ -455,25 +463,12 @@ func (v vtxos) toVtxos() []client.Vtxo {
}
func toProtoInput(i client.Input) *arkv1.Input {
if len(i.GetDescriptor()) > 0 {
return &arkv1.Input{
Input: &arkv1.Input_BoardingInput{
BoardingInput: &arkv1.BoardingInput{
Txid: i.GetTxID(),
Vout: i.GetVOut(),
Descriptor_: i.GetDescriptor(),
},
},
}
}
return &arkv1.Input{
Input: &arkv1.Input_VtxoInput{
VtxoInput: &arkv1.VtxoInput{
Txid: i.GetTxID(),
Vout: i.GetVOut(),
},
Outpoint: &arkv1.Outpoint{
Txid: i.Txid,
Vout: i.VOut,
},
Descriptor_: i.Descriptor,
}
}

View File

@@ -21,6 +21,7 @@ import (
"github.com/decred/dcrd/dcrec/secp256k1/v4"
httptransport "github.com/go-openapi/runtime/client"
"github.com/go-openapi/strfmt"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
)
type restClient struct {
@@ -146,7 +147,7 @@ func (a *restClient) ListVtxos(
expiresAt = &t
}
amount, err := strconv.Atoi(v.Receiver.Amount)
amount, err := strconv.Atoi(v.Amount)
if err != nil {
return nil, nil, err
}
@@ -159,9 +160,9 @@ func (a *restClient) ListVtxos(
}
spendableVtxos = append(spendableVtxos, client.Vtxo{
VtxoKey: client.VtxoKey{
Txid: v.Outpoint.VtxoInput.Txid,
VOut: uint32(v.Outpoint.VtxoInput.Vout),
Outpoint: client.Outpoint{
Txid: v.Outpoint.Txid,
VOut: uint32(v.Outpoint.Vout),
},
Amount: uint64(amount),
RoundTxid: v.PoolTxid,
@@ -170,6 +171,7 @@ func (a *restClient) ListVtxos(
RedeemTx: redeemTx,
UnconditionalForfeitTxs: uncondForfeitTxs,
SpentBy: v.SpentBy,
Descriptor: v.Descriptor,
})
}
@@ -185,20 +187,21 @@ func (a *restClient) ListVtxos(
expiresAt = &t
}
amount, err := strconv.Atoi(v.Receiver.Amount)
amount, err := strconv.Atoi(v.Amount)
if err != nil {
return nil, nil, err
}
spentVtxos = append(spentVtxos, client.Vtxo{
VtxoKey: client.VtxoKey{
Txid: v.Outpoint.VtxoInput.Txid,
VOut: uint32(v.Outpoint.VtxoInput.Vout),
Outpoint: client.Outpoint{
Txid: v.Outpoint.Txid,
VOut: uint32(v.Outpoint.Vout),
},
Amount: uint64(amount),
RoundTxid: v.PoolTxid,
ExpiresAt: expiresAt,
SpentBy: v.SpentBy,
Amount: uint64(amount),
RoundTxid: v.PoolTxid,
ExpiresAt: expiresAt,
SpentBy: v.SpentBy,
Descriptor: v.Descriptor,
})
}
@@ -249,26 +252,13 @@ func (a *restClient) RegisterPayment(
) (string, error) {
ins := make([]*models.V1Input, 0, len(inputs))
for _, i := range inputs {
var input *models.V1Input
if len(i.GetDescriptor()) > 0 {
input = &models.V1Input{
BoardingInput: &models.V1BoardingInput{
Txid: i.GetTxID(),
Vout: int64(i.GetVOut()),
Descriptor: i.GetDescriptor(),
},
}
} else {
input = &models.V1Input{
VtxoInput: &models.V1VtxoInput{
Txid: i.GetTxID(),
Vout: int64(i.GetVOut()),
},
}
}
ins = append(ins, input)
ins = append(ins, &models.V1Input{
Outpoint: &models.V1Outpoint{
Txid: i.Txid,
Vout: int64(i.VOut),
},
Descriptor: i.Descriptor,
})
}
body := &models.V1RegisterPaymentRequest{
Inputs: ins,
@@ -328,12 +318,18 @@ func (a *restClient) Ping(
}
if e := payload.RoundFinalization; e != nil {
tree := treeFromProto{e.CongestionTree}.parse()
minRelayFeeRate, err := strconv.Atoi(e.MinRelayFeeRate)
if err != nil {
return nil, err
}
return client.RoundFinalizationEvent{
ID: e.ID,
Tx: e.PoolTx,
ForfeitTxs: e.ForfeitTxs,
Tree: tree,
Connectors: e.Connectors,
ID: e.ID,
Tx: e.PoolTx,
Tree: tree,
Connectors: e.Connectors,
MinRelayFeeRate: chainfee.SatPerKVByte(minRelayFeeRate),
}, nil
}
@@ -394,26 +390,24 @@ func (a *restClient) FinalizePayment(
}
func (a *restClient) CreatePayment(
ctx context.Context, inputs []client.VtxoKey, outputs []client.Output,
ctx context.Context, inputs []client.Input, outputs []client.Output,
) (string, []string, error) {
ins := make([]*models.V1Input, 0, len(inputs))
for _, i := range inputs {
if len(i.GetDescriptor()) > 0 {
return "", nil, fmt.Errorf("boarding inputs are not allowed in create payment")
}
ins = append(ins, &models.V1Input{
VtxoInput: &models.V1VtxoInput{
Outpoint: &models.V1Outpoint{
Txid: i.Txid,
Vout: int64(i.VOut),
},
Descriptor: i.Descriptor,
})
}
outs := make([]*models.V1Output, 0, len(outputs))
for _, o := range outputs {
outs = append(outs, &models.V1Output{
Address: o.Address,
Amount: strconv.Itoa(int(o.Amount)),
Address: o.Address,
Amount: strconv.Itoa(int(o.Amount)),
Descriptor: o.Descriptor,
})
}
body := models.V1CreatePaymentRequest{

View File

@@ -18,22 +18,18 @@ import (
// swagger:model v1Input
type V1Input struct {
// boarding input
BoardingInput *V1BoardingInput `json:"boardingInput,omitempty"`
// descriptor
Descriptor string `json:"descriptor,omitempty"`
// vtxo input
VtxoInput *V1VtxoInput `json:"vtxoInput,omitempty"`
// outpoint
Outpoint *V1Outpoint `json:"outpoint,omitempty"`
}
// Validate validates this v1 input
func (m *V1Input) Validate(formats strfmt.Registry) error {
var res []error
if err := m.validateBoardingInput(formats); err != nil {
res = append(res, err)
}
if err := m.validateVtxoInput(formats); err != nil {
if err := m.validateOutpoint(formats); err != nil {
res = append(res, err)
}
@@ -43,36 +39,17 @@ func (m *V1Input) Validate(formats strfmt.Registry) error {
return nil
}
func (m *V1Input) validateBoardingInput(formats strfmt.Registry) error {
if swag.IsZero(m.BoardingInput) { // not required
func (m *V1Input) validateOutpoint(formats strfmt.Registry) error {
if swag.IsZero(m.Outpoint) { // not required
return nil
}
if m.BoardingInput != nil {
if err := m.BoardingInput.Validate(formats); err != nil {
if m.Outpoint != nil {
if err := m.Outpoint.Validate(formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("boardingInput")
return ve.ValidateName("outpoint")
} else if ce, ok := err.(*errors.CompositeError); ok {
return ce.ValidateName("boardingInput")
}
return err
}
}
return nil
}
func (m *V1Input) validateVtxoInput(formats strfmt.Registry) error {
if swag.IsZero(m.VtxoInput) { // not required
return nil
}
if m.VtxoInput != nil {
if err := m.VtxoInput.Validate(formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("vtxoInput")
} else if ce, ok := err.(*errors.CompositeError); ok {
return ce.ValidateName("vtxoInput")
return ce.ValidateName("outpoint")
}
return err
}
@@ -85,11 +62,7 @@ func (m *V1Input) validateVtxoInput(formats strfmt.Registry) error {
func (m *V1Input) ContextValidate(ctx context.Context, formats strfmt.Registry) error {
var res []error
if err := m.contextValidateBoardingInput(ctx, formats); err != nil {
res = append(res, err)
}
if err := m.contextValidateVtxoInput(ctx, formats); err != nil {
if err := m.contextValidateOutpoint(ctx, formats); err != nil {
res = append(res, err)
}
@@ -99,40 +72,19 @@ func (m *V1Input) ContextValidate(ctx context.Context, formats strfmt.Registry)
return nil
}
func (m *V1Input) contextValidateBoardingInput(ctx context.Context, formats strfmt.Registry) error {
func (m *V1Input) contextValidateOutpoint(ctx context.Context, formats strfmt.Registry) error {
if m.BoardingInput != nil {
if m.Outpoint != nil {
if swag.IsZero(m.BoardingInput) { // not required
if swag.IsZero(m.Outpoint) { // not required
return nil
}
if err := m.BoardingInput.ContextValidate(ctx, formats); err != nil {
if err := m.Outpoint.ContextValidate(ctx, formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("boardingInput")
return ve.ValidateName("outpoint")
} else if ce, ok := err.(*errors.CompositeError); ok {
return ce.ValidateName("boardingInput")
}
return err
}
}
return nil
}
func (m *V1Input) contextValidateVtxoInput(ctx context.Context, formats strfmt.Registry) error {
if m.VtxoInput != nil {
if swag.IsZero(m.VtxoInput) { // not required
return nil
}
if err := m.VtxoInput.ContextValidate(ctx, formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("vtxoInput")
} else if ce, ok := err.(*errors.CompositeError); ok {
return ce.ValidateName("vtxoInput")
return ce.ValidateName("outpoint")
}
return err
}

View File

@@ -0,0 +1,53 @@
// 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"
)
// V1Outpoint v1 outpoint
//
// swagger:model v1Outpoint
type V1Outpoint struct {
// txid
Txid string `json:"txid,omitempty"`
// vout
Vout int64 `json:"vout,omitempty"`
}
// Validate validates this v1 outpoint
func (m *V1Outpoint) Validate(formats strfmt.Registry) error {
return nil
}
// ContextValidate validates this v1 outpoint based on context it is used
func (m *V1Outpoint) ContextValidate(ctx context.Context, formats strfmt.Registry) error {
return nil
}
// MarshalBinary interface implementation
func (m *V1Outpoint) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
return swag.WriteJSON(m)
}
// UnmarshalBinary interface implementation
func (m *V1Outpoint) UnmarshalBinary(b []byte) error {
var res V1Outpoint
if err := swag.ReadJSON(b, &res); err != nil {
return err
}
*m = res
return nil
}

View File

@@ -17,11 +17,14 @@ import (
// swagger:model v1Output
type V1Output struct {
// Either the offchain or onchain address.
// onchain
Address string `json:"address,omitempty"`
// Amount to send in satoshis.
Amount string `json:"amount,omitempty"`
// offchain
Descriptor string `json:"descriptor,omitempty"`
}
// Validate validates this v1 output

View File

@@ -24,12 +24,12 @@ type V1RoundFinalizationEvent struct {
// connectors
Connectors []string `json:"connectors"`
// forfeit txs
ForfeitTxs []string `json:"forfeitTxs"`
// id
ID string `json:"id,omitempty"`
// min relay fee rate
MinRelayFeeRate string `json:"minRelayFeeRate,omitempty"`
// pool tx
PoolTx string `json:"poolTx,omitempty"`
}

View File

@@ -18,11 +18,17 @@ import (
// swagger:model v1Vtxo
type V1Vtxo struct {
// amount
Amount string `json:"amount,omitempty"`
// descriptor
Descriptor string `json:"descriptor,omitempty"`
// expire at
ExpireAt string `json:"expireAt,omitempty"`
// outpoint
Outpoint *V1Input `json:"outpoint,omitempty"`
Outpoint *V1Outpoint `json:"outpoint,omitempty"`
// pending
Pending bool `json:"pending,omitempty"`
@@ -33,9 +39,6 @@ type V1Vtxo struct {
// pool txid
PoolTxid string `json:"poolTxid,omitempty"`
// receiver
Receiver *V1Output `json:"receiver,omitempty"`
// spent
Spent bool `json:"spent,omitempty"`
@@ -58,10 +61,6 @@ func (m *V1Vtxo) Validate(formats strfmt.Registry) error {
res = append(res, err)
}
if err := m.validateReceiver(formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
@@ -106,25 +105,6 @@ func (m *V1Vtxo) validatePendingData(formats strfmt.Registry) error {
return nil
}
func (m *V1Vtxo) validateReceiver(formats strfmt.Registry) error {
if swag.IsZero(m.Receiver) { // not required
return nil
}
if m.Receiver != nil {
if err := m.Receiver.Validate(formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("receiver")
} else if ce, ok := err.(*errors.CompositeError); ok {
return ce.ValidateName("receiver")
}
return err
}
}
return nil
}
// ContextValidate validate this v1 vtxo based on the context it is used
func (m *V1Vtxo) ContextValidate(ctx context.Context, formats strfmt.Registry) error {
var res []error
@@ -137,10 +117,6 @@ func (m *V1Vtxo) ContextValidate(ctx context.Context, formats strfmt.Registry) e
res = append(res, err)
}
if err := m.contextValidateReceiver(ctx, formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
@@ -189,27 +165,6 @@ func (m *V1Vtxo) contextValidatePendingData(ctx context.Context, formats strfmt.
return nil
}
func (m *V1Vtxo) contextValidateReceiver(ctx context.Context, formats strfmt.Registry) error {
if m.Receiver != nil {
if swag.IsZero(m.Receiver) { // not required
return nil
}
if err := m.Receiver.ContextValidate(ctx, formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("receiver")
} else if ce, ok := err.(*errors.CompositeError); ok {
return ce.ValidateName("receiver")
}
return err
}
}
return nil
}
// MarshalBinary interface implementation
func (m *V1Vtxo) MarshalBinary() ([]byte, error) {
if m == nil {

View File

@@ -226,7 +226,7 @@ func loadFixtures(jsonStr string) (vtxos, []Transaction, error) {
return vtxos{}, nil, err
}
spendable[i] = client.Vtxo{
VtxoKey: client.VtxoKey{
Outpoint: client.Outpoint{
Txid: vtxo.Outpoint.Txid,
VOut: vtxo.Outpoint.Vout,
},
@@ -251,7 +251,7 @@ func loadFixtures(jsonStr string) (vtxos, []Transaction, error) {
return vtxos{}, nil, err
}
spent[i] = client.Vtxo{
VtxoKey: client.VtxoKey{
Outpoint: client.Outpoint{
Txid: vtxo.Outpoint.Txid,
VOut: vtxo.Outpoint.Vout,
},

View File

@@ -12,7 +12,6 @@ import (
"time"
"github.com/ark-network/ark/common"
"github.com/ark-network/ark/common/descriptor"
"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"
@@ -23,9 +22,11 @@ import (
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
log "github.com/sirupsen/logrus"
"github.com/vulpemventures/go-elements/address"
"github.com/vulpemventures/go-elements/psetv2"
"github.com/vulpemventures/go-elements/taproot"
)
type liquidReceiver struct {
@@ -435,18 +436,27 @@ func (a *covenantArkClient) CollaborativeRedeem(
if err != nil {
return "", err
}
desc, err := a.offchainAddressToDefaultVtxoDescriptor(offchainAddr)
if err != nil {
return "", err
}
receivers = append(receivers, client.Output{
Address: offchainAddr,
Amount: changeAmount,
Descriptor: desc,
Amount: changeAmount,
})
}
inputs := make([]client.Input, 0, len(selectedCoins))
for _, coin := range selectedCoins {
inputs = append(inputs, client.VtxoKey{
Txid: coin.Txid,
VOut: coin.VOut,
inputs = append(inputs, client.Input{
Outpoint: client.Outpoint{
Txid: coin.Txid,
VOut: coin.VOut,
},
Descriptor: coin.Descriptor,
})
}
@@ -460,7 +470,7 @@ func (a *covenantArkClient) CollaborativeRedeem(
}
poolTxID, err := a.handleRoundStream(
ctx, paymentID, selectedCoins, false, receivers,
ctx, paymentID, selectedCoins, nil, "", receivers,
)
if err != nil {
return "", err
@@ -500,14 +510,22 @@ func (a *covenantArkClient) Claim(ctx context.Context) (string, error) {
return "", fmt.Errorf("no funds to claim")
}
receiver := client.Output{
Address: myselfOffchain,
Amount: pendingBalance,
desc, err := a.offchainAddressToDefaultVtxoDescriptor(myselfOffchain)
if err != nil {
return "", err
}
desc := strings.ReplaceAll(a.BoardingDescriptorTemplate, "USER", hex.EncodeToString(schnorr.SerializePubKey(mypubkey)))
receiver := client.Output{
Descriptor: desc,
Amount: pendingBalance,
}
return a.selfTransferAllPendingPayments(ctx, boardingUtxos, receiver, desc)
return a.selfTransferAllPendingPayments(
ctx,
boardingUtxos,
receiver,
hex.EncodeToString(mypubkey.SerializeCompressed()),
)
}
func (a *covenantArkClient) GetTransactionHistory(ctx context.Context) ([]Transaction, error) {
@@ -577,14 +595,18 @@ func (a *covenantArkClient) getClaimableBoardingUtxos(ctx context.Context) ([]ex
descriptorStr := strings.ReplaceAll(
a.BoardingDescriptorTemplate, "USER", myPubkeyStr,
)
desc, err := descriptor.ParseTaprootDescriptor(descriptorStr)
boardingScript, err := tree.ParseVtxoScript(descriptorStr)
if err != nil {
return nil, err
}
_, boardingTimeout, err := descriptor.ParseBoardingDescriptor(*desc)
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", descriptorStr)
}
for _, addr := range boardingAddrs {
@@ -800,9 +822,14 @@ func (a *covenantArkClient) sendOffchain(
return "", fmt.Errorf("invalid amount (%d), must be greater than dust %d", receiver.Amount(), a.Dust)
}
desc, err := a.offchainAddressToDefaultVtxoDescriptor(receiver.To())
if err != nil {
return "", err
}
receiversOutput = append(receiversOutput, client.Output{
Address: receiver.To(),
Amount: receiver.Amount(),
Descriptor: desc,
Amount: receiver.Amount(),
})
sumOfReceivers += receiver.Amount()
}
@@ -828,18 +855,27 @@ func (a *covenantArkClient) sendOffchain(
if err != nil {
return "", err
}
desc, err := a.offchainAddressToDefaultVtxoDescriptor(offchainAddr)
if err != nil {
return "", err
}
changeReceiver := client.Output{
Address: offchainAddr,
Amount: changeAmount,
Descriptor: desc,
Amount: changeAmount,
}
receiversOutput = append(receiversOutput, changeReceiver)
}
inputs := make([]client.Input, 0, len(selectedCoins))
for _, coin := range selectedCoins {
inputs = append(inputs, client.VtxoKey{
Txid: coin.Txid,
VOut: coin.VOut,
inputs = append(inputs, client.Input{
Outpoint: client.Outpoint{
Txid: coin.Txid,
VOut: coin.VOut,
},
Descriptor: coin.Descriptor,
})
}
@@ -859,7 +895,7 @@ func (a *covenantArkClient) sendOffchain(
log.Infof("payment registered with id: %s", paymentID)
poolTxID, err := a.handleRoundStream(
ctx, paymentID, selectedCoins, false, receiversOutput,
ctx, paymentID, selectedCoins, nil, "", receiversOutput,
)
if err != nil {
return "", err
@@ -900,16 +936,46 @@ func (a *covenantArkClient) addInputs(
return err
}
_, leafProof, _, _, err := tree.ComputeVtxoTaprootScript(
userPubkey, aspPubkey, utxo.Delay, utils.ToElementsNetwork(a.Network),
)
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(inputIndex, psetv2.NewTapLeafScript(*leafProof, tree.UnspendableKey())); err != nil {
if err := updater.AddInTapLeafScript(
inputIndex,
psetv2.TapLeafScript{
TapElementsLeaf: taproot.NewBaseTapElementsLeaf(leafProof.Script),
ControlBlock: *controlBlock,
},
); err != nil {
return err
}
}
@@ -921,7 +987,8 @@ func (a *covenantArkClient) handleRoundStream(
ctx context.Context,
paymentID string,
vtxosToSign []client.Vtxo,
mustSignRoundTx bool,
boardingUtxos []explorer.Utxo,
boardingDescriptor string,
receivers []client.Output,
) (string, error) {
eventsCh, err := a.client.GetEventStream(ctx, paymentID)
@@ -955,7 +1022,7 @@ func (a *covenantArkClient) handleRoundStream(
log.Info("a round finalization started")
signedForfeitTxs, signedRoundTx, err := a.handleRoundFinalization(
ctx, event.(client.RoundFinalizationEvent), vtxosToSign, mustSignRoundTx, receivers,
ctx, event.(client.RoundFinalizationEvent), vtxosToSign, boardingUtxos, boardingDescriptor, receivers,
)
if err != nil {
return "", err
@@ -979,30 +1046,104 @@ func (a *covenantArkClient) handleRoundStream(
}
func (a *covenantArkClient) handleRoundFinalization(
ctx context.Context, event client.RoundFinalizationEvent,
vtxos []client.Vtxo, mustSignRoundTx bool, receivers []client.Output,
ctx context.Context,
event client.RoundFinalizationEvent,
vtxos []client.Vtxo,
boardingUtxos []explorer.Utxo,
boardingDescriptor string,
receivers []client.Output,
) (signedForfeits []string, signedRoundTx string, err error) {
if err = a.validateCongestionTree(event, receivers); err != nil {
return
}
offchainAddr, _, err := a.wallet.NewAddress(ctx, false)
if err != nil {
return
}
_, myPubkey, _, err := common.DecodeAddress(offchainAddr)
if err != nil {
return
}
if len(vtxos) > 0 {
signedForfeits, err = a.loopAndSign(
ctx, event.ForfeitTxs, vtxos, event.Connectors,
)
signedForfeits, err = a.createAndSignForfeits(ctx, vtxos, event.Connectors, event.MinRelayFeeRate, myPubkey)
if err != nil {
return
}
}
if mustSignRoundTx {
signedRoundTx, err = a.wallet.SignTransaction(ctx, a.explorer, event.Tx)
if len(boardingUtxos) > 0 {
boardingVtxoScript, err := tree.ParseVtxoScript(boardingDescriptor)
if err != nil {
return
return nil, "", err
}
roundPtx, err := psetv2.NewPsetFromBase64(event.Tx)
if err != nil {
return nil, "", err
}
// add tapscript leaf
forfeitClosure := &tree.MultisigClosure{
Pubkey: myPubkey,
AspPubkey: a.AspPubkey,
}
forfeitLeaf, err := forfeitClosure.Leaf()
if err != nil {
return nil, "", err
}
_, taprootTree, err := boardingVtxoScript.TapTree()
if err != nil {
return nil, "", err
}
forfeitProof, err := taprootTree.GetTaprootMerkleProof(forfeitLeaf.TapHash())
if err != nil {
return nil, "", err
}
ctrlBlock, err := taproot.ParseControlBlock(forfeitProof.ControlBlock)
if err != nil {
return nil, "", err
}
tapscript := psetv2.TapLeafScript{
TapElementsLeaf: taproot.NewBaseTapElementsLeaf(forfeitProof.Script),
ControlBlock: *ctrlBlock,
}
updater, err := psetv2.NewUpdater(roundPtx)
if err != nil {
return nil, "", err
}
for i, input := range updater.Pset.Inputs {
for _, boardingUtxo := range boardingUtxos {
if chainhash.Hash(input.PreviousTxid).String() == boardingUtxo.Txid && boardingUtxo.Vout == input.PreviousTxIndex {
if err := updater.AddInTapLeafScript(i, tapscript); err != nil {
return nil, "", err
}
break
}
}
}
b64, err := updater.Pset.ToBase64()
if err != nil {
return nil, "", err
}
signedRoundTx, err = a.wallet.SignTransaction(ctx, a.explorer, b64)
if err != nil {
return nil, "", err
}
}
return
return signedForfeits, signedRoundTx, nil
}
func (a *covenantArkClient) validateCongestionTree(
@@ -1016,7 +1157,7 @@ func (a *covenantArkClient) validateCongestionTree(
connectors := event.Connectors
if !utils.IsLiquidOnchainOnly(receivers) {
if !utils.IsOnchainOnly(receivers) {
if err := tree.ValidateCongestionTree(
event.Tree, poolTx, a.StoreData.AspPubkey, a.RoundLifetime,
); err != nil {
@@ -1029,7 +1170,7 @@ func (a *covenantArkClient) validateCongestionTree(
}
if err := a.validateReceivers(
ptx, receivers, event.Tree, a.StoreData.AspPubkey,
ptx, receivers, event.Tree,
); err != nil {
return err
}
@@ -1043,14 +1184,13 @@ func (a *covenantArkClient) validateReceivers(
ptx *psetv2.Pset,
receivers []client.Output,
congestionTree tree.CongestionTree,
aspPubkey *secp256k1.PublicKey,
) error {
for _, receiver := range receivers {
isOnChain, onchainScript, userPubkey, err := utils.ParseLiquidAddress(
isOnChain, onchainScript, err := utils.ParseLiquidAddress(
receiver.Address,
)
if err != nil {
return err
return fmt.Errorf("invalid receiver address: %s err = %s", receiver.Address, err)
}
if isOnChain {
@@ -1059,7 +1199,7 @@ func (a *covenantArkClient) validateReceivers(
}
} else {
if err := a.validateOffChainReceiver(
congestionTree, receiver, userPubkey, aspPubkey,
congestionTree, receiver,
); err != nil {
return err
}
@@ -1095,13 +1235,15 @@ func (a *covenantArkClient) validateOnChainReceiver(
func (a *covenantArkClient) validateOffChainReceiver(
congestionTree tree.CongestionTree,
receiver client.Output,
userPubkey, aspPubkey *secp256k1.PublicKey,
) error {
found := false
net := utils.ToElementsNetwork(a.Network)
outputTapKey, _, _, _, err := tree.ComputeVtxoTaprootScript(
userPubkey, aspPubkey, uint(a.UnilateralExitDelay), net,
)
receiverVtxoScript, err := tree.ParseVtxoScript(receiver.Descriptor)
if err != nil {
return err
}
outputTapKey, _, err := receiverVtxoScript.TapTree()
if err != nil {
return err
}
@@ -1136,36 +1278,105 @@ func (a *covenantArkClient) validateOffChainReceiver(
return nil
}
func (a *covenantArkClient) loopAndSign(
func (a *covenantArkClient) createAndSignForfeits(
ctx context.Context,
forfeitTxs []string, vtxosToSign []client.Vtxo, connectors []string,
vtxosToSign []client.Vtxo,
connectors []string,
feeRate chainfee.SatPerKVByte,
myPubKey *secp256k1.PublicKey,
) ([]string, error) {
signedForfeits := make([]string, 0)
connectorsPsets := make([]*psetv2.Pset, 0, len(connectors))
connectorsTxids := make([]string, 0, len(connectors))
for _, connector := range connectors {
p, _ := psetv2.NewPsetFromBase64(connector)
utx, _ := p.UnsignedTx()
txid := utx.TxHash().String()
connectorsTxids = append(connectorsTxids, txid)
}
for _, forfeitTx := range forfeitTxs {
pset, err := psetv2.NewPsetFromBase64(forfeitTx)
p, err := psetv2.NewPsetFromBase64(connector)
if err != nil {
return nil, err
}
for _, input := range pset.Inputs {
inputTxid := chainhash.Hash(input.PreviousTxid).String()
for _, coin := range vtxosToSign {
if inputTxid == coin.Txid {
signedPset, err := a.signForfeitTx(ctx, forfeitTx, pset, connectorsTxids)
if err != nil {
return nil, err
}
signedForfeits = append(signedForfeits, signedPset)
connectorsPsets = append(connectorsPsets, p)
}
for _, vtxo := range vtxosToSign {
vtxoScript, err := tree.ParseVtxoScript(vtxo.Descriptor)
if err != nil {
return nil, err
}
vtxoTapKey, vtxoTapTree, err := vtxoScript.TapTree()
if err != nil {
return nil, err
}
feeAmount, err := common.ComputeForfeitMinRelayFee(feeRate, vtxoTapTree)
if err != nil {
return nil, err
}
vtxoOutputScript, err := common.P2TRScript(vtxoTapKey)
if err != nil {
return nil, err
}
vtxoInput := psetv2.InputArgs{
Txid: vtxo.Txid,
TxIndex: vtxo.VOut,
}
forfeitClosure := &tree.MultisigClosure{
Pubkey: myPubKey,
AspPubkey: a.AspPubkey,
}
forfeitLeaf, err := forfeitClosure.Leaf()
if err != nil {
return nil, err
}
leafProof, err := vtxoTapTree.GetTaprootMerkleProof(forfeitLeaf.TapHash())
if err != nil {
return nil, err
}
ctrlBlock, err := taproot.ParseControlBlock(leafProof.ControlBlock)
if err != nil {
return nil, err
}
tapscript := psetv2.TapLeafScript{
TapElementsLeaf: taproot.NewBaseTapElementsLeaf(leafProof.Script),
ControlBlock: *ctrlBlock,
}
for _, connectorPset := range connectorsPsets {
forfeits, err := tree.BuildForfeitTxs(
connectorPset, vtxoInput, vtxo.Amount, a.Dust, feeAmount, vtxoOutputScript, a.AspPubkey,
)
if err != nil {
return nil, err
}
for _, forfeit := range forfeits {
updater, err := psetv2.NewUpdater(forfeit)
if err != nil {
return nil, err
}
if err := updater.AddInTapLeafScript(1, tapscript); err != nil {
return nil, err
}
b64, err := updater.Pset.ToBase64()
if err != nil {
return nil, err
}
signedForfeit, err := a.wallet.SignTransaction(ctx, a.explorer, b64)
if err != nil {
return nil, err
}
signedForfeits = append(signedForfeits, signedForfeit)
}
}
}
@@ -1173,24 +1384,6 @@ func (a *covenantArkClient) loopAndSign(
return signedForfeits, nil
}
func (a *covenantArkClient) signForfeitTx(
ctx context.Context, txStr string, tx *psetv2.Pset, connectorsTxids []string,
) (string, error) {
connectorTxid := chainhash.Hash(tx.Inputs[0].PreviousTxid).String()
connectorFound := false
for _, id := range connectorsTxids {
if id == connectorTxid {
connectorFound = true
break
}
}
if !connectorFound {
return "", fmt.Errorf("connector txid %s not found in the connectors list", connectorTxid)
}
return a.wallet.SignTransaction(ctx, a.explorer, txStr)
}
func (a *covenantArkClient) coinSelectOnchain(
ctx context.Context, targetAmount uint64, exclude []explorer.Utxo,
) ([]explorer.Utxo, uint64, error) {
@@ -1208,14 +1401,17 @@ func (a *covenantArkClient) coinSelectOnchain(
descriptorStr := strings.ReplaceAll(
a.BoardingDescriptorTemplate, "USER", myPubkeyStr,
)
desc, err := descriptor.ParseTaprootDescriptor(descriptorStr)
boardingScript, err := tree.ParseVtxoScript(descriptorStr)
if err != nil {
return nil, 0, err
}
_, boardingTimeout, err := descriptor.ParseBoardingDescriptor(*desc)
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", descriptorStr)
}
now := time.Now()
@@ -1387,13 +1583,17 @@ func (a *covenantArkClient) getVtxos(
}
func (a *covenantArkClient) selfTransferAllPendingPayments(
ctx context.Context, boardingUtxo []explorer.Utxo, myself client.Output, boardingDescriptor string,
ctx context.Context, boardingUtxos []explorer.Utxo, myself client.Output, mypubkey string,
) (string, error) {
inputs := make([]client.Input, 0, len(boardingUtxo))
inputs := make([]client.Input, 0, len(boardingUtxos))
for _, utxo := range boardingUtxo {
inputs = append(inputs, client.BoardingInput{
VtxoKey: client.VtxoKey{
boardingDescriptor := strings.ReplaceAll(
a.BoardingDescriptorTemplate, "USER", mypubkey[2:],
)
for _, utxo := range boardingUtxos {
inputs = append(inputs, client.Input{
Outpoint: client.Outpoint{
Txid: utxo.Txid,
VOut: utxo.Vout,
},
@@ -1413,7 +1613,7 @@ func (a *covenantArkClient) selfTransferAllPendingPayments(
}
roundTxid, err := a.handleRoundStream(
ctx, paymentID, make([]client.Vtxo, 0), len(boardingUtxo) > 0, outputs,
ctx, paymentID, make([]client.Vtxo, 0), boardingUtxos, boardingDescriptor, outputs,
)
if err != nil {
return "", err
@@ -1422,6 +1622,21 @@ func (a *covenantArkClient) selfTransferAllPendingPayments(
return roundTxid, nil
}
func (a *covenantArkClient) offchainAddressToDefaultVtxoDescriptor(addr string) (string, error) {
_, userPubKey, aspPubkey, err := common.DecodeAddress(addr)
if err != nil {
return "", err
}
vtxoScript := tree.DefaultVtxoScript{
Owner: userPubKey,
Asp: aspPubkey,
ExitDelay: uint(a.UnilateralExitDelay),
}
return vtxoScript.ToDescriptor(), nil
}
func (a *covenantArkClient) getBoardingTxs(ctx context.Context) (transactions []Transaction) {
utxos, err := a.getClaimableBoardingUtxos(ctx)
if err != nil {

View File

@@ -13,7 +13,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/tree"
"github.com/ark-network/ark/pkg/client-sdk/client"
"github.com/ark-network/ark/pkg/client-sdk/explorer"
@@ -28,6 +27,7 @@ import (
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
log "github.com/sirupsen/logrus"
)
@@ -425,18 +425,27 @@ func (a *covenantlessArkClient) CollaborativeRedeem(
if err != nil {
return "", err
}
desc, err := a.offchainAddressToDefaultVtxoDescriptor(offchainAddr)
if err != nil {
return "", err
}
receivers = append(receivers, client.Output{
Address: offchainAddr,
Amount: changeAmount,
Descriptor: desc,
Amount: changeAmount,
})
}
inputs := make([]client.Input, 0, len(selectedCoins))
for _, coin := range selectedCoins {
inputs = append(inputs, client.VtxoKey{
Txid: coin.Txid,
VOut: coin.VOut,
inputs = append(inputs, client.Input{
Outpoint: client.Outpoint{
Txid: coin.Txid,
VOut: coin.VOut,
},
Descriptor: coin.Descriptor,
})
}
@@ -459,7 +468,7 @@ func (a *covenantlessArkClient) CollaborativeRedeem(
}
poolTxID, err := a.handleRoundStream(
ctx, paymentID, selectedCoins, false, receivers, roundEphemeralKey,
ctx, paymentID, selectedCoins, nil, "", receivers, roundEphemeralKey,
)
if err != nil {
return "", err
@@ -478,7 +487,7 @@ func (a *covenantlessArkClient) SendAsync(
netParams := utils.ToBitcoinNetwork(a.Network)
for _, receiver := range receivers {
isOnchain, _, _, err := utils.ParseBitcoinAddress(receiver.To(), netParams)
isOnchain, _, err := utils.ParseBitcoinAddress(receiver.To(), netParams)
if err != nil {
return "", err
}
@@ -516,9 +525,27 @@ func (a *covenantlessArkClient) SendAsync(
return "", fmt.Errorf("invalid amount (%d), must be greater than dust %d", receiver.Amount(), a.Dust)
}
isSelfTransfer := offchainAddrs[0] == receiver.To()
var desc string
// reversible vtxo does not make sense for self transfer
// if the receiver is the same as the sender, handle the output like the change
if !isSelfTransfer {
desc, err = a.offchainAddressToReversibleVtxoDescriptor(offchainAddrs[0], receiver.To())
if err != nil {
return "", err
}
} else {
desc, err = a.offchainAddressToDefaultVtxoDescriptor(receiver.To())
if err != nil {
return "", err
}
}
receiversOutput = append(receiversOutput, client.Output{
Address: receiver.To(),
Amount: receiver.Amount(),
Descriptor: desc,
Amount: receiver.Amount(),
})
sumOfReceivers += receiver.Amount()
}
@@ -535,17 +562,28 @@ func (a *covenantlessArkClient) SendAsync(
}
if changeAmount > 0 {
changeDesc, err := a.offchainAddressToDefaultVtxoDescriptor(offchainAddrs[0])
if err != nil {
return "", err
}
changeReceiver := client.Output{
Address: offchainAddrs[0],
Amount: changeAmount,
Descriptor: changeDesc,
Amount: changeAmount,
}
receiversOutput = append(receiversOutput, changeReceiver)
}
inputs := make([]client.VtxoKey, 0, len(selectedCoins))
inputs := make([]client.Input, 0, len(selectedCoins))
for _, coin := range selectedCoins {
inputs = append(inputs, coin.VtxoKey)
inputs = append(inputs, client.Input{
Outpoint: client.Outpoint{
Txid: coin.Txid,
VOut: coin.VOut,
},
Descriptor: coin.Descriptor,
})
}
redeemTx, unconditionalForfeitTxs, err := a.client.CreatePayment(
@@ -556,23 +594,13 @@ func (a *covenantlessArkClient) SendAsync(
// TODO verify the redeem tx signature
signedUnconditionalForfeitTxs := make([]string, 0, len(unconditionalForfeitTxs))
for _, tx := range unconditionalForfeitTxs {
signedForfeitTx, err := a.wallet.SignTransaction(ctx, a.explorer, tx)
if err != nil {
return "", err
}
signedUnconditionalForfeitTxs = append(signedUnconditionalForfeitTxs, signedForfeitTx)
}
signedRedeemTx, err := a.wallet.SignTransaction(ctx, a.explorer, redeemTx)
if err != nil {
return "", err
}
if err = a.client.CompletePayment(
ctx, signedRedeemTx, signedUnconditionalForfeitTxs,
ctx, signedRedeemTx, unconditionalForfeitTxs,
); err != nil {
return "", err
}
@@ -612,13 +640,23 @@ func (a *covenantlessArkClient) Claim(ctx context.Context) (string, error) {
return "", nil
}
receiver := client.Output{
Address: myselfOffchain,
Amount: pendingBalance,
desc, err := a.offchainAddressToDefaultVtxoDescriptor(myselfOffchain)
if err != nil {
return "", err
}
desc := strings.ReplaceAll(a.BoardingDescriptorTemplate, "USER", hex.EncodeToString(schnorr.SerializePubKey(mypubkey)))
return a.selfTransferAllPendingPayments(ctx, pendingVtxos, boardingUtxos, receiver, desc)
receiver := client.Output{
Descriptor: desc,
Amount: pendingBalance,
}
return a.selfTransferAllPendingPayments(
ctx,
pendingVtxos,
boardingUtxos,
receiver,
hex.EncodeToString(mypubkey.SerializeCompressed()),
)
}
func (a *covenantlessArkClient) GetTransactionHistory(ctx context.Context) ([]Transaction, error) {
@@ -818,9 +856,14 @@ func (a *covenantlessArkClient) sendOffchain(
return "", fmt.Errorf("invalid amount (%d), must be greater than dust %d", receiver.Amount(), a.Dust)
}
desc, err := a.offchainAddressToDefaultVtxoDescriptor(receiver.To())
if err != nil {
return "", err
}
receiversOutput = append(receiversOutput, client.Output{
Address: receiver.To(),
Amount: receiver.Amount(),
Descriptor: desc,
Amount: receiver.Amount(),
})
sumOfReceivers += receiver.Amount()
}
@@ -846,18 +889,27 @@ func (a *covenantlessArkClient) sendOffchain(
if err != nil {
return "", err
}
desc, err := a.offchainAddressToDefaultVtxoDescriptor(offchainAddr)
if err != nil {
return "", err
}
changeReceiver := client.Output{
Address: offchainAddr,
Amount: changeAmount,
Descriptor: desc,
Amount: changeAmount,
}
receiversOutput = append(receiversOutput, changeReceiver)
}
inputs := make([]client.Input, 0, len(selectedCoins))
for _, coin := range selectedCoins {
inputs = append(inputs, client.VtxoKey{
Txid: coin.Txid,
VOut: coin.VOut,
inputs = append(inputs, client.Input{
Outpoint: client.Outpoint{
Txid: coin.Txid,
VOut: coin.VOut,
},
Descriptor: coin.Descriptor,
})
}
@@ -882,7 +934,7 @@ func (a *covenantlessArkClient) sendOffchain(
log.Infof("payment registered with id: %s", paymentID)
poolTxID, err := a.handleRoundStream(
ctx, paymentID, selectedCoins, false, receiversOutput, roundEphemeralKey,
ctx, paymentID, selectedCoins, nil, "", receiversOutput, roundEphemeralKey,
)
if err != nil {
return "", err
@@ -926,25 +978,38 @@ func (a *covenantlessArkClient) addInputs(
Sequence: sequence,
})
_, leafProof, err := bitcointree.ComputeVtxoTaprootScript(
userPubkey, aspPubkey, utxo.Delay,
)
vtxoScript := &bitcointree.DefaultVtxoScript{
Owner: userPubkey,
Asp: aspPubkey,
ExitDelay: utxo.Delay,
}
exitClosure := &bitcointree.CSVSigClosure{
Pubkey: userPubkey,
Seconds: uint(utxo.Delay),
}
exitLeaf, err := exitClosure.Leaf()
if err != nil {
return err
}
controlBlock := leafProof.ToControlBlock(bitcointree.UnspendableKey())
controlBlockBytes, err := controlBlock.ToBytes()
_, taprootTree, err := vtxoScript.TapTree()
if err != nil {
return err
}
leafProof, err := taprootTree.GetTaprootMerkleProof(exitLeaf.TapHash())
if err != nil {
return fmt.Errorf("failed to get taproot merkle proof: %s", err)
}
updater.Upsbt.Inputs = append(updater.Upsbt.Inputs, psbt.PInput{
TaprootLeafScript: []*psbt.TaprootTapLeafScript{
{
ControlBlock: controlBlockBytes,
ControlBlock: leafProof.ControlBlock,
Script: leafProof.Script,
LeafVersion: leafProof.LeafVersion,
LeafVersion: txscript.BaseLeafVersion,
},
},
})
@@ -957,7 +1022,8 @@ func (a *covenantlessArkClient) handleRoundStream(
ctx context.Context,
paymentID string,
vtxosToSign []client.Vtxo,
mustSignRoundTx bool,
boardingUtxos []explorer.Utxo,
boardingDescriptor string,
receivers []client.Output,
roundEphemeralKey *secp256k1.PrivateKey,
) (string, error) {
@@ -1033,7 +1099,7 @@ func (a *covenantlessArkClient) handleRoundStream(
log.Info("a round finalization started")
signedForfeitTxs, signedRoundTx, err := a.handleRoundFinalization(
ctx, event.(client.RoundFinalizationEvent), vtxosToSign, mustSignRoundTx, receivers,
ctx, event.(client.RoundFinalizationEvent), vtxosToSign, boardingUtxos, boardingDescriptor, receivers,
)
if err != nil {
return "", err
@@ -1134,30 +1200,103 @@ func (a *covenantlessArkClient) handleRoundSigningNoncesGenerated(
}
func (a *covenantlessArkClient) handleRoundFinalization(
ctx context.Context, event client.RoundFinalizationEvent,
vtxos []client.Vtxo, mustSignRoundTx bool, receivers []client.Output,
) (signedForfeits []string, signedRoundTx string, err error) {
ctx context.Context,
event client.RoundFinalizationEvent,
vtxos []client.Vtxo,
boardingUtxos []explorer.Utxo,
boardingDescriptor string,
receivers []client.Output,
) ([]string, string, error) {
if err := a.validateCongestionTree(event, receivers); err != nil {
return nil, "", fmt.Errorf("failed to verify congestion tree: %s", err)
}
offchainAddr, _, err := a.wallet.NewAddress(ctx, false)
if err != nil {
return nil, "", err
}
_, myPubkey, _, err := common.DecodeAddress(offchainAddr)
if err != nil {
return nil, "", err
}
var forfeits []string
if len(vtxos) > 0 {
signedForfeits, err = a.loopAndSign(
ctx, event.ForfeitTxs, vtxos, event.Connectors,
signedForfeits, err := a.createAndSignForfeits(
ctx, vtxos, event.Connectors, event.MinRelayFeeRate, myPubkey,
)
if err != nil {
return
return nil, "", err
}
forfeits = signedForfeits
}
if mustSignRoundTx {
signedRoundTx, err = a.wallet.SignTransaction(ctx, a.explorer, event.Tx)
if len(boardingUtxos) > 0 {
boardingVtxoScript, err := bitcointree.ParseVtxoScript(boardingDescriptor)
if err != nil {
return
return nil, "", err
}
roundPtx, err := psbt.NewFromRawBytes(strings.NewReader(event.Tx), true)
if err != nil {
return nil, "", err
}
// add tapscript leaf
forfeitClosure := &bitcointree.MultisigClosure{
Pubkey: myPubkey,
AspPubkey: a.AspPubkey,
}
forfeitLeaf, err := forfeitClosure.Leaf()
if err != nil {
return nil, "", err
}
_, taprootTree, err := boardingVtxoScript.TapTree()
if err != nil {
return nil, "", err
}
forfeitProof, err := taprootTree.GetTaprootMerkleProof(forfeitLeaf.TapHash())
if err != nil {
return nil, "", fmt.Errorf("failed to get taproot merkle proof for boarding utxo: %s", err)
}
tapscript := &psbt.TaprootTapLeafScript{
ControlBlock: forfeitProof.ControlBlock,
Script: forfeitProof.Script,
LeafVersion: txscript.BaseLeafVersion,
}
for i := range roundPtx.Inputs {
previousOutpoint := roundPtx.UnsignedTx.TxIn[i].PreviousOutPoint
for _, boardingUtxo := range boardingUtxos {
if boardingUtxo.Txid == previousOutpoint.Hash.String() && boardingUtxo.Vout == previousOutpoint.Index {
roundPtx.Inputs[i].TaprootLeafScript = []*psbt.TaprootTapLeafScript{tapscript}
break
}
}
}
b64, err := roundPtx.B64Encode()
if err != nil {
return nil, "", err
}
signedRoundTx, err := a.wallet.SignTransaction(ctx, a.explorer, b64)
if err != nil {
return nil, "", err
}
return forfeits, signedRoundTx, nil
}
return
return forfeits, "", nil
}
func (a *covenantlessArkClient) validateCongestionTree(
@@ -1169,8 +1308,7 @@ func (a *covenantlessArkClient) validateCongestionTree(
return err
}
netParams := utils.ToBitcoinNetwork(a.Network)
if !utils.IsBitcoinOnchainOnly(receivers, netParams) {
if !utils.IsOnchainOnly(receivers) {
if err := bitcointree.ValidateCongestionTree(
event.Tree, poolTx, a.StoreData.AspPubkey, a.RoundLifetime,
); err != nil {
@@ -1183,7 +1321,7 @@ func (a *covenantlessArkClient) validateCongestionTree(
// }
if err := a.validateReceivers(
ptx, receivers, event.Tree, a.StoreData.AspPubkey,
ptx, receivers, event.Tree,
); err != nil {
return err
}
@@ -1197,15 +1335,14 @@ func (a *covenantlessArkClient) validateReceivers(
ptx *psbt.Packet,
receivers []client.Output,
congestionTree tree.CongestionTree,
aspPubkey *secp256k1.PublicKey,
) error {
netParams := utils.ToBitcoinNetwork(a.Network)
for _, receiver := range receivers {
isOnChain, onchainScript, userPubkey, err := utils.ParseBitcoinAddress(
isOnChain, onchainScript, err := utils.ParseBitcoinAddress(
receiver.Address, netParams,
)
if err != nil {
return err
return fmt.Errorf("invalid receiver address: %s err = %s", receiver.Address, err)
}
if isOnChain {
@@ -1214,7 +1351,7 @@ func (a *covenantlessArkClient) validateReceivers(
}
} else {
if err := a.validateOffChainReceiver(
congestionTree, receiver, userPubkey, aspPubkey,
congestionTree, receiver,
); err != nil {
return err
}
@@ -1250,12 +1387,15 @@ func (a *covenantlessArkClient) validateOnChainReceiver(
func (a *covenantlessArkClient) validateOffChainReceiver(
congestionTree tree.CongestionTree,
receiver client.Output,
userPubkey, aspPubkey *secp256k1.PublicKey,
) error {
found := false
outputTapKey, _, err := bitcointree.ComputeVtxoTaprootScript(
userPubkey, aspPubkey, uint(a.UnilateralExitDelay),
)
receiverVtxoScript, err := bitcointree.ParseVtxoScript(receiver.Descriptor)
if err != nil {
return err
}
outputTapKey, _, err := receiverVtxoScript.TapTree()
if err != nil {
return err
}
@@ -1298,57 +1438,107 @@ func (a *covenantlessArkClient) validateOffChainReceiver(
return nil
}
func (a *covenantlessArkClient) loopAndSign(
func (a *covenantlessArkClient) createAndSignForfeits(
ctx context.Context,
forfeitTxs []string, vtxosToSign []client.Vtxo, connectors []string,
vtxosToSign []client.Vtxo,
connectors []string,
feeRate chainfee.SatPerKVByte,
myPubkey *secp256k1.PublicKey,
) ([]string, error) {
signedForfeits := make([]string, 0)
connectorsTxids := make([]string, 0, len(connectors))
signedForfeits := make([]string, 0)
connectorsPsets := make([]*psbt.Packet, 0, len(connectors))
for _, connector := range connectors {
ptx, err := psbt.NewFromRawBytes(strings.NewReader(connector), true)
p, err := psbt.NewFromRawBytes(strings.NewReader(connector), true)
if err != nil {
return nil, err
}
txid := ptx.UnsignedTx.TxHash().String()
connectorsTxids = append(connectorsTxids, txid)
connectorsPsets = append(connectorsPsets, p)
}
for _, forfeitTx := range forfeitTxs {
ptx, err := psbt.NewFromRawBytes(strings.NewReader(forfeitTx), true)
for _, vtxo := range vtxosToSign {
vtxoScript, err := bitcointree.ParseVtxoScript(vtxo.Descriptor)
if err != nil {
return nil, err
}
for _, input := range ptx.UnsignedTx.TxIn {
inputTxid := input.PreviousOutPoint.Hash.String()
vtxoTapKey, vtxoTapTree, err := vtxoScript.TapTree()
if err != nil {
return nil, err
}
for _, coin := range vtxosToSign {
// check if it contains one of the input to sign
if inputTxid == coin.Txid {
// verify that the connector is in the connectors list
connectorTxid := ptx.UnsignedTx.TxIn[0].PreviousOutPoint.Hash.String()
connectorFound := false
for _, txid := range connectorsTxids {
if txid == connectorTxid {
connectorFound = true
break
}
}
feeAmount, err := common.ComputeForfeitMinRelayFee(feeRate, vtxoTapTree)
if err != nil {
return nil, err
}
if !connectorFound {
return nil, fmt.Errorf("connector txid %s not found in the connectors list", connectorTxid)
}
vtxoOutputScript, err := common.P2TRScript(vtxoTapKey)
if err != nil {
return nil, err
}
signedForfeitTx, err := a.wallet.SignTransaction(ctx, a.explorer, forfeitTx)
if err != nil {
return nil, err
}
vtxoTxHash, err := chainhash.NewHashFromStr(vtxo.Txid)
if err != nil {
return nil, err
}
signedForfeits = append(signedForfeits, signedForfeitTx)
vtxoInput := &wire.OutPoint{
Hash: *vtxoTxHash,
Index: vtxo.VOut,
}
forfeitClosure := &bitcointree.MultisigClosure{
Pubkey: myPubkey,
AspPubkey: a.AspPubkey,
}
forfeitLeaf, err := forfeitClosure.Leaf()
if err != nil {
return nil, err
}
leafProof, err := vtxoTapTree.GetTaprootMerkleProof(forfeitLeaf.TapHash())
if err != nil {
return nil, err
}
tapscript := psbt.TaprootTapLeafScript{
ControlBlock: leafProof.ControlBlock,
Script: leafProof.Script,
LeafVersion: txscript.BaseLeafVersion,
}
for _, connectorPset := range connectorsPsets {
forfeits, err := bitcointree.BuildForfeitTxs(
connectorPset, vtxoInput, vtxo.Amount, a.Dust, feeAmount, vtxoOutputScript, a.AspPubkey,
)
if err != nil {
return nil, err
}
if len(forfeits) <= 0 {
return nil, fmt.Errorf("no forfeit txs created dust = %d", a.Dust)
}
for _, forfeit := range forfeits {
forfeit.Inputs[1].TaprootLeafScript = []*psbt.TaprootTapLeafScript{&tapscript}
b64, err := forfeit.B64Encode()
if err != nil {
return nil, err
}
signedForfeit, err := a.wallet.SignTransaction(ctx, a.explorer, b64)
if err != nil {
return nil, err
}
signedForfeits = append(signedForfeits, signedForfeit)
}
}
}
return signedForfeits, nil
@@ -1371,14 +1561,18 @@ func (a *covenantlessArkClient) coinSelectOnchain(
descriptorStr := strings.ReplaceAll(
a.BoardingDescriptorTemplate, "USER", myPubkeyStr,
)
desc, err := descriptor.ParseTaprootDescriptor(descriptorStr)
boardingScript, err := bitcointree.ParseVtxoScript(descriptorStr)
if err != nil {
return nil, 0, err
}
_, boardingTimeout, err := descriptor.ParseBoardingDescriptor(*desc)
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", descriptorStr)
}
now := time.Now()
@@ -1567,14 +1761,18 @@ func (a *covenantlessArkClient) getClaimableBoardingUtxos(ctx context.Context) (
descriptorStr := strings.ReplaceAll(
a.BoardingDescriptorTemplate, "USER", myPubkeyStr,
)
desc, err := descriptor.ParseTaprootDescriptor(descriptorStr)
boardingScript, err := bitcointree.ParseVtxoScript(descriptorStr)
if err != nil {
return nil, err
}
_, boardingTimeout, err := descriptor.ParseBoardingDescriptor(*desc)
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", descriptorStr)
}
claimable := make([]explorer.Utxo, 0)
@@ -1644,17 +1842,27 @@ func (a *covenantlessArkClient) getVtxos(
}
func (a *covenantlessArkClient) selfTransferAllPendingPayments(
ctx context.Context, pendingVtxos []client.Vtxo, boardingUtxo []explorer.Utxo, myself client.Output, boardingDescriptor string,
ctx context.Context, pendingVtxos []client.Vtxo, boardingUtxos []explorer.Utxo, myself client.Output, mypubkey string,
) (string, error) {
inputs := make([]client.Input, 0, len(pendingVtxos)+len(boardingUtxo))
inputs := make([]client.Input, 0, len(pendingVtxos)+len(boardingUtxos))
boardingDescriptor := strings.ReplaceAll(
a.BoardingDescriptorTemplate, "USER", mypubkey[2:],
)
for _, coin := range pendingVtxos {
inputs = append(inputs, coin.VtxoKey)
inputs = append(inputs, client.Input{
Outpoint: client.Outpoint{
Txid: coin.Txid,
VOut: coin.VOut,
},
Descriptor: coin.Descriptor,
})
}
for _, utxo := range boardingUtxo {
inputs = append(inputs, client.BoardingInput{
VtxoKey: client.VtxoKey{
for _, utxo := range boardingUtxos {
inputs = append(inputs, client.Input{
Outpoint: client.Outpoint{
Txid: utxo.Txid,
VOut: utxo.Vout,
},
@@ -1682,7 +1890,7 @@ func (a *covenantlessArkClient) selfTransferAllPendingPayments(
}
roundTxid, err := a.handleRoundStream(
ctx, paymentID, pendingVtxos, len(boardingUtxo) > 0, outputs, roundEphemeralKey,
ctx, paymentID, pendingVtxos, boardingUtxos, boardingDescriptor, outputs, roundEphemeralKey,
)
if err != nil {
return "", err
@@ -1691,6 +1899,42 @@ func (a *covenantlessArkClient) selfTransferAllPendingPayments(
return roundTxid, nil
}
func (a *covenantlessArkClient) offchainAddressToReversibleVtxoDescriptor(myaddr string, receiveraddr string) (string, error) {
_, receiverPubkey, aspPubkey, err := common.DecodeAddress(receiveraddr)
if err != nil {
return "", err
}
_, userPubKey, _, err := common.DecodeAddress(myaddr)
if err != nil {
return "", err
}
vtxoScript := bitcointree.ReversibleVtxoScript{
Owner: receiverPubkey,
Sender: userPubKey,
Asp: aspPubkey,
ExitDelay: uint(a.UnilateralExitDelay),
}
return vtxoScript.ToDescriptor(), nil
}
func (a *covenantlessArkClient) offchainAddressToDefaultVtxoDescriptor(addr string) (string, error) {
_, userPubKey, aspPubkey, err := common.DecodeAddress(addr)
if err != nil {
return "", err
}
vtxoScript := bitcointree.DefaultVtxoScript{
Owner: userPubKey,
Asp: aspPubkey,
ExitDelay: uint(a.UnilateralExitDelay),
}
return vtxoScript.ToDescriptor(), nil
}
func (a *covenantlessArkClient) getBoardingTxs(ctx context.Context) (transactions []Transaction) {
utxos, err := a.getClaimableBoardingUtxos(ctx)
if err != nil {

View File

@@ -18,6 +18,7 @@ require (
github.com/go-openapi/strfmt v0.23.0
github.com/go-openapi/swag v0.23.0
github.com/go-openapi/validate v0.24.0
github.com/lightningnetwork/lnd v0.18.2-beta
github.com/sirupsen/logrus v1.9.3
github.com/stretchr/testify v1.9.0
github.com/vulpemventures/go-elements v0.5.4
@@ -38,6 +39,7 @@ require (
github.com/btcsuite/btcwallet/wtxmgr v1.5.3 // indirect
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd // indirect
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 // indirect
github.com/btcsuite/winsvc v1.0.0 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/containerd/continuity v0.4.3 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
@@ -45,6 +47,7 @@ require (
github.com/decred/dcrd/lru v1.1.3 // indirect
github.com/docker/docker v27.1.1+incompatible // indirect
github.com/docker/go-connections v0.5.0 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/analysis v0.23.0 // indirect
@@ -52,16 +55,18 @@ require (
github.com/go-openapi/jsonreference v0.21.0 // indirect
github.com/go-openapi/loads v0.22.0 // indirect
github.com/go-openapi/spec v0.21.0 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.21.0 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jessevdk/go-flags v1.6.1 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/jrick/logrotate v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kkdai/bstream v1.0.0 // indirect
github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf // indirect
github.com/lightninglabs/neutrino v0.16.1-0.20240425105051-602843d34ffd // indirect
github.com/lightninglabs/neutrino/cache v1.1.2 // indirect
github.com/lightningnetwork/lnd v0.18.2-beta // indirect
github.com/lightningnetwork/lnd/clock v1.1.1 // indirect
github.com/lightningnetwork/lnd/fn v1.2.1 // indirect
github.com/lightningnetwork/lnd/queue v1.1.1 // indirect
@@ -74,11 +79,13 @@ require (
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/oklog/ulid v1.3.1 // indirect
github.com/onsi/ginkgo v1.16.4 // indirect
github.com/opencontainers/image-spec v1.1.0 // indirect
github.com/opencontainers/runc v1.1.13 // indirect
github.com/opentracing/opentracing-go v1.2.0 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect
github.com/vulpemventures/fastsha256 v0.0.0-20160815193821-637e65642941 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
go.etcd.io/etcd/client/v2 v2.305.15 // indirect

View File

@@ -93,6 +93,7 @@ github.com/fergusstrange/embedded-postgres v1.28.0 h1:Atixd24HCuBHBavnG4eiZAjRiz
github.com/fergusstrange/embedded-postgres v1.28.0/go.mod h1:t/MLs0h9ukYM6FSt99R7InCHs1nW0ordoVCcnzmpTYw=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk=
github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
@@ -263,16 +264,19 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8m
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc=
github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.26.0 h1:03cDLK28U6hWvCAns6NeydX3zIm4SF3ci69ulidS32Q=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
@@ -432,6 +436,7 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

View File

@@ -67,60 +67,34 @@ func CoinSelect(
}
func ParseLiquidAddress(addr string) (
bool, []byte, *secp256k1.PublicKey, error,
bool, []byte, error,
) {
outputScript, err := address.ToOutputScript(addr)
if err != nil {
_, userPubkey, _, err := common.DecodeAddress(addr)
if err != nil {
return false, nil, nil, err
}
return false, nil, userPubkey, nil
return false, nil, nil
}
return true, outputScript, nil, nil
return true, outputScript, nil
}
func ParseBitcoinAddress(addr string, net chaincfg.Params) (
bool, []byte, *secp256k1.PublicKey, error,
bool, []byte, error,
) {
btcAddr, err := btcutil.DecodeAddress(addr, &net)
if err != nil {
_, userPubkey, _, err := common.DecodeAddress(addr)
if err != nil {
return false, nil, nil, err
}
return false, nil, userPubkey, nil
return false, nil, nil
}
onchainScript, err := txscript.PayToAddrScript(btcAddr)
if err != nil {
return false, nil, nil, err
return false, nil, err
}
return true, onchainScript, nil, nil
return true, onchainScript, nil
}
func IsBitcoinOnchainOnly(receivers []client.Output, net chaincfg.Params) bool {
func IsOnchainOnly(receivers []client.Output) bool {
for _, receiver := range receivers {
isOnChain, _, _, err := ParseBitcoinAddress(receiver.Address, net)
if err != nil {
continue
}
if !isOnChain {
return false
}
}
return true
}
func IsLiquidOnchainOnly(receivers []client.Output) bool {
for _, receiver := range receivers {
isOnChain, _, _, err := ParseLiquidAddress(receiver.Address)
if err != nil {
continue
}
isOnChain := len(receiver.Address) > 0
if !isOnChain {
return false

View File

@@ -9,7 +9,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/pkg/client-sdk/explorer"
"github.com/ark-network/ark/pkg/client-sdk/internal/utils"
"github.com/ark-network/ark/pkg/client-sdk/store"
@@ -219,9 +218,13 @@ func (w *bitcoinWallet) getAddress(
netParams := utils.ToBitcoinNetwork(data.Network)
vtxoTapKey, _, err := bitcointree.ComputeVtxoTaprootScript(
w.walletData.Pubkey, data.AspPubkey, uint(data.UnilateralExitDelay),
)
defaultVtxoScript := &bitcointree.DefaultVtxoScript{
Asp: data.AspPubkey,
Owner: w.walletData.Pubkey,
ExitDelay: uint(data.UnilateralExitDelay),
}
vtxoTapKey, _, err := defaultVtxoScript.TapTree()
if err != nil {
return "", "", "", err
}
@@ -239,19 +242,12 @@ func (w *bitcoinWallet) getAddress(
data.BoardingDescriptorTemplate, "USER", myPubkeyStr,
)
desc, err := descriptor.ParseTaprootDescriptor(descriptorStr)
boardingVtxoScript, err := bitcointree.ParseVtxoScript(descriptorStr)
if err != nil {
return "", "", "", err
}
_, boardingTimeout, err := descriptor.ParseBoardingDescriptor(*desc)
if err != nil {
return "", "", "", err
}
boardingTapKey, _, err := bitcointree.ComputeVtxoTaprootScript(
w.walletData.Pubkey, data.AspPubkey, boardingTimeout,
)
boardingTapKey, _, err := boardingVtxoScript.TapTree()
if err != nil {
return "", "", "", err
}

View File

@@ -8,7 +8,6 @@ import (
"strings"
"github.com/ark-network/ark/common"
"github.com/ark-network/ark/common/descriptor"
"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"
@@ -18,6 +17,7 @@ import (
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/vulpemventures/go-elements/payment"
"github.com/vulpemventures/go-elements/psetv2"
"github.com/vulpemventures/go-elements/transaction"
)
@@ -165,7 +165,7 @@ func (s *liquidWallet) SignTransaction(
switch c := closure.(type) {
case *tree.CSVSigClosure:
sign = bytes.Equal(c.Pubkey.SerializeCompressed()[1:], serializedPubKey[1:])
case *tree.ForfeitClosure:
case *tree.MultisigClosure:
sign = bytes.Equal(c.Pubkey.SerializeCompressed()[1:], serializedPubKey[1:])
}
@@ -242,9 +242,23 @@ func (w *liquidWallet) getAddress(
liquidNet := utils.ToElementsNetwork(data.Network)
_, _, _, redemptionAddr, err := tree.ComputeVtxoTaprootScript(
w.walletData.Pubkey, data.AspPubkey, uint(data.UnilateralExitDelay), liquidNet,
)
vtxoScript := &tree.DefaultVtxoScript{
Owner: w.walletData.Pubkey,
Asp: data.AspPubkey,
ExitDelay: uint(data.UnilateralExitDelay),
}
vtxoTapKey, _, err := vtxoScript.TapTree()
if err != nil {
return "", "", "", err
}
vtxoP2tr, err := payment.FromTweakedKey(vtxoTapKey, &liquidNet, nil)
if err != nil {
return "", "", "", err
}
redemptionAddr, err := vtxoP2tr.TaprootAddress()
if err != nil {
return "", "", "", err
}
@@ -254,19 +268,22 @@ func (w *liquidWallet) getAddress(
data.BoardingDescriptorTemplate, "USER", myPubkeyStr,
)
desc, err := descriptor.ParseTaprootDescriptor(descriptorStr)
onboardingScript, err := tree.ParseVtxoScript(descriptorStr)
if err != nil {
return "", "", "", err
}
_, boardingTimeout, err := descriptor.ParseBoardingDescriptor(*desc)
tapKey, _, err := onboardingScript.TapTree()
if err != nil {
return "", "", "", err
}
_, _, _, boardingAddr, err := tree.ComputeVtxoTaprootScript(
w.walletData.Pubkey, data.AspPubkey, boardingTimeout, liquidNet,
)
p2tr, err := payment.FromTweakedKey(tapKey, &liquidNet, nil)
if err != nil {
return "", "", "", err
}
boardingAddr, err := p2tr.TaprootAddress()
if err != nil {
return "", "", "", err
}