New address encoding (#356)

* [common] rework address encoding

* new address encoding

* replace offchain address by vtxo output key in DB

* merge migrations files into init one

* fix txbuilder fixtures

* fix transaction events
This commit is contained in:
Louis Singer
2024-10-18 16:50:07 +02:00
committed by GitHub
parent b1c9261f14
commit b536a9e652
58 changed files with 2243 additions and 1896 deletions

View File

@@ -506,6 +506,17 @@
}
}
},
"v1AsyncPaymentInput": {
"type": "object",
"properties": {
"input": {
"$ref": "#/definitions/v1Input"
},
"forfeitLeafHash": {
"type": "string"
}
}
},
"v1CompletePaymentRequest": {
"type": "object",
"properties": {
@@ -524,7 +535,7 @@
"type": "array",
"items": {
"type": "object",
"$ref": "#/definitions/v1Input"
"$ref": "#/definitions/v1AsyncPaymentInput"
}
},
"outputs": {
@@ -717,9 +728,6 @@
"type": "string",
"format": "uint64",
"description": "Amount to send in satoshis."
},
"descriptor": {
"type": "string"
}
}
},
@@ -1046,9 +1054,6 @@
"outpoint": {
"$ref": "#/definitions/v1Outpoint"
},
"descriptor": {
"type": "string"
},
"spent": {
"type": "boolean"
},
@@ -1074,6 +1079,9 @@
"amount": {
"type": "string",
"format": "uint64"
},
"pubkey": {
"type": "string"
}
}
}

View File

@@ -187,8 +187,13 @@ message PingResponse {
/* Async Payment API messages */
message AsyncPaymentInput {
Input input = 1;
string forfeit_leaf_hash = 2;
}
message CreatePaymentRequest {
repeated Input inputs = 1;
repeated AsyncPaymentInput inputs = 1;
repeated Output outputs = 2;
}
message CreatePaymentResponse {
@@ -290,7 +295,6 @@ message Input {
message Output {
string address = 1; // onchain or off-chain
uint64 amount = 2; // Amount to send in satoshis.
string descriptor = 3;
}
message Tree {
@@ -309,18 +313,17 @@ message Node {
message Vtxo {
Outpoint outpoint = 1;
string descriptor = 2;
bool spent = 3;
string round_txid = 4;
string spent_by = 5;
int64 expire_at = 6;
bool swept = 7;
bool pending = 8;
string redeem_tx = 9;
uint64 amount = 10;
bool spent = 2;
string round_txid = 3;
string spent_by = 4;
int64 expire_at = 5;
bool swept = 6;
bool pending = 7;
string redeem_tx = 8;
uint64 amount = 9;
string pubkey = 10;
}
message GetTransactionsStreamRequest {}
message GetTransactionsStreamResponse {
oneof tx {

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,10 @@
package bitcointree
import (
"encoding/hex"
"fmt"
"github.com/ark-network/ark/common"
"github.com/ark-network/ark/common/tree"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
@@ -15,7 +17,7 @@ import (
// CraftSharedOutput returns the taproot script and the amount of the initial root output
func CraftSharedOutput(
cosigners []*secp256k1.PublicKey, aspPubkey *secp256k1.PublicKey, receivers []Receiver,
cosigners []*secp256k1.PublicKey, aspPubkey *secp256k1.PublicKey, receivers []tree.VtxoLeaf,
feeSatsPerNode uint64, roundLifetime int64,
) ([]byte, int64, error) {
aggregatedKey, _, err := createAggregatedKeyWithSweep(
@@ -32,7 +34,7 @@ func CraftSharedOutput(
amount := root.getAmount() + int64(feeSatsPerNode)
scriptPubKey, err := taprootOutputScript(aggregatedKey.FinalKey)
scriptPubKey, err := common.P2TRScript(aggregatedKey.FinalKey)
if err != nil {
return nil, 0, err
}
@@ -42,7 +44,7 @@ func CraftSharedOutput(
// CraftCongestionTree creates all the tree's transactions
func CraftCongestionTree(
initialInput *wire.OutPoint, cosigners []*secp256k1.PublicKey, aspPubkey *secp256k1.PublicKey, receivers []Receiver,
initialInput *wire.OutPoint, cosigners []*secp256k1.PublicKey, aspPubkey *secp256k1.PublicKey, receivers []tree.VtxoLeaf,
feeSatsPerNode uint64, roundLifetime int64,
) (tree.CongestionTree, error) {
aggregatedKey, sweepTapLeaf, err := createAggregatedKeyWithSweep(
@@ -108,8 +110,8 @@ type node interface {
}
type leaf struct {
vtxoScript VtxoScript
amount int64
amount int64
pubkey *secp256k1.PublicKey
}
type branch struct {
@@ -142,12 +144,7 @@ func (l *leaf) getAmount() int64 {
}
func (l *leaf) getOutputs() ([]*wire.TxOut, error) {
taprootKey, _, err := l.vtxoScript.TapTree()
if err != nil {
return nil, err
}
script, err := taprootOutputScript(taprootKey)
script, err := common.P2TRScript(l.pubkey)
if err != nil {
return nil, err
}
@@ -161,7 +158,7 @@ func (l *leaf) getOutputs() ([]*wire.TxOut, error) {
}
func (b *branch) getOutputs() ([]*wire.TxOut, error) {
sharedOutputScript, err := taprootOutputScript(b.aggregatedKey.FinalKey)
sharedOutputScript, err := common.P2TRScript(b.aggregatedKey.FinalKey)
if err != nil {
return nil, err
}
@@ -246,7 +243,7 @@ func getTx(
func createRootNode(
aggregatedKey *musig2.AggregateKey,
cosigners []*secp256k1.PublicKey,
receivers []Receiver,
receivers []tree.VtxoLeaf,
feeSatsPerNode uint64,
) (root node, err error) {
if len(receivers) == 0 {
@@ -255,9 +252,19 @@ func createRootNode(
nodes := make([]node, 0, len(receivers))
for _, r := range receivers {
pubkeyBytes, err := hex.DecodeString(r.Pubkey)
if err != nil {
return nil, err
}
pubkey, err := schnorr.ParsePubKey(pubkeyBytes)
if err != nil {
return nil, err
}
leafNode := &leaf{
vtxoScript: r.Script,
amount: int64(r.Amount),
amount: int64(r.Amount),
pubkey: pubkey,
}
nodes = append(nodes, leafNode)
}
@@ -339,9 +346,3 @@ func createUpperLevel(nodes []node, aggregatedKey *musig2.AggregateKey, cosigner
}
return pairs, nil
}
func taprootOutputScript(taprootKey *secp256k1.PublicKey) ([]byte, error) {
return txscript.NewScriptBuilder().AddOp(txscript.OP_1).AddData(
schnorr.SerializePubKey(taprootKey),
).Script()
}

View File

@@ -7,6 +7,7 @@ import (
"io"
"strings"
"github.com/ark-network/ark/common"
"github.com/ark-network/ark/common/tree"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
@@ -512,7 +513,7 @@ func prevOutFetcherFactory(
func(partial *psbt.Packet) (txscript.PrevOutputFetcher, error),
error,
) {
pkscript, err := taprootOutputScript(finalAggregatedKey)
pkscript, err := common.P2TRScript(finalAggregatedKey)
if err != nil {
return nil, err
}

View File

@@ -2,13 +2,13 @@ package bitcointree_test
import (
"bytes"
"encoding/hex"
"encoding/json"
"fmt"
"os"
"testing"
"github.com/ark-network/ark/common/bitcointree"
"github.com/ark-network/ark/common/tree"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
@@ -44,7 +44,7 @@ func TestRoundTripSignTree(t *testing.T) {
_, sharedOutputAmount, err := bitcointree.CraftSharedOutput(
cosigners,
asp.PubKey(),
castReceivers(f.Receivers, asp.PubKey()),
castReceivers(f.Receivers),
minRelayFee,
lifetime,
)
@@ -58,7 +58,7 @@ func TestRoundTripSignTree(t *testing.T) {
},
cosigners,
asp.PubKey(),
castReceivers(f.Receivers, asp.PubKey()),
castReceivers(f.Receivers),
minRelayFee,
lifetime,
)
@@ -222,29 +222,11 @@ type receiverFixture struct {
Pubkey string `json:"pubkey"`
}
func (r receiverFixture) toVtxoScript(asp *secp256k1.PublicKey) bitcointree.VtxoScript {
bytesKey, err := hex.DecodeString(r.Pubkey)
if err != nil {
panic(err)
}
pubkey, err := secp256k1.ParsePubKey(bytesKey)
if err != nil {
panic(err)
}
return &bitcointree.DefaultVtxoScript{
Owner: pubkey,
Asp: asp,
ExitDelay: exitDelay,
}
}
func castReceivers(receivers []receiverFixture, asp *secp256k1.PublicKey) []bitcointree.Receiver {
receiversOut := make([]bitcointree.Receiver, 0, len(receivers))
func castReceivers(receivers []receiverFixture) []tree.VtxoLeaf {
receiversOut := make([]tree.VtxoLeaf, 0, len(receivers))
for _, r := range receivers {
receiversOut = append(receiversOut, bitcointree.Receiver{
Script: r.toVtxoScript(asp),
receiversOut = append(receiversOut, tree.VtxoLeaf{
Pubkey: r.Pubkey,
Amount: uint64(r.Amount),
})
}

View File

@@ -4,7 +4,7 @@
{
"receivers": [
{
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002",
"pubkey": "0000000000000000000000000000000000000000000000000000000000000002",
"amount": 1100
}
]
@@ -12,11 +12,11 @@
{
"receivers": [
{
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002",
"pubkey": "0000000000000000000000000000000000000000000000000000000000000002",
"amount": 1100
},
{
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002",
"pubkey": "0000000000000000000000000000000000000000000000000000000000000002",
"amount": 8000
}
]
@@ -24,23 +24,23 @@
{
"receivers": [
{
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002",
"pubkey": "0000000000000000000000000000000000000000000000000000000000000002",
"amount": 1100
},
{
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002",
"pubkey": "0000000000000000000000000000000000000000000000000000000000000002",
"amount": 1100
},
{
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002",
"pubkey": "0000000000000000000000000000000000000000000000000000000000000002",
"amount": 1100
},
{
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002",
"pubkey": "0000000000000000000000000000000000000000000000000000000000000002",
"amount": 1000
},
{
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002",
"pubkey": "0000000000000000000000000000000000000000000000000000000000000002",
"amount": 1100
}
]

View File

@@ -1,6 +0,0 @@
package bitcointree
type Receiver struct {
Script VtxoScript
Amount uint64
}

View File

@@ -3,63 +3,68 @@ package common
import (
"fmt"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/btcutil/bech32"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
)
func EncodeAddress(
hrp string, userKey, aspKey *secp256k1.PublicKey,
) (addr string, err error) {
if userKey == nil {
err = fmt.Errorf("missing public key")
return
// Address represents an Ark address with HRP, ASP public key, and VTXO Taproot public key
type Address struct {
HRP string
Asp *secp256k1.PublicKey
VtxoTapKey *secp256k1.PublicKey
}
// Encode converts the address to its bech32m string representation
func (a *Address) Encode() (string, error) {
if a.Asp == nil {
return "", fmt.Errorf("missing asp public key")
}
if aspKey == nil {
err = fmt.Errorf("missing asp public key")
return
}
if hrp != Liquid.Addr && hrp != LiquidTestNet.Addr {
err = fmt.Errorf("invalid prefix")
return
if a.VtxoTapKey == nil {
return "", fmt.Errorf("missing vtxo tap public key")
}
combinedKey := append(
aspKey.SerializeCompressed(), userKey.SerializeCompressed()...,
schnorr.SerializePubKey(a.Asp), schnorr.SerializePubKey(a.VtxoTapKey)...,
)
grp, err := bech32.ConvertBits(combinedKey, 8, 5, true)
if err != nil {
return
return "", err
}
addr, err = bech32.EncodeM(hrp, grp)
return
return bech32.EncodeM(a.HRP, grp)
}
func DecodeAddress(
addr string,
) (hrp string, userKey, aspKey *secp256k1.PublicKey, err error) {
// DecodeAddress parses a bech32m encoded address string and returns an Address object
func DecodeAddress(addr string) (*Address, error) {
if len(addr) == 0 {
return nil, fmt.Errorf("address is empty")
}
prefix, buf, err := bech32.DecodeNoLimit(addr)
if err != nil {
return
return nil, err
}
if prefix != Liquid.Addr && prefix != LiquidTestNet.Addr && prefix != LiquidRegTest.Addr {
err = fmt.Errorf("invalid prefix")
return
return nil, fmt.Errorf("invalid prefix")
}
grp, err := bech32.ConvertBits(buf, 5, 8, false)
if err != nil {
return
return nil, err
}
aKey, err := secp256k1.ParsePubKey(grp[:33])
aKey, err := schnorr.ParsePubKey(grp[:32])
if err != nil {
err = fmt.Errorf("failed to parse public key: %s", err)
return
return nil, fmt.Errorf("failed to parse public key: %s", err)
}
uKey, err := secp256k1.ParsePubKey(grp[33:])
vtxoKey, err := schnorr.ParsePubKey(grp[32:])
if err != nil {
err = fmt.Errorf("failed to parse asp public key: %s", err)
return
return nil, fmt.Errorf("failed to parse asp public key: %s", err)
}
hrp = prefix
userKey = uKey
aspKey = aKey
return
return &Address{
HRP: prefix,
Asp: aKey,
VtxoTapKey: vtxoKey,
}, nil
}

View File

@@ -40,31 +40,29 @@ func TestAddressEncoding(t *testing.T) {
t.Run("valid", func(t *testing.T) {
for _, f := range fixtures.Address.Valid {
hrp, userKey, aspKey, err := common.DecodeAddress(f.Addr)
addr, err := common.DecodeAddress(f.Addr)
require.NoError(t, err)
require.NotEmpty(t, hrp)
require.NotNil(t, userKey)
require.NotNil(t, aspKey)
require.NotEmpty(t, addr.HRP)
require.NotNil(t, addr.Asp)
require.NotNil(t, addr.VtxoTapKey)
require.NoError(t, err)
require.Equal(t, f.ExpectedUserKey, hex.EncodeToString(userKey.SerializeCompressed()))
require.Equal(t, f.ExpectedUserKey, hex.EncodeToString(addr.VtxoTapKey.SerializeCompressed()))
require.NoError(t, err)
require.Equal(t, f.ExpectedAspKey, hex.EncodeToString(aspKey.SerializeCompressed()))
require.Equal(t, f.ExpectedAspKey, hex.EncodeToString(addr.Asp.SerializeCompressed()))
addr, err := common.EncodeAddress(hrp, userKey, aspKey)
encoded, err := addr.Encode()
require.NoError(t, err)
require.Equal(t, f.Addr, addr)
require.Equal(t, f.Addr, encoded)
}
})
t.Run("invalid", func(t *testing.T) {
for _, f := range fixtures.Address.Invalid {
hrp, userKey, aspKey, err := common.DecodeAddress(f.Addr)
addr, err := common.DecodeAddress(f.Addr)
require.EqualError(t, err, f.ExpectedError)
require.Empty(t, hrp)
require.Nil(t, userKey)
require.Nil(t, aspKey)
require.Nil(t, addr)
}
})
}

View File

@@ -2,9 +2,9 @@
"address": {
"valid": [
{
"addr": "ark1qgvdtj5ttpuhkldavhq8thtm5auyk0ec4dcmrfdgu0u5hgp9we22vqa7mdkrrulzu48law4zzvzz8k59hul0ayl2urt905we5wf6gee68sfrfj35",
"expectedUserKey": "03bedb6c31f3e2e54ffebaa2130423da85bf3efe93eae0d657d1d9a393a4673a3c",
"expectedAspKey": "0218d5ca8b58797b7dbd65c075dd7ba7784b3f38ab71b1a5a8e3f94ba0257654a6"
"addr": "tark1x0lm8hhr2wc6n6lyemtyh9rz8rg2ftpkfun46aca56kjg3ws0tsztfpuanaquxc6faedvjk3tax0575y6perapg3e95654pk8r4fjecs5fyd2",
"expectedUserKey": "0225a43cecfa0e1b1a4f72d64ad15f4cfa7a84d0723e8511c969aa543638ea9967",
"expectedAspKey": "0233ffb3dee353b1a9ebe4ced64b946238d0a4ac364f275d771da6ad2445d07ae0"
}
],
"invalid": [

View File

@@ -1,9 +1,9 @@
package tree
import (
"encoding/hex"
"fmt"
"github.com/ark-network/ark/common"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
@@ -13,7 +13,7 @@ import (
)
func CraftCongestionTree(
asset string, aspPubkey *secp256k1.PublicKey, receivers []Receiver,
asset string, aspPubkey *secp256k1.PublicKey, receivers []VtxoLeaf,
feeSatsPerNode uint64, roundLifetime int64,
) (
buildCongestionTree TreeFactory,
@@ -41,9 +41,14 @@ func CraftCongestionTree(
return
}
type vtxoOutput struct {
pubkey *secp256k1.PublicKey
amount uint64
}
type node struct {
sweepKey *secp256k1.PublicKey
receivers []Receiver
receivers []vtxoOutput
left *node
right *node
asset string
@@ -61,7 +66,7 @@ func (n *node) isLeaf() bool {
func (n *node) getAmount() uint64 {
var amount uint64
for _, r := range n.receivers {
amount += r.Amount
amount += r.amount
}
if n.isLeaf() {
@@ -107,7 +112,7 @@ func (n *node) getChildren() []*node {
func (n *node) getOutputs() ([]psetv2.OutputArgs, error) {
if n.isLeaf() {
taprootKey, _, err := n.getVtxoWitnessData()
taprootKey, err := n.getVtxoWitnessData()
if err != nil {
return nil, err
}
@@ -168,7 +173,7 @@ func (n *node) getWitnessData() (
}
if n.isLeaf() {
taprootKey, _, err := n.getVtxoWitnessData()
taprootKey, err := n.getVtxoWitnessData()
if err != nil {
return nil, nil, err
}
@@ -241,15 +246,14 @@ func (n *node) getWitnessData() (
}
func (n *node) getVtxoWitnessData() (
*secp256k1.PublicKey, common.TaprootTree, error,
*secp256k1.PublicKey, error,
) {
if !n.isLeaf() {
return nil, nil, fmt.Errorf("cannot call vtxoWitness on a non-leaf node")
return nil, fmt.Errorf("cannot call vtxoWitness on a non-leaf node")
}
receiver := n.receivers[0]
return receiver.Script.TapTree()
return receiver.pubkey, nil
}
func (n *node) getTreeNode(
@@ -373,7 +377,7 @@ func (n *node) createFinalCongestionTree() TreeFactory {
}
func createPartialCongestionTree(
asset string, aspPubkey *secp256k1.PublicKey, receivers []Receiver,
asset string, aspPubkey *secp256k1.PublicKey, receivers []VtxoLeaf,
feeSatsPerNode uint64, roundLifetime int64,
) (root *node, err error) {
if len(receivers) == 0 {
@@ -382,9 +386,19 @@ func createPartialCongestionTree(
nodes := make([]*node, 0, len(receivers))
for _, r := range receivers {
pubkeyBytes, err := hex.DecodeString(r.Pubkey)
if err != nil {
return nil, err
}
pubkey, err := schnorr.ParsePubKey(pubkeyBytes)
if err != nil {
return nil, err
}
leafNode := &node{
sweepKey: aspPubkey,
receivers: []Receiver{r},
receivers: []vtxoOutput{{pubkey, r.Amount}},
asset: asset,
feeSats: feeSatsPerNode,
roundLifetime: roundLifetime,

View File

@@ -6,7 +6,7 @@ import (
type TreeFactory func(outpoint psetv2.InputArgs) (CongestionTree, error)
type Receiver struct {
Script VtxoScript
type VtxoLeaf struct {
Pubkey string
Amount uint64
}

View File

@@ -94,7 +94,7 @@ func (a *arkClient) Receive(ctx context.Context) (string, string, error) {
return "", "", err
}
return offchainAddr, boardingAddr, nil
return offchainAddr.Address, boardingAddr.Address, nil
}
func (a *arkClient) GetTransactionEventChannel() chan types.TransactionEvent {

View File

@@ -2,10 +2,14 @@ package client
import (
"context"
"encoding/hex"
"time"
"github.com/ark-network/ark/common"
"github.com/ark-network/ark/common/bitcointree"
"github.com/ark-network/ark/common/tree"
"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"
)
@@ -41,7 +45,7 @@ type ASPClient interface {
) (<-chan RoundEventChannel, func(), error)
Ping(ctx context.Context, paymentID string) (RoundEvent, error)
CreatePayment(
ctx context.Context, inputs []Input, outputs []Output,
ctx context.Context, inputs []AsyncPaymentInput, outputs []Output,
) (string, error)
CompletePayment(
ctx context.Context, signedRedeemTx string,
@@ -79,21 +83,50 @@ type Input struct {
Descriptor string
}
type AsyncPaymentInput struct {
Input
ForfeitLeafHash chainhash.Hash
}
type Vtxo struct {
Outpoint
Pubkey string
Amount uint64
RoundTxid string
ExpiresAt *time.Time
RedeemTx string
Pending bool
SpentBy string
}
func (v Vtxo) Address(asp *secp256k1.PublicKey, net common.Network) (string, error) {
pubkeyBytes, err := hex.DecodeString(v.Pubkey)
if err != nil {
return "", err
}
pubkey, err := schnorr.ParsePubKey(pubkeyBytes)
if err != nil {
return "", err
}
a := &common.Address{
HRP: net.Addr,
Asp: asp,
VtxoTapKey: pubkey,
}
return a.Encode()
}
type DescriptorVtxo struct {
Vtxo
Descriptor string
Amount uint64
RoundTxid string
ExpiresAt *time.Time
RedeemTx string
Pending bool
SpentBy string
}
type Output struct {
Address string // onchain output address
Descriptor string // offchain vtxo descriptor
Amount uint64
Address string // onchain or offchain address
Amount uint64
}
type RoundStage int

View File

@@ -238,10 +238,10 @@ func (a *grpcClient) Ping(
}
func (a *grpcClient) CreatePayment(
ctx context.Context, inputs []client.Input, outputs []client.Output,
ctx context.Context, inputs []client.AsyncPaymentInput, outputs []client.Output,
) (string, error) {
req := &arkv1.CreatePaymentRequest{
Inputs: ins(inputs).toProto(),
Inputs: asyncIns(inputs).toProto(),
Outputs: outs(outputs).toProto(),
}
resp, err := a.svc.CreatePayment(ctx, req)
@@ -404,13 +404,13 @@ func vtxosFromProto(protoVtxos []*arkv1.Vtxo) []client.Vtxo {
Txid: v.Outpoint.Txid,
VOut: v.Outpoint.Vout,
},
Descriptor: v.Descriptor_,
Amount: v.Amount,
RoundTxid: v.RoundTxid,
ExpiresAt: &expiresAt,
RedeemTx: v.RedeemTx,
Pending: v.Pending,
SpentBy: v.SpentBy,
Pubkey: v.Pubkey,
Amount: v.Amount,
RoundTxid: v.RoundTxid,
ExpiresAt: &expiresAt,
RedeemTx: v.RedeemTx,
Pending: v.Pending,
SpentBy: v.SpentBy,
}
}
return vtxos

View File

@@ -18,9 +18,8 @@ type out client.Output
func (o out) toProto() *arkv1.Output {
return &arkv1.Output{
Address: o.Address,
Descriptor_: o.Descriptor,
Amount: o.Amount,
Address: o.Address,
Amount: o.Amount,
}
}
@@ -123,13 +122,13 @@ func (v vtxo) toVtxo() client.Vtxo {
Txid: v.GetOutpoint().GetTxid(),
VOut: v.GetOutpoint().GetVout(),
},
Amount: v.GetAmount(),
RoundTxid: v.GetRoundTxid(),
ExpiresAt: expiresAt,
Pending: v.GetPending(),
RedeemTx: v.GetRedeemTx(),
SpentBy: v.GetSpentBy(),
Descriptor: v.GetDescriptor_(),
Amount: v.GetAmount(),
RoundTxid: v.GetRoundTxid(),
ExpiresAt: expiresAt,
Pending: v.GetPending(),
RedeemTx: v.GetRedeemTx(),
SpentBy: v.GetSpentBy(),
Pubkey: v.GetPubkey(),
}
}
@@ -153,6 +152,23 @@ func toProtoInput(i client.Input) *arkv1.Input {
}
}
func toAsyncProtoInput(i client.AsyncPaymentInput) *arkv1.AsyncPaymentInput {
return &arkv1.AsyncPaymentInput{
Input: toProtoInput(i.Input),
ForfeitLeafHash: i.ForfeitLeafHash.String(),
}
}
type asyncIns []client.AsyncPaymentInput
func (i asyncIns) toProto() []*arkv1.AsyncPaymentInput {
list := make([]*arkv1.AsyncPaymentInput, 0, len(i))
for _, ii := range i {
list = append(list, toAsyncProtoInput(ii))
}
return list
}
type ins []client.Input
func (i ins) toProto() []*arkv1.Input {

View File

@@ -138,9 +138,8 @@ func (a *restClient) RegisterOutputsForNextRound(
outs := make([]*models.V1Output, 0, len(outputs))
for _, o := range outputs {
outs = append(outs, &models.V1Output{
Address: o.Address,
Descriptor: o.Descriptor,
Amount: strconv.Itoa(int(o.Amount)),
Address: o.Address,
Amount: strconv.Itoa(int(o.Amount)),
})
}
body := models.V1RegisterOutputsForNextRoundRequest{
@@ -337,24 +336,26 @@ func (a *restClient) Ping(
}
func (a *restClient) CreatePayment(
ctx context.Context, inputs []client.Input, outputs []client.Output,
ctx context.Context, inputs []client.AsyncPaymentInput, outputs []client.Output,
) (string, error) {
ins := make([]*models.V1Input, 0, len(inputs))
ins := make([]*models.V1AsyncPaymentInput, 0, len(inputs))
for _, i := range inputs {
ins = append(ins, &models.V1Input{
Outpoint: &models.V1Outpoint{
Txid: i.Txid,
Vout: int64(i.VOut),
ins = append(ins, &models.V1AsyncPaymentInput{
Input: &models.V1Input{
Outpoint: &models.V1Outpoint{
Txid: i.Input.Txid,
Vout: int64(i.VOut),
},
Descriptor: i.Input.Descriptor,
},
Descriptor: i.Descriptor,
ForfeitLeafHash: i.ForfeitLeafHash.String(),
})
}
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)),
Descriptor: o.Descriptor,
Address: o.Address,
Amount: strconv.Itoa(int(o.Amount)),
})
}
body := models.V1CreatePaymentRequest{
@@ -495,13 +496,13 @@ func (a *restClient) ListVtxos(
Txid: v.Outpoint.Txid,
VOut: uint32(v.Outpoint.Vout),
},
Amount: uint64(amount),
RoundTxid: v.RoundTxid,
ExpiresAt: expiresAt,
Pending: v.Pending,
RedeemTx: v.RedeemTx,
SpentBy: v.SpentBy,
Descriptor: v.Descriptor,
Amount: uint64(amount),
RoundTxid: v.RoundTxid,
ExpiresAt: expiresAt,
Pending: v.Pending,
RedeemTx: v.RedeemTx,
SpentBy: v.SpentBy,
Pubkey: v.Pubkey,
})
}
@@ -527,11 +528,11 @@ func (a *restClient) ListVtxos(
Txid: v.Outpoint.Txid,
VOut: uint32(v.Outpoint.Vout),
},
Amount: uint64(amount),
RoundTxid: v.RoundTxid,
ExpiresAt: expiresAt,
SpentBy: v.SpentBy,
Descriptor: v.Descriptor,
Amount: uint64(amount),
RoundTxid: v.RoundTxid,
ExpiresAt: expiresAt,
SpentBy: v.SpentBy,
Pubkey: v.Pubkey,
})
}
@@ -695,13 +696,13 @@ func vtxosFromRest(restVtxos []*models.V1Vtxo) []client.Vtxo {
Txid: v.Outpoint.Txid,
VOut: uint32(v.Outpoint.Vout),
},
Descriptor: v.Descriptor,
Amount: uint64(amount),
RoundTxid: v.RoundTxid,
ExpiresAt: expiresAt,
RedeemTx: v.RedeemTx,
Pending: v.Pending,
SpentBy: v.SpentBy,
Pubkey: v.Pubkey,
Amount: uint64(amount),
RoundTxid: v.RoundTxid,
ExpiresAt: expiresAt,
RedeemTx: v.RedeemTx,
Pending: v.Pending,
SpentBy: v.SpentBy,
}
}
return vtxos

View File

@@ -0,0 +1,112 @@
// 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/errors"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
)
// V1AsyncPaymentInput v1 async payment input
//
// swagger:model v1AsyncPaymentInput
type V1AsyncPaymentInput struct {
// forfeit leaf hash
ForfeitLeafHash string `json:"forfeitLeafHash,omitempty"`
// input
Input *V1Input `json:"input,omitempty"`
}
// Validate validates this v1 async payment input
func (m *V1AsyncPaymentInput) Validate(formats strfmt.Registry) error {
var res []error
if err := m.validateInput(formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
func (m *V1AsyncPaymentInput) validateInput(formats strfmt.Registry) error {
if swag.IsZero(m.Input) { // not required
return nil
}
if m.Input != nil {
if err := m.Input.Validate(formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("input")
} else if ce, ok := err.(*errors.CompositeError); ok {
return ce.ValidateName("input")
}
return err
}
}
return nil
}
// ContextValidate validate this v1 async payment input based on the context it is used
func (m *V1AsyncPaymentInput) ContextValidate(ctx context.Context, formats strfmt.Registry) error {
var res []error
if err := m.contextValidateInput(ctx, formats); err != nil {
res = append(res, err)
}
if len(res) > 0 {
return errors.CompositeValidationError(res...)
}
return nil
}
func (m *V1AsyncPaymentInput) contextValidateInput(ctx context.Context, formats strfmt.Registry) error {
if m.Input != nil {
if swag.IsZero(m.Input) { // not required
return nil
}
if err := m.Input.ContextValidate(ctx, formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("input")
} else if ce, ok := err.(*errors.CompositeError); ok {
return ce.ValidateName("input")
}
return err
}
}
return nil
}
// MarshalBinary interface implementation
func (m *V1AsyncPaymentInput) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
return swag.WriteJSON(m)
}
// UnmarshalBinary interface implementation
func (m *V1AsyncPaymentInput) UnmarshalBinary(b []byte) error {
var res V1AsyncPaymentInput
if err := swag.ReadJSON(b, &res); err != nil {
return err
}
*m = res
return nil
}

View File

@@ -20,7 +20,7 @@ import (
type V1CreatePaymentRequest struct {
// inputs
Inputs []*V1Input `json:"inputs"`
Inputs []*V1AsyncPaymentInput `json:"inputs"`
// outputs
Outputs []*V1Output `json:"outputs"`

View File

@@ -22,9 +22,6 @@ type V1Output struct {
// Amount to send in satoshis.
Amount string `json:"amount,omitempty"`
// descriptor
Descriptor string `json:"descriptor,omitempty"`
}
// Validate validates this v1 output

View File

@@ -21,9 +21,6 @@ type V1Vtxo struct {
// amount
Amount string `json:"amount,omitempty"`
// descriptor
Descriptor string `json:"descriptor,omitempty"`
// expire at
ExpireAt string `json:"expireAt,omitempty"`
@@ -33,6 +30,9 @@ type V1Vtxo struct {
// pending
Pending bool `json:"pending,omitempty"`
// pubkey
Pubkey string `json:"pubkey,omitempty"`
// redeem tx
RedeemTx string `json:"redeemTx,omitempty"`

View File

@@ -260,7 +260,7 @@ func (a *covenantArkClient) ListVtxos(
}
for _, addr := range offchainAddrs {
spendable, spent, err := a.client.ListVtxos(ctx, addr)
spendable, spent, err := a.client.ListVtxos(ctx, addr.Address)
if err != nil {
return nil, nil, err
}
@@ -303,7 +303,7 @@ func (a *covenantArkClient) Balance(
offchainBalance: balance,
offchainBalanceByExpiration: amountByExpiration,
}
}(offchainAddr)
}(offchainAddr.Address)
getDelayedBalance := func(addr string) {
defer wg.Done()
@@ -323,8 +323,8 @@ func (a *covenantArkClient) Balance(
}
}
go getDelayedBalance(boardingAddr)
go getDelayedBalance(redeemAddr)
go getDelayedBalance(boardingAddr.Address)
go getDelayedBalance(redeemAddr.Address)
}
wg.Wait()
@@ -455,7 +455,7 @@ func (a *covenantArkClient) UnilateralRedeem(ctx context.Context) error {
vtxos := make([]client.Vtxo, 0)
for _, offchainAddr := range offchainAddrs {
fetchedVtxos, _, err := a.client.ListVtxos(ctx, offchainAddr)
fetchedVtxos, _, err := a.client.ListVtxos(ctx, offchainAddr.Address)
if err != nil {
return err
}
@@ -549,13 +549,19 @@ func (a *covenantArkClient) CollaborativeRedeem(
},
}
vtxos := make([]client.Vtxo, 0)
vtxos := make([]client.DescriptorVtxo, 0)
for _, offchainAddr := range offchainAddrs {
spendableVtxos, err := a.getVtxos(ctx, offchainAddr, withExpiryCoinselect)
spendableVtxos, err := a.getVtxos(ctx, offchainAddr.Address, withExpiryCoinselect)
if err != nil {
return "", err
}
vtxos = append(vtxos, spendableVtxos...)
for _, vtxo := range spendableVtxos {
vtxos = append(vtxos, client.DescriptorVtxo{
Vtxo: vtxo,
Descriptor: offchainAddr.Descriptor,
})
}
}
selectedCoins, changeAmount, err := utils.CoinSelect(
@@ -571,14 +577,9 @@ func (a *covenantArkClient) CollaborativeRedeem(
return "", err
}
desc, err := a.offchainAddressToDefaultVtxoDescriptor(offchainAddr)
if err != nil {
return "", err
}
receivers = append(receivers, client.Output{
Descriptor: desc,
Amount: changeAmount,
Address: offchainAddr.Address,
Amount: changeAmount,
})
}
@@ -621,12 +622,7 @@ func (a *covenantArkClient) SendAsync(
}
func (a *covenantArkClient) Claim(ctx context.Context) (string, error) {
myselfOffchain, _, err := a.wallet.NewAddress(ctx, false)
if err != nil {
return "", err
}
_, mypubkey, _, err := common.DecodeAddress(myselfOffchain)
myselfOffchain, boardingAddr, err := a.wallet.NewAddress(ctx, false)
if err != nil {
return "", err
}
@@ -644,21 +640,16 @@ func (a *covenantArkClient) Claim(ctx context.Context) (string, error) {
return "", fmt.Errorf("no funds to claim")
}
desc, err := a.offchainAddressToDefaultVtxoDescriptor(myselfOffchain)
if err != nil {
return "", err
}
receiver := client.Output{
Descriptor: desc,
Amount: pendingBalance,
Address: myselfOffchain.Address,
Amount: pendingBalance,
}
return a.selfTransferAllPendingPayments(
ctx,
boardingUtxos,
receiver,
hex.EncodeToString(mypubkey.SerializeCompressed()),
boardingAddr.Descriptor,
)
}
@@ -692,13 +683,13 @@ func (a *covenantArkClient) getAllBoardingUtxos(ctx context.Context) ([]explorer
utxos := []explorer.Utxo{}
for _, addr := range boardingAddrs {
txs, err := a.explorer.GetTxs(addr)
txs, err := a.explorer.GetTxs(addr.Address)
if err != nil {
continue
}
for _, tx := range txs {
for i, vout := range tx.Vout {
if vout.Address == addr {
if vout.Address == addr.Address {
createdAt := time.Time{}
if tx.Status.Confirmed {
createdAt = time.Unix(tx.Status.Blocktime, 0)
@@ -718,43 +709,34 @@ func (a *covenantArkClient) getAllBoardingUtxos(ctx context.Context) ([]explorer
}
func (a *covenantArkClient) getClaimableBoardingUtxos(ctx context.Context) ([]explorer.Utxo, error) {
offchainAddrs, boardingAddrs, _, err := a.wallet.GetAddresses(ctx)
_, boardingAddrs, _, err := a.wallet.GetAddresses(ctx)
if err != nil {
return nil, err
}
claimable := make([]explorer.Utxo, 0)
now := time.Now()
_, myPubkey, _, err := common.DecodeAddress(offchainAddrs[0])
if err != nil {
return nil, err
}
myPubkeyStr := hex.EncodeToString(schnorr.SerializePubKey(myPubkey))
descriptorStr := strings.ReplaceAll(
a.BoardingDescriptorTemplate, "USER", myPubkeyStr,
)
boardingScript, err := tree.ParseVtxoScript(descriptorStr)
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 {
boardingUtxos, err := a.explorer.GetUtxos(addr)
boardingScript, err := tree.ParseVtxoScript(addr.Descriptor)
if err != nil {
return nil, err
}
var boardingTimeout uint
if defaultVtxo, ok := boardingScript.(*tree.DefaultVtxoScript); ok {
boardingTimeout = defaultVtxo.ExitDelay
} else {
return nil, fmt.Errorf("unsupported boarding descriptor: %s", addr.Descriptor)
}
boardingUtxos, err := a.explorer.GetUtxos(addr.Address)
if err != nil {
return nil, err
}
now := time.Now()
for _, utxo := range boardingUtxos {
u := utxo.ToUtxo(boardingTimeout)
@@ -826,7 +808,7 @@ func (a *covenantArkClient) sendOnchain(
return "", err
}
changeScript, err := address.ToOutputScript(changeAddr)
changeScript, err := address.ToOutputScript(changeAddr.Address)
if err != nil {
return "", err
}
@@ -876,7 +858,7 @@ func (a *covenantArkClient) sendOnchain(
return "", err
}
changeScript, err := address.ToOutputScript(changeAddr)
changeScript, err := address.ToOutputScript(changeAddr.Address)
if err != nil {
return "", err
}
@@ -938,49 +920,47 @@ func (a *covenantArkClient) sendOffchain(
return "", fmt.Errorf("no funds detected")
}
_, _, aspPubKey, err := common.DecodeAddress(offchainAddrs[0])
if err != nil {
return "", err
}
expectedAspPubkey := schnorr.SerializePubKey(a.AspPubkey)
receiversOutput := make([]client.Output, 0)
sumOfReceivers := uint64(0)
for _, receiver := range receivers {
_, _, aspKey, err := common.DecodeAddress(receiver.To())
rcvAddr, err := common.DecodeAddress(receiver.To())
if err != nil {
return "", fmt.Errorf("invalid receiver address: %s", err)
}
if !bytes.Equal(
aspPubKey.SerializeCompressed(), aspKey.SerializeCompressed(),
) {
return "", fmt.Errorf("invalid receiver address '%s': must be associated with the connected service provider", receiver.To())
rcvAspPubkey := schnorr.SerializePubKey(rcvAddr.Asp)
if !bytes.Equal(rcvAspPubkey, expectedAspPubkey) {
return "", fmt.Errorf("invalid receiver address '%s': expected ASP %s, got %s", receiver.To(), hex.EncodeToString(expectedAspPubkey), hex.EncodeToString(rcvAspPubkey))
}
if receiver.Amount() < a.Dust {
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{
Descriptor: desc,
Amount: receiver.Amount(),
Address: receiver.To(),
Amount: receiver.Amount(),
})
sumOfReceivers += receiver.Amount()
}
vtxos := make([]client.Vtxo, 0)
vtxos := make([]client.DescriptorVtxo, 0)
for _, offchainAddr := range offchainAddrs {
spendableVtxos, err := a.getVtxos(ctx, offchainAddr, withExpiryCoinselect)
spendableVtxos, err := a.getVtxos(ctx, offchainAddr.Address, withExpiryCoinselect)
if err != nil {
return "", err
}
vtxos = append(vtxos, spendableVtxos...)
for _, vtxo := range spendableVtxos {
vtxos = append(vtxos, client.DescriptorVtxo{
Vtxo: vtxo,
Descriptor: offchainAddr.Descriptor,
})
}
}
selectedCoins, changeAmount, err := utils.CoinSelect(
@@ -996,14 +976,9 @@ func (a *covenantArkClient) sendOffchain(
return "", err
}
desc, err := a.offchainAddressToDefaultVtxoDescriptor(offchainAddr)
if err != nil {
return "", err
}
changeReceiver := client.Output{
Descriptor: desc,
Amount: changeAmount,
Address: offchainAddr.Address,
Amount: changeAmount,
}
receiversOutput = append(receiversOutput, changeReceiver)
}
@@ -1044,6 +1019,7 @@ func (a *covenantArkClient) sendOffchain(
return poolTxID, nil
}
// addInputs adds the inputs to the pset for send onchain
func (a *covenantArkClient) addInputs(
ctx context.Context,
updater *psetv2.Updater,
@@ -1055,11 +1031,21 @@ func (a *covenantArkClient) addInputs(
return err
}
_, userPubkey, aspPubkey, err := common.DecodeAddress(offchain)
vtxoScript, err := tree.ParseVtxoScript(offchain.Descriptor)
if err != nil {
return err
}
var userPubkey, aspPubkey *secp256k1.PublicKey
switch s := vtxoScript.(type) {
case *tree.DefaultVtxoScript:
userPubkey = s.Owner
aspPubkey = s.Asp
default:
return fmt.Errorf("unsupported vtxo script: %T", s)
}
for _, utxo := range utxos {
sequence, err := utxo.Sequence()
if err != nil {
@@ -1126,7 +1112,7 @@ func (a *covenantArkClient) addInputs(
func (a *covenantArkClient) handleRoundStream(
ctx context.Context,
paymentID string,
vtxosToSign []client.Vtxo,
vtxosToSign []client.DescriptorVtxo,
boardingUtxos []explorer.Utxo,
boardingDescriptor string,
receivers []client.Output,
@@ -1191,7 +1177,7 @@ func (a *covenantArkClient) handleRoundStream(
func (a *covenantArkClient) handleRoundFinalization(
ctx context.Context,
event client.RoundFinalizationEvent,
vtxos []client.Vtxo,
vtxos []client.DescriptorVtxo,
boardingUtxos []explorer.Utxo,
boardingDescriptor string,
receivers []client.Output,
@@ -1200,18 +1186,8 @@ func (a *covenantArkClient) handleRoundFinalization(
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.createAndSignForfeits(ctx, vtxos, event.Connectors, event.MinRelayFeeRate, myPubkey)
signedForfeits, err = a.createAndSignForfeits(ctx, vtxos, event.Connectors, event.MinRelayFeeRate)
if err != nil {
return
}
@@ -1228,10 +1204,16 @@ func (a *covenantArkClient) handleRoundFinalization(
return nil, "", err
}
// add tapscript leaf
forfeitClosure := &tree.MultisigClosure{
Pubkey: myPubkey,
AspPubkey: a.AspPubkey,
var forfeitClosure tree.Closure
switch s := boardingVtxoScript.(type) {
case *tree.DefaultVtxoScript:
forfeitClosure = &tree.MultisigClosure{
Pubkey: s.Owner,
AspPubkey: a.AspPubkey,
}
default:
return nil, "", fmt.Errorf("unsupported boarding descriptor: %s", boardingDescriptor)
}
forfeitLeaf, err := forfeitClosure.Leaf()
@@ -1381,15 +1363,12 @@ func (a *covenantArkClient) validateOffChainReceiver(
) error {
found := false
receiverVtxoScript, err := tree.ParseVtxoScript(receiver.Descriptor)
addr, err := common.DecodeAddress(receiver.Address)
if err != nil {
return err
}
outputTapKey, _, err := receiverVtxoScript.TapTree()
if err != nil {
return err
}
vtxoTapKey := schnorr.SerializePubKey(addr.VtxoTapKey)
leaves := congestionTree.Leaves()
for _, leaf := range leaves {
@@ -1402,7 +1381,7 @@ func (a *covenantArkClient) validateOffChainReceiver(
if len(output.Script) == 0 {
continue
}
if bytes.Equal(output.Script[2:], schnorr.SerializePubKey(outputTapKey)) {
if bytes.Equal(output.Script[2:], vtxoTapKey) {
if output.Value == receiver.Amount {
found = true
break
@@ -1423,10 +1402,9 @@ func (a *covenantArkClient) validateOffChainReceiver(
func (a *covenantArkClient) createAndSignForfeits(
ctx context.Context,
vtxosToSign []client.Vtxo,
vtxosToSign []client.DescriptorVtxo,
connectors []string,
feeRate chainfee.SatPerKVByte,
myPubKey *secp256k1.PublicKey,
) ([]string, error) {
signedForfeits := make([]string, 0)
connectorsPsets := make([]*psetv2.Pset, 0, len(connectors))
@@ -1471,9 +1449,16 @@ func (a *covenantArkClient) createAndSignForfeits(
TxIndex: vtxo.VOut,
}
forfeitClosure := &tree.MultisigClosure{
Pubkey: myPubKey,
AspPubkey: a.AspPubkey,
var forfeitClosure tree.Closure
switch s := vtxoScript.(type) {
case *tree.DefaultVtxoScript:
forfeitClosure = &tree.MultisigClosure{
Pubkey: s.Owner,
AspPubkey: a.AspPubkey,
}
default:
return nil, fmt.Errorf("unsupported vtxo script: %T", s)
}
forfeitLeaf, err := forfeitClosure.Leaf()
@@ -1535,38 +1520,31 @@ func (a *covenantArkClient) createAndSignForfeits(
func (a *covenantArkClient) coinSelectOnchain(
ctx context.Context, targetAmount uint64, exclude []explorer.Utxo,
) ([]explorer.Utxo, uint64, error) {
offchainAddrs, boardingAddrs, redemptionAddrs, err := a.wallet.GetAddresses(ctx)
_, boardingAddrs, redemptionAddrs, err := a.wallet.GetAddresses(ctx)
if err != nil {
return nil, 0, err
}
_, myPubkey, _, err := common.DecodeAddress(offchainAddrs[0])
if err != nil {
return nil, 0, err
}
myPubkeyStr := hex.EncodeToString(schnorr.SerializePubKey(myPubkey))
descriptorStr := strings.ReplaceAll(
a.BoardingDescriptorTemplate, "USER", myPubkeyStr,
)
boardingScript, err := tree.ParseVtxoScript(descriptorStr)
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()
fetchedUtxos := make([]explorer.Utxo, 0)
for _, addr := range boardingAddrs {
utxos, err := a.explorer.GetUtxos(addr)
boardingDescriptor := addr.Descriptor
boardingScript, err := tree.ParseVtxoScript(boardingDescriptor)
if err != nil {
return nil, 0, err
}
var boardingTimeout uint
if defaultVtxo, ok := boardingScript.(*tree.DefaultVtxoScript); ok {
boardingTimeout = defaultVtxo.ExitDelay
} else {
return nil, 0, fmt.Errorf("unsupported boarding descriptor: %s", boardingDescriptor)
}
utxos, err := a.explorer.GetUtxos(addr.Address)
if err != nil {
return nil, 0, err
}
@@ -1602,7 +1580,7 @@ func (a *covenantArkClient) coinSelectOnchain(
fetchedUtxos = make([]explorer.Utxo, 0)
for _, addr := range redemptionAddrs {
utxos, err := a.explorer.GetUtxos(addr)
utxos, err := a.explorer.GetUtxos(addr.Address)
if err != nil {
return nil, 0, err
}
@@ -1731,14 +1709,10 @@ func (a *covenantArkClient) getVtxos(
}
func (a *covenantArkClient) selfTransferAllPendingPayments(
ctx context.Context, boardingUtxos []explorer.Utxo, myself client.Output, mypubkey string,
ctx context.Context, boardingUtxos []explorer.Utxo, myself client.Output, boardingDescriptor string,
) (string, error) {
inputs := make([]client.Input, 0, len(boardingUtxos))
boardingDescriptor := strings.ReplaceAll(
a.BoardingDescriptorTemplate, "USER", mypubkey[2:],
)
for _, utxo := range boardingUtxos {
inputs = append(inputs, client.Input{
Outpoint: client.Outpoint{
@@ -1761,7 +1735,7 @@ func (a *covenantArkClient) selfTransferAllPendingPayments(
}
roundTxid, err := a.handleRoundStream(
ctx, paymentID, make([]client.Vtxo, 0), boardingUtxos, boardingDescriptor, outputs,
ctx, paymentID, make([]client.DescriptorVtxo, 0), boardingUtxos, boardingDescriptor, outputs,
)
if err != nil {
return "", err
@@ -1770,24 +1744,7 @@ 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 []types.Transaction) {
func (a *covenantArkClient) getBoardingTxs(ctx context.Context) (transactions []types.Transaction) {
utxos, err := a.getClaimableBoardingUtxos(ctx)
if err != nil {
return nil

View File

@@ -223,12 +223,14 @@ func (a *covenantlessArkClient) listenForTransactions(ctx context.Context) {
return
}
desc, err := a.offchainAddressToDefaultVtxoDescriptor(offchainAddr)
addr, err := common.DecodeAddress(offchainAddr.Address)
if err != nil {
log.WithError(err).Error("Failed to get descriptor for new address")
log.WithError(err).Error("Failed to decode address")
return
}
addrPubkey := hex.EncodeToString(schnorr.SerializePubKey(addr.VtxoTapKey))
for {
select {
case event, ok := <-eventChan:
@@ -253,7 +255,7 @@ func (a *covenantlessArkClient) listenForTransactions(ctx context.Context) {
continue
}
a.processTransactionEvent(desc, event, pendingBoardingTxsMap)
a.processTransactionEvent(addrPubkey, event, pendingBoardingTxsMap)
case <-ctx.Done():
return
}
@@ -319,7 +321,7 @@ func (a *covenantlessArkClient) getBoardingPendingTransactions(
}
func (a *covenantlessArkClient) processTransactionEvent(
descriptor string,
pubkey string,
event client.TransactionEvent,
pendingBoardingTxsMap map[string]types.Transaction,
) {
@@ -372,7 +374,7 @@ func (a *covenantlessArkClient) processTransactionEvent(
vtxosToInsert := make([]types.Vtxo, 0)
txsToInsert := make([]types.Transaction, 0)
for _, v := range event.Round.SpendableVtxos {
if v.Descriptor == descriptor {
if v.Pubkey == pubkey {
vtxosToInsert = append(vtxosToInsert, types.Vtxo{
VtxoKey: types.VtxoKey{
Txid: v.Txid,
@@ -449,7 +451,7 @@ func (a *covenantlessArkClient) processTransactionEvent(
outputAmount := uint64(0)
for _, v := range event.Redeem.SpendableVtxos {
if v.Descriptor == descriptor {
if v.Pubkey == pubkey {
vtxosToInsert = append(vtxosToInsert, types.Vtxo{
VtxoKey: types.VtxoKey{
Txid: v.Txid,
@@ -482,7 +484,7 @@ func (a *covenantlessArkClient) processTransactionEvent(
}
} else {
for _, v := range event.Redeem.SpendableVtxos {
if v.Descriptor == descriptor {
if v.Pubkey == pubkey {
vtxosToInsert = append(vtxosToInsert, types.Vtxo{
VtxoKey: types.VtxoKey{
Txid: v.Txid,
@@ -524,17 +526,25 @@ func (a *covenantlessArkClient) processTransactionEvent(
func (a *covenantlessArkClient) ListVtxos(
ctx context.Context,
) (spendableVtxos, spentVtxos []client.Vtxo, err error) {
offchainAddrs, _, _, err := a.wallet.GetAddresses(ctx)
offchainAddrs, boardingAddrs, _, err := a.wallet.GetAddresses(ctx)
if err != nil {
return
}
_, pubkey, _, err := common.DecodeAddress(offchainAddrs[0])
boardingAddrScript, err := bitcointree.ParseVtxoScript(boardingAddrs[0].Descriptor)
if err != nil {
return
}
myPubkey := schnorr.SerializePubKey(pubkey)
var myPubkey []byte
if boardingScript, ok := boardingAddrScript.(*bitcointree.DefaultVtxoScript); ok {
myPubkey = schnorr.SerializePubKey(boardingScript.Owner)
}
if myPubkey == nil {
return nil, nil, fmt.Errorf("invalid boarding address descriptor")
}
// The ASP returns the vtxos sent to others via async payments as spendable
// because they are actually revertable. Since we do not provide any revert
@@ -544,22 +554,25 @@ func (a *covenantlessArkClient) ListVtxos(
// The auxiliary variables below are used to make these checks in an
// efficient way.
for _, addr := range offchainAddrs {
spendable, spent, err := a.client.ListVtxos(ctx, addr)
spendable, spent, err := a.client.ListVtxos(ctx, addr.Address)
if err != nil {
return nil, nil, err
}
script, err := bitcointree.ParseVtxoScript(addr.Descriptor)
if err != nil {
return nil, nil, err
}
reversibleVtxo, isReversible := script.(*bitcointree.ReversibleVtxoScript)
for _, v := range spendable {
if !v.Pending {
spendableVtxos = append(spendableVtxos, v)
continue
}
script, err := bitcointree.ParseVtxoScript(v.Descriptor)
if err != nil {
return nil, nil, err
}
reversibleVtxo, ok := script.(*bitcointree.ReversibleVtxoScript)
if !ok {
if !isReversible {
spendableVtxos = append(spendableVtxos, v)
continue
}
@@ -568,17 +581,13 @@ func (a *covenantlessArkClient) ListVtxos(
spendableVtxos = append(spendableVtxos, v)
}
}
for _, v := range spent {
script, err := bitcointree.ParseVtxoScript(v.Descriptor)
if err != nil {
return nil, nil, err
}
reversibleVtxo, ok := script.(*bitcointree.ReversibleVtxoScript)
if !ok {
for _, v := range spent {
if !isReversible {
spentVtxos = append(spentVtxos, v)
continue
}
if !bytes.Equal(schnorr.SerializePubKey(reversibleVtxo.Sender), myPubkey) {
spentVtxos = append(spentVtxos, v)
}
@@ -602,14 +611,13 @@ func (a *covenantlessArkClient) Balance(
chRes := make(chan balanceRes, nbWorkers*len(offchainAddrs))
for i := range offchainAddrs {
offchainAddr := offchainAddrs[i]
boardingAddr := boardingAddrs[i]
redeemAddr := redeemAddrs[i]
go func(addr string) {
go func() {
defer wg.Done()
balance, amountByExpiration, err := a.getOffchainBalance(
ctx, addr, computeVtxoExpiration,
ctx, computeVtxoExpiration,
)
if err != nil {
chRes <- balanceRes{err: err}
@@ -620,7 +628,7 @@ func (a *covenantlessArkClient) Balance(
offchainBalance: balance,
offchainBalanceByExpiration: amountByExpiration,
}
}(offchainAddr)
}()
getDelayedBalance := func(addr string) {
defer wg.Done()
@@ -640,8 +648,8 @@ func (a *covenantlessArkClient) Balance(
}
}
go getDelayedBalance(boardingAddr)
go getDelayedBalance(redeemAddr)
go getDelayedBalance(boardingAddr.Address)
go getDelayedBalance(redeemAddr.Address)
}
wg.Wait()
@@ -765,20 +773,11 @@ func (a *covenantlessArkClient) UnilateralRedeem(ctx context.Context) error {
return fmt.Errorf("wallet is locked")
}
offchainAddrs, _, _, err := a.wallet.GetAddresses(ctx)
vtxos, _, err := a.getVtxos(ctx, false)
if err != nil {
return err
}
vtxos := make([]client.Vtxo, 0)
for _, offchainAddr := range offchainAddrs {
spendableVtxos, _, err := a.getVtxos(ctx, offchainAddr, false)
if err != nil {
return err
}
vtxos = append(vtxos, spendableVtxos...)
}
totalVtxosAmount := uint64(0)
for _, vtxo := range vtxos {
totalVtxosAmount += vtxo.Amount
@@ -853,13 +852,26 @@ func (a *covenantlessArkClient) CollaborativeRedeem(
},
}
vtxos := make([]client.Vtxo, 0)
vtxos := make([]client.DescriptorVtxo, 0)
spendableVtxos, _, err := a.getVtxos(ctx, withExpiryCoinselect)
if err != nil {
return "", err
}
for _, offchainAddr := range offchainAddrs {
spendableVtxos, _, err := a.getVtxos(ctx, offchainAddr, withExpiryCoinselect)
if err != nil {
return "", err
for _, v := range spendableVtxos {
vtxoAddr, err := v.Address(a.AspPubkey, a.Network)
if err != nil {
return "", err
}
if vtxoAddr == offchainAddr.Address {
vtxos = append(vtxos, client.DescriptorVtxo{
Vtxo: v,
Descriptor: offchainAddr.Descriptor,
})
}
}
vtxos = append(vtxos, spendableVtxos...)
}
selectedCoins, changeAmount, err := utils.CoinSelect(
@@ -875,14 +887,9 @@ func (a *covenantlessArkClient) CollaborativeRedeem(
return "", err
}
desc, err := a.offchainAddressToDefaultVtxoDescriptor(offchainAddr)
if err != nil {
return "", err
}
receivers = append(receivers, client.Output{
Descriptor: desc,
Amount: changeAmount,
Address: offchainAddr.Address,
Amount: changeAmount,
})
}
@@ -950,59 +957,57 @@ func (a *covenantlessArkClient) SendAsync(
return "", err
}
_, _, aspPubKey, err := common.DecodeAddress(offchainAddrs[0])
if err != nil {
return "", err
}
expectedAspPubKey := schnorr.SerializePubKey(a.AspPubkey)
receiversOutput := make([]client.Output, 0)
sumOfReceivers := uint64(0)
for _, receiver := range receivers {
_, _, aspKey, err := common.DecodeAddress(receiver.To())
rcvAddr, err := common.DecodeAddress(receiver.To())
if err != nil {
return "", fmt.Errorf("invalid receiver address: %s", err)
}
if !bytes.Equal(
aspPubKey.SerializeCompressed(), aspKey.SerializeCompressed(),
) {
return "", fmt.Errorf("invalid receiver address '%s': must be associated with the connected service provider", receiver)
rcvAspPubKey := schnorr.SerializePubKey(rcvAddr.Asp)
if !bytes.Equal(expectedAspPubKey, rcvAspPubKey) {
return "", fmt.Errorf("invalid receiver address '%s': expected ASP %s, got %s", receiver.To(), hex.EncodeToString(expectedAspPubKey), hex.EncodeToString(rcvAspPubKey))
}
if receiver.Amount() < a.Dust {
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{
Descriptor: desc,
Amount: receiver.Amount(),
Address: receiver.To(),
Amount: receiver.Amount(),
})
sumOfReceivers += receiver.Amount()
}
vtxos, _, err := a.getVtxos(ctx, offchainAddrs[0], withExpiryCoinselect)
vtxos := make([]client.DescriptorVtxo, 0)
spendableVtxos, _, err := a.getVtxos(ctx, withExpiryCoinselect)
if err != nil {
return "", err
}
for _, offchainAddr := range offchainAddrs {
for _, v := range spendableVtxos {
vtxoAddr, err := v.Address(a.AspPubkey, a.Network)
if err != nil {
return "", err
}
if vtxoAddr == offchainAddr.Address {
vtxos = append(vtxos, client.DescriptorVtxo{
Vtxo: v,
Descriptor: offchainAddr.Descriptor,
})
}
}
}
selectedCoins, changeAmount, err := utils.CoinSelect(
vtxos, sumOfReceivers, a.Dust, withExpiryCoinselect,
)
@@ -1011,27 +1016,52 @@ func (a *covenantlessArkClient) SendAsync(
}
if changeAmount > 0 {
changeDesc, err := a.offchainAddressToDefaultVtxoDescriptor(offchainAddrs[0])
if err != nil {
return "", err
}
changeReceiver := client.Output{
Descriptor: changeDesc,
Amount: changeAmount,
Address: offchainAddrs[0].Address,
Amount: changeAmount,
}
receiversOutput = append(receiversOutput, changeReceiver)
}
inputs := make([]client.Input, 0, len(selectedCoins))
inputs := make([]client.AsyncPaymentInput, 0, len(selectedCoins))
for _, coin := range selectedCoins {
inputs = append(inputs, client.Input{
Outpoint: client.Outpoint{
Txid: coin.Txid,
VOut: coin.VOut,
vtxoScript, err := bitcointree.ParseVtxoScript(coin.Descriptor)
if err != nil {
return "", err
}
var forfeitClosure bitcointree.Closure
switch s := vtxoScript.(type) {
case *bitcointree.DefaultVtxoScript:
forfeitClosure = &bitcointree.MultisigClosure{
Pubkey: s.Owner,
AspPubkey: s.Asp,
}
case *bitcointree.ReversibleVtxoScript:
forfeitClosure = &bitcointree.MultisigClosure{
Pubkey: s.Owner,
AspPubkey: s.Asp,
}
default:
return "", fmt.Errorf("unsupported vtxo script: %T", s)
}
forfeitLeaf, err := forfeitClosure.Leaf()
if err != nil {
return "", err
}
inputs = append(inputs, client.AsyncPaymentInput{
Input: client.Input{
Outpoint: client.Outpoint{
Txid: coin.Txid,
VOut: coin.VOut,
},
Descriptor: coin.Descriptor,
},
Descriptor: coin.Descriptor,
ForfeitLeafHash: forfeitLeaf.TapHash(),
})
}
@@ -1057,19 +1087,22 @@ func (a *covenantlessArkClient) SendAsync(
}
func (a *covenantlessArkClient) Claim(ctx context.Context) (string, error) {
myselfOffchain, _, err := a.wallet.NewAddress(ctx, false)
myselfOffchain, boardingAddr, err := a.wallet.NewAddress(ctx, false)
if err != nil {
return "", err
}
_, pendingVtxos, err := a.getVtxos(ctx, myselfOffchain, false)
_, pendingVtxos, err := a.getVtxos(ctx, false)
if err != nil {
return "", err
}
_, mypubkey, _, err := common.DecodeAddress(myselfOffchain)
if err != nil {
return "", err
pendingVtxosWithDescriptor := make([]client.DescriptorVtxo, 0)
for _, vtxo := range pendingVtxos {
pendingVtxosWithDescriptor = append(pendingVtxosWithDescriptor, client.DescriptorVtxo{
Vtxo: vtxo,
Descriptor: myselfOffchain.Descriptor,
})
}
boardingUtxos, err := a.getClaimableBoardingUtxos(ctx)
@@ -1088,22 +1121,17 @@ func (a *covenantlessArkClient) Claim(ctx context.Context) (string, error) {
return "", nil
}
desc, err := a.offchainAddressToDefaultVtxoDescriptor(myselfOffchain)
if err != nil {
return "", err
}
receiver := client.Output{
Descriptor: desc,
Amount: pendingBalance,
Address: myselfOffchain.Address,
Amount: pendingBalance,
}
return a.selfTransferAllPendingPayments(
ctx,
pendingVtxos,
pendingVtxosWithDescriptor,
boardingUtxos,
receiver,
hex.EncodeToString(mypubkey.SerializeCompressed()),
boardingAddr.Descriptor,
)
}
@@ -1208,7 +1236,7 @@ func (a *covenantlessArkClient) sendOnchain(
if err != nil {
return "", err
}
addr, _ := btcutil.DecodeAddress(changeAddr, &netParams)
addr, _ := btcutil.DecodeAddress(changeAddr.Address, &netParams)
pkscript, err := txscript.PayToAddrScript(addr)
if err != nil {
@@ -1255,7 +1283,7 @@ func (a *covenantlessArkClient) sendOnchain(
if err != nil {
return "", err
}
addr, _ := btcutil.DecodeAddress(changeAddr, &netParams)
addr, _ := btcutil.DecodeAddress(changeAddr.Address, &netParams)
pkscript, err := txscript.PayToAddrScript(addr)
if err != nil {
@@ -1306,49 +1334,55 @@ func (a *covenantlessArkClient) sendOffchain(
return "", fmt.Errorf("no funds detected")
}
_, _, aspPubKey, err := common.DecodeAddress(offchainAddrs[0])
if err != nil {
return "", err
}
expectedAspPubKey := schnorr.SerializePubKey(a.AspPubkey)
receiversOutput := make([]client.Output, 0)
sumOfReceivers := uint64(0)
for _, receiver := range receivers {
_, _, aspKey, err := common.DecodeAddress(receiver.To())
rcvAddr, err := common.DecodeAddress(receiver.To())
if err != nil {
return "", fmt.Errorf("invalid receiver address: %s", err)
}
if !bytes.Equal(
aspPubKey.SerializeCompressed(), aspKey.SerializeCompressed(),
) {
return "", fmt.Errorf("invalid receiver address '%s': must be associated with the connected service provider", receiver.To())
rcvAspPubKey := schnorr.SerializePubKey(rcvAddr.Asp)
if !bytes.Equal(expectedAspPubKey, rcvAspPubKey) {
return "", fmt.Errorf("invalid receiver address '%s': expected ASP %s, got %s", receiver.To(), hex.EncodeToString(expectedAspPubKey), hex.EncodeToString(rcvAspPubKey))
}
if receiver.Amount() < a.Dust {
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{
Descriptor: desc,
Amount: receiver.Amount(),
Address: receiver.To(),
Amount: receiver.Amount(),
})
sumOfReceivers += receiver.Amount()
}
vtxos := make([]client.Vtxo, 0)
vtxos := make([]client.DescriptorVtxo, 0)
spendableVtxos, _, err := a.getVtxos(ctx, withExpiryCoinselect)
if err != nil {
return "", err
}
for _, offchainAddr := range offchainAddrs {
spendableVtxos, _, err := a.getVtxos(ctx, offchainAddr, withExpiryCoinselect)
if err != nil {
return "", err
for _, v := range spendableVtxos {
vtxoAddr, err := v.Address(a.AspPubkey, a.Network)
if err != nil {
return "", err
}
if vtxoAddr == offchainAddr.Address {
vtxos = append(vtxos, client.DescriptorVtxo{
Vtxo: v,
Descriptor: offchainAddr.Descriptor,
})
}
}
vtxos = append(vtxos, spendableVtxos...)
}
selectedCoins, changeAmount, err := utils.CoinSelect(
@@ -1363,15 +1397,9 @@ func (a *covenantlessArkClient) sendOffchain(
if err != nil {
return "", err
}
desc, err := a.offchainAddressToDefaultVtxoDescriptor(offchainAddr)
if err != nil {
return "", err
}
changeReceiver := client.Output{
Descriptor: desc,
Amount: changeAmount,
Address: offchainAddr.Address,
Amount: changeAmount,
}
receiversOutput = append(receiversOutput, changeReceiver)
}
@@ -1428,11 +1456,21 @@ func (a *covenantlessArkClient) addInputs(
return err
}
_, userPubkey, aspPubkey, err := common.DecodeAddress(offchain)
vtxoScript, err := tree.ParseVtxoScript(offchain.Descriptor)
if err != nil {
return err
}
var userPubkey, aspPubkey *secp256k1.PublicKey
switch s := vtxoScript.(type) {
case *tree.DefaultVtxoScript:
userPubkey = s.Owner
aspPubkey = s.Asp
default:
return fmt.Errorf("unsupported vtxo script: %T", s)
}
for _, utxo := range utxos {
previousHash, err := chainhash.NewHashFromStr(utxo.Txid)
if err != nil {
@@ -1495,7 +1533,7 @@ func (a *covenantlessArkClient) addInputs(
func (a *covenantlessArkClient) handleRoundStream(
ctx context.Context,
paymentID string,
vtxosToSign []client.Vtxo,
vtxosToSign []client.DescriptorVtxo,
boardingUtxos []explorer.Utxo,
boardingDescriptor string,
receivers []client.Output,
@@ -1681,7 +1719,7 @@ func (a *covenantlessArkClient) handleRoundSigningNoncesGenerated(
func (a *covenantlessArkClient) handleRoundFinalization(
ctx context.Context,
event client.RoundFinalizationEvent,
vtxos []client.Vtxo,
vtxos []client.DescriptorVtxo,
boardingUtxos []explorer.Utxo,
boardingDescriptor string,
receivers []client.Output,
@@ -1690,21 +1728,11 @@ func (a *covenantlessArkClient) handleRoundFinalization(
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.createAndSignForfeits(
ctx, vtxos, event.Connectors, event.MinRelayFeeRate, myPubkey,
ctx, vtxos, event.Connectors, event.MinRelayFeeRate,
)
if err != nil {
return nil, "", err
@@ -1724,6 +1752,15 @@ func (a *covenantlessArkClient) handleRoundFinalization(
return nil, "", err
}
var myPubkey *secp256k1.PublicKey
switch v := boardingVtxoScript.(type) {
case *bitcointree.DefaultVtxoScript:
myPubkey = v.Owner
default:
return nil, "", fmt.Errorf("unsupported boarding descriptor: %s", boardingDescriptor)
}
// add tapscript leaf
forfeitClosure := &bitcointree.MultisigClosure{
Pubkey: myPubkey,
@@ -1869,15 +1906,12 @@ func (a *covenantlessArkClient) validateOffChainReceiver(
) error {
found := false
receiverVtxoScript, err := bitcointree.ParseVtxoScript(receiver.Descriptor)
rcvAddr, err := common.DecodeAddress(receiver.Address)
if err != nil {
return err
}
outputTapKey, _, err := receiverVtxoScript.TapTree()
if err != nil {
return err
}
vtxoTapKey := schnorr.SerializePubKey(rcvAddr.VtxoTapKey)
leaves := congestionTree.Leaves()
for _, leaf := range leaves {
@@ -1891,9 +1925,7 @@ func (a *covenantlessArkClient) validateOffChainReceiver(
continue
}
if bytes.Equal(
output.PkScript[2:], schnorr.SerializePubKey(outputTapKey),
) {
if bytes.Equal(output.PkScript[2:], vtxoTapKey) {
if output.Value != int64(receiver.Amount) {
continue
}
@@ -1919,10 +1951,9 @@ func (a *covenantlessArkClient) validateOffChainReceiver(
func (a *covenantlessArkClient) createAndSignForfeits(
ctx context.Context,
vtxosToSign []client.Vtxo,
vtxosToSign []client.DescriptorVtxo,
connectors []string,
feeRate chainfee.SatPerKVByte,
myPubkey *secp256k1.PublicKey,
) ([]string, error) {
parsedForfeitAddr, err := btcutil.DecodeAddress(a.ForfeitAddress, nil)
if err != nil {
@@ -1982,9 +2013,21 @@ func (a *covenantlessArkClient) createAndSignForfeits(
Index: vtxo.VOut,
}
forfeitClosure := &bitcointree.MultisigClosure{
Pubkey: myPubkey,
AspPubkey: a.AspPubkey,
var forfeitClosure bitcointree.Closure
switch v := vtxoScript.(type) {
case *bitcointree.DefaultVtxoScript:
forfeitClosure = &bitcointree.MultisigClosure{
Pubkey: v.Owner,
AspPubkey: a.AspPubkey,
}
case *bitcointree.ReversibleVtxoScript:
forfeitClosure = &bitcointree.MultisigClosure{
Pubkey: v.Owner,
AspPubkey: a.AspPubkey,
}
default:
return nil, fmt.Errorf("unsupported vtxo script: %T", vtxoScript)
}
forfeitLeaf, err := forfeitClosure.Leaf()
@@ -2040,39 +2083,28 @@ func (a *covenantlessArkClient) createAndSignForfeits(
func (a *covenantlessArkClient) coinSelectOnchain(
ctx context.Context, targetAmount uint64, exclude []explorer.Utxo,
) ([]explorer.Utxo, uint64, error) {
offchainAddrs, boardingAddrs, redemptionAddrs, err := a.wallet.GetAddresses(ctx)
_, boardingAddrs, redemptionAddrs, err := a.wallet.GetAddresses(ctx)
if err != nil {
return nil, 0, err
}
_, myPubkey, _, err := common.DecodeAddress(offchainAddrs[0])
if err != nil {
return nil, 0, err
}
myPubkeyStr := hex.EncodeToString(schnorr.SerializePubKey(myPubkey))
descriptorStr := strings.ReplaceAll(
a.BoardingDescriptorTemplate, "USER", myPubkeyStr,
)
boardingScript, err := bitcointree.ParseVtxoScript(descriptorStr)
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()
fetchedUtxos := make([]explorer.Utxo, 0)
for _, addr := range boardingAddrs {
utxos, err := a.explorer.GetUtxos(addr)
boardingScript, err := bitcointree.ParseVtxoScript(addr.Descriptor)
if err != nil {
return nil, 0, err
}
var boardingTimeout uint
if defaultVtxo, ok := boardingScript.(*bitcointree.DefaultVtxoScript); ok {
boardingTimeout = defaultVtxo.ExitDelay
} else {
return nil, 0, fmt.Errorf("unsupported boarding descriptor: %s", addr.Descriptor)
}
utxos, err := a.explorer.GetUtxos(addr.Address)
if err != nil {
return nil, 0, err
}
@@ -2108,7 +2140,7 @@ func (a *covenantlessArkClient) coinSelectOnchain(
fetchedUtxos = make([]explorer.Utxo, 0)
for _, addr := range redemptionAddrs {
utxos, err := a.explorer.GetUtxos(addr)
utxos, err := a.explorer.GetUtxos(addr.Address)
if err != nil {
return nil, 0, err
}
@@ -2185,11 +2217,11 @@ func (a *covenantlessArkClient) getRedeemBranches(
// Currently, the returned balance is calculated from both spendable and
// pending vtxos.
func (a *covenantlessArkClient) getOffchainBalance(
ctx context.Context, addr string, computeVtxoExpiration bool,
ctx context.Context, computeVtxoExpiration bool,
) (uint64, map[int64]uint64, error) {
amountByExpiration := make(map[int64]uint64, 0)
vtxos, _, err := a.getVtxos(ctx, addr, computeVtxoExpiration)
vtxos, _, err := a.getVtxos(ctx, computeVtxoExpiration)
if err != nil {
return 0, nil, err
}
@@ -2222,13 +2254,13 @@ func (a *covenantlessArkClient) getAllBoardingUtxos(
utxos := []explorer.Utxo{}
ignoreVtxos := make(map[string]struct{}, 0)
for _, addr := range boardingAddrs {
txs, err := a.explorer.GetTxs(addr)
txs, err := a.explorer.GetTxs(addr.Address)
if err != nil {
return nil, nil, err
}
for _, tx := range txs {
for i, vout := range tx.Vout {
if vout.Address == addr {
if vout.Address == addr.Address {
spentStatuses, err := a.explorer.GetTxOutspends(tx.Txid)
if err != nil {
return nil, nil, err
@@ -2255,39 +2287,29 @@ func (a *covenantlessArkClient) getAllBoardingUtxos(
}
func (a *covenantlessArkClient) getClaimableBoardingUtxos(ctx context.Context) ([]explorer.Utxo, error) {
offchainAddrs, boardingAddrs, _, err := a.wallet.GetAddresses(ctx)
_, boardingAddrs, _, err := a.wallet.GetAddresses(ctx)
if err != nil {
return nil, err
}
_, myPubkey, _, err := common.DecodeAddress(offchainAddrs[0])
if err != nil {
return nil, err
}
myPubkeyStr := hex.EncodeToString(schnorr.SerializePubKey(myPubkey))
descriptorStr := strings.ReplaceAll(
a.BoardingDescriptorTemplate, "USER", myPubkeyStr,
)
boardingScript, err := bitcointree.ParseVtxoScript(descriptorStr)
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)
now := time.Now()
for _, addr := range boardingAddrs {
boardingUtxos, err := a.explorer.GetUtxos(addr)
boardingScript, err := bitcointree.ParseVtxoScript(addr.Descriptor)
if err != nil {
return nil, err
}
var boardingTimeout uint
if defaultVtxo, ok := boardingScript.(*bitcointree.DefaultVtxoScript); ok {
boardingTimeout = defaultVtxo.ExitDelay
} else {
return nil, fmt.Errorf("unsupported boarding descriptor: %s", addr.Descriptor)
}
boardingUtxos, err := a.explorer.GetUtxos(addr.Address)
if err != nil {
return nil, err
}
@@ -2306,7 +2328,7 @@ func (a *covenantlessArkClient) getClaimableBoardingUtxos(ctx context.Context) (
}
func (a *covenantlessArkClient) getVtxos(
ctx context.Context, _ string, computeVtxoExpiration bool,
ctx context.Context, computeVtxoExpiration bool,
) ([]client.Vtxo, []client.Vtxo, error) {
spendableVtxos, _, err := a.ListVtxos(ctx)
if err != nil {
@@ -2347,14 +2369,10 @@ func (a *covenantlessArkClient) getVtxos(
}
func (a *covenantlessArkClient) selfTransferAllPendingPayments(
ctx context.Context, pendingVtxos []client.Vtxo, boardingUtxos []explorer.Utxo, myself client.Output, mypubkey string,
ctx context.Context, pendingVtxos []client.DescriptorVtxo, boardingUtxos []explorer.Utxo, myself client.Output, boardingDescriptor string,
) (string, error) {
inputs := make([]client.Input, 0, len(pendingVtxos)+len(boardingUtxos))
boardingDescriptor := strings.ReplaceAll(
a.BoardingDescriptorTemplate, "USER", mypubkey[2:],
)
for _, coin := range pendingVtxos {
inputs = append(inputs, client.Input{
Outpoint: client.Outpoint{
@@ -2404,42 +2422,6 @@ 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
}
// getBoardingTxs builds the boarding tx history from onchain utxos:
// - unspent utxo => pending boarding tx
// - spent utxo => claimed boarding tx

View File

@@ -23,10 +23,10 @@ import (
)
func CoinSelect(
vtxos []client.Vtxo, amount, dust uint64, sortByExpirationTime bool,
) ([]client.Vtxo, uint64, error) {
selected := make([]client.Vtxo, 0)
notSelected := make([]client.Vtxo, 0)
vtxos []client.DescriptorVtxo, amount, dust uint64, sortByExpirationTime bool,
) ([]client.DescriptorVtxo, uint64, error) {
selected := make([]client.DescriptorVtxo, 0)
notSelected := make([]client.DescriptorVtxo, 0)
selectedAmount := uint64(0)
if sortByExpirationTime {

View File

@@ -43,41 +43,96 @@ func NewBitcoinWallet(
func (w *bitcoinWallet) GetAddresses(
ctx context.Context,
) ([]string, []string, []string, error) {
offchainAddr, boardingAddr, redemptionAddr, err := w.getAddress(ctx)
) ([]wallet.DescriptorAddress, []wallet.DescriptorAddress, []wallet.DescriptorAddress, error) {
offchainAddr, boardingAddr, err := w.getAddress(ctx)
if err != nil {
return nil, nil, nil, err
}
offchainAddrs := []string{offchainAddr}
boardingAddrs := []string{boardingAddr}
redemptionAddrs := []string{redemptionAddr}
encodedOffchainAddr, err := offchainAddr.Address.Encode()
if err != nil {
return nil, nil, nil, err
}
data, err := w.configStore.GetData(ctx)
if err != nil {
return nil, nil, nil, err
}
netParams := utils.ToBitcoinNetwork(data.Network)
redemptionAddr, err := btcutil.NewAddressTaproot(
schnorr.SerializePubKey(offchainAddr.Address.VtxoTapKey),
&netParams,
)
if err != nil {
return nil, nil, nil, err
}
offchainAddrs := []wallet.DescriptorAddress{
{
Descriptor: offchainAddr.Descriptor,
Address: encodedOffchainAddr,
},
}
boardingAddrs := []wallet.DescriptorAddress{
{
Descriptor: boardingAddr.Descriptor,
Address: boardingAddr.Address,
},
}
redemptionAddrs := []wallet.DescriptorAddress{
{
Descriptor: offchainAddr.Descriptor,
Address: redemptionAddr.EncodeAddress(),
},
}
return offchainAddrs, boardingAddrs, redemptionAddrs, nil
}
func (w *bitcoinWallet) NewAddress(
ctx context.Context, _ bool,
) (string, string, error) {
offchainAddr, boardingAddr, _, err := w.getAddress(ctx)
if err != nil {
return "", "", err
}
return offchainAddr, boardingAddr, nil
}
func (w *bitcoinWallet) NewAddresses(
ctx context.Context, _ bool, num int,
) ([]string, []string, error) {
offchainAddr, boardingAddr, _, err := w.getAddress(ctx)
) (*wallet.DescriptorAddress, *wallet.DescriptorAddress, error) {
offchainAddr, boardingAddr, err := w.getAddress(ctx)
if err != nil {
return nil, nil, err
}
offchainAddrs := make([]string, 0, num)
boardingAddrs := make([]string, 0, num)
encodedOffchainAddr, err := offchainAddr.Address.Encode()
if err != nil {
return nil, nil, err
}
return &wallet.DescriptorAddress{
Descriptor: offchainAddr.Descriptor,
Address: encodedOffchainAddr,
}, boardingAddr, nil
}
func (w *bitcoinWallet) NewAddresses(
ctx context.Context, _ bool, num int,
) ([]wallet.DescriptorAddress, []wallet.DescriptorAddress, error) {
offchainAddr, boardingAddr, err := w.getAddress(ctx)
if err != nil {
return nil, nil, err
}
offchainAddrs := make([]wallet.DescriptorAddress, 0, num)
boardingAddrs := make([]wallet.DescriptorAddress, 0, num)
for i := 0; i < num; i++ {
offchainAddrs = append(offchainAddrs, offchainAddr)
boardingAddrs = append(boardingAddrs, boardingAddr)
encodedOffchainAddr, err := offchainAddr.Address.Encode()
if err != nil {
return nil, nil, err
}
offchainAddrs = append(offchainAddrs, wallet.DescriptorAddress{
Descriptor: offchainAddr.Descriptor,
Address: encodedOffchainAddr,
})
boardingAddrs = append(boardingAddrs, wallet.DescriptorAddress{
Descriptor: boardingAddr.Descriptor,
Address: boardingAddr.Address,
})
}
return offchainAddrs, boardingAddrs, nil
}
@@ -205,19 +260,21 @@ func (s *bitcoinWallet) SignTransaction(
func (w *bitcoinWallet) getAddress(
ctx context.Context,
) (string, string, string, error) {
) (
*struct {
Address common.Address
Descriptor string
},
*wallet.DescriptorAddress,
error,
) {
if w.walletData == nil {
return "", "", "", fmt.Errorf("wallet not initialized")
return nil, nil, fmt.Errorf("wallet not initialized")
}
data, err := w.configStore.GetData(ctx)
if err != nil {
return "", "", "", err
}
offchainAddr, err := common.EncodeAddress(data.Network.Addr, w.walletData.Pubkey, data.AspPubkey)
if err != nil {
return "", "", "", err
return nil, nil, err
}
netParams := utils.ToBitcoinNetwork(data.Network)
@@ -230,15 +287,13 @@ func (w *bitcoinWallet) getAddress(
vtxoTapKey, _, err := defaultVtxoScript.TapTree()
if err != nil {
return "", "", "", err
return nil, nil, err
}
redemptionAddr, err := btcutil.NewAddressTaproot(
schnorr.SerializePubKey(vtxoTapKey),
&netParams,
)
if err != nil {
return "", "", "", err
offchainAddress := &common.Address{
HRP: data.Network.Addr,
Asp: data.AspPubkey,
VtxoTapKey: vtxoTapKey,
}
myPubkeyStr := hex.EncodeToString(schnorr.SerializePubKey(w.walletData.Pubkey))
@@ -248,12 +303,12 @@ func (w *bitcoinWallet) getAddress(
boardingVtxoScript, err := bitcointree.ParseVtxoScript(descriptorStr)
if err != nil {
return "", "", "", err
return nil, nil, err
}
boardingTapKey, _, err := boardingVtxoScript.TapTree()
if err != nil {
return "", "", "", err
return nil, nil, err
}
boardingAddr, err := btcutil.NewAddressTaproot(
@@ -261,8 +316,18 @@ func (w *bitcoinWallet) getAddress(
&netParams,
)
if err != nil {
return "", "", "", err
return nil, nil, err
}
return offchainAddr, boardingAddr.EncodeAddress(), redemptionAddr.EncodeAddress(), nil
return &struct {
Address common.Address
Descriptor string
}{
*offchainAddress, defaultVtxoScript.ToDescriptor(),
},
&wallet.DescriptorAddress{
Descriptor: descriptorStr,
Address: boardingAddr.EncodeAddress(),
},
nil
}

View File

@@ -44,41 +44,103 @@ func NewLiquidWallet(
func (w *liquidWallet) GetAddresses(
ctx context.Context,
) ([]string, []string, []string, error) {
offchainAddr, boardingAddr, redemptionAddr, err := w.getAddress(ctx)
) ([]wallet.DescriptorAddress, []wallet.DescriptorAddress, []wallet.DescriptorAddress, error) {
offchainAddr, boardingAddr, err := w.getAddress(ctx)
if err != nil {
return nil, nil, nil, err
}
offchainAddrs := []string{offchainAddr}
boardingAddrs := []string{boardingAddr}
redemptionAddrs := []string{redemptionAddr}
encodedOffchainAddr, err := offchainAddr.Address.Encode()
if err != nil {
return nil, nil, nil, err
}
data, err := w.configStore.GetData(ctx)
if err != nil {
return nil, nil, nil, err
}
liquidNet := utils.ToElementsNetwork(data.Network)
vtxoP2TR, err := payment.FromTweakedKey(offchainAddr.Address.VtxoTapKey, &liquidNet, nil)
if err != nil {
return nil, nil, nil, err
}
redemptionAddr, err := vtxoP2TR.TaprootAddress()
if err != nil {
return nil, nil, nil, err
}
offchainAddrs := []wallet.DescriptorAddress{
{
Descriptor: offchainAddr.Descriptor,
Address: encodedOffchainAddr,
},
}
boardingAddrs := []wallet.DescriptorAddress{
{
Descriptor: boardingAddr.Descriptor,
Address: boardingAddr.Address,
},
}
redemptionAddrs := []wallet.DescriptorAddress{
{
Descriptor: offchainAddr.Descriptor,
Address: redemptionAddr,
},
}
return offchainAddrs, boardingAddrs, redemptionAddrs, nil
}
func (w *liquidWallet) NewAddress(
ctx context.Context, _ bool,
) (string, string, error) {
offchainAddr, boardingAddr, _, err := w.getAddress(ctx)
if err != nil {
return "", "", err
}
return offchainAddr, boardingAddr, nil
}
func (w *liquidWallet) NewAddresses(
ctx context.Context, _ bool, num int,
) ([]string, []string, error) {
offchainAddr, boardingAddr, _, err := w.getAddress(ctx)
) (*wallet.DescriptorAddress, *wallet.DescriptorAddress, error) {
offchainAddr, boardingAddr, err := w.getAddress(ctx)
if err != nil {
return nil, nil, err
}
offchainAddrs := make([]string, 0, num)
boardingAddrs := make([]string, 0, num)
encodedOffchainAddr, err := offchainAddr.Address.Encode()
if err != nil {
return nil, nil, err
}
return &wallet.DescriptorAddress{
Descriptor: offchainAddr.Descriptor,
Address: encodedOffchainAddr,
}, &wallet.DescriptorAddress{
Descriptor: boardingAddr.Descriptor,
Address: boardingAddr.Address,
}, nil
}
func (w *liquidWallet) NewAddresses(
ctx context.Context, _ bool, num int,
) ([]wallet.DescriptorAddress, []wallet.DescriptorAddress, error) {
offchainAddr, boardingAddr, err := w.getAddress(ctx)
if err != nil {
return nil, nil, err
}
offchainAddrs := make([]wallet.DescriptorAddress, 0, num)
boardingAddrs := make([]wallet.DescriptorAddress, 0, num)
for i := 0; i < num; i++ {
offchainAddrs = append(offchainAddrs, offchainAddr)
boardingAddrs = append(boardingAddrs, boardingAddr)
encodedOffchainAddr, err := offchainAddr.Address.Encode()
if err != nil {
return nil, nil, err
}
offchainAddrs = append(offchainAddrs, wallet.DescriptorAddress{
Descriptor: offchainAddr.Descriptor,
Address: encodedOffchainAddr,
})
boardingAddrs = append(boardingAddrs, wallet.DescriptorAddress{
Descriptor: boardingAddr.Descriptor,
Address: boardingAddr.Address,
})
}
return offchainAddrs, boardingAddrs, nil
}
@@ -227,21 +289,21 @@ func (s *liquidWallet) SignTransaction(
func (w *liquidWallet) getAddress(
ctx context.Context,
) (string, string, string, error) {
) (
*struct {
Address common.Address
Descriptor string
},
*wallet.DescriptorAddress,
error,
) {
if w.walletData == nil {
return "", "", "", fmt.Errorf("wallet not initialized")
return nil, nil, fmt.Errorf("wallet not initialized")
}
data, err := w.configStore.GetData(ctx)
if err != nil {
return "", "", "", err
}
offchainAddr, err := common.EncodeAddress(
data.Network.Addr, w.walletData.Pubkey, data.AspPubkey,
)
if err != nil {
return "", "", "", err
return nil, nil, err
}
liquidNet := utils.ToElementsNetwork(data.Network)
@@ -254,17 +316,13 @@ func (w *liquidWallet) getAddress(
vtxoTapKey, _, err := vtxoScript.TapTree()
if err != nil {
return "", "", "", err
return nil, nil, err
}
vtxoP2tr, err := payment.FromTweakedKey(vtxoTapKey, &liquidNet, nil)
if err != nil {
return "", "", "", err
}
redemptionAddr, err := vtxoP2tr.TaprootAddress()
if err != nil {
return "", "", "", err
offchainAddr := &common.Address{
HRP: data.Network.Addr,
Asp: data.AspPubkey,
VtxoTapKey: vtxoTapKey,
}
myPubkeyStr := hex.EncodeToString(schnorr.SerializePubKey(w.walletData.Pubkey))
@@ -274,23 +332,32 @@ func (w *liquidWallet) getAddress(
onboardingScript, err := tree.ParseVtxoScript(descriptorStr)
if err != nil {
return "", "", "", err
return nil, nil, err
}
tapKey, _, err := onboardingScript.TapTree()
if err != nil {
return "", "", "", err
return nil, nil, err
}
p2tr, err := payment.FromTweakedKey(tapKey, &liquidNet, nil)
if err != nil {
return "", "", "", err
return nil, nil, err
}
boardingAddr, err := p2tr.TaprootAddress()
if err != nil {
return "", "", "", err
return nil, nil, err
}
return offchainAddr, boardingAddr, redemptionAddr, nil
return &struct {
Address common.Address
Descriptor string
}{
Address: *offchainAddr,
Descriptor: vtxoScript.ToDescriptor(),
}, &wallet.DescriptorAddress{
Descriptor: descriptorStr,
Address: boardingAddr,
}, nil
}

View File

@@ -10,6 +10,11 @@ const (
SingleKeyWallet = "singlekey"
)
type DescriptorAddress struct {
Descriptor string
Address string
}
type WalletService interface {
GetType() string
Create(
@@ -20,13 +25,13 @@ type WalletService interface {
IsLocked() bool
GetAddresses(
ctx context.Context,
) (offchainAddresses, boardingAddresses, redemptionAddresses []string, err error)
) (offchainAddresses, boardingAddresses, redemptionAddresses []DescriptorAddress, err error)
NewAddress(
ctx context.Context, change bool,
) (offchainAddr, onchainAddr string, err error)
) (offchainAddr, onchainAddr *DescriptorAddress, err error)
NewAddresses(
ctx context.Context, change bool, num int,
) (offchainAddresses, onchainAddresses []string, err error)
) (offchainAddresses, onchainAddresses []DescriptorAddress, err error)
SignTransaction(
ctx context.Context, explorerSvc explorer.Explorer, tx string,
) (signedTx string, err error)

View File

@@ -156,6 +156,7 @@ func (s *covenantService) GetBoardingAddress(ctx context.Context, userPubkey *se
func (s *covenantService) SpendVtxos(ctx context.Context, inputs []ports.Input) (string, error) {
vtxosInputs := make([]domain.Vtxo, 0)
boardingInputs := make([]ports.BoardingInput, 0)
descriptors := make(map[domain.VtxoKey]string)
now := time.Now().Unix()
@@ -218,13 +219,14 @@ func (s *covenantService) SpendVtxos(ctx context.Context, inputs []ports.Input)
}
vtxosInputs = append(vtxosInputs, vtxo)
descriptors[vtxo.VtxoKey] = input.Descriptor
}
payment, err := domain.NewPayment(vtxosInputs)
if err != nil {
return "", err
}
if err := s.paymentRequests.push(*payment, boardingInputs); err != nil {
if err := s.paymentRequests.push(*payment, boardingInputs, descriptors); err != nil {
return "", err
}
return payment.Id, nil
@@ -324,7 +326,7 @@ func (s *covenantService) CompleteAsyncPayment(ctx context.Context, redeemTx str
return fmt.Errorf("unimplemented")
}
func (s *covenantService) CreateAsyncPayment(ctx context.Context, inputs []ports.Input, receivers []domain.Receiver) (string, error) {
func (s *covenantService) CreateAsyncPayment(_ context.Context, _ []AsyncPaymentInput, _ []domain.Receiver) (string, error) {
return "", fmt.Errorf("unimplemented")
}
@@ -345,9 +347,14 @@ func (s *covenantService) SignRoundTx(ctx context.Context, signedRoundTx string)
return nil
}
func (s *covenantService) ListVtxos(ctx context.Context, pubkey *secp256k1.PublicKey) ([]domain.Vtxo, []domain.Vtxo, error) {
pk := hex.EncodeToString(pubkey.SerializeCompressed())
return s.repoManager.Vtxos().GetAllVtxos(ctx, pk)
func (s *covenantService) ListVtxos(ctx context.Context, address string) ([]domain.Vtxo, []domain.Vtxo, error) {
decodedAddress, err := common.DecodeAddress(address)
if err != nil {
return nil, nil, fmt.Errorf("failed to decode address: %s", err)
}
pubkey := hex.EncodeToString(schnorr.SerializePubKey(decodedAddress.VtxoTapKey))
return s.repoManager.Vtxos().GetAllVtxos(ctx, pubkey)
}
func (s *covenantService) GetEventsChannel(ctx context.Context) <-chan domain.RoundEvent {
@@ -482,7 +489,7 @@ func (s *covenantService) startFinalization() {
if num > paymentsThreshold {
num = paymentsThreshold
}
payments, boardingInputs, _ := s.paymentRequests.pop(num)
payments, boardingInputs, descriptors, _ := s.paymentRequests.pop(num)
if _, err := round.RegisterPayments(payments); err != nil {
round.Fail(fmt.Errorf("failed to register payments: %s", err))
log.WithError(err).Warn("failed to register payments")
@@ -517,7 +524,7 @@ func (s *covenantService) startFinalization() {
minRelayFeeRate := s.wallet.MinRelayFeeRate(ctx)
if needForfeits {
connectors, forfeitTxs, err = s.builder.BuildForfeitTxs(unsignedPoolTx, payments, minRelayFeeRate)
connectors, forfeitTxs, err = s.builder.BuildForfeitTxs(unsignedPoolTx, payments, descriptors, minRelayFeeRate)
if err != nil {
round.Fail(fmt.Errorf("failed to create connectors and forfeit txs: %s", err))
log.WithError(err).Warn("failed to create connectors and forfeit txs")
@@ -933,53 +940,20 @@ func (s *covenantService) getNewVtxos(round *domain.Round) []domain.Vtxo {
continue // skip fee outputs
}
desc := ""
found := false
for _, p := range round.Payments {
if found {
break
}
for _, r := range p.Receivers {
if r.IsOnchain() {
continue
}
vtxoScript, err := tree.ParseVtxoScript(r.Descriptor)
if err != nil {
log.WithError(err).Warn("failed to parse vtxo descriptor")
continue
}
tapKey, _, err := vtxoScript.TapTree()
if err != nil {
log.WithError(err).Warn("failed to compute vtxo tap key")
continue
}
script, err := common.P2TRScript(tapKey)
if err != nil {
log.WithError(err).Warn("failed to create vtxo scriptpubkey")
continue
}
if bytes.Equal(script, out.Script) {
found = true
desc = r.Descriptor
break
}
}
vtxoTapKey, err := schnorr.ParsePubKey(out.Script[2:])
if err != nil {
log.WithError(err).Warn("failed to parse vtxo tap key")
continue
}
if found {
vtxos = append(vtxos, domain.Vtxo{
VtxoKey: domain.VtxoKey{Txid: node.Txid, VOut: uint32(i)},
Receiver: domain.Receiver{Descriptor: desc, Amount: uint64(out.Value)},
RoundTxid: round.Txid,
})
break
}
vtxoPubkey := hex.EncodeToString(schnorr.SerializePubKey(vtxoTapKey))
vtxos = append(vtxos, domain.Vtxo{
VtxoKey: domain.VtxoKey{Txid: node.Txid, VOut: uint32(i)},
Pubkey: vtxoPubkey,
Amount: uint64(out.Value),
RoundTxid: round.Txid,
})
}
}
return vtxos
@@ -1050,17 +1024,17 @@ func (s *covenantService) restoreWatchingVtxos() error {
func (s *covenantService) extractVtxosScripts(vtxos []domain.Vtxo) ([]string, error) {
indexedScripts := make(map[string]struct{})
for _, vtxo := range vtxos {
vtxoScript, err := tree.ParseVtxoScript(vtxo.Receiver.Descriptor)
vtxoTapKeyBytes, err := hex.DecodeString(vtxo.Pubkey)
if err != nil {
return nil, err
}
tapKey, _, err := vtxoScript.TapTree()
vtxoTapKey, err := schnorr.ParsePubKey(vtxoTapKeyBytes)
if err != nil {
return nil, err
}
script, err := common.P2TRScript(tapKey)
script, err := common.P2TRScript(vtxoTapKey)
if err != nil {
return nil, err
}

View File

@@ -19,6 +19,7 @@ import (
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/btcutil/psbt"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
@@ -177,37 +178,28 @@ func (s *covenantlessService) CompleteAsyncPayment(
// verify that the vtxo is spendable
vtxo, err := vtxoRepo.GetVtxos(ctx, []domain.VtxoKey{{Txid: vtxoOutpoint.Hash.String(), VOut: vtxoOutpoint.Index}})
vtxos, err := vtxoRepo.GetVtxos(ctx, []domain.VtxoKey{{Txid: vtxoOutpoint.Hash.String(), VOut: vtxoOutpoint.Index}})
if err != nil {
return fmt.Errorf("failed to get vtxo: %s", err)
}
if len(vtxo) == 0 {
if len(vtxos) == 0 {
return fmt.Errorf("vtxo not found")
}
if vtxo[0].Spent {
vtxo := vtxos[0]
if vtxo.Spent {
return fmt.Errorf("vtxo already spent")
}
if vtxo[0].Redeemed {
if vtxo.Redeemed {
return fmt.Errorf("vtxo already redeemed")
}
if vtxo[0].Swept {
if vtxo.Swept {
return fmt.Errorf("vtxo already swept")
}
vtxoScript, err := bitcointree.ParseVtxoScript(vtxo[0].Descriptor)
if err != nil {
return fmt.Errorf("failed to parse vtxo script: %s", err)
}
vtxoTapKey, _, err := vtxoScript.TapTree()
if err != nil {
return fmt.Errorf("failed to get taproot key: %s", err)
}
// verify that the user signs a forfeit closure
var userPubKey *secp256k1.PublicKey
@@ -228,6 +220,16 @@ func (s *covenantlessService) CompleteAsyncPayment(
return fmt.Errorf("redeem transaction is not signed")
}
vtxoPublicKeyBytes, err := hex.DecodeString(vtxo.Pubkey)
if err != nil {
return fmt.Errorf("failed to decode vtxo pubkey: %s", err)
}
vtxoTapKey, err := schnorr.ParsePubKey(vtxoPublicKeyBytes)
if err != nil {
return fmt.Errorf("failed to parse vtxo pubkey: %s", err)
}
// verify witness utxo
pkscript, err := common.P2TRScript(vtxoTapKey)
if err != nil {
@@ -238,7 +240,7 @@ func (s *covenantlessService) CompleteAsyncPayment(
return fmt.Errorf("witness utxo script mismatch")
}
if input.WitnessUtxo.Value != int64(vtxo[0].Amount) {
if input.WitnessUtxo.Value != int64(vtxo.Amount) {
return fmt.Errorf("witness utxo value mismatch")
}
}
@@ -260,19 +262,23 @@ func (s *covenantlessService) CompleteAsyncPayment(
vtxos := make([]domain.Vtxo, 0, len(asyncPayData.receivers))
for outIndex, out := range redeemPtx.UnsignedTx.TxOut {
desc := asyncPayData.receivers[outIndex].Descriptor
_, _, _, _, err := descriptor.ParseReversibleVtxoDescriptor(desc)
isPending := err == nil
vtxoTapKey, err := schnorr.ParsePubKey(out.PkScript[2:])
if err != nil {
return fmt.Errorf("failed to parse vtxo taproot key: %s", err)
}
vtxoPubkey := hex.EncodeToString(schnorr.SerializePubKey(vtxoTapKey))
// all pending except the last one
isPending := outIndex < len(asyncPayData.receivers)-1
vtxos = append(vtxos, domain.Vtxo{
VtxoKey: domain.VtxoKey{
Txid: redeemTxid,
VOut: uint32(outIndex),
},
Receiver: domain.Receiver{
Descriptor: desc,
Amount: uint64(out.Value),
},
Pubkey: vtxoPubkey,
Amount: uint64(out.Value),
ExpireAt: asyncPayData.expireAt,
RedeemTx: redeemTx,
Pending: isPending,
@@ -309,11 +315,16 @@ func (s *covenantlessService) CompleteAsyncPayment(
}
func (s *covenantlessService) CreateAsyncPayment(
ctx context.Context, inputs []ports.Input, receivers []domain.Receiver,
ctx context.Context, inputs []AsyncPaymentInput, receivers []domain.Receiver,
) (string, error) {
vtxosKeys := make([]domain.VtxoKey, 0, len(inputs))
descriptors := 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
forfeitLeaves[in.VtxoKey] = in.ForfeitLeafHash
}
vtxos, err := s.repoManager.Vtxos().GetVtxos(ctx, vtxosKeys)
@@ -351,7 +362,7 @@ func (s *covenantlessService) CreateAsyncPayment(
}
redeemTx, err := s.builder.BuildAsyncPaymentTransactions(
vtxosInputs, s.pubkey, receivers,
vtxosInputs, descriptors, forfeitLeaves, receivers,
)
if err != nil {
return "", fmt.Errorf("failed to build async payment txs: %s", err)
@@ -404,7 +415,7 @@ func (s *covenantlessService) SpendVtxos(ctx context.Context, inputs []ports.Inp
now := time.Now().Unix()
boardingTxs := make(map[string]wire.MsgTx, 0) // txid -> txhex
descriptors := make(map[domain.VtxoKey]string)
for _, input := range inputs {
vtxosResult, err := s.repoManager.Vtxos().GetVtxos(ctx, []domain.VtxoKey{input.VtxoKey})
if err != nil || len(vtxosResult) == 0 {
@@ -461,6 +472,8 @@ func (s *covenantlessService) SpendVtxos(ctx context.Context, inputs []ports.Inp
return "", fmt.Errorf("input %s:%d already swept", vtxo.Txid, vtxo.VOut)
}
descriptors[vtxo.VtxoKey] = input.Descriptor
vtxosInputs = append(vtxosInputs, vtxo)
}
@@ -468,7 +481,7 @@ func (s *covenantlessService) SpendVtxos(ctx context.Context, inputs []ports.Inp
if err != nil {
return "", err
}
if err := s.paymentRequests.push(*payment, boardingInputs); err != nil {
if err := s.paymentRequests.push(*payment, boardingInputs, descriptors); err != nil {
return "", err
}
return payment.Id, nil
@@ -572,9 +585,14 @@ func (s *covenantlessService) SignRoundTx(ctx context.Context, signedRoundTx str
return nil
}
func (s *covenantlessService) ListVtxos(ctx context.Context, pubkey *secp256k1.PublicKey) ([]domain.Vtxo, []domain.Vtxo, error) {
pk := hex.EncodeToString(pubkey.SerializeCompressed())
return s.repoManager.Vtxos().GetAllVtxos(ctx, pk)
func (s *covenantlessService) ListVtxos(ctx context.Context, address string) ([]domain.Vtxo, []domain.Vtxo, error) {
decodedAddress, err := common.DecodeAddress(address)
if err != nil {
return nil, nil, fmt.Errorf("failed to decode address: %s", err)
}
pubkey := hex.EncodeToString(schnorr.SerializePubKey(decodedAddress.VtxoTapKey))
return s.repoManager.Vtxos().GetAllVtxos(ctx, pubkey)
}
func (s *covenantlessService) GetEventsChannel(ctx context.Context) <-chan domain.RoundEvent {
@@ -771,7 +789,7 @@ func (s *covenantlessService) startFinalization() {
if num > paymentsThreshold {
num = paymentsThreshold
}
payments, boardingInputs, cosigners := s.paymentRequests.pop(num)
payments, boardingInputs, descriptors, cosigners := s.paymentRequests.pop(num)
if len(payments) > len(cosigners) {
err := fmt.Errorf("missing ephemeral key for payments")
round.Fail(fmt.Errorf("round aborted: %s", err))
@@ -973,7 +991,7 @@ func (s *covenantlessService) startFinalization() {
minRelayFeeRate := s.wallet.MinRelayFeeRate(ctx)
if needForfeits {
connectors, forfeitTxs, err = s.builder.BuildForfeitTxs(unsignedRoundTx, payments, minRelayFeeRate)
connectors, forfeitTxs, err = s.builder.BuildForfeitTxs(unsignedRoundTx, payments, descriptors, minRelayFeeRate)
if err != nil {
round.Fail(fmt.Errorf("failed to create connectors and forfeit txs: %s", err))
log.WithError(err).Warn("failed to create connectors and forfeit txs")
@@ -1332,53 +1350,18 @@ func (s *covenantlessService) getNewVtxos(round *domain.Round) []domain.Vtxo {
continue
}
for i, out := range tx.UnsignedTx.TxOut {
desc := ""
found := false
for _, p := range round.Payments {
if found {
break
}
for _, r := range p.Receivers {
if r.IsOnchain() {
continue
}
vtxoScript, err := bitcointree.ParseVtxoScript(r.Descriptor)
if err != nil {
log.WithError(err).Warn("failed to parse vtxo descriptor")
continue
}
tapKey, _, err := vtxoScript.TapTree()
if err != nil {
log.WithError(err).Warn("failed to compute vtxo tap key")
continue
}
script, err := common.P2TRScript(tapKey)
if err != nil {
log.WithError(err).Warn("failed to create vtxo scriptpubkey")
continue
}
if bytes.Equal(script, out.PkScript) {
found = true
desc = r.Descriptor
break
}
}
vtxoTapKey, err := schnorr.ParsePubKey(out.PkScript[2:])
if err != nil {
log.WithError(err).Warn("failed to parse vtxo tap key")
continue
}
if found {
vtxos = append(vtxos, domain.Vtxo{
VtxoKey: domain.VtxoKey{Txid: node.Txid, VOut: uint32(i)},
Receiver: domain.Receiver{Descriptor: desc, Amount: uint64(out.Value)},
RoundTxid: round.Txid,
})
break
}
vtxos = append(vtxos, domain.Vtxo{
VtxoKey: domain.VtxoKey{Txid: node.Txid, VOut: uint32(i)},
Pubkey: hex.EncodeToString(schnorr.SerializePubKey(vtxoTapKey)),
Amount: uint64(out.Value),
RoundTxid: round.Txid,
})
}
}
return vtxos
@@ -1450,17 +1433,17 @@ func (s *covenantlessService) extractVtxosScripts(vtxos []domain.Vtxo) ([]string
indexedScripts := make(map[string]struct{})
for _, vtxo := range vtxos {
vtxoScript, err := bitcointree.ParseVtxoScript(vtxo.Receiver.Descriptor)
vtxoTapKeyBytes, err := hex.DecodeString(vtxo.Pubkey)
if err != nil {
return nil, err
}
tapKey, _, err := vtxoScript.TapTree()
vtxoTapKey, err := schnorr.ParsePubKey(vtxoTapKeyBytes)
if err != nil {
return nil, err
}
script, err := common.P2TRScript(tapKey)
script, err := common.P2TRScript(vtxoTapKey)
if err != nil {
return nil, err
}

View File

@@ -5,6 +5,7 @@ import (
"github.com/ark-network/ark/server/internal/core/domain"
"github.com/ark-network/ark/server/internal/core/ports"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
)
@@ -12,6 +13,11 @@ var (
paymentsThreshold = int64(128)
)
type AsyncPaymentInput struct {
ports.Input
ForfeitLeafHash chainhash.Hash
}
type Service interface {
Start() error
Stop()
@@ -27,12 +33,12 @@ type Service interface {
ctx context.Context, paymentId string,
) (lastEvent domain.RoundEvent, err error)
ListVtxos(
ctx context.Context, pubkey *secp256k1.PublicKey,
ctx context.Context, address string,
) (spendableVtxos, spentVtxos []domain.Vtxo, err error)
GetInfo(ctx context.Context) (*ServiceInfo, error)
// Async payments
CreateAsyncPayment(
ctx context.Context, inputs []ports.Input, receivers []domain.Receiver,
ctx context.Context, inputs []AsyncPaymentInput, receivers []domain.Receiver,
) (string, error)
CompleteAsyncPayment(
ctx context.Context, redeemTx string,

View File

@@ -24,13 +24,14 @@ type timedPayment struct {
type paymentsMap struct {
lock *sync.RWMutex
payments map[string]*timedPayment
descriptors map[domain.VtxoKey]string
ephemeralKeys map[string]*secp256k1.PublicKey
}
func newPaymentsMap() *paymentsMap {
paymentsById := make(map[string]*timedPayment)
lock := &sync.RWMutex{}
return &paymentsMap{lock, paymentsById, make(map[string]*secp256k1.PublicKey)}
return &paymentsMap{lock, paymentsById, make(map[domain.VtxoKey]string), make(map[string]*secp256k1.PublicKey)}
}
func (m *paymentsMap) len() int64 {
@@ -58,7 +59,11 @@ func (m *paymentsMap) delete(id string) error {
return nil
}
func (m *paymentsMap) push(payment domain.Payment, boardingInputs []ports.BoardingInput) error {
func (m *paymentsMap) push(
payment domain.Payment,
boardingInputs []ports.BoardingInput,
descriptors map[domain.VtxoKey]string,
) error {
m.lock.Lock()
defer m.lock.Unlock()
@@ -86,6 +91,10 @@ func (m *paymentsMap) push(payment domain.Payment, boardingInputs []ports.Boardi
}
}
for key, desc := range descriptors {
m.descriptors[key] = desc
}
m.payments[payment.Id] = &timedPayment{payment, boardingInputs, time.Now(), time.Time{}}
return nil
}
@@ -102,7 +111,7 @@ func (m *paymentsMap) pushEphemeralKey(paymentId string, pubkey *secp256k1.Publi
return nil
}
func (m *paymentsMap) pop(num int64) ([]domain.Payment, []ports.BoardingInput, []*secp256k1.PublicKey) {
func (m *paymentsMap) pop(num int64) ([]domain.Payment, []ports.BoardingInput, map[domain.VtxoKey]string, []*secp256k1.PublicKey) {
m.lock.Lock()
defer m.lock.Unlock()
@@ -129,6 +138,7 @@ func (m *paymentsMap) pop(num int64) ([]domain.Payment, []ports.BoardingInput, [
payments := make([]domain.Payment, 0, num)
boardingInputs := make([]ports.BoardingInput, 0)
cosigners := make([]*secp256k1.PublicKey, 0, num)
descriptors := make(map[domain.VtxoKey]string)
for _, p := range paymentsByTime[:num] {
boardingInputs = append(boardingInputs, p.boardingInputs...)
payments = append(payments, p.Payment)
@@ -136,9 +146,15 @@ func (m *paymentsMap) pop(num int64) ([]domain.Payment, []ports.BoardingInput, [
cosigners = append(cosigners, pubkey)
delete(m.ephemeralKeys, p.Payment.Id)
}
for _, input := range payments {
for _, vtxo := range input.Inputs {
descriptors[vtxo.VtxoKey] = m.descriptors[vtxo.VtxoKey]
delete(m.descriptors, vtxo.VtxoKey)
}
}
delete(m.payments, p.Id)
}
return payments, boardingInputs, cosigners
return payments, boardingInputs, descriptors, cosigners
}
func (m *paymentsMap) update(payment domain.Payment) error {

View File

@@ -68,9 +68,12 @@ func (p Payment) validate(ignoreOuts bool) error {
return fmt.Errorf("missing outputs")
}
for _, r := range p.Receivers {
if len(r.OnchainAddress) <= 0 && len(r.Descriptor) <= 0 {
if len(r.OnchainAddress) <= 0 && len(r.Pubkey) <= 0 {
return fmt.Errorf("missing receiver destination")
}
if r.Amount == 0 {
return fmt.Errorf("missing receiver amount")
}
}
return nil
}
@@ -80,6 +83,10 @@ type VtxoKey struct {
VOut uint32
}
func (k VtxoKey) String() string {
return fmt.Sprintf("%s:%d", k.Txid, k.VOut)
}
func (k VtxoKey) Hash() string {
calcHash := func(buf []byte, hasher hash.Hash) []byte {
_, _ = hasher.Write(buf)
@@ -96,9 +103,9 @@ func (k VtxoKey) Hash() string {
}
type Receiver struct {
Descriptor string
Amount uint64
OnchainAddress string
OnchainAddress string // onchain
Pubkey string // offchain
}
func (r Receiver) IsOnchain() bool {
@@ -107,7 +114,8 @@ func (r Receiver) IsOnchain() bool {
type Vtxo struct {
VtxoKey
Receiver
Amount uint64
Pubkey string
RoundTxid string
SpentBy string // round txid or async redeem txid
Spent bool

View File

@@ -1,22 +1,14 @@
package domain_test
import (
"fmt"
"testing"
"github.com/ark-network/ark/common/descriptor"
"github.com/ark-network/ark/server/internal/core/domain"
"github.com/stretchr/testify/require"
)
var desc = fmt.Sprintf(
descriptor.DefaultVtxoDescriptorTemplate,
"030000000000000000000000000000000000000000000000000000000000000001",
"0000000000000000000000000000000000000000000000000000000000000001",
"0000000000000000000000000000000000000000000000000000000000000001",
512,
"0000000000000000000000000000000000000000000000000000000000000001",
)
// x-only pubkey
const pubkey = "25a43cecfa0e1b1a4f72d64ad15f4cfa7a84d0723e8511c969aa543638ea9967"
var inputs = []domain.Vtxo{
{
@@ -24,10 +16,8 @@ var inputs = []domain.Vtxo{
Txid: "0000000000000000000000000000000000000000000000000000000000000000",
VOut: 0,
},
Receiver: domain.Receiver{
Descriptor: desc,
Amount: 1000,
},
Pubkey: pubkey,
Amount: 1000,
},
}
@@ -51,12 +41,12 @@ func TestPayment(t *testing.T) {
err = payment.AddReceivers([]domain.Receiver{
{
Descriptor: desc,
Amount: 450,
Pubkey: pubkey,
Amount: 450,
},
{
Descriptor: desc,
Amount: 550,
Pubkey: pubkey,
Amount: 550,
},
})
require.NoError(t, err)

View File

@@ -20,24 +20,22 @@ var (
Txid: txid,
VOut: 0,
},
Receiver: domain.Receiver{
Descriptor: desc,
Amount: 2000,
},
Pubkey: pubkey,
Amount: 2000,
},
},
Receivers: []domain.Receiver{
{
Descriptor: desc,
Amount: 700,
Pubkey: pubkey,
Amount: 700,
},
{
Descriptor: desc,
Amount: 700,
Pubkey: pubkey,
Amount: 700,
},
{
Descriptor: desc,
Amount: 600,
Pubkey: pubkey,
Amount: 600,
},
},
},
@@ -49,25 +47,21 @@ var (
Txid: txid,
VOut: 0,
},
Receiver: domain.Receiver{
Descriptor: desc,
Amount: 1000,
},
Pubkey: pubkey,
Amount: 1000,
},
{
VtxoKey: domain.VtxoKey{
Txid: txid,
VOut: 0,
},
Receiver: domain.Receiver{
Descriptor: desc,
Amount: 1000,
},
Pubkey: pubkey,
Amount: 1000,
},
},
Receivers: []domain.Receiver{{
Descriptor: desc,
Amount: 2000,
Pubkey: pubkey,
Amount: 2000,
}},
},
}

View File

@@ -32,7 +32,12 @@ type TxBuilder interface {
aspPubkey *secp256k1.PublicKey, payments []domain.Payment, boardingInputs []BoardingInput, sweptRounds []domain.Round,
cosigners ...*secp256k1.PublicKey,
) (roundTx string, congestionTree tree.CongestionTree, connectorAddress string, err error)
BuildForfeitTxs(poolTx string, payments []domain.Payment, minRelayFeeRate chainfee.SatPerKVByte) (connectors []string, forfeitTxs []string, err error)
BuildForfeitTxs(
roundTx string,
payments []domain.Payment,
descriptors map[domain.VtxoKey]string,
minRelayFeeRate chainfee.SatPerKVByte,
) (connectors []string, forfeitTxs []string, err error)
BuildSweepTx(inputs []SweepInput) (signedSweepTx string, err error)
GetSweepInput(node tree.Node) (lifetime int64, sweepInput SweepInput, err error)
FinalizeAndExtract(tx string) (txhex string, err error)
@@ -41,7 +46,9 @@ type TxBuilder interface {
FindLeaves(congestionTree tree.CongestionTree, fromtxid string, vout uint32) (leaves []tree.Node, err error)
BuildAsyncPaymentTransactions(
vtxosToSpend []domain.Vtxo,
aspPubKey *secp256k1.PublicKey, receivers []domain.Receiver,
descriptors map[domain.VtxoKey]string,
forfeitsLeaves map[domain.VtxoKey]chainhash.Hash,
receivers []domain.Receiver,
) (string, error)
VerifyAndCombinePartialTx(dest string, src string) (string, error)
GetTxID(tx string) (string, error)

View File

@@ -4,7 +4,6 @@ import (
"context"
"fmt"
"path/filepath"
"regexp"
"strings"
"github.com/ark-network/ark/server/internal/core/domain"
@@ -101,13 +100,7 @@ func (r *vtxoRepository) GetAllVtxos(
) ([]domain.Vtxo, []domain.Vtxo, error) {
query := badgerhold.Where("Redeemed").Eq(false)
if len(pubkey) > 0 {
if len(pubkey) == 66 {
pubkey = pubkey[2:]
}
query = query.And("Descriptor").RegExp(
regexp.MustCompile(fmt.Sprintf(".*%s.*", pubkey)),
)
query = query.And("Pubkey").Eq(pubkey)
}
vtxos, err := r.findVtxos(ctx, query)
if err != nil {

View File

@@ -4,14 +4,12 @@ import (
"context"
"crypto/rand"
"encoding/hex"
"fmt"
"os"
"reflect"
"sort"
"testing"
"time"
"github.com/ark-network/ark/common/descriptor"
"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"
@@ -24,26 +22,8 @@ import (
const (
emptyPtx = "cHNldP8BAgQCAAAAAQQBAAEFAQABBgEDAfsEAgAAAAA="
emptyTx = "0200000000000000000000"
pubkey1 = "00000000000000000000000000000000000000000000000000000000000000001"
pubkey2 = "00000000000000000000000000000000000000000000000000000000000000002"
)
var desc1 = fmt.Sprintf(
descriptor.DefaultVtxoDescriptorTemplate,
randomString(66),
pubkey1,
pubkey1,
512,
pubkey1,
)
var desc2 = fmt.Sprintf(
descriptor.DefaultVtxoDescriptorTemplate,
randomString(66),
pubkey2,
pubkey2,
512,
pubkey2,
pubkey = "25a43cecfa0e1b1a4f72d64ad15f4cfa7a84d0723e8511c969aa543638ea9967"
pubkey2 = "33ffb3dee353b1a9ebe4ced64b946238d0a4ac364f275d771da6ad2445d07ae0"
)
var congestionTree = [][]tree.Node{
@@ -270,15 +250,13 @@ func testRoundRepository(t *testing.T, svc ports.RepoManager) {
},
RoundTxid: randomString(32),
ExpireAt: 7980322,
Receiver: domain.Receiver{
Descriptor: randomString(120),
Amount: 300,
},
Pubkey: randomString(32),
Amount: 300,
},
},
Receivers: []domain.Receiver{{
Descriptor: randomString(120),
Amount: 300,
Pubkey: randomString(32),
Amount: 300,
}},
},
{
@@ -292,20 +270,18 @@ func testRoundRepository(t *testing.T, svc ports.RepoManager) {
},
RoundTxid: randomString(32),
ExpireAt: 7980322,
Receiver: domain.Receiver{
Descriptor: randomString(120),
Amount: 600,
},
Pubkey: randomString(32),
Amount: 600,
},
},
Receivers: []domain.Receiver{
{
Descriptor: randomString(120),
Amount: 400,
Pubkey: randomString(32),
Amount: 400,
},
{
Descriptor: randomString(120),
Amount: 200,
Pubkey: randomString(32),
Amount: 200,
},
},
},
@@ -370,20 +346,16 @@ func testVtxoRepository(t *testing.T, svc ports.RepoManager) {
Txid: randomString(32),
VOut: 0,
},
Receiver: domain.Receiver{
Descriptor: desc1,
Amount: 1000,
},
Pubkey: pubkey,
Amount: 1000,
},
{
VtxoKey: domain.VtxoKey{
Txid: randomString(32),
VOut: 1,
},
Receiver: domain.Receiver{
Descriptor: desc1,
Amount: 2000,
},
Pubkey: pubkey,
Amount: 2000,
},
}
newVtxos := append(userVtxos, domain.Vtxo{
@@ -391,10 +363,8 @@ func testVtxoRepository(t *testing.T, svc ports.RepoManager) {
Txid: randomString(32),
VOut: 1,
},
Receiver: domain.Receiver{
Descriptor: desc2,
Amount: 2000,
},
Pubkey: pubkey2,
Amount: 2000,
})
vtxoKeys := make([]domain.VtxoKey, 0, len(userVtxos))
@@ -406,7 +376,7 @@ func testVtxoRepository(t *testing.T, svc ports.RepoManager) {
require.Error(t, err)
require.Empty(t, vtxos)
spendableVtxos, spentVtxos, err := svc.Vtxos().GetAllVtxos(ctx, pubkey1)
spendableVtxos, spentVtxos, err := svc.Vtxos().GetAllVtxos(ctx, pubkey)
require.NoError(t, err)
require.Empty(t, spendableVtxos)
require.Empty(t, spentVtxos)
@@ -423,7 +393,7 @@ func testVtxoRepository(t *testing.T, svc ports.RepoManager) {
require.NoError(t, err)
require.Exactly(t, userVtxos, vtxos)
spendableVtxos, spentVtxos, err = svc.Vtxos().GetAllVtxos(ctx, pubkey1)
spendableVtxos, spentVtxos, err = svc.Vtxos().GetAllVtxos(ctx, pubkey)
require.NoError(t, err)
sortedVtxos := sortVtxos(userVtxos)
@@ -449,7 +419,7 @@ func testVtxoRepository(t *testing.T, svc ports.RepoManager) {
require.True(t, v.Spent)
}
spendableVtxos, spentVtxos, err = svc.Vtxos().GetAllVtxos(ctx, pubkey1)
spendableVtxos, spentVtxos, err = svc.Vtxos().GetAllVtxos(ctx, pubkey)
require.NoError(t, err)
require.Exactly(t, vtxos[1:], spendableVtxos)
require.Len(t, spentVtxos, len(vtxoKeys[:1]))

View File

@@ -21,11 +21,11 @@ CREATE TABLE IF NOT EXISTS payment (
CREATE TABLE IF NOT EXISTS receiver (
payment_id TEXT NOT NULL,
pubkey TEXT NOT NULL,
pubkey TEXT,
onchain_address TEXT,
amount INTEGER NOT NULL,
onchain_address TEXT NOT NULL,
FOREIGN KEY (payment_id) REFERENCES payment(id),
PRIMARY KEY (payment_id, pubkey)
PRIMARY KEY (payment_id, pubkey, onchain_address)
);
CREATE TABLE IF NOT EXISTS tx (
@@ -54,6 +54,7 @@ CREATE TABLE IF NOT EXISTS vtxo (
expire_at INTEGER NOT NULL,
payment_id TEXT,
redeem_tx TEXT,
pending BOOLEAN NOT NULL,
PRIMARY KEY (txid, vout),
FOREIGN KEY (payment_id) REFERENCES payment(id)
);

View File

@@ -1,17 +0,0 @@
CREATE TABLE IF NOT EXISTS old_receiver (
payment_id TEXT NOT NULL,
pubkey TEXT NOT NULL,
amount INTEGER NOT NULL,
onchain_address TEXT NOT NULL,
FOREIGN KEY (payment_id) REFERENCES payment(id),
PRIMARY KEY (payment_id, pubkey)
);
INSERT INTO old_receiver SELECT * FROM receiver;
DROP TABLE receiver;
ALTER TABLE old_receiver RENAME TO receiver;
ALTER TABLE vtxo DROP COLUMN descriptor;
ALTER TABLE vtxo ADD COLUMN pubkey TEXT NOT NULL;

View File

@@ -1,28 +0,0 @@
CREATE TABLE IF NOT EXISTS new_receiver (
payment_id TEXT NOT NULL,
descriptor TEXT NOT NULL,
amount INTEGER NOT NULL,
onchain_address TEXT NOT NULL,
FOREIGN KEY (payment_id) REFERENCES payment(id),
PRIMARY KEY (payment_id, descriptor)
);
INSERT INTO new_receiver SELECT * FROM receiver;
DROP VIEW payment_vtxo_vw;
DROP VIEW payment_receiver_vw;
DROP TABLE receiver;
ALTER TABLE new_receiver RENAME TO receiver;
ALTER TABLE vtxo ADD COLUMN descriptor TEXT;
ALTER TABLE vtxo DROP COLUMN pubkey;
CREATE VIEW payment_vtxo_vw AS SELECT vtxo.*
FROM payment
LEFT OUTER JOIN vtxo
ON payment.id=vtxo.payment_id;
CREATE VIEW payment_receiver_vw AS SELECT receiver.*
FROM payment
LEFT OUTER JOIN receiver
ON payment.id=receiver.payment_id;

View File

@@ -1 +0,0 @@
ALTER TABLE vtxo DROP COLUMN pending;

View File

@@ -1,14 +0,0 @@
ALTER TABLE vtxo ADD COLUMN pending BOOLEAN NOT NULL;
DROP VIEW payment_vtxo_vw;
DROP VIEW payment_receiver_vw;
CREATE VIEW payment_vtxo_vw AS SELECT vtxo.*
FROM payment
LEFT OUTER JOIN vtxo
ON payment.id=vtxo.payment_id;
CREATE VIEW payment_receiver_vw AS SELECT receiver.*
FROM payment
LEFT OUTER JOIN receiver
ON payment.id=receiver.payment_id;

View File

@@ -164,10 +164,16 @@ func (r *roundRepository) AddOrUpdateRound(ctx context.Context, round domain.Rou
if err := querierWithTx.UpsertReceiver(
ctx,
queries.UpsertReceiverParams{
PaymentID: payment.Id,
Descriptor: receiver.Descriptor,
Amount: int64(receiver.Amount),
OnchainAddress: receiver.OnchainAddress,
PaymentID: payment.Id,
Amount: int64(receiver.Amount),
Pubkey: sql.NullString{
String: receiver.Pubkey,
Valid: len(receiver.Pubkey) > 0,
},
OnchainAddress: sql.NullString{
String: receiver.OnchainAddress,
Valid: len(receiver.OnchainAddress) > 0,
},
},
); err != nil {
return fmt.Errorf("failed to upsert receiver: %w", err)
@@ -320,8 +326,8 @@ func (r *roundRepository) GetSweptRounds(ctx context.Context) ([]domain.Round, e
func rowToReceiver(row queries.PaymentReceiverVw) domain.Receiver {
return domain.Receiver{
Descriptor: row.Descriptor.String,
Amount: uint64(row.Amount.Int64),
Pubkey: row.Pubkey.String,
OnchainAddress: row.OnchainAddress.String,
}
}
@@ -413,8 +419,8 @@ func readRoundRows(rows []roundPaymentTxReceiverVtxoRow) ([]*domain.Round, error
found := false
for _, rcv := range payment.Receivers {
if v.receiver.Descriptor.Valid && v.receiver.Amount.Valid {
if rcv.Descriptor == v.receiver.Descriptor.String && int64(rcv.Amount) == v.receiver.Amount.Int64 {
if (v.receiver.Pubkey.Valid || v.receiver.OnchainAddress.Valid) && v.receiver.Amount.Valid {
if rcv.Pubkey == v.receiver.Pubkey.String && rcv.OnchainAddress == v.receiver.OnchainAddress.String && int64(rcv.Amount) == v.receiver.Amount.Int64 {
found = true
break
}
@@ -469,10 +475,8 @@ func rowToPaymentVtxoVw(row queries.PaymentVtxoVw) domain.Vtxo {
Txid: row.Txid.String,
VOut: uint32(row.Vout.Int64),
},
Receiver: domain.Receiver{
Descriptor: row.Descriptor.String,
Amount: uint64(row.Amount.Int64),
},
Amount: uint64(row.Amount.Int64),
Pubkey: row.Pubkey.String,
RoundTxid: row.PoolTx.String,
SpentBy: row.SpentBy.String,
Spent: row.Spent.Bool,

View File

@@ -15,32 +15,32 @@ type Payment struct {
type PaymentReceiverVw struct {
PaymentID sql.NullString
Descriptor sql.NullString
Amount sql.NullInt64
Pubkey sql.NullString
OnchainAddress sql.NullString
Amount sql.NullInt64
}
type PaymentVtxoVw struct {
Txid sql.NullString
Vout sql.NullInt64
Amount sql.NullInt64
PoolTx sql.NullString
SpentBy sql.NullString
Spent sql.NullBool
Redeemed sql.NullBool
Swept sql.NullBool
ExpireAt sql.NullInt64
PaymentID sql.NullString
RedeemTx sql.NullString
Descriptor sql.NullString
Pending sql.NullBool
Txid sql.NullString
Vout sql.NullInt64
Pubkey sql.NullString
Amount sql.NullInt64
PoolTx sql.NullString
SpentBy sql.NullString
Spent sql.NullBool
Redeemed sql.NullBool
Swept sql.NullBool
ExpireAt sql.NullInt64
PaymentID sql.NullString
RedeemTx sql.NullString
Pending sql.NullBool
}
type Receiver struct {
PaymentID string
Descriptor string
Pubkey sql.NullString
OnchainAddress sql.NullString
Amount int64
OnchainAddress string
}
type Round struct {
@@ -88,17 +88,17 @@ type Tx struct {
}
type Vtxo struct {
Txid string
Vout int64
Amount int64
PoolTx string
SpentBy string
Spent bool
Redeemed bool
Swept bool
ExpireAt int64
PaymentID sql.NullString
RedeemTx sql.NullString
Descriptor sql.NullString
Pending bool
Txid string
Vout int64
Pubkey string
Amount int64
PoolTx string
SpentBy string
Spent bool
Redeemed bool
Swept bool
ExpireAt int64
PaymentID sql.NullString
RedeemTx sql.NullString
Pending bool
}

View File

@@ -54,7 +54,7 @@ func (q *Queries) MarkVtxoAsSwept(ctx context.Context, arg MarkVtxoAsSweptParams
}
const selectNotRedeemedVtxos = `-- name: SelectNotRedeemedVtxos :many
SELECT vtxo.txid, vtxo.vout, vtxo.amount, vtxo.pool_tx, vtxo.spent_by, vtxo.spent, vtxo.redeemed, vtxo.swept, vtxo.expire_at, vtxo.payment_id, vtxo.redeem_tx, vtxo.descriptor, vtxo.pending FROM vtxo
SELECT vtxo.txid, vtxo.vout, vtxo.pubkey, vtxo.amount, vtxo.pool_tx, vtxo.spent_by, vtxo.spent, vtxo.redeemed, vtxo.swept, vtxo.expire_at, vtxo.payment_id, vtxo.redeem_tx, vtxo.pending FROM vtxo
WHERE redeemed = false
`
@@ -74,6 +74,7 @@ func (q *Queries) SelectNotRedeemedVtxos(ctx context.Context) ([]SelectNotRedeem
if err := rows.Scan(
&i.Vtxo.Txid,
&i.Vtxo.Vout,
&i.Vtxo.Pubkey,
&i.Vtxo.Amount,
&i.Vtxo.PoolTx,
&i.Vtxo.SpentBy,
@@ -83,7 +84,6 @@ func (q *Queries) SelectNotRedeemedVtxos(ctx context.Context) ([]SelectNotRedeem
&i.Vtxo.ExpireAt,
&i.Vtxo.PaymentID,
&i.Vtxo.RedeemTx,
&i.Vtxo.Descriptor,
&i.Vtxo.Pending,
); err != nil {
return nil, err
@@ -100,16 +100,16 @@ func (q *Queries) SelectNotRedeemedVtxos(ctx context.Context) ([]SelectNotRedeem
}
const selectNotRedeemedVtxosWithPubkey = `-- name: SelectNotRedeemedVtxosWithPubkey :many
SELECT vtxo.txid, vtxo.vout, vtxo.amount, vtxo.pool_tx, vtxo.spent_by, vtxo.spent, vtxo.redeemed, vtxo.swept, vtxo.expire_at, vtxo.payment_id, vtxo.redeem_tx, vtxo.descriptor, vtxo.pending FROM vtxo
WHERE redeemed = false AND INSTR(descriptor, ?) > 0
SELECT vtxo.txid, vtxo.vout, vtxo.pubkey, vtxo.amount, vtxo.pool_tx, vtxo.spent_by, vtxo.spent, vtxo.redeemed, vtxo.swept, vtxo.expire_at, vtxo.payment_id, vtxo.redeem_tx, vtxo.pending FROM vtxo
WHERE redeemed = false AND pubkey = ?
`
type SelectNotRedeemedVtxosWithPubkeyRow struct {
Vtxo Vtxo
}
func (q *Queries) SelectNotRedeemedVtxosWithPubkey(ctx context.Context, instr string) ([]SelectNotRedeemedVtxosWithPubkeyRow, error) {
rows, err := q.db.QueryContext(ctx, selectNotRedeemedVtxosWithPubkey, instr)
func (q *Queries) SelectNotRedeemedVtxosWithPubkey(ctx context.Context, pubkey string) ([]SelectNotRedeemedVtxosWithPubkeyRow, error) {
rows, err := q.db.QueryContext(ctx, selectNotRedeemedVtxosWithPubkey, pubkey)
if err != nil {
return nil, err
}
@@ -120,6 +120,7 @@ func (q *Queries) SelectNotRedeemedVtxosWithPubkey(ctx context.Context, instr st
if err := rows.Scan(
&i.Vtxo.Txid,
&i.Vtxo.Vout,
&i.Vtxo.Pubkey,
&i.Vtxo.Amount,
&i.Vtxo.PoolTx,
&i.Vtxo.SpentBy,
@@ -129,7 +130,6 @@ func (q *Queries) SelectNotRedeemedVtxosWithPubkey(ctx context.Context, instr st
&i.Vtxo.ExpireAt,
&i.Vtxo.PaymentID,
&i.Vtxo.RedeemTx,
&i.Vtxo.Descriptor,
&i.Vtxo.Pending,
); err != nil {
return nil, err
@@ -208,8 +208,8 @@ const selectRoundWithRoundId = `-- name: SelectRoundWithRoundId :many
SELECT round.id, round.starting_timestamp, round.ending_timestamp, round.ended, round.failed, round.stage_code, round.txid, round.unsigned_tx, round.connector_address, round.dust_amount, round.version, round.swept,
round_payment_vw.id, round_payment_vw.round_id,
round_tx_vw.id, round_tx_vw.tx, round_tx_vw.round_id, round_tx_vw.type, round_tx_vw.position, round_tx_vw.txid, round_tx_vw.tree_level, round_tx_vw.parent_txid, round_tx_vw.is_leaf,
payment_receiver_vw.payment_id, payment_receiver_vw.descriptor, payment_receiver_vw.amount, payment_receiver_vw.onchain_address,
payment_vtxo_vw.txid, payment_vtxo_vw.vout, payment_vtxo_vw.amount, payment_vtxo_vw.pool_tx, payment_vtxo_vw.spent_by, payment_vtxo_vw.spent, payment_vtxo_vw.redeemed, payment_vtxo_vw.swept, payment_vtxo_vw.expire_at, payment_vtxo_vw.payment_id, payment_vtxo_vw.redeem_tx, payment_vtxo_vw.descriptor, payment_vtxo_vw.pending
payment_receiver_vw.payment_id, payment_receiver_vw.pubkey, payment_receiver_vw.onchain_address, payment_receiver_vw.amount,
payment_vtxo_vw.txid, payment_vtxo_vw.vout, payment_vtxo_vw.pubkey, payment_vtxo_vw.amount, payment_vtxo_vw.pool_tx, payment_vtxo_vw.spent_by, payment_vtxo_vw.spent, payment_vtxo_vw.redeemed, payment_vtxo_vw.swept, payment_vtxo_vw.expire_at, payment_vtxo_vw.payment_id, payment_vtxo_vw.redeem_tx, payment_vtxo_vw.pending
FROM round
LEFT OUTER JOIN round_payment_vw ON round.id=round_payment_vw.round_id
LEFT OUTER JOIN round_tx_vw ON round.id=round_tx_vw.round_id
@@ -260,11 +260,12 @@ func (q *Queries) SelectRoundWithRoundId(ctx context.Context, id string) ([]Sele
&i.RoundTxVw.ParentTxid,
&i.RoundTxVw.IsLeaf,
&i.PaymentReceiverVw.PaymentID,
&i.PaymentReceiverVw.Descriptor,
&i.PaymentReceiverVw.Amount,
&i.PaymentReceiverVw.Pubkey,
&i.PaymentReceiverVw.OnchainAddress,
&i.PaymentReceiverVw.Amount,
&i.PaymentVtxoVw.Txid,
&i.PaymentVtxoVw.Vout,
&i.PaymentVtxoVw.Pubkey,
&i.PaymentVtxoVw.Amount,
&i.PaymentVtxoVw.PoolTx,
&i.PaymentVtxoVw.SpentBy,
@@ -274,7 +275,6 @@ func (q *Queries) SelectRoundWithRoundId(ctx context.Context, id string) ([]Sele
&i.PaymentVtxoVw.ExpireAt,
&i.PaymentVtxoVw.PaymentID,
&i.PaymentVtxoVw.RedeemTx,
&i.PaymentVtxoVw.Descriptor,
&i.PaymentVtxoVw.Pending,
); err != nil {
return nil, err
@@ -294,8 +294,8 @@ const selectRoundWithRoundTxId = `-- name: SelectRoundWithRoundTxId :many
SELECT round.id, round.starting_timestamp, round.ending_timestamp, round.ended, round.failed, round.stage_code, round.txid, round.unsigned_tx, round.connector_address, round.dust_amount, round.version, round.swept,
round_payment_vw.id, round_payment_vw.round_id,
round_tx_vw.id, round_tx_vw.tx, round_tx_vw.round_id, round_tx_vw.type, round_tx_vw.position, round_tx_vw.txid, round_tx_vw.tree_level, round_tx_vw.parent_txid, round_tx_vw.is_leaf,
payment_receiver_vw.payment_id, payment_receiver_vw.descriptor, payment_receiver_vw.amount, payment_receiver_vw.onchain_address,
payment_vtxo_vw.txid, payment_vtxo_vw.vout, payment_vtxo_vw.amount, payment_vtxo_vw.pool_tx, payment_vtxo_vw.spent_by, payment_vtxo_vw.spent, payment_vtxo_vw.redeemed, payment_vtxo_vw.swept, payment_vtxo_vw.expire_at, payment_vtxo_vw.payment_id, payment_vtxo_vw.redeem_tx, payment_vtxo_vw.descriptor, payment_vtxo_vw.pending
payment_receiver_vw.payment_id, payment_receiver_vw.pubkey, payment_receiver_vw.onchain_address, payment_receiver_vw.amount,
payment_vtxo_vw.txid, payment_vtxo_vw.vout, payment_vtxo_vw.pubkey, payment_vtxo_vw.amount, payment_vtxo_vw.pool_tx, payment_vtxo_vw.spent_by, payment_vtxo_vw.spent, payment_vtxo_vw.redeemed, payment_vtxo_vw.swept, payment_vtxo_vw.expire_at, payment_vtxo_vw.payment_id, payment_vtxo_vw.redeem_tx, payment_vtxo_vw.pending
FROM round
LEFT OUTER JOIN round_payment_vw ON round.id=round_payment_vw.round_id
LEFT OUTER JOIN round_tx_vw ON round.id=round_tx_vw.round_id
@@ -346,11 +346,12 @@ func (q *Queries) SelectRoundWithRoundTxId(ctx context.Context, txid string) ([]
&i.RoundTxVw.ParentTxid,
&i.RoundTxVw.IsLeaf,
&i.PaymentReceiverVw.PaymentID,
&i.PaymentReceiverVw.Descriptor,
&i.PaymentReceiverVw.Amount,
&i.PaymentReceiverVw.Pubkey,
&i.PaymentReceiverVw.OnchainAddress,
&i.PaymentReceiverVw.Amount,
&i.PaymentVtxoVw.Txid,
&i.PaymentVtxoVw.Vout,
&i.PaymentVtxoVw.Pubkey,
&i.PaymentVtxoVw.Amount,
&i.PaymentVtxoVw.PoolTx,
&i.PaymentVtxoVw.SpentBy,
@@ -360,7 +361,6 @@ func (q *Queries) SelectRoundWithRoundTxId(ctx context.Context, txid string) ([]
&i.PaymentVtxoVw.ExpireAt,
&i.PaymentVtxoVw.PaymentID,
&i.PaymentVtxoVw.RedeemTx,
&i.PaymentVtxoVw.Descriptor,
&i.PaymentVtxoVw.Pending,
); err != nil {
return nil, err
@@ -380,8 +380,8 @@ const selectSweepableRounds = `-- name: SelectSweepableRounds :many
SELECT round.id, round.starting_timestamp, round.ending_timestamp, round.ended, round.failed, round.stage_code, round.txid, round.unsigned_tx, round.connector_address, round.dust_amount, round.version, round.swept,
round_payment_vw.id, round_payment_vw.round_id,
round_tx_vw.id, round_tx_vw.tx, round_tx_vw.round_id, round_tx_vw.type, round_tx_vw.position, round_tx_vw.txid, round_tx_vw.tree_level, round_tx_vw.parent_txid, round_tx_vw.is_leaf,
payment_receiver_vw.payment_id, payment_receiver_vw.descriptor, payment_receiver_vw.amount, payment_receiver_vw.onchain_address,
payment_vtxo_vw.txid, payment_vtxo_vw.vout, payment_vtxo_vw.amount, payment_vtxo_vw.pool_tx, payment_vtxo_vw.spent_by, payment_vtxo_vw.spent, payment_vtxo_vw.redeemed, payment_vtxo_vw.swept, payment_vtxo_vw.expire_at, payment_vtxo_vw.payment_id, payment_vtxo_vw.redeem_tx, payment_vtxo_vw.descriptor, payment_vtxo_vw.pending
payment_receiver_vw.payment_id, payment_receiver_vw.pubkey, payment_receiver_vw.onchain_address, payment_receiver_vw.amount,
payment_vtxo_vw.txid, payment_vtxo_vw.vout, payment_vtxo_vw.pubkey, payment_vtxo_vw.amount, payment_vtxo_vw.pool_tx, payment_vtxo_vw.spent_by, payment_vtxo_vw.spent, payment_vtxo_vw.redeemed, payment_vtxo_vw.swept, payment_vtxo_vw.expire_at, payment_vtxo_vw.payment_id, payment_vtxo_vw.redeem_tx, payment_vtxo_vw.pending
FROM round
LEFT OUTER JOIN round_payment_vw ON round.id=round_payment_vw.round_id
LEFT OUTER JOIN round_tx_vw ON round.id=round_tx_vw.round_id
@@ -432,11 +432,12 @@ func (q *Queries) SelectSweepableRounds(ctx context.Context) ([]SelectSweepableR
&i.RoundTxVw.ParentTxid,
&i.RoundTxVw.IsLeaf,
&i.PaymentReceiverVw.PaymentID,
&i.PaymentReceiverVw.Descriptor,
&i.PaymentReceiverVw.Amount,
&i.PaymentReceiverVw.Pubkey,
&i.PaymentReceiverVw.OnchainAddress,
&i.PaymentReceiverVw.Amount,
&i.PaymentVtxoVw.Txid,
&i.PaymentVtxoVw.Vout,
&i.PaymentVtxoVw.Pubkey,
&i.PaymentVtxoVw.Amount,
&i.PaymentVtxoVw.PoolTx,
&i.PaymentVtxoVw.SpentBy,
@@ -446,7 +447,6 @@ func (q *Queries) SelectSweepableRounds(ctx context.Context) ([]SelectSweepableR
&i.PaymentVtxoVw.ExpireAt,
&i.PaymentVtxoVw.PaymentID,
&i.PaymentVtxoVw.RedeemTx,
&i.PaymentVtxoVw.Descriptor,
&i.PaymentVtxoVw.Pending,
); err != nil {
return nil, err
@@ -463,7 +463,7 @@ func (q *Queries) SelectSweepableRounds(ctx context.Context) ([]SelectSweepableR
}
const selectSweepableVtxos = `-- name: SelectSweepableVtxos :many
SELECT vtxo.txid, vtxo.vout, vtxo.amount, vtxo.pool_tx, vtxo.spent_by, vtxo.spent, vtxo.redeemed, vtxo.swept, vtxo.expire_at, vtxo.payment_id, vtxo.redeem_tx, vtxo.descriptor, vtxo.pending FROM vtxo
SELECT vtxo.txid, vtxo.vout, vtxo.pubkey, vtxo.amount, vtxo.pool_tx, vtxo.spent_by, vtxo.spent, vtxo.redeemed, vtxo.swept, vtxo.expire_at, vtxo.payment_id, vtxo.redeem_tx, vtxo.pending FROM vtxo
WHERE redeemed = false AND swept = false
`
@@ -483,6 +483,7 @@ func (q *Queries) SelectSweepableVtxos(ctx context.Context) ([]SelectSweepableVt
if err := rows.Scan(
&i.Vtxo.Txid,
&i.Vtxo.Vout,
&i.Vtxo.Pubkey,
&i.Vtxo.Amount,
&i.Vtxo.PoolTx,
&i.Vtxo.SpentBy,
@@ -492,7 +493,6 @@ func (q *Queries) SelectSweepableVtxos(ctx context.Context) ([]SelectSweepableVt
&i.Vtxo.ExpireAt,
&i.Vtxo.PaymentID,
&i.Vtxo.RedeemTx,
&i.Vtxo.Descriptor,
&i.Vtxo.Pending,
); err != nil {
return nil, err
@@ -512,8 +512,8 @@ const selectSweptRounds = `-- name: SelectSweptRounds :many
SELECT round.id, round.starting_timestamp, round.ending_timestamp, round.ended, round.failed, round.stage_code, round.txid, round.unsigned_tx, round.connector_address, round.dust_amount, round.version, round.swept,
round_payment_vw.id, round_payment_vw.round_id,
round_tx_vw.id, round_tx_vw.tx, round_tx_vw.round_id, round_tx_vw.type, round_tx_vw.position, round_tx_vw.txid, round_tx_vw.tree_level, round_tx_vw.parent_txid, round_tx_vw.is_leaf,
payment_receiver_vw.payment_id, payment_receiver_vw.descriptor, payment_receiver_vw.amount, payment_receiver_vw.onchain_address,
payment_vtxo_vw.txid, payment_vtxo_vw.vout, payment_vtxo_vw.amount, payment_vtxo_vw.pool_tx, payment_vtxo_vw.spent_by, payment_vtxo_vw.spent, payment_vtxo_vw.redeemed, payment_vtxo_vw.swept, payment_vtxo_vw.expire_at, payment_vtxo_vw.payment_id, payment_vtxo_vw.redeem_tx, payment_vtxo_vw.descriptor, payment_vtxo_vw.pending
payment_receiver_vw.payment_id, payment_receiver_vw.pubkey, payment_receiver_vw.onchain_address, payment_receiver_vw.amount,
payment_vtxo_vw.txid, payment_vtxo_vw.vout, payment_vtxo_vw.pubkey, payment_vtxo_vw.amount, payment_vtxo_vw.pool_tx, payment_vtxo_vw.spent_by, payment_vtxo_vw.spent, payment_vtxo_vw.redeemed, payment_vtxo_vw.swept, payment_vtxo_vw.expire_at, payment_vtxo_vw.payment_id, payment_vtxo_vw.redeem_tx, payment_vtxo_vw.pending
FROM round
LEFT OUTER JOIN round_payment_vw ON round.id=round_payment_vw.round_id
LEFT OUTER JOIN round_tx_vw ON round.id=round_tx_vw.round_id
@@ -564,11 +564,12 @@ func (q *Queries) SelectSweptRounds(ctx context.Context) ([]SelectSweptRoundsRow
&i.RoundTxVw.ParentTxid,
&i.RoundTxVw.IsLeaf,
&i.PaymentReceiverVw.PaymentID,
&i.PaymentReceiverVw.Descriptor,
&i.PaymentReceiverVw.Amount,
&i.PaymentReceiverVw.Pubkey,
&i.PaymentReceiverVw.OnchainAddress,
&i.PaymentReceiverVw.Amount,
&i.PaymentVtxoVw.Txid,
&i.PaymentVtxoVw.Vout,
&i.PaymentVtxoVw.Pubkey,
&i.PaymentVtxoVw.Amount,
&i.PaymentVtxoVw.PoolTx,
&i.PaymentVtxoVw.SpentBy,
@@ -578,7 +579,6 @@ func (q *Queries) SelectSweptRounds(ctx context.Context) ([]SelectSweptRoundsRow
&i.PaymentVtxoVw.ExpireAt,
&i.PaymentVtxoVw.PaymentID,
&i.PaymentVtxoVw.RedeemTx,
&i.PaymentVtxoVw.Descriptor,
&i.PaymentVtxoVw.Pending,
); err != nil {
return nil, err
@@ -595,7 +595,7 @@ func (q *Queries) SelectSweptRounds(ctx context.Context) ([]SelectSweptRoundsRow
}
const selectVtxoByOutpoint = `-- name: SelectVtxoByOutpoint :one
SELECT vtxo.txid, vtxo.vout, vtxo.amount, vtxo.pool_tx, vtxo.spent_by, vtxo.spent, vtxo.redeemed, vtxo.swept, vtxo.expire_at, vtxo.payment_id, vtxo.redeem_tx, vtxo.descriptor, vtxo.pending FROM vtxo
SELECT vtxo.txid, vtxo.vout, vtxo.pubkey, vtxo.amount, vtxo.pool_tx, vtxo.spent_by, vtxo.spent, vtxo.redeemed, vtxo.swept, vtxo.expire_at, vtxo.payment_id, vtxo.redeem_tx, vtxo.pending FROM vtxo
WHERE txid = ? AND vout = ?
`
@@ -614,6 +614,7 @@ func (q *Queries) SelectVtxoByOutpoint(ctx context.Context, arg SelectVtxoByOutp
err := row.Scan(
&i.Vtxo.Txid,
&i.Vtxo.Vout,
&i.Vtxo.Pubkey,
&i.Vtxo.Amount,
&i.Vtxo.PoolTx,
&i.Vtxo.SpentBy,
@@ -623,14 +624,13 @@ func (q *Queries) SelectVtxoByOutpoint(ctx context.Context, arg SelectVtxoByOutp
&i.Vtxo.ExpireAt,
&i.Vtxo.PaymentID,
&i.Vtxo.RedeemTx,
&i.Vtxo.Descriptor,
&i.Vtxo.Pending,
)
return i, err
}
const selectVtxosByPoolTxid = `-- name: SelectVtxosByPoolTxid :many
SELECT vtxo.txid, vtxo.vout, vtxo.amount, vtxo.pool_tx, vtxo.spent_by, vtxo.spent, vtxo.redeemed, vtxo.swept, vtxo.expire_at, vtxo.payment_id, vtxo.redeem_tx, vtxo.descriptor, vtxo.pending FROM vtxo
SELECT vtxo.txid, vtxo.vout, vtxo.pubkey, vtxo.amount, vtxo.pool_tx, vtxo.spent_by, vtxo.spent, vtxo.redeemed, vtxo.swept, vtxo.expire_at, vtxo.payment_id, vtxo.redeem_tx, vtxo.pending FROM vtxo
WHERE pool_tx = ?
`
@@ -650,6 +650,7 @@ func (q *Queries) SelectVtxosByPoolTxid(ctx context.Context, poolTx string) ([]S
if err := rows.Scan(
&i.Vtxo.Txid,
&i.Vtxo.Vout,
&i.Vtxo.Pubkey,
&i.Vtxo.Amount,
&i.Vtxo.PoolTx,
&i.Vtxo.SpentBy,
@@ -659,7 +660,6 @@ func (q *Queries) SelectVtxosByPoolTxid(ctx context.Context, poolTx string) ([]S
&i.Vtxo.ExpireAt,
&i.Vtxo.PaymentID,
&i.Vtxo.RedeemTx,
&i.Vtxo.Descriptor,
&i.Vtxo.Pending,
); err != nil {
return nil, err
@@ -721,26 +721,26 @@ func (q *Queries) UpsertPayment(ctx context.Context, arg UpsertPaymentParams) er
}
const upsertReceiver = `-- name: UpsertReceiver :exec
INSERT INTO receiver (payment_id, descriptor, amount, onchain_address) VALUES (?, ?, ?, ?)
ON CONFLICT(payment_id, descriptor) DO UPDATE SET
INSERT INTO receiver (payment_id, pubkey, onchain_address, amount) VALUES (?, ?, ?, ?)
ON CONFLICT(payment_id, pubkey, onchain_address) DO UPDATE SET
amount = EXCLUDED.amount,
onchain_address = EXCLUDED.onchain_address,
descriptor = EXCLUDED.descriptor
pubkey = EXCLUDED.pubkey,
onchain_address = EXCLUDED.onchain_address
`
type UpsertReceiverParams struct {
PaymentID string
Descriptor string
Pubkey sql.NullString
OnchainAddress sql.NullString
Amount int64
OnchainAddress string
}
func (q *Queries) UpsertReceiver(ctx context.Context, arg UpsertReceiverParams) error {
_, err := q.db.ExecContext(ctx, upsertReceiver,
arg.PaymentID,
arg.Descriptor,
arg.Amount,
arg.Pubkey,
arg.OnchainAddress,
arg.Amount,
)
return err
}
@@ -847,9 +847,9 @@ func (q *Queries) UpsertTransaction(ctx context.Context, arg UpsertTransactionPa
}
const upsertVtxo = `-- name: UpsertVtxo :exec
INSERT INTO vtxo (txid, vout, descriptor, amount, pool_tx, spent_by, spent, redeemed, swept, expire_at, redeem_tx, pending)
INSERT INTO vtxo (txid, vout, pubkey, amount, pool_tx, spent_by, spent, redeemed, swept, expire_at, redeem_tx, pending)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(txid, vout) DO UPDATE SET
descriptor = EXCLUDED.descriptor,
pubkey = EXCLUDED.pubkey,
amount = EXCLUDED.amount,
pool_tx = EXCLUDED.pool_tx,
spent_by = EXCLUDED.spent_by,
@@ -862,25 +862,25 @@ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(txid, vout) DO UPDATE SE
`
type UpsertVtxoParams struct {
Txid string
Vout int64
Descriptor sql.NullString
Amount int64
PoolTx string
SpentBy string
Spent bool
Redeemed bool
Swept bool
ExpireAt int64
RedeemTx sql.NullString
Pending bool
Txid string
Vout int64
Pubkey string
Amount int64
PoolTx string
SpentBy string
Spent bool
Redeemed bool
Swept bool
ExpireAt int64
RedeemTx sql.NullString
Pending bool
}
func (q *Queries) UpsertVtxo(ctx context.Context, arg UpsertVtxoParams) error {
_, err := q.db.ExecContext(ctx, upsertVtxo,
arg.Txid,
arg.Vout,
arg.Descriptor,
arg.Pubkey,
arg.Amount,
arg.PoolTx,
arg.SpentBy,

View File

@@ -44,11 +44,11 @@ INSERT INTO payment (id, round_id) VALUES (?, ?)
ON CONFLICT(id) DO UPDATE SET round_id = EXCLUDED.round_id;
-- name: UpsertReceiver :exec
INSERT INTO receiver (payment_id, descriptor, amount, onchain_address) VALUES (?, ?, ?, ?)
ON CONFLICT(payment_id, descriptor) DO UPDATE SET
INSERT INTO receiver (payment_id, pubkey, onchain_address, amount) VALUES (?, ?, ?, ?)
ON CONFLICT(payment_id, pubkey, onchain_address) DO UPDATE SET
amount = EXCLUDED.amount,
onchain_address = EXCLUDED.onchain_address,
descriptor = EXCLUDED.descriptor;
pubkey = EXCLUDED.pubkey,
onchain_address = EXCLUDED.onchain_address;
-- name: UpdateVtxoPaymentId :exec
UPDATE vtxo SET payment_id = ? WHERE txid = ? AND vout = ?;
@@ -112,9 +112,9 @@ SELECT id FROM round WHERE starting_timestamp > ? AND starting_timestamp < ?;
SELECT id FROM round;
-- name: UpsertVtxo :exec
INSERT INTO vtxo (txid, vout, descriptor, amount, pool_tx, spent_by, spent, redeemed, swept, expire_at, redeem_tx, pending)
INSERT INTO vtxo (txid, vout, pubkey, amount, pool_tx, spent_by, spent, redeemed, swept, expire_at, redeem_tx, pending)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(txid, vout) DO UPDATE SET
descriptor = EXCLUDED.descriptor,
pubkey = EXCLUDED.pubkey,
amount = EXCLUDED.amount,
pool_tx = EXCLUDED.pool_tx,
spent_by = EXCLUDED.spent_by,
@@ -135,7 +135,7 @@ WHERE redeemed = false;
-- name: SelectNotRedeemedVtxosWithPubkey :many
SELECT sqlc.embed(vtxo) FROM vtxo
WHERE redeemed = false AND INSTR(descriptor, ?) > 0;
WHERE redeemed = false AND pubkey = ?;
-- name: SelectVtxoByOutpoint :one
SELECT sqlc.embed(vtxo) FROM vtxo

View File

@@ -37,20 +37,21 @@ func (v *vxtoRepository) AddVtxos(ctx context.Context, vtxos []domain.Vtxo) erro
txBody := func(querierWithTx *queries.Queries) error {
for i := range vtxos {
vtxo := vtxos[i]
if err := querierWithTx.UpsertVtxo(
ctx, queries.UpsertVtxoParams{
Txid: vtxo.Txid,
Vout: int64(vtxo.VOut),
Descriptor: sql.NullString{String: vtxo.Descriptor, Valid: true},
Amount: int64(vtxo.Amount),
PoolTx: vtxo.RoundTxid,
SpentBy: vtxo.SpentBy,
Spent: vtxo.Spent,
Redeemed: vtxo.Redeemed,
Swept: vtxo.Swept,
ExpireAt: vtxo.ExpireAt,
RedeemTx: sql.NullString{String: vtxo.RedeemTx, Valid: true},
Pending: vtxo.Pending,
Txid: vtxo.Txid,
Vout: int64(vtxo.VOut),
Pubkey: vtxo.Pubkey,
Amount: int64(vtxo.Amount),
PoolTx: vtxo.RoundTxid,
SpentBy: vtxo.SpentBy,
Spent: vtxo.Spent,
Redeemed: vtxo.Redeemed,
Swept: vtxo.Swept,
ExpireAt: vtxo.ExpireAt,
RedeemTx: sql.NullString{String: vtxo.RedeemTx, Valid: true},
Pending: vtxo.Pending,
},
); err != nil {
return err
@@ -81,10 +82,6 @@ func (v *vxtoRepository) GetAllVtxos(ctx context.Context, pubkey string) ([]doma
var rows []queries.Vtxo
if withPubkey {
if len(pubkey) == 66 {
pubkey = pubkey[2:]
}
res, err := v.querier.SelectNotRedeemedVtxosWithPubkey(ctx, pubkey)
if err != nil {
return nil, nil, err
@@ -253,10 +250,8 @@ func rowToVtxo(row queries.Vtxo) domain.Vtxo {
Txid: row.Txid,
VOut: uint32(row.Vout),
},
Receiver: domain.Receiver{
Descriptor: row.Descriptor.String,
Amount: uint64(row.Amount),
},
Amount: uint64(row.Amount),
Pubkey: row.Pubkey,
RoundTxid: row.PoolTx,
SpentBy: row.SpentBy,
Spent: row.Spent,

View File

@@ -84,6 +84,7 @@ func (b *txBuilder) BuildSweepTx(inputs []ports.SweepInput) (signedSweepTx strin
func (b *txBuilder) BuildForfeitTxs(
poolTx string,
payments []domain.Payment,
descriptors map[domain.VtxoKey]string,
minRelayFeeRate chainfee.SatPerKVByte,
) (connectors []string, forfeitTxs []string, err error) {
connectorAddress, err := b.getConnectorAddress(poolTx)
@@ -106,7 +107,7 @@ func (b *txBuilder) BuildForfeitTxs(
return nil, nil, err
}
forfeitTxs, err = b.createForfeitTxs(payments, connectorTxs, connectorAmount, minRelayFeeRate)
forfeitTxs, err = b.createForfeitTxs(payments, descriptors, connectorTxs, connectorAmount, minRelayFeeRate)
if err != nil {
return nil, nil, err
}
@@ -147,13 +148,13 @@ func (b *txBuilder) BuildRoundTx(
return "", nil, "", err
}
receivers, err := getOffchainReceivers(payments)
vtxosLeaves, err := getOutputVtxosLeaves(payments)
if err != nil {
return "", nil, "", err
}
treeFactoryFn, sharedOutputScript, sharedOutputAmount, err = tree.CraftCongestionTree(
b.onchainNetwork().AssetID, aspPubkey, receivers, feeSatsPerNode, b.roundLifetime,
b.onchainNetwork().AssetID, aspPubkey, vtxosLeaves, feeSatsPerNode, b.roundLifetime,
)
if err != nil {
return "", nil, "", err
@@ -362,7 +363,10 @@ func (b *txBuilder) FindLeaves(
}
func (b *txBuilder) BuildAsyncPaymentTransactions(
_ []domain.Vtxo, _ *secp256k1.PublicKey, _ []domain.Receiver,
_ []domain.Vtxo,
_ map[domain.VtxoKey]string,
_ map[domain.VtxoKey]chainhash.Hash,
_ []domain.Receiver,
) (string, error) {
return "", fmt.Errorf("not implemented")
}
@@ -396,7 +400,6 @@ func (b *txBuilder) createPoolTx(
return nil, err
}
receivers := getOnchainReceivers(payments)
nbOfInputs := countSpentVtxos(payments)
connectorsAmount := (dustAmount + connectorMinRelayFee) * nbOfInputs
if nbOfInputs > 1 {
@@ -424,21 +427,17 @@ func (b *txBuilder) createPoolTx(
})
}
for _, receiver := range receivers {
targetAmount += receiver.Amount
receiverScript, err := address.ToOutputScript(receiver.OnchainAddress)
if err != nil {
return nil, err
}
outputs = append(outputs, psetv2.OutputArgs{
Asset: b.onchainNetwork().AssetID,
Amount: receiver.Amount,
Script: receiverScript,
})
onchainOutputs, err := getOnchainOutputs(payments, b.onchainNetwork())
if err != nil {
return nil, err
}
for _, out := range onchainOutputs {
targetAmount += out.Amount
}
outputs = append(outputs, onchainOutputs...)
for _, in := range boardingInputs {
targetAmount -= in.Amount
}
@@ -786,6 +785,7 @@ func (b *txBuilder) createConnectors(
func (b *txBuilder) createForfeitTxs(
payments []domain.Payment,
descriptors map[domain.VtxoKey]string,
connectors []*psetv2.Pset,
connectorAmount uint64,
minRelayFeeRate chainfee.SatPerKVByte,
@@ -803,7 +803,12 @@ func (b *txBuilder) createForfeitTxs(
forfeitTxs := make([]string, 0)
for _, payment := range payments {
for _, vtxo := range payment.Inputs {
offchainScript, err := tree.ParseVtxoScript(vtxo.Descriptor)
desc, ok := descriptors[vtxo.VtxoKey]
if !ok {
return nil, fmt.Errorf("descriptor not found for vtxo %s:%d", vtxo.VtxoKey.Txid, vtxo.VtxoKey.VOut)
}
offchainScript, err := tree.ParseVtxoScript(desc)
if err != nil {
return nil, err
}

View File

@@ -113,7 +113,7 @@ func TestBuildForfeitTxs(t *testing.T) {
t.Run("valid", func(t *testing.T) {
for _, f := range fixtures.Valid {
connectors, forfeitTxs, err := builder.BuildForfeitTxs(
f.PoolTx, f.Payments, minRelayFeeRate,
f.PoolTx, f.Payments, f.Descriptors, minRelayFeeRate,
)
require.NoError(t, err)
require.Len(t, connectors, f.ExpectedNumOfConnectors)
@@ -151,7 +151,7 @@ func TestBuildForfeitTxs(t *testing.T) {
t.Run("invalid", func(t *testing.T) {
for _, f := range fixtures.Invalid {
connectors, forfeitTxs, err := builder.BuildForfeitTxs(
f.PoolTx, f.Payments, minRelayFeeRate,
f.PoolTx, f.Payments, f.Descriptors, minRelayFeeRate,
)
require.EqualError(t, err, f.ExpectedErr)
require.Empty(t, connectors)
@@ -215,6 +215,7 @@ func parsePoolTxFixtures() (*poolTxFixtures, error) {
type forfeitTxsFixtures struct {
Valid []struct {
Payments []domain.Payment
Descriptors map[domain.VtxoKey]string
ExpectedNumOfConnectors int
ExpectedNumOfForfeitTxs int
PoolTx string
@@ -222,6 +223,7 @@ type forfeitTxsFixtures struct {
}
Invalid []struct {
Payments []domain.Payment
Descriptors map[domain.VtxoKey]string
ExpectedErr string
PoolTx string
}
@@ -244,6 +246,42 @@ func parseForfeitTxsFixtures() (*forfeitTxsFixtures, error) {
return nil, err
}
valid := vv["valid"].([]interface{})
for i, v := range valid {
val := v.(map[string]interface{})
payments := val["payments"].([]interface{})
descriptors := make(map[domain.VtxoKey]string)
for _, p := range payments {
inputs := p.(map[string]interface{})["inputs"].([]interface{})
for _, in := range inputs {
inMap := in.(map[string]interface{})
descriptors[domain.VtxoKey{
Txid: inMap["txid"].(string),
VOut: uint32(inMap["vout"].(float64)),
}] = inMap["descriptor"].(string)
}
}
fixtures.Valid[i].Descriptors = descriptors
}
invalid := vv["invalid"].([]interface{})
for i, v := range invalid {
val := v.(map[string]interface{})
payments := val["payments"].([]interface{})
descriptors := make(map[domain.VtxoKey]string)
for _, p := range payments {
inputs := p.(map[string]interface{})["inputs"].([]interface{})
for _, in := range inputs {
inMap := in.(map[string]interface{})
descriptors[domain.VtxoKey{
Txid: inMap["txid"].(string),
VOut: uint32(inMap["vout"].(float64)),
}] = inMap["descriptor"].(string)
}
}
fixtures.Invalid[i].Descriptors = descriptors
}
return &fixtures, nil
}

View File

@@ -9,14 +9,14 @@
{
"txid": "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6",
"vout": 0,
"pubkey": "0000000000000000000000000000000000000000000000000000000000000001",
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"signerPubkey": "020000000000000000000000000000000000000000000000000000000000000001",
"amount": 1100
}
],
"receivers": [
{
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"pubkey": "25a43cecfa0e1b1a4f72d64ad15f4cfa7a84d0723e8511c969aa543638ea9967",
"amount": 1100
}
]
@@ -33,18 +33,18 @@
{
"txid": "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6",
"vout": 0,
"pubkey": "0000000000000000000000000000000000000000000000000000000000000001",
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"signerPubkey": "020000000000000000000000000000000000000000000000000000000000000001",
"amount": 1100
}
],
"receivers": [
{
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"pubkey": "25a43cecfa0e1b1a4f72d64ad15f4cfa7a84d0723e8511c969aa543638ea9967",
"amount": 600
},
{
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"pubkey": "25a43cecfa0e1b1a4f72d64ad15f4cfa7a84d0723e8511c969aa543638ea9967",
"amount": 500
}
]
@@ -61,18 +61,18 @@
{
"txid": "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6",
"vout": 0,
"pubkey": "0000000000000000000000000000000000000000000000000000000000000001",
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"signerPubkey": "020000000000000000000000000000000000000000000000000000000000000001",
"amount": 1100
}
],
"receivers": [
{
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"pubkey": "25a43cecfa0e1b1a4f72d64ad15f4cfa7a84d0723e8511c969aa543638ea9967",
"amount": 600
},
{
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"pubkey": "25a43cecfa0e1b1a4f72d64ad15f4cfa7a84d0723e8511c969aa543638ea9967",
"amount": 500
}
]
@@ -83,18 +83,18 @@
{
"txid": "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6",
"vout": 0,
"pubkey": "0000000000000000000000000000000000000000000000000000000000000001",
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"signerPubkey": "020000000000000000000000000000000000000000000000000000000000000001",
"amount": 1100
}
],
"receivers": [
{
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"pubkey": "25a43cecfa0e1b1a4f72d64ad15f4cfa7a84d0723e8511c969aa543638ea9967",
"amount": 600
},
{
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"pubkey": "25a43cecfa0e1b1a4f72d64ad15f4cfa7a84d0723e8511c969aa543638ea9967",
"amount": 500
}
]
@@ -105,18 +105,18 @@
{
"txid": "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6",
"vout": 0,
"pubkey": "0000000000000000000000000000000000000000000000000000000000000001",
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"signerPubkey": "020000000000000000000000000000000000000000000000000000000000000001",
"amount": 1100
}
],
"receivers": [
{
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"pubkey": "25a43cecfa0e1b1a4f72d64ad15f4cfa7a84d0723e8511c969aa543638ea9967",
"amount": 600
},
{
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"pubkey": "25a43cecfa0e1b1a4f72d64ad15f4cfa7a84d0723e8511c969aa543638ea9967",
"amount": 500
}
]
@@ -133,58 +133,58 @@
{
"txid": "755c820771284d85ea4bbcc246565b4eddadc44237a7e57a0f9cb78a840d1d41",
"vout": 0,
"pubkey": "0000000000000000000000000000000000000000000000000000000000000001",
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"signerPubkey": "020000000000000000000000000000000000000000000000000000000000000001",
"amount": 1000
},
{
"txid": "66a0df86fcdeb84b8877adfe0b2c556dba30305d72ddbd4c49355f6930355357",
"vout": 0,
"pubkey": "0000000000000000000000000000000000000000000000000000000000000001",
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"signerPubkey": "020000000000000000000000000000000000000000000000000000000000000001",
"amount": 1000
},
{
"txid": "9913159bc7aa493ca53cbb9cbc88f97ba01137c814009dc7ef520c3fafc67909",
"vout": 1,
"pubkey": "0000000000000000000000000000000000000000000000000000000000000001",
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"signerPubkey": "020000000000000000000000000000000000000000000000000000000000000001",
"amount": 500
},
{
"txid": "5e10e77a7cdedc153be5193a4b6055a7802706ded4f2a9efefe86ed2f9a6ae60",
"vout": 0,
"pubkey":"0000000000000000000000000000000000000000000000000000000000000001",
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"signerPubkey": "020000000000000000000000000000000000000000000000000000000000000001",
"amount": 1000
},
{
"txid": "5e10e77a7cdedc153be5193a4b6055a7802706ded4f2a9efefe86ed2f9a6ae60",
"vout": 1,
"pubkey": "0000000000000000000000000000000000000000000000000000000000000001",
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"signerPubkey": "020000000000000000000000000000000000000000000000000000000000000001",
"amount": 1000
}
],
"receivers": [
{
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"pubkey": "25a43cecfa0e1b1a4f72d64ad15f4cfa7a84d0723e8511c969aa543638ea9967",
"amount": 1000
},
{
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"pubkey": "25a43cecfa0e1b1a4f72d64ad15f4cfa7a84d0723e8511c969aa543638ea9967",
"amount": 1000
},
{
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"pubkey": "25a43cecfa0e1b1a4f72d64ad15f4cfa7a84d0723e8511c969aa543638ea9967",
"amount": 1000
},
{
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"pubkey": "25a43cecfa0e1b1a4f72d64ad15f4cfa7a84d0723e8511c969aa543638ea9967",
"amount": 1000
},
{
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"pubkey": "25a43cecfa0e1b1a4f72d64ad15f4cfa7a84d0723e8511c969aa543638ea9967",
"amount": 500
}
]
@@ -206,25 +206,25 @@
{
"txid": "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6",
"vout": 0,
"pubkey": "0000000000000000000000000000000000000000000000000000000000000001",
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"signerPubkey": "020000000000000000000000000000000000000000000000000000000000000001",
"amount": 600
},
{
"txid": "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6",
"vout": 1,
"pubkey": "0000000000000000000000000000000000000000000000000000000000000001",
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"signerPubkey": "020000000000000000000000000000000000000000000000000000000000000001",
"amount": 500
}
],
"receivers": [
{
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"pubkey": "25a43cecfa0e1b1a4f72d64ad15f4cfa7a84d0723e8511c969aa543638ea9967",
"amount": 600
},
{
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"pubkey": "25a43cecfa0e1b1a4f72d64ad15f4cfa7a84d0723e8511c969aa543638ea9967",
"amount": 500
}
]

View File

@@ -45,34 +45,38 @@ func getPsetId(pset *psetv2.Pset) (string, error) {
return utx.TxHash().String(), nil
}
func getOnchainReceivers(
payments []domain.Payment,
) []domain.Receiver {
receivers := make([]domain.Receiver, 0)
func getOnchainOutputs(
payments []domain.Payment, net *network.Network,
) ([]psetv2.OutputArgs, error) {
outputs := make([]psetv2.OutputArgs, 0)
for _, payment := range payments {
for _, receiver := range payment.Receivers {
if receiver.IsOnchain() {
receivers = append(receivers, receiver)
}
}
}
return receivers
}
func getOffchainReceivers(
payments []domain.Payment,
) ([]tree.Receiver, error) {
receivers := make([]tree.Receiver, 0)
for _, payment := range payments {
for _, receiver := range payment.Receivers {
if !receiver.IsOnchain() {
vtxoScript, err := tree.ParseVtxoScript(receiver.Descriptor)
receiverScript, err := address.ToOutputScript(receiver.OnchainAddress)
if err != nil {
return nil, err
}
receivers = append(receivers, tree.Receiver{
Script: vtxoScript,
outputs = append(outputs, psetv2.OutputArgs{
Script: receiverScript,
Amount: receiver.Amount,
Asset: net.AssetID,
})
}
}
}
return outputs, nil
}
func getOutputVtxosLeaves(
payments []domain.Payment,
) ([]tree.VtxoLeaf, error) {
receivers := make([]tree.VtxoLeaf, 0)
for _, payment := range payments {
for _, receiver := range payment.Receivers {
if !receiver.IsOnchain() {
receivers = append(receivers, tree.VtxoLeaf{
Pubkey: receiver.Pubkey,
Amount: receiver.Amount,
})
}

View File

@@ -228,7 +228,10 @@ func (b *txBuilder) BuildSweepTx(inputs []ports.SweepInput) (signedSweepTx strin
}
func (b *txBuilder) BuildForfeitTxs(
poolTx string, payments []domain.Payment, minRelayFeeRate chainfee.SatPerKVByte,
poolTx string,
payments []domain.Payment,
descriptors map[domain.VtxoKey]string,
minRelayFeeRate chainfee.SatPerKVByte,
) (connectors []string, forfeitTxs []string, err error) {
connectorPkScript, err := b.getConnectorPkScript(poolTx)
if err != nil {
@@ -245,7 +248,7 @@ func (b *txBuilder) BuildForfeitTxs(
return nil, nil, err
}
forfeitTxs, err = b.createForfeitTxs(payments, connectorTxs, minRelayFeeRate)
forfeitTxs, err = b.createForfeitTxs(payments, descriptors, connectorTxs, minRelayFeeRate)
if err != nil {
return nil, nil, err
}
@@ -271,7 +274,7 @@ func (b *txBuilder) BuildRoundTx(
return "", nil, "", fmt.Errorf("missing cosigners")
}
receivers, err := getOffchainReceivers(payments)
receivers, err := getOutputVtxosLeaves(payments)
if err != nil {
return "", nil, "", err
}
@@ -399,7 +402,10 @@ func (b *txBuilder) FindLeaves(congestionTree tree.CongestionTree, fromtxid stri
}
func (b *txBuilder) BuildAsyncPaymentTransactions(
vtxos []domain.Vtxo, aspPubKey *secp256k1.PublicKey, receivers []domain.Receiver,
vtxos []domain.Vtxo,
descriptors map[domain.VtxoKey]string,
forfeitsLeaves map[domain.VtxoKey]chainhash.Hash,
receivers []domain.Receiver,
) (string, error) {
if len(vtxos) <= 0 {
return "", fmt.Errorf("missing vtxos")
@@ -412,6 +418,16 @@ func (b *txBuilder) BuildAsyncPaymentTransactions(
redeemTxWeightEstimator := &input.TxWeightEstimator{}
for index, vtxo := range vtxos {
desc, ok := descriptors[vtxo.VtxoKey]
if !ok {
return "", fmt.Errorf("missing descriptor for vtxo %s", vtxo.VtxoKey)
}
forfeitLeafHash, ok := forfeitsLeaves[vtxo.VtxoKey]
if !ok {
return "", fmt.Errorf("missing forfeit leaf hash for vtxo %s", vtxo.VtxoKey)
}
if vtxo.Spent || vtxo.Redeemed || vtxo.Swept {
return "", fmt.Errorf("all vtxos must be unspent")
}
@@ -426,7 +442,7 @@ func (b *txBuilder) BuildAsyncPaymentTransactions(
Index: vtxo.VOut,
}
vtxoScript, err := bitcointree.ParseVtxoScript(vtxo.Descriptor)
vtxoScript, err := bitcointree.ParseVtxoScript(desc)
if err != nil {
return "", err
}
@@ -446,41 +462,27 @@ func (b *txBuilder) BuildAsyncPaymentTransactions(
PkScript: vtxoOutputScript,
}
if defaultVtxoScript, ok := vtxoScript.(*bitcointree.DefaultVtxoScript); ok {
forfeitLeaf := bitcointree.MultisigClosure{
Pubkey: defaultVtxoScript.Owner,
AspPubkey: defaultVtxoScript.Asp,
}
tapLeaf, err := forfeitLeaf.Leaf()
if err != nil {
return "", err
}
leafProof, err := vtxoTree.GetTaprootMerkleProof(tapLeaf.TapHash())
if err != nil {
return "", err
}
tapscripts[index] = &psbt.TaprootTapLeafScript{
ControlBlock: leafProof.ControlBlock,
Script: leafProof.Script,
LeafVersion: txscript.BaseLeafVersion,
}
ctrlBlock, err := txscript.ParseControlBlock(leafProof.ControlBlock)
if err != nil {
return "", err
}
redeemTxWeightEstimator.AddTapscriptInput(64*2, &waddrmgr.Tapscript{
RevealedScript: leafProof.Script,
ControlBlock: ctrlBlock,
})
} else {
return "", fmt.Errorf("vtxo %s:%d script is not default script, can't be async spent", vtxo.Txid, vtxo.VOut)
leafProof, err := vtxoTree.GetTaprootMerkleProof(forfeitLeafHash)
if err != nil {
return "", err
}
tapscripts[index] = &psbt.TaprootTapLeafScript{
ControlBlock: leafProof.ControlBlock,
Script: leafProof.Script,
LeafVersion: txscript.BaseLeafVersion,
}
ctrlBlock, err := txscript.ParseControlBlock(leafProof.ControlBlock)
if err != nil {
return "", err
}
redeemTxWeightEstimator.AddTapscriptInput(64*2+40, &waddrmgr.Tapscript{
RevealedScript: leafProof.Script,
ControlBlock: ctrlBlock,
})
ins = append(ins, vtxoOutpoint)
}
@@ -498,17 +500,21 @@ func (b *txBuilder) BuildAsyncPaymentTransactions(
}
for i, receiver := range receivers {
offchainScript, err := bitcointree.ParseVtxoScript(receiver.Descriptor)
if receiver.IsOnchain() {
return "", fmt.Errorf("receiver %d is onchain", i)
}
pubkeyBytes, err := hex.DecodeString(receiver.Pubkey)
if err != nil {
return "", err
}
receiverVtxoTaprootKey, _, err := offchainScript.TapTree()
pubkey, err := schnorr.ParsePubKey(pubkeyBytes)
if err != nil {
return "", err
}
newVtxoScript, err := common.P2TRScript(receiverVtxoTaprootKey)
newVtxoScript, err := common.P2TRScript(pubkey)
if err != nil {
return "", err
}
@@ -588,7 +594,6 @@ func (b *txBuilder) createRoundTx(
connectorAmount := dustLimit
receivers := getOnchainReceivers(payments)
nbOfInputs := countSpentVtxos(payments)
connectorsAmount := (connectorAmount + connectorMinRelayFee) * nbOfInputs
if nbOfInputs > 1 {
@@ -614,25 +619,17 @@ func (b *txBuilder) createRoundTx(
})
}
for _, receiver := range receivers {
targetAmount += receiver.Amount
receiverAddr, err := btcutil.DecodeAddress(receiver.OnchainAddress, b.onchainNetwork())
if err != nil {
return nil, err
}
receiverScript, err := txscript.PayToAddrScript(receiverAddr)
if err != nil {
return nil, err
}
outputs = append(outputs, &wire.TxOut{
Value: int64(receiver.Amount),
PkScript: receiverScript,
})
onchainOutputs, err := getOnchainOutputs(payments, b.onchainNetwork())
if err != nil {
return nil, err
}
for _, output := range onchainOutputs {
targetAmount += uint64(output.Value)
}
outputs = append(outputs, onchainOutputs...)
for _, input := range boardingInputs {
targetAmount -= input.Amount
}
@@ -1016,6 +1013,7 @@ func (b *txBuilder) minRelayFeeTreeTx() (uint64, error) {
func (b *txBuilder) createForfeitTxs(
payments []domain.Payment,
descriptors map[domain.VtxoKey]string,
connectors []*psbt.Packet,
minRelayFeeRate chainfee.SatPerKVByte,
) ([]string, error) {
@@ -1042,7 +1040,12 @@ func (b *txBuilder) createForfeitTxs(
forfeitTxs := make([]string, 0)
for _, payment := range payments {
for _, vtxo := range payment.Inputs {
offchainscript, err := bitcointree.ParseVtxoScript(vtxo.Descriptor)
desc, ok := descriptors[vtxo.VtxoKey]
if !ok {
return nil, err
}
offchainscript, err := bitcointree.ParseVtxoScript(desc)
if err != nil {
return nil, err
}

View File

@@ -123,7 +123,7 @@ func TestBuildForfeitTxs(t *testing.T) {
t.Run("valid", func(t *testing.T) {
for _, f := range fixtures.Valid {
connectors, forfeitTxs, err := builder.BuildForfeitTxs(
f.PoolTx, f.Payments, minRelayFeeRate,
f.PoolTx, f.Payments, f.Descriptors, minRelayFeeRate,
)
require.NoError(t, err)
require.Len(t, connectors, f.ExpectedNumOfConnectors)
@@ -161,7 +161,7 @@ func TestBuildForfeitTxs(t *testing.T) {
t.Run("invalid", func(t *testing.T) {
for _, f := range fixtures.Invalid {
connectors, forfeitTxs, err := builder.BuildForfeitTxs(
f.PoolTx, f.Payments, minRelayFeeRate,
f.PoolTx, f.Payments, f.Descriptors, minRelayFeeRate,
)
require.EqualError(t, err, f.ExpectedErr)
require.Empty(t, connectors)
@@ -225,6 +225,7 @@ func parsePoolTxFixtures() (*poolTxFixtures, error) {
type forfeitTxsFixtures struct {
Valid []struct {
Payments []domain.Payment
Descriptors map[domain.VtxoKey]string
ExpectedNumOfConnectors int
ExpectedNumOfForfeitTxs int
PoolTx string
@@ -232,6 +233,7 @@ type forfeitTxsFixtures struct {
}
Invalid []struct {
Payments []domain.Payment
Descriptors map[domain.VtxoKey]string
ExpectedErr string
PoolTx string
}
@@ -254,5 +256,41 @@ func parseForfeitTxsFixtures() (*forfeitTxsFixtures, error) {
return nil, err
}
valid := vv["valid"].([]interface{})
for i, v := range valid {
val := v.(map[string]interface{})
payments := val["payments"].([]interface{})
descriptors := make(map[domain.VtxoKey]string)
for _, p := range payments {
inputs := p.(map[string]interface{})["inputs"].([]interface{})
for _, in := range inputs {
inMap := in.(map[string]interface{})
descriptors[domain.VtxoKey{
Txid: inMap["txid"].(string),
VOut: uint32(inMap["vout"].(float64)),
}] = inMap["descriptor"].(string)
}
}
fixtures.Valid[i].Descriptors = descriptors
}
invalid := vv["invalid"].([]interface{})
for i, v := range invalid {
val := v.(map[string]interface{})
payments := val["payments"].([]interface{})
descriptors := make(map[domain.VtxoKey]string)
for _, p := range payments {
inputs := p.(map[string]interface{})["inputs"].([]interface{})
for _, in := range inputs {
inMap := in.(map[string]interface{})
descriptors[domain.VtxoKey{
Txid: inMap["txid"].(string),
VOut: uint32(inMap["vout"].(float64)),
}] = inMap["descriptor"].(string)
}
}
fixtures.Invalid[i].Descriptors = descriptors
}
return &fixtures, nil
}

View File

@@ -9,13 +9,14 @@
{
"txid": "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6",
"vout": 0,
"pubkey": "0000000000000000000000000000000000000000000000000000000000000001",
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"amount": 1100
}
],
"receivers": [
{
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"pubkey": "25a43cecfa0e1b1a4f72d64ad15f4cfa7a84d0723e8511c969aa543638ea9967",
"amount": 1100
}
]
@@ -32,17 +33,18 @@
{
"txid": "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6",
"vout": 0,
"pubkey": "0000000000000000000000000000000000000000000000000000000000000001",
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"amount": 1100
}
],
"receivers": [
{
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"pubkey": "25a43cecfa0e1b1a4f72d64ad15f4cfa7a84d0723e8511c969aa543638ea9967",
"amount": 600
},
{
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"pubkey": "25a43cecfa0e1b1a4f72d64ad15f4cfa7a84d0723e8511c969aa543638ea9967",
"amount": 500
}
]
@@ -59,17 +61,18 @@
{
"txid": "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6",
"vout": 0,
"pubkey": "0000000000000000000000000000000000000000000000000000000000000001",
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"amount": 1100
}
],
"receivers": [
{
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"pubkey": "25a43cecfa0e1b1a4f72d64ad15f4cfa7a84d0723e8511c969aa543638ea9967",
"amount": 600
},
{
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"pubkey": "25a43cecfa0e1b1a4f72d64ad15f4cfa7a84d0723e8511c969aa543638ea9967",
"amount": 500
}
]
@@ -80,17 +83,18 @@
{
"txid": "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6",
"vout": 0,
"pubkey": "0000000000000000000000000000000000000000000000000000000000000001",
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"amount": 1100
}
],
"receivers": [
{
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"pubkey": "25a43cecfa0e1b1a4f72d64ad15f4cfa7a84d0723e8511c969aa543638ea9967",
"amount": 600
},
{
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"pubkey": "25a43cecfa0e1b1a4f72d64ad15f4cfa7a84d0723e8511c969aa543638ea9967",
"amount": 500
}
]
@@ -101,17 +105,18 @@
{
"txid": "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6",
"vout": 0,
"pubkey": "0000000000000000000000000000000000000000000000000000000000000001",
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"amount": 1100
}
],
"receivers": [
{
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"pubkey": "25a43cecfa0e1b1a4f72d64ad15f4cfa7a84d0723e8511c969aa543638ea9967",
"amount": 600
},
{
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"pubkey": "25a43cecfa0e1b1a4f72d64ad15f4cfa7a84d0723e8511c969aa543638ea9967",
"amount": 500
}
]
@@ -128,54 +133,58 @@
{
"txid": "755c820771284d85ea4bbcc246565b4eddadc44237a7e57a0f9cb78a840d1d41",
"vout": 0,
"pubkey": "0000000000000000000000000000000000000000000000000000000000000001",
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"amount": 1000
},
{
"txid": "66a0df86fcdeb84b8877adfe0b2c556dba30305d72ddbd4c49355f6930355357",
"vout": 0,
"pubkey": "0000000000000000000000000000000000000000000000000000000000000001",
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"amount": 1000
},
{
"txid": "9913159bc7aa493ca53cbb9cbc88f97ba01137c814009dc7ef520c3fafc67909",
"vout": 1,
"pubkey": "0000000000000000000000000000000000000000000000000000000000000001",
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"amount": 500
},
{
"txid": "5e10e77a7cdedc153be5193a4b6055a7802706ded4f2a9efefe86ed2f9a6ae60",
"vout": 0,
"pubkey": "0000000000000000000000000000000000000000000000000000000000000001",
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"amount": 1000
},
{
"txid": "5e10e77a7cdedc153be5193a4b6055a7802706ded4f2a9efefe86ed2f9a6ae60",
"vout": 1,
"pubkey": "0000000000000000000000000000000000000000000000000000000000000001",
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"amount": 1000
}
],
"receivers": [
{
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"pubkey": "25a43cecfa0e1b1a4f72d64ad15f4cfa7a84d0723e8511c969aa543638ea9967",
"amount": 1000
},
{
"pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5",
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"pubkey": "25a43cecfa0e1b1a4f72d64ad15f4cfa7a84d0723e8511c969aa543638ea9967",
"amount": 1000
},
{
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"pubkey": "25a43cecfa0e1b1a4f72d64ad15f4cfa7a84d0723e8511c969aa543638ea9967",
"amount": 1000
},
{
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"pubkey": "25a43cecfa0e1b1a4f72d64ad15f4cfa7a84d0723e8511c969aa543638ea9967",
"amount": 1000
},
{
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"pubkey": "25a43cecfa0e1b1a4f72d64ad15f4cfa7a84d0723e8511c969aa543638ea9967",
"amount": 500
}
]
@@ -197,53 +206,58 @@
{
"txid": "755c820771284d85ea4bbcc246565b4eddadc44237a7e57a0f9cb78a840d1d41",
"vout": 0,
"pubkey": "0000000000000000000000000000000000000000000000000000000000000001",
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"amount": 1000
},
{
"txid": "66a0df86fcdeb84b8877adfe0b2c556dba30305d72ddbd4c49355f6930355357",
"vout": 0,
"pubkey": "0000000000000000000000000000000000000000000000000000000000000001",
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"amount": 1000
},
{
"txid": "9913159bc7aa493ca53cbb9cbc88f97ba01137c814009dc7ef520c3fafc67909",
"vout": 1,
"pubkey": "0000000000000000000000000000000000000000000000000000000000000001",
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"amount": 500
},
{
"txid": "5e10e77a7cdedc153be5193a4b6055a7802706ded4f2a9efefe86ed2f9a6ae60",
"vout": 0,
"pubkey": "0000000000000000000000000000000000000000000000000000000000000001",
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"amount": 1000
},
{
"txid": "5e10e77a7cdedc153be5193a4b6055a7802706ded4f2a9efefe86ed2f9a6ae60",
"vout": 1,
"pubkey": "0000000000000000000000000000000000000000000000000000000000000001",
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"amount": 1000
}
],
"receivers": [
{
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"pubkey": "25a43cecfa0e1b1a4f72d64ad15f4cfa7a84d0723e8511c969aa543638ea9967",
"amount": 1000
},
{
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"pubkey": "25a43cecfa0e1b1a4f72d64ad15f4cfa7a84d0723e8511c969aa543638ea9967",
"amount": 1000
},
{
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"pubkey": "25a43cecfa0e1b1a4f72d64ad15f4cfa7a84d0723e8511c969aa543638ea9967",
"amount": 1000
},
{
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"pubkey": "25a43cecfa0e1b1a4f72d64ad15f4cfa7a84d0723e8511c969aa543638ea9967",
"amount": 1000
},
{
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"pubkey": "25a43cecfa0e1b1a4f72d64ad15f4cfa7a84d0723e8511c969aa543638ea9967",
"amount": 500
}
]
@@ -252,7 +266,7 @@
"poolTx": "cHNidP8BALICAAAAAnonOnsJBkHUUaKf/7fdS0/sVyBCgDPusYzGSZZiXPbtAAAAAAD/////VLtr81ZII3QJnXgrIwgcnbsq3aa4L3qdHOAn2evlFtEAAAAAAP////8CohIAAAAAAAAiUSBZarBUuSIHnlkuIoel9MmvexqTGK8jCZaRjt8L+Pb3s+gDAAAAAAAAIlEgI95L4kHEn2fAA+vysD+RIR4eD3AIQwc+FyCInJ8HivYAAAAAAAEBIOgDAAAAAAAAF6kU6p9IboLvs92Dpp/Zbj8BE3V9oDyHAAEBIOgDAAAAAAAAF6kU6p9IboLvs92Dpp/Zbj8BE3V9oDyHAAAA",
"poolTxid": "7c0c10756cdb9ab8e605f1c82e25989761308cf4c60e6a6f42b72d46144c4ce0",
"expectedNumOfForfeitTxs": 25,
"expectedNumOfConnectors": 4
"expectedNumOfConnectors": 4
}
],
"invalid": []

View File

@@ -1,47 +1,58 @@
package txbuilder
import (
"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/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
)
func getOnchainReceivers(
payments []domain.Payment,
) []domain.Receiver {
receivers := make([]domain.Receiver, 0)
func getOnchainOutputs(
payments []domain.Payment, network *chaincfg.Params,
) ([]*wire.TxOut, error) {
outputs := make([]*wire.TxOut, 0)
for _, payment := range payments {
for _, receiver := range payment.Receivers {
if receiver.IsOnchain() {
receivers = append(receivers, receiver)
}
}
}
return receivers
}
func getOffchainReceivers(
payments []domain.Payment,
) ([]bitcointree.Receiver, error) {
receivers := make([]bitcointree.Receiver, 0)
for _, payment := range payments {
for _, receiver := range payment.Receivers {
if !receiver.IsOnchain() {
vtxoScript, err := bitcointree.ParseVtxoScript(receiver.Descriptor)
receiverAddr, err := btcutil.DecodeAddress(receiver.OnchainAddress, network)
if err != nil {
return nil, err
}
receivers = append(receivers, bitcointree.Receiver{
Script: vtxoScript,
receiverScript, err := txscript.PayToAddrScript(receiverAddr)
if err != nil {
return nil, err
}
outputs = append(outputs, &wire.TxOut{
Value: int64(receiver.Amount),
PkScript: receiverScript,
})
}
}
}
return outputs, nil
}
func getOutputVtxosLeaves(
payments []domain.Payment,
) ([]tree.VtxoLeaf, error) {
leaves := make([]tree.VtxoLeaf, 0)
for _, payment := range payments {
for _, receiver := range payment.Receivers {
if !receiver.IsOnchain() {
leaves = append(leaves, tree.VtxoLeaf{
Pubkey: receiver.Pubkey,
Amount: receiver.Amount,
})
}
}
}
return receivers, nil
return leaves, nil
}
func countSpentVtxos(payments []domain.Payment) uint64 {

View File

@@ -338,7 +338,7 @@ func (h *handler) Ping(
func (h *handler) CreatePayment(
ctx context.Context, req *arkv1.CreatePaymentRequest,
) (*arkv1.CreatePaymentResponse, error) {
inputs, err := parseInputs(req.GetInputs())
inputs, err := parseAsyncPaymentInputs(req.GetInputs())
if err != nil {
return nil, status.Error(codes.InvalidArgument, err.Error())
}
@@ -353,12 +353,12 @@ func (h *handler) CreatePayment(
return nil, status.Error(codes.InvalidArgument, "output amount must be greater than 0")
}
if len(receiver.OnchainAddress) > 0 {
return nil, status.Error(codes.InvalidArgument, "onchain address is not supported as async payment destination")
if len(receiver.OnchainAddress) <= 0 && len(receiver.Pubkey) <= 0 {
return nil, status.Error(codes.InvalidArgument, "missing address")
}
if len(receiver.Descriptor) <= 0 {
return nil, status.Error(codes.InvalidArgument, "missing output descriptor")
if receiver.IsOnchain() {
return nil, status.Error(codes.InvalidArgument, "onchain outputs are not supported as async payment destination")
}
}
@@ -462,12 +462,12 @@ func (h *handler) GetRoundById(
func (h *handler) ListVtxos(
ctx context.Context, req *arkv1.ListVtxosRequest,
) (*arkv1.ListVtxosResponse, error) {
_, userPubkey, _, err := parseAddress(req.GetAddress())
_, err := parseAddress(req.GetAddress())
if err != nil {
return nil, status.Error(codes.InvalidArgument, err.Error())
}
spendableVtxos, spentVtxos, err := h.svc.ListVtxos(ctx, userPubkey)
spendableVtxos, spentVtxos, err := h.svc.ListVtxos(ctx, req.GetAddress())
if err != nil {
return nil, err
}

View File

@@ -1,25 +1,55 @@
package handlers
import (
"encoding/hex"
"fmt"
arkv1 "github.com/ark-network/ark/api-spec/protobuf/gen/ark/v1"
"github.com/ark-network/ark/common"
"github.com/ark-network/ark/common/tree"
"github.com/ark-network/ark/server/internal/core/application"
"github.com/ark-network/ark/server/internal/core/domain"
"github.com/ark-network/ark/server/internal/core/ports"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/chaincfg/chainhash"
)
// From interface type to app type
func parseAddress(addr string) (string, *secp256k1.PublicKey, *secp256k1.PublicKey, error) {
func parseAddress(addr string) (*common.Address, error) {
if len(addr) <= 0 {
return "", nil, nil, fmt.Errorf("missing address")
return nil, fmt.Errorf("missing address")
}
return common.DecodeAddress(addr)
}
func parseAsyncPaymentInputs(ins []*arkv1.AsyncPaymentInput) ([]application.AsyncPaymentInput, error) {
if len(ins) <= 0 {
return nil, fmt.Errorf("missing inputs")
}
inputs := make([]application.AsyncPaymentInput, 0, len(ins))
for _, input := range ins {
forfeitLeafHash, err := chainhash.NewHashFromStr(input.GetForfeitLeafHash())
if err != nil {
return nil, fmt.Errorf("invalid forfeit leaf hash: %s", err)
}
inputs = append(inputs, application.AsyncPaymentInput{
Input: ports.Input{
VtxoKey: domain.VtxoKey{
Txid: input.GetInput().GetOutpoint().GetTxid(),
VOut: input.GetInput().GetOutpoint().GetVout(),
},
Descriptor: input.GetInput().GetDescriptor_(),
},
ForfeitLeafHash: *forfeitLeafHash,
})
}
return inputs, nil
}
func parseInputs(ins []*arkv1.Input) ([]ports.Input, error) {
if len(ins) <= 0 {
return nil, fmt.Errorf("missing inputs")
@@ -39,26 +69,43 @@ func parseInputs(ins []*arkv1.Input) ([]ports.Input, error) {
return inputs, nil
}
func parseReceiver(out *arkv1.Output) (domain.Receiver, error) {
decodedAddr, err := common.DecodeAddress(out.GetAddress())
if err != nil {
// onchain address
return domain.Receiver{
Amount: out.GetAmount(),
OnchainAddress: out.GetAddress(),
}, nil
}
return domain.Receiver{
Amount: out.GetAmount(),
Pubkey: hex.EncodeToString(schnorr.SerializePubKey(decodedAddr.VtxoTapKey)),
}, nil
}
func parseReceivers(outs []*arkv1.Output) ([]domain.Receiver, error) {
receivers := make([]domain.Receiver, 0, len(outs))
for _, out := range outs {
if out.GetAmount() == 0 {
return nil, fmt.Errorf("missing output amount")
}
if len(out.GetAddress()) <= 0 && len(out.GetDescriptor_()) <= 0 {
if len(out.GetAddress()) <= 0 {
return nil, fmt.Errorf("missing output destination")
}
receivers = append(receivers, domain.Receiver{
Descriptor: out.GetDescriptor_(),
Amount: out.GetAmount(),
OnchainAddress: out.GetAddress(),
})
rcv, err := parseReceiver(out)
if err != nil {
return nil, err
}
receivers = append(receivers, rcv)
}
return receivers, nil
}
// From app typeto interface type
// From app type to interface type
type vtxoList []domain.Vtxo
@@ -70,15 +117,15 @@ func (v vtxoList) toProto() []*arkv1.Vtxo {
Txid: vv.Txid,
Vout: vv.VOut,
},
Descriptor_: vv.Descriptor,
Amount: vv.Amount,
RoundTxid: vv.RoundTxid,
Spent: vv.Spent,
ExpireAt: vv.ExpireAt,
SpentBy: vv.SpentBy,
Swept: vv.Swept,
RedeemTx: vv.RedeemTx,
Pending: vv.Pending,
Amount: vv.Amount,
RoundTxid: vv.RoundTxid,
Spent: vv.Spent,
ExpireAt: vv.ExpireAt,
SpentBy: vv.SpentBy,
Swept: vv.Swept,
RedeemTx: vv.RedeemTx,
Pending: vv.Pending,
Pubkey: vv.Pubkey,
})
}