[Server] Validate forfeit txs without re-building them (#382)

* compute forfeit partial tx client-side first

* fix conflict

* go work sync

* move verify sig in VerifyForfeits

* move check after len(inputs)
This commit is contained in:
Louis Singer
2024-11-18 19:08:10 +01:00
committed by GitHub
parent 6ed4e30b6d
commit 0d2db92173
14 changed files with 712 additions and 729 deletions

View File

@@ -28,28 +28,16 @@ var ConnectorTxSize = (&input.TxWeightEstimator{}).
func ComputeForfeitMinRelayFee(
feeRate chainfee.SatPerKVByte,
vtxoScriptTapTree TaprootTree,
tapscript *waddrmgr.Tapscript,
witnessSize int,
aspScriptClass txscript.ScriptClass,
) (uint64, error) {
txWeightEstimator := &input.TxWeightEstimator{}
biggestVtxoLeafProof, err := BiggestLeafMerkleProof(vtxoScriptTapTree)
if err != nil {
return 0, err
}
ctrlBlock, err := txscript.ParseControlBlock(biggestVtxoLeafProof.ControlBlock)
if err != nil {
return 0, err
}
txWeightEstimator.AddP2PKHInput() // connector input
txWeightEstimator.AddTapscriptInput(
64*2, // forfeit witness = 2 signatures
&waddrmgr.Tapscript{
RevealedScript: biggestVtxoLeafProof.Script,
ControlBlock: ctrlBlock,
},
lntypes.WeightUnit(witnessSize),
tapscript,
)
switch aspScriptClass {

View File

@@ -21,6 +21,7 @@ import (
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcwallet/waddrmgr"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
log "github.com/sirupsen/logrus"
@@ -1461,11 +1462,6 @@ func (a *covenantArkClient) createAndSignForfeits(
return nil, err
}
feeAmount, err := common.ComputeForfeitMinRelayFee(feeRate, vtxoTapTree, txscript.WitnessV0PubKeyHashTy)
if err != nil {
return nil, err
}
vtxoOutputScript, err := common.P2TRScript(vtxoTapKey)
if err != nil {
return nil, err
@@ -1477,6 +1473,7 @@ func (a *covenantArkClient) createAndSignForfeits(
}
var forfeitClosure tree.Closure
var witnessSize int
switch s := vtxoScript.(type) {
case *tree.DefaultVtxoScript:
@@ -1484,6 +1481,7 @@ func (a *covenantArkClient) createAndSignForfeits(
Pubkey: s.Owner,
AspPubkey: a.AspPubkey,
}
witnessSize = 64 * 2
default:
return nil, fmt.Errorf("unsupported vtxo script: %T", s)
}
@@ -1508,6 +1506,19 @@ func (a *covenantArkClient) createAndSignForfeits(
ControlBlock: *ctrlBlock,
}
feeAmount, err := common.ComputeForfeitMinRelayFee(
feeRate,
&waddrmgr.Tapscript{
RevealedScript: leafProof.Script,
ControlBlock: &ctrlBlock.ControlBlock,
},
witnessSize,
txscript.WitnessV0PubKeyHashTy,
)
if err != nil {
return nil, err
}
for _, connectorPset := range connectorsPsets {
forfeits, err := tree.BuildForfeitTxs(
connectorPset, vtxoInput, vtxo.Amount, a.Dust, feeAmount, vtxoOutputScript, forfeitPkScript,

View File

@@ -28,6 +28,7 @@ import (
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcwallet/waddrmgr"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
log "github.com/sirupsen/logrus"
@@ -2111,11 +2112,6 @@ func (a *covenantlessArkClient) createAndSignForfeits(
return nil, err
}
feeAmount, err := common.ComputeForfeitMinRelayFee(feeRate, vtxoTapTree, parsedScript.Class())
if err != nil {
return nil, err
}
vtxoOutputScript, err := common.P2TRScript(vtxoTapKey)
if err != nil {
return nil, err
@@ -2132,6 +2128,7 @@ func (a *covenantlessArkClient) createAndSignForfeits(
}
var forfeitClosure bitcointree.Closure
var witnessSize int
switch v := vtxoScript.(type) {
case *bitcointree.DefaultVtxoScript:
@@ -2139,6 +2136,7 @@ func (a *covenantlessArkClient) createAndSignForfeits(
Pubkey: v.Owner,
AspPubkey: a.AspPubkey,
}
witnessSize = 64 * 2
default:
return nil, fmt.Errorf("unsupported vtxo script: %T", vtxoScript)
}
@@ -2159,6 +2157,24 @@ func (a *covenantlessArkClient) createAndSignForfeits(
LeafVersion: txscript.BaseLeafVersion,
}
ctrlBlock, err := txscript.ParseControlBlock(leafProof.ControlBlock)
if err != nil {
return nil, err
}
feeAmount, err := common.ComputeForfeitMinRelayFee(
feeRate,
&waddrmgr.Tapscript{
RevealedScript: leafProof.Script,
ControlBlock: ctrlBlock,
},
witnessSize,
parsedScript.Class(),
)
if err != nil {
return nil, err
}
for _, connectorPset := range connectorsPsets {
forfeits, err := bitcointree.BuildForfeitTxs(
connectorPset, vtxoInput, vtxo.Amount, a.Dust, feeAmount, vtxoOutputScript, forfeitPkScript,

View File

@@ -10,6 +10,7 @@ require (
github.com/btcsuite/btcd/btcutil v1.1.5
github.com/btcsuite/btcd/btcutil/psbt v1.1.9
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0
github.com/btcsuite/btcwallet v0.16.10-0.20240718224643-db3a4a2543bd
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0
github.com/dgraph-io/badger/v4 v4.3.0
github.com/go-openapi/errors v0.22.0
@@ -30,6 +31,7 @@ require (
github.com/aead/siphash v1.0.1 // indirect
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f // indirect
github.com/btcsuite/btcwallet/walletdb v1.4.2 // indirect
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd // indirect
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 // indirect
github.com/btcsuite/winsvc v1.0.0 // indirect
@@ -59,7 +61,9 @@ require (
github.com/jrick/logrotate v1.0.0 // indirect
github.com/kkdai/bstream v1.0.0 // indirect
github.com/klauspost/compress v1.17.9 // indirect
github.com/lightninglabs/neutrino/cache v1.1.2 // indirect
github.com/lightningnetwork/lnd/fn v1.2.1 // indirect
github.com/lightningnetwork/lnd/tlv v1.2.6 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/oklog/ulid v1.3.1 // indirect
@@ -71,6 +75,7 @@ require (
github.com/stretchr/objx v0.5.2 // indirect
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect
github.com/vulpemventures/fastsha256 v0.0.0-20160815193821-637e65642941 // indirect
go.etcd.io/bbolt v1.3.10 // indirect
go.mongodb.org/mongo-driver v1.14.0 // indirect
go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/otel v1.30.0 // indirect

View File

@@ -24,6 +24,8 @@ github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0/go.mod h1:7SFka0XMvUgj3hfZtyd
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f h1:bAs4lUbRJpnnkd9VhRV3jjAVU7DJVjMaK+IsvSeZvFo=
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA=
github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
github.com/btcsuite/btcwallet v0.16.10-0.20240718224643-db3a4a2543bd h1:QDb8foTCRoXrfoZVEzSYgSde16MJh4gCtCin8OCS0kI=
github.com/btcsuite/btcwallet/walletdb v1.4.2 h1:zwZZ+zaHo4mK+FAN6KeK85S3oOm+92x2avsHvFAhVBE=
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd h1:R/opQEbFEy9JGkIguV40SvRY1uliPX8ifOvi6ICsFCw=
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg=
github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY=
@@ -175,10 +177,12 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lightninglabs/neutrino/cache v1.1.2 h1:C9DY/DAPaPxbFC+xNNEI/z1SJY9GS3shmlu5hIQ798g=
github.com/lightningnetwork/lnd v0.18.2-beta h1:Qv4xQ2ka05vqzmdkFdISHCHP6CzHoYNVKfD18XPjHsM=
github.com/lightningnetwork/lnd v0.18.2-beta/go.mod h1:cGQR1cVEZFZQcCx2VBbDY8xwGjCz+SupSopU1HpjP2I=
github.com/lightningnetwork/lnd/fn v1.2.1 h1:pPsVGrwi9QBwdLJzaEGK33wmiVKOxs/zc8H7+MamFf0=
github.com/lightningnetwork/lnd/fn v1.2.1/go.mod h1:SyFohpVrARPKH3XVAJZlXdVe+IwMYc4OMAvrDY32kw0=
github.com/lightningnetwork/lnd/tlv v1.2.6 h1:icvQG2yDr6k3ZuZzfRdG3EJp6pHurcuh3R6dg0gv/Mw=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
@@ -252,6 +256,7 @@ github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.etcd.io/bbolt v1.3.10 h1:+BqfJTcCzTItrop8mq/lbzL8wSGtj94UO/3U31shqG0=
go.mongodb.org/mongo-driver v1.14.0 h1:P98w8egYRjYe3XDjxhYJagTokP/H6HzlsnojRgZRd80=
go.mongodb.org/mongo-driver v1.14.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c=
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=

View File

@@ -181,7 +181,6 @@ func (s *covenantService) SpendNotes(_ context.Context, _ []note.Note) (string,
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()
@@ -243,15 +242,33 @@ func (s *covenantService) SpendVtxos(ctx context.Context, inputs []ports.Input)
return "", fmt.Errorf("input %s:%d already swept", vtxo.Txid, vtxo.VOut)
}
vtxoScript, err := tree.ParseVtxoScript(input.Descriptor)
if err != nil {
return "", fmt.Errorf("failed to parse boarding descriptor: %s", err)
}
tapKey, _, err := vtxoScript.TapTree()
if err != nil {
return "", fmt.Errorf("failed to get taproot key: %s", err)
}
expectedTapKey, err := vtxo.TapKey()
if err != nil {
return "", fmt.Errorf("failed to get taproot key: %s", err)
}
if !bytes.Equal(schnorr.SerializePubKey(tapKey), schnorr.SerializePubKey(expectedTapKey)) {
return "", fmt.Errorf("descriptor does not match vtxo pubkey")
}
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, descriptors); err != nil {
if err := s.paymentRequests.push(*payment, boardingInputs); err != nil {
return "", err
}
return payment.Id, nil
@@ -519,7 +536,7 @@ func (s *covenantService) startFinalization() {
if num > paymentsThreshold {
num = paymentsThreshold
}
payments, boardingInputs, descriptors, _, _ := s.paymentRequests.pop(num)
payments, boardingInputs, _, _ := 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")
@@ -533,7 +550,7 @@ func (s *covenantService) startFinalization() {
return
}
unsignedPoolTx, tree, connectorAddress, err := s.builder.BuildRoundTx(s.pubkey, payments, boardingInputs, sweptRounds)
unsignedPoolTx, tree, connectorAddress, connectors, err := s.builder.BuildRoundTx(s.pubkey, payments, boardingInputs, sweptRounds)
if err != nil {
round.Fail(fmt.Errorf("failed to create pool tx: %s", err))
log.WithError(err).Warn("failed to create pool tx")
@@ -541,34 +558,7 @@ func (s *covenantService) startFinalization() {
}
log.Debugf("pool tx created for round %s", round.Id)
needForfeits := false
for _, pay := range payments {
if len(pay.Inputs) > 0 {
needForfeits = true
break
}
}
var forfeitTxs, connectors []string
minRelayFeeRate := s.wallet.MinRelayFeeRate(ctx)
if needForfeits {
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")
return
}
log.Debugf("forfeit transactions created for round %s", round.Id)
if err := s.forfeitTxs.push(forfeitTxs); err != nil {
round.Fail(fmt.Errorf("failed to cache forfeit txs: %s", err))
log.WithError(err).Warn("failed to cache forfeit txs")
return
}
}
s.forfeitTxs.init(connectors, payments)
if _, err := round.StartFinalization(
connectorAddress, connectors, tree, unsignedPoolTx,
@@ -598,9 +588,8 @@ func (s *covenantService) finalizeRound() {
}
}()
forfeitTxs, leftUnsigned := s.forfeitTxs.pop()
if len(leftUnsigned) > 0 {
err := fmt.Errorf("%d forfeit txs left to sign", len(leftUnsigned))
forfeitTxs, err := s.forfeitTxs.pop()
if err != nil {
changes = round.Fail(fmt.Errorf("failed to finalize round: %s", err))
log.WithError(err).Warn("failed to finalize round")
return

View File

@@ -463,7 +463,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 {
@@ -520,7 +520,24 @@ 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
vtxoScript, err := bitcointree.ParseVtxoScript(input.Descriptor)
if err != nil {
return "", fmt.Errorf("failed to parse boarding descriptor: %s", err)
}
tapKey, _, err := vtxoScript.TapTree()
if err != nil {
return "", fmt.Errorf("failed to get taproot key: %s", err)
}
expectedTapKey, err := vtxo.TapKey()
if err != nil {
return "", fmt.Errorf("failed to get taproot key: %s", err)
}
if !bytes.Equal(schnorr.SerializePubKey(tapKey), schnorr.SerializePubKey(expectedTapKey)) {
return "", fmt.Errorf("descriptor does not match vtxo pubkey")
}
vtxosInputs = append(vtxosInputs, vtxo)
}
@@ -529,7 +546,7 @@ func (s *covenantlessService) SpendVtxos(ctx context.Context, inputs []ports.Inp
if err != nil {
return "", err
}
if err := s.paymentRequests.push(*payment, boardingInputs, descriptors); err != nil {
if err := s.paymentRequests.push(*payment, boardingInputs); err != nil {
return "", err
}
return payment.Id, nil
@@ -872,7 +889,7 @@ func (s *covenantlessService) startFinalization() {
if num > paymentsThreshold {
num = paymentsThreshold
}
payments, boardingInputs, descriptors, cosigners, paymentsNotes := s.paymentRequests.pop(num)
payments, boardingInputs, cosigners, paymentsNotes := 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))
@@ -904,13 +921,21 @@ func (s *covenantlessService) startFinalization() {
cosigners = append(cosigners, ephemeralKey.PubKey())
unsignedRoundTx, tree, connectorAddress, err := s.builder.BuildRoundTx(s.pubkey, payments, boardingInputs, sweptRounds, cosigners...)
unsignedRoundTx, tree, connectorAddress, connectors, err := s.builder.BuildRoundTx(
s.pubkey,
payments,
boardingInputs,
sweptRounds,
cosigners...,
)
if err != nil {
round.Fail(fmt.Errorf("failed to create pool tx: %s", err))
log.WithError(err).Warn("failed to create pool tx")
round.Fail(fmt.Errorf("failed to create round tx: %s", err))
log.WithError(err).Warn("failed to create round tx")
return
}
log.Debugf("pool tx created for round %s", round.Id)
log.Debugf("round tx created for round %s", round.Id)
s.forfeitTxs.init(connectors, payments)
if len(tree) > 0 {
log.Debugf("signing congestion tree for round %s", round.Id)
@@ -1063,34 +1088,6 @@ func (s *covenantlessService) startFinalization() {
tree = signedTree
}
needForfeits := false
for _, pay := range payments {
if len(pay.Inputs) > 0 {
needForfeits = true
break
}
}
var forfeitTxs, connectors []string
minRelayFeeRate := s.wallet.MinRelayFeeRate(ctx)
if needForfeits {
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")
return
}
log.Debugf("forfeit transactions created for round %s", round.Id)
if err := s.forfeitTxs.push(forfeitTxs); err != nil {
round.Fail(fmt.Errorf("failed to store forfeit txs: %s", err))
log.WithError(err).Warn("failed to store forfeit txs")
return
}
}
if _, err := round.StartFinalization(
connectorAddress, connectors, tree, unsignedRoundTx,
); err != nil {
@@ -1143,9 +1140,8 @@ func (s *covenantlessService) finalizeRound(notes []note.Note) {
}
}()
forfeitTxs, leftUnsigned := s.forfeitTxs.pop()
if len(leftUnsigned) > 0 {
err := fmt.Errorf("%d forfeit txs left to sign", len(leftUnsigned))
forfeitTxs, err := s.forfeitTxs.pop()
if err != nil {
changes = round.Fail(fmt.Errorf("failed to finalize round: %s", err))
log.WithError(err).Warn("failed to finalize round")
return

View File

@@ -14,7 +14,6 @@ import (
"github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/nbd-wtf/go-nostr"
"github.com/nbd-wtf/go-nostr/nip19"
"github.com/sirupsen/logrus"
)
type timedPayment struct {
@@ -28,14 +27,13 @@ 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[domain.VtxoKey]string), make(map[string]*secp256k1.PublicKey)}
return &paymentsMap{lock, paymentsById, make(map[string]*secp256k1.PublicKey)}
}
func (m *paymentsMap) len() int64 {
@@ -88,7 +86,6 @@ func (m *paymentsMap) pushWithNotes(payment domain.Payment, notes []note.Note) e
func (m *paymentsMap) push(
payment domain.Payment,
boardingInputs []ports.BoardingInput,
descriptors map[domain.VtxoKey]string,
) error {
m.lock.Lock()
defer m.lock.Unlock()
@@ -117,10 +114,6 @@ func (m *paymentsMap) push(
}
}
for key, desc := range descriptors {
m.descriptors[key] = desc
}
m.payments[payment.Id] = &timedPayment{payment, boardingInputs, make([]note.Note, 0), time.Now(), time.Time{}}
return nil
}
@@ -137,7 +130,7 @@ func (m *paymentsMap) pushEphemeralKey(paymentId string, pubkey *secp256k1.Publi
return nil
}
func (m *paymentsMap) pop(num int64) ([]domain.Payment, []ports.BoardingInput, map[domain.VtxoKey]string, []*secp256k1.PublicKey, []note.Note) {
func (m *paymentsMap) pop(num int64) ([]domain.Payment, []ports.BoardingInput, []*secp256k1.PublicKey, []note.Note) {
m.lock.Lock()
defer m.lock.Unlock()
@@ -164,7 +157,6 @@ func (m *paymentsMap) pop(num int64) ([]domain.Payment, []ports.BoardingInput, m
payments := make([]domain.Payment, 0, num)
boardingInputs := make([]ports.BoardingInput, 0)
cosigners := make([]*secp256k1.PublicKey, 0, num)
descriptors := make(map[domain.VtxoKey]string)
notes := make([]note.Note, 0)
for _, p := range paymentsByTime[:num] {
boardingInputs = append(boardingInputs, p.boardingInputs...)
@@ -174,13 +166,9 @@ func (m *paymentsMap) pop(num int64) ([]domain.Payment, []ports.BoardingInput, m
delete(m.ephemeralKeys, p.Payment.Id)
}
notes = append(notes, p.notes...)
for _, vtxo := range p.Payment.Inputs {
descriptors[vtxo.VtxoKey] = m.descriptors[vtxo.VtxoKey]
delete(m.descriptors, vtxo.VtxoKey)
}
delete(m.payments, p.Id)
}
return payments, boardingInputs, descriptors, cosigners, notes
return payments, boardingInputs, cosigners, notes
}
func (m *paymentsMap) update(payment domain.Payment) error {
@@ -250,73 +238,84 @@ func (m *paymentsMap) view(id string) (*domain.Payment, bool) {
}, true
}
type signedTx struct {
tx string
signed bool
}
type forfeitTxsMap struct {
lock *sync.RWMutex
forfeitTxs map[string]*signedTx
builder ports.TxBuilder
forfeitTxs map[domain.VtxoKey][]string
connectors []string
vtxos []domain.Vtxo
}
func newForfeitTxsMap(txBuilder ports.TxBuilder) *forfeitTxsMap {
return &forfeitTxsMap{&sync.RWMutex{}, make(map[string]*signedTx), txBuilder}
return &forfeitTxsMap{&sync.RWMutex{}, txBuilder, make(map[domain.VtxoKey][]string), nil, nil}
}
func (m *forfeitTxsMap) push(txs []string) error {
func (m *forfeitTxsMap) init(connectors []string, payments []domain.Payment) {
vtxosToSign := make([]domain.Vtxo, 0)
for _, payment := range payments {
vtxosToSign = append(vtxosToSign, payment.Inputs...)
}
m.lock.Lock()
defer m.lock.Unlock()
for _, tx := range txs {
txid, err := m.builder.GetTxID(tx)
if err != nil {
return err
m.vtxos = vtxosToSign
m.connectors = connectors
for _, vtxo := range vtxosToSign {
m.forfeitTxs[vtxo.VtxoKey] = make([]string, 0)
}
m.forfeitTxs[txid] = &signedTx{tx, false}
}
return nil
}
func (m *forfeitTxsMap) sign(txs []string) error {
m.lock.Lock()
defer m.lock.Unlock()
if len(txs) == 0 {
return nil
}
for _, tx := range txs {
valid, txid, err := m.builder.VerifyTapscriptPartialSigs(tx)
if len(m.vtxos) == 0 || len(m.connectors) == 0 {
return fmt.Errorf("forfeit txs map not initialized")
}
// verify the txs are valid
validTxs, err := m.builder.VerifyForfeitTxs(m.vtxos, m.connectors, txs)
if err != nil {
return err
}
if _, ok := m.forfeitTxs[txid]; ok {
if valid {
m.forfeitTxs[txid].tx = tx
m.forfeitTxs[txid].signed = true
} else {
logrus.Warnf("invalid forfeit tx signature (%s)", txid)
}
}
m.lock.Lock()
defer m.lock.Unlock()
for vtxoKey, txs := range validTxs {
m.forfeitTxs[vtxoKey] = txs
}
return nil
}
func (m *forfeitTxsMap) pop() (signed, unsigned []string) {
func (m *forfeitTxsMap) reset() {
m.lock.Lock()
defer m.lock.Unlock()
for _, t := range m.forfeitTxs {
if t.signed {
signed = append(signed, t.tx)
} else {
unsigned = append(unsigned, t.tx)
m.forfeitTxs = make(map[domain.VtxoKey][]string)
m.connectors = nil
}
func (m *forfeitTxsMap) pop() ([]string, error) {
m.lock.Lock()
defer func() {
m.lock.Unlock()
m.reset()
}()
txs := make([]string, 0)
for vtxoKey, signed := range m.forfeitTxs {
if len(signed) == 0 {
return nil, fmt.Errorf("missing forfeit txs for vtxo %s", vtxoKey)
}
txs = append(txs, signed...)
}
m.forfeitTxs = make(map[string]*signedTx)
return signed, unsigned
return txs, nil
}
// onchainOutputs iterates over all the nodes' outputs in the congestion tree and checks their onchain state

View File

@@ -6,6 +6,8 @@ import (
"fmt"
"hash"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/google/uuid"
)
@@ -125,3 +127,11 @@ type Vtxo struct {
RedeemTx string // empty if in-round vtxo
CreatedAt int64
}
func (v Vtxo) TapKey() (*secp256k1.PublicKey, error) {
pubkeyBytes, err := hex.DecodeString(v.Pubkey)
if err != nil {
return nil, err
}
return schnorr.ParsePubKey(pubkeyBytes)
}

View File

@@ -5,7 +5,6 @@ import (
"github.com/ark-network/ark/server/internal/core/domain"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
)
type SweepInput interface {
@@ -28,16 +27,25 @@ type BoardingInput struct {
}
type TxBuilder interface {
// BuildRoundTx builds a round tx for the given payments, boarding inputs
// it selects coin from swept rounds and ASP wallet
// returns the round partial tx, the vtxo tree and the set of connectors
BuildRoundTx(
aspPubkey *secp256k1.PublicKey, payments []domain.Payment, boardingInputs []BoardingInput, sweptRounds []domain.Round,
cosigners ...*secp256k1.PublicKey,
) (roundTx string, congestionTree tree.CongestionTree, connectorAddress string, err error)
BuildForfeitTxs(
) (
roundTx string,
payments []domain.Payment,
descriptors map[domain.VtxoKey]string,
minRelayFeeRate chainfee.SatPerKVByte,
) (connectors []string, forfeitTxs []string, err error)
congestionTree tree.CongestionTree,
connectorAddress string,
connectors []string,
err error,
)
// VerifyForfeitTxs verifies the given forfeit txs for the given vtxos and connectors
VerifyForfeitTxs(
vtxos []domain.Vtxo,
connectors []string,
txs []string,
) (valid map[domain.VtxoKey][]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)

View File

@@ -4,6 +4,7 @@ import (
"bytes"
"context"
"fmt"
"math"
"github.com/ark-network/ark/common"
"github.com/ark-network/ark/common/tree"
@@ -12,12 +13,11 @@ import (
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcwallet/waddrmgr"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
"github.com/vulpemventures/go-elements/address"
"github.com/vulpemventures/go-elements/elementsutil"
"github.com/vulpemventures/go-elements/network"
"github.com/vulpemventures/go-elements/payment"
"github.com/vulpemventures/go-elements/psetv2"
"github.com/vulpemventures/go-elements/taproot"
"github.com/vulpemventures/go-elements/transaction"
@@ -81,42 +81,235 @@ func (b *txBuilder) BuildSweepTx(inputs []ports.SweepInput) (signedSweepTx strin
return extractedTx.ToHex()
}
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)
func (b *txBuilder) VerifyForfeitTxs(vtxos []domain.Vtxo, connectors []string, forfeitTxs []string) (map[domain.VtxoKey][]string, error) {
connectorsPsets := make([]*psetv2.Pset, 0, len(connectors))
var connectorAmount uint64
for i, connector := range connectors {
pset, err := psetv2.NewPsetFromBase64(connector)
if err != nil {
return nil, nil, err
return nil, err
}
connectorFeeAmount, err := b.minRelayFeeConnectorTx()
if err != nil {
return nil, nil, err
if i == len(connectors)-1 {
var lastOutput *psetv2.Output
for i := len(pset.Outputs) - 1; i >= 0; i-- {
if len(pset.Outputs[i].Script) > 0 {
lastOutput = &pset.Outputs[i]
break
}
}
connectorAmount, err := b.wallet.GetDustAmount(context.Background())
if err != nil {
return nil, nil, err
if lastOutput == nil {
return nil, fmt.Errorf("invalid connector tx")
}
connectorTxs, err := b.createConnectors(poolTx, payments, connectorAddress, connectorAmount, connectorFeeAmount)
if err != nil {
return nil, nil, err
connectorAmount = uint64(lastOutput.Value)
}
forfeitTxs, err = b.createForfeitTxs(payments, descriptors, connectorTxs, connectorAmount, minRelayFeeRate)
if err != nil {
return nil, nil, err
connectorsPsets = append(connectorsPsets, pset)
}
for _, tx := range connectorTxs {
buf, _ := tx.ToBase64()
connectors = append(connectors, buf)
// decode forfeit txs, map by vtxo key
forfeitTxsPsets := make(map[domain.VtxoKey][]*psetv2.Pset)
for _, forfeitTx := range forfeitTxs {
pset, err := psetv2.NewPsetFromBase64(forfeitTx)
if err != nil {
return nil, err
}
return connectors, forfeitTxs, nil
if len(pset.Inputs) != 2 {
return nil, fmt.Errorf("invalid forfeit tx, expect 2 inputs, got %d", len(pset.Inputs))
}
valid, _, err := b.verifyTapscriptPartialSigs(pset)
if err != nil {
return nil, err
}
if !valid {
return nil, fmt.Errorf("invalid forfeit tx signature")
}
vtxoInput := pset.Inputs[1]
vtxoKey := domain.VtxoKey{
Txid: chainhash.Hash(vtxoInput.PreviousTxid).String(),
VOut: vtxoInput.PreviousTxIndex,
}
if _, ok := forfeitTxsPsets[vtxoKey]; !ok {
forfeitTxsPsets[vtxoKey] = make([]*psetv2.Pset, 0)
}
forfeitTxsPsets[vtxoKey] = append(forfeitTxsPsets[vtxoKey], pset)
}
forfeitAddress, err := b.wallet.GetForfeitAddress(context.Background())
if err != nil {
return nil, err
}
forfeitScript, err := address.ToOutputScript(forfeitAddress)
if err != nil {
return nil, err
}
minRate := b.wallet.MinRelayFeeRate(context.Background())
validForfeitTxs := make(map[domain.VtxoKey][]string)
for vtxoKey, psets := range forfeitTxsPsets {
if len(psets) == 0 {
continue
}
var vtxo *domain.Vtxo
for _, v := range vtxos {
if v.VtxoKey == vtxoKey {
vtxo = &v
break
}
}
if vtxo == nil {
return nil, fmt.Errorf("missing vtxo %s", vtxoKey)
}
feeAmount := uint64(0)
// only take the first forfeit tx, as all forfeit must have the same output
firstForfeit := psets[0]
for _, output := range firstForfeit.Outputs {
if len(output.Script) <= 0 {
feeAmount = output.Value
break
}
}
if feeAmount == 0 {
return nil, fmt.Errorf("missing forfeit tx fee output")
}
inputAmount := vtxo.Amount + connectorAmount
if feeAmount > inputAmount {
return nil, fmt.Errorf("forfeit tx fee is higher than the input amount, %d > %d", feeAmount, inputAmount)
}
if len(firstForfeit.Inputs[1].TapLeafScript) <= 0 {
return nil, fmt.Errorf("missing taproot leaf script for vtxo input, invalid forfeit tx")
}
vtxoTapscript := firstForfeit.Inputs[1].TapLeafScript[0]
minFee, err := common.ComputeForfeitMinRelayFee(
minRate,
&waddrmgr.Tapscript{
RevealedScript: vtxoTapscript.Script,
ControlBlock: &vtxoTapscript.ControlBlock.ControlBlock,
},
64*2,
txscript.GetScriptClass(forfeitScript),
)
if err != nil {
return nil, err
}
dustAmount, err := b.wallet.GetDustAmount(context.Background())
if err != nil {
return nil, err
}
if inputAmount-feeAmount < dustAmount {
return nil, fmt.Errorf("forfeit tx output amount is dust, %d < %d", inputAmount-feeAmount, dustAmount)
}
if feeAmount < uint64(minFee) {
return nil, fmt.Errorf("forfeit tx fee is lower than the min relay fee, %d < %d", feeAmount, minFee)
}
feeThreshold := uint64(math.Ceil(float64(minFee) * 1.05))
if feeAmount > feeThreshold {
return nil, fmt.Errorf("forfeit tx fee is higher than 5%% of the min relay fee, %d > %d", feeAmount, feeThreshold)
}
vtxoInput := psetv2.InputArgs{
Txid: vtxoKey.Txid,
TxIndex: vtxoKey.VOut,
}
vtxoTapKey, err := vtxo.TapKey()
if err != nil {
return nil, err
}
vtxoScript, err := common.P2TRScript(vtxoTapKey)
if err != nil {
return nil, err
}
rebuiltForfeits := make([]*psetv2.Pset, 0)
for _, connector := range connectorsPsets {
forfeits, err := tree.BuildForfeitTxs(
connector,
vtxoInput,
vtxo.Amount,
connectorAmount,
feeAmount,
vtxoScript,
forfeitScript,
)
if err != nil {
return nil, err
}
rebuiltForfeits = append(rebuiltForfeits, forfeits...)
}
if len(rebuiltForfeits) != len(psets) {
return nil, fmt.Errorf("missing forfeits, expect %d, got %d", len(psets), len(rebuiltForfeits))
}
for _, forfeit := range rebuiltForfeits {
found := false
utx, err := forfeit.UnsignedTx()
if err != nil {
return nil, err
}
txid := utx.TxHash().String()
for _, pset := range psets {
utx, err := pset.UnsignedTx()
if err != nil {
return nil, err
}
if txid == utx.TxHash().String() {
found = true
break
}
}
if !found {
return nil, fmt.Errorf("missing forfeit tx %s", txid)
}
}
b64Txs := make([]string, 0, len(psets))
for _, forfeit := range psets {
b64, err := forfeit.ToBase64()
if err != nil {
return nil, err
}
b64Txs = append(b64Txs, b64)
}
validForfeitTxs[vtxoKey] = b64Txs
}
return validForfeitTxs, nil
}
func (b *txBuilder) BuildRoundTx(
@@ -125,7 +318,7 @@ func (b *txBuilder) BuildRoundTx(
boardingInputs []ports.BoardingInput,
sweptRounds []domain.Round,
_ ...*secp256k1.PublicKey, // cosigners are not used in the covenant
) (poolTx string, congestionTree tree.CongestionTree, connectorAddress string, err error) {
) (roundTx string, congestionTree tree.CongestionTree, connectorAddress string, connectors []string, err error) {
// The creation of the tree and the pool tx are tightly coupled:
// - building the tree requires knowing the shared outpoint (txid:vout)
// - building the pool tx requires knowing the shared output script and amount
@@ -145,19 +338,19 @@ func (b *txBuilder) BuildRoundTx(
if !isOnchainOnly(payments) {
feeSatsPerNode, err := b.wallet.MinRelayFee(context.Background(), uint64(common.CovenantTreeTxSize))
if err != nil {
return "", nil, "", err
return "", nil, "", nil, err
}
vtxosLeaves, err := getOutputVtxosLeaves(payments)
if err != nil {
return "", nil, "", err
return "", nil, "", nil, err
}
treeFactoryFn, sharedOutputScript, sharedOutputAmount, err = tree.CraftCongestionTree(
b.onchainNetwork().AssetID, aspPubkey, vtxosLeaves, feeSatsPerNode, b.roundLifetime,
)
if err != nil {
return "", nil, "", err
return "", nil, "", nil, err
}
}
@@ -188,11 +381,38 @@ func (b *txBuilder) BuildRoundTx(
}
}
poolTx, err = ptx.ToBase64()
roundTx, err = ptx.ToBase64()
if err != nil {
return
}
if countSpentVtxos(payments) <= 0 {
return
}
connectorFeeAmount, err := b.minRelayFeeConnectorTx()
if err != nil {
return "", nil, "", nil, err
}
connectorAmount, err := b.wallet.GetDustAmount(context.Background())
if err != nil {
return "", nil, "", nil, err
}
connectorsPsets, err := b.createConnectors(roundTx, payments, connectorAddress, connectorAmount, connectorFeeAmount)
if err != nil {
return "", nil, "", nil, err
}
for _, pset := range connectorsPsets {
b64, err := pset.ToBase64()
if err != nil {
return "", nil, "", nil, err
}
connectors = append(connectors, b64)
}
return
}
@@ -242,13 +462,20 @@ func (b *txBuilder) GetSweepInput(node tree.Node) (lifetime int64, sweepInput po
return lifetime, sweepInput, nil
}
func (b *txBuilder) VerifyTapscriptPartialSigs(tx string) (bool, string, error) {
ptx, _ := psetv2.NewPsetFromBase64(tx)
utx, _ := ptx.UnsignedTx()
pset, err := psetv2.NewPsetFromBase64(tx)
if err != nil {
return false, "", err
}
return b.verifyTapscriptPartialSigs(pset)
}
func (b *txBuilder) verifyTapscriptPartialSigs(pset *psetv2.Pset) (bool, string, error) {
utx, _ := pset.UnsignedTx()
txid := utx.TxHash().String()
for index, input := range ptx.Inputs {
for index, input := range pset.Inputs {
if len(input.TapLeafScript) == 0 {
continue
}
@@ -274,7 +501,7 @@ func (b *txBuilder) VerifyTapscriptPartialSigs(tx string) (bool, string, error)
leafHash := taproot.NewBaseTapElementsLeaf(tapLeaf.Script).TapHash()
preimage, err := b.getTaprootPreimage(
tx,
pset,
index,
&leafHash,
)
@@ -679,7 +906,7 @@ func (b *txBuilder) VerifyAndCombinePartialTx(dest string, src string) (string,
return "", err
}
preimage, err := b.getTaprootPreimage(src, i, leafHash)
preimage, err := b.getTaprootPreimage(sourcePset, i, leafHash)
if err != nil {
return "", err
}
@@ -711,11 +938,11 @@ func (b *txBuilder) VerifyAndCombinePartialTx(dest string, src string) (string,
}
func (b *txBuilder) createConnectors(
poolTx string, payments []domain.Payment,
roundTx string, payments []domain.Payment,
connectorAddress string,
connectorAmount, feeAmount uint64,
) ([]*psetv2.Pset, error) {
txid, _ := getTxid(poolTx)
txid, _ := getTxid(roundTx)
aspScript, err := address.ToOutputScript(connectorAddress)
if err != nil {
@@ -783,107 +1010,7 @@ func (b *txBuilder) createConnectors(
return connectors, nil
}
func (b *txBuilder) createForfeitTxs(
payments []domain.Payment,
descriptors map[domain.VtxoKey]string,
connectors []*psetv2.Pset,
connectorAmount uint64,
minRelayFeeRate chainfee.SatPerKVByte,
) ([]string, error) {
forfeitAddr, err := b.wallet.GetForfeitAddress(context.Background())
if err != nil {
return nil, err
}
forfeitPkScript, err := address.ToOutputScript(forfeitAddr)
if err != nil {
return nil, err
}
forfeitTxs := make([]string, 0)
for _, payment := range payments {
for _, vtxo := range payment.Inputs {
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
}
vtxoTapKey, vtxoTree, err := offchainScript.TapTree()
if err != nil {
return nil, err
}
vtxoScript, err := common.P2TRScript(vtxoTapKey)
if err != nil {
return nil, err
}
feeAmount, err := common.ComputeForfeitMinRelayFee(minRelayFeeRate, vtxoTree, txscript.WitnessV0PubKeyHashTy)
if err != nil {
return nil, err
}
for _, connector := range connectors {
txs, err := tree.BuildForfeitTxs(
connector,
psetv2.InputArgs{
Txid: vtxo.Txid,
TxIndex: vtxo.VOut,
},
vtxo.Amount,
connectorAmount,
feeAmount,
vtxoScript,
forfeitPkScript,
)
if err != nil {
return nil, err
}
for _, tx := range txs {
b64, err := tx.ToBase64()
if err != nil {
return nil, err
}
forfeitTxs = append(forfeitTxs, b64)
}
}
}
}
return forfeitTxs, nil
}
func (b *txBuilder) getConnectorAddress(poolTx string) (string, error) {
pset, err := psetv2.NewPsetFromBase64(poolTx)
if err != nil {
return "", err
}
if len(pset.Outputs) < 1 {
return "", fmt.Errorf("connector output not found in pool tx")
}
connectorOutput := pset.Outputs[1]
pay, err := payment.FromScript(connectorOutput.Script, b.onchainNetwork(), nil)
if err != nil {
return "", err
}
return pay.WitnessPubKeyHash()
}
func (b *txBuilder) getTaprootPreimage(tx string, inputIndex int, leafHash *chainhash.Hash) ([]byte, error) {
pset, err := psetv2.NewPsetFromBase64(tx)
if err != nil {
return nil, err
}
func (b *txBuilder) getTaprootPreimage(pset *psetv2.Pset, inputIndex int, leafHash *chainhash.Hash) ([]byte, error) {
prevoutScripts := make([][]byte, 0)
prevoutAssets := make([][]byte, 0)
prevoutValues := make([][]byte, 0)

View File

@@ -12,11 +12,9 @@ import (
"github.com/ark-network/ark/server/internal/core/domain"
"github.com/ark-network/ark/server/internal/core/ports"
txbuilder "github.com/ark-network/ark/server/internal/infrastructure/tx-builder/covenant"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/vulpemventures/go-elements/psetv2"
)
const (
@@ -67,7 +65,7 @@ func TestBuildPoolTx(t *testing.T) {
if len(fixtures.Valid) > 0 {
t.Run("valid", func(t *testing.T) {
for _, f := range fixtures.Valid {
poolTx, congestionTree, connAddr, err := builder.BuildRoundTx(
poolTx, congestionTree, connAddr, _, err := builder.BuildRoundTx(
pubkey, f.Payments, []ports.BoardingInput{}, []domain.Round{},
)
require.NoError(t, err)
@@ -88,7 +86,7 @@ func TestBuildPoolTx(t *testing.T) {
if len(fixtures.Invalid) > 0 {
t.Run("invalid", func(t *testing.T) {
for _, f := range fixtures.Invalid {
poolTx, congestionTree, connAddr, err := builder.BuildRoundTx(
poolTx, congestionTree, connAddr, _, err := builder.BuildRoundTx(
pubkey, f.Payments, []ports.BoardingInput{}, []domain.Round{},
)
require.EqualError(t, err, f.ExpectedErr)
@@ -100,67 +98,6 @@ func TestBuildPoolTx(t *testing.T) {
}
}
func TestBuildForfeitTxs(t *testing.T) {
builder := txbuilder.NewTxBuilder(
wallet, common.Liquid, 1209344, boardingExitDelay,
)
fixtures, err := parseForfeitTxsFixtures()
require.NoError(t, err)
require.NotEmpty(t, fixtures)
if len(fixtures.Valid) > 0 {
t.Run("valid", func(t *testing.T) {
for _, f := range fixtures.Valid {
connectors, forfeitTxs, err := builder.BuildForfeitTxs(
f.PoolTx, f.Payments, f.Descriptors, minRelayFeeRate,
)
require.NoError(t, err)
require.Len(t, connectors, f.ExpectedNumOfConnectors)
require.Len(t, forfeitTxs, f.ExpectedNumOfForfeitTxs)
expectedInputTxid := f.PoolTxid
// Verify the chain of connectors
for _, connector := range connectors {
tx, err := psetv2.NewPsetFromBase64(connector)
require.NoError(t, err)
require.NotNil(t, tx)
require.Len(t, tx.Inputs, 1)
require.Len(t, tx.Outputs, 3)
inputTxid := chainhash.Hash(tx.Inputs[0].PreviousTxid).String()
require.Equal(t, expectedInputTxid, inputTxid)
require.Equal(t, 1, int(tx.Inputs[0].PreviousTxIndex))
expectedInputTxid = getTxid(tx)
}
// decode and check forfeit txs
for _, forfeitTx := range forfeitTxs {
tx, err := psetv2.NewPsetFromBase64(forfeitTx)
require.NoError(t, err)
require.Len(t, tx.Inputs, 2)
require.Len(t, tx.Outputs, 2)
}
}
})
}
if len(fixtures.Invalid) > 0 {
t.Run("invalid", func(t *testing.T) {
for _, f := range fixtures.Invalid {
connectors, forfeitTxs, err := builder.BuildForfeitTxs(
f.PoolTx, f.Payments, f.Descriptors, minRelayFeeRate,
)
require.EqualError(t, err, f.ExpectedErr)
require.Empty(t, connectors)
require.Empty(t, forfeitTxs)
}
})
}
}
func randomInput() []ports.TxInput {
txid := randomHex(32)
input := &mockedInput{}
@@ -211,81 +148,3 @@ func parsePoolTxFixtures() (*poolTxFixtures, error) {
return &fixtures, nil
}
type forfeitTxsFixtures struct {
Valid []struct {
Payments []domain.Payment
Descriptors map[domain.VtxoKey]string
ExpectedNumOfConnectors int
ExpectedNumOfForfeitTxs int
PoolTx string
PoolTxid string
}
Invalid []struct {
Payments []domain.Payment
Descriptors map[domain.VtxoKey]string
ExpectedErr string
PoolTx string
}
}
func parseForfeitTxsFixtures() (*forfeitTxsFixtures, error) {
file, err := os.ReadFile("testdata/fixtures.json")
if err != nil {
return nil, err
}
v := map[string]interface{}{}
if err := json.Unmarshal(file, &v); err != nil {
return nil, err
}
vv := v["buildForfeitTxs"].(map[string]interface{})
file, _ = json.Marshal(vv)
var fixtures forfeitTxsFixtures
if err := json.Unmarshal(file, &fixtures); err != nil {
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
}
func getTxid(tx *psetv2.Pset) string {
utx, _ := tx.UnsignedTx()
return utx.TxHash().String()
}

View File

@@ -5,6 +5,7 @@ import (
"context"
"encoding/hex"
"fmt"
"math"
"strings"
"github.com/ark-network/ark/common"
@@ -22,7 +23,6 @@ import (
"github.com/btcsuite/btcwallet/waddrmgr"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
)
type txBuilder struct {
@@ -48,7 +48,15 @@ func (b *txBuilder) GetTxID(tx string) (string, error) {
}
func (b *txBuilder) VerifyTapscriptPartialSigs(tx string) (bool, string, error) {
ptx, _ := psbt.NewFromRawBytes(strings.NewReader(tx), true)
ptx, err := psbt.NewFromRawBytes(strings.NewReader(tx), true)
if err != nil {
return false, "", err
}
return b.verifyTapscriptPartialSigs(ptx)
}
func (b *txBuilder) verifyTapscriptPartialSigs(ptx *psbt.Packet) (bool, string, error) {
txid := ptx.UnsignedTx.TxID()
for index, input := range ptx.Inputs {
@@ -83,7 +91,7 @@ func (b *txBuilder) VerifyTapscriptPartialSigs(tx string) (bool, string, error)
}
preimage, err := b.getTaprootPreimage(
tx,
ptx,
index,
tapLeaf.Script,
)
@@ -227,37 +235,217 @@ func (b *txBuilder) BuildSweepTx(inputs []ports.SweepInput) (signedSweepTx strin
return hex.EncodeToString(buf.Bytes()), nil
}
func (b *txBuilder) BuildForfeitTxs(
poolTx string,
payments []domain.Payment,
descriptors map[domain.VtxoKey]string,
minRelayFeeRate chainfee.SatPerKVByte,
) (connectors []string, forfeitTxs []string, err error) {
connectorPkScript, err := b.getConnectorPkScript(poolTx)
func (b *txBuilder) VerifyForfeitTxs(vtxos []domain.Vtxo, connectors []string, forfeitTxs []string) (map[domain.VtxoKey][]string, error) {
connectorsPtxs := make([]*psbt.Packet, 0, len(connectors))
var connectorAmount uint64
for i, connector := range connectors {
ptx, err := psbt.NewFromRawBytes(strings.NewReader(connector), true)
if err != nil {
return nil, nil, err
return nil, err
}
minRelayFeeConnectorTx, err := b.minRelayFeeConnectorTx()
if err != nil {
return nil, nil, err
if i == len(connectors)-1 {
lastOutput := ptx.UnsignedTx.TxOut[len(ptx.UnsignedTx.TxOut)-1]
connectorAmount = uint64(lastOutput.Value)
}
connectorTxs, err := b.createConnectors(poolTx, payments, connectorPkScript, minRelayFeeConnectorTx)
if err != nil {
return nil, nil, err
connectorsPtxs = append(connectorsPtxs, ptx)
}
forfeitTxs, err = b.createForfeitTxs(payments, descriptors, connectorTxs, minRelayFeeRate)
// decode forfeit txs, map by vtxo key
forfeitTxsPtxs := make(map[domain.VtxoKey][]*psbt.Packet)
for _, forfeitTx := range forfeitTxs {
ptx, err := psbt.NewFromRawBytes(strings.NewReader(forfeitTx), true)
if err != nil {
return nil, nil, err
return nil, err
}
for _, tx := range connectorTxs {
buf, _ := tx.B64Encode()
connectors = append(connectors, buf)
if len(ptx.Inputs) != 2 {
return nil, fmt.Errorf("invalid forfeit tx, expect 2 inputs, got %d", len(ptx.Inputs))
}
return connectors, forfeitTxs, nil
valid, _, err := b.verifyTapscriptPartialSigs(ptx)
if err != nil {
return nil, err
}
if !valid {
return nil, fmt.Errorf("invalid forfeit tx signature")
}
vtxoInput := ptx.UnsignedTx.TxIn[1]
vtxoKey := domain.VtxoKey{
Txid: vtxoInput.PreviousOutPoint.Hash.String(),
VOut: vtxoInput.PreviousOutPoint.Index,
}
if _, ok := forfeitTxsPtxs[vtxoKey]; !ok {
forfeitTxsPtxs[vtxoKey] = make([]*psbt.Packet, 0)
}
forfeitTxsPtxs[vtxoKey] = append(forfeitTxsPtxs[vtxoKey], ptx)
}
forfeitAddress, err := b.wallet.GetForfeitAddress(context.Background())
if err != nil {
return nil, err
}
addr, err := btcutil.DecodeAddress(forfeitAddress, nil)
if err != nil {
return nil, err
}
forfeitScript, err := txscript.PayToAddrScript(addr)
if err != nil {
return nil, err
}
minRate := b.wallet.MinRelayFeeRate(context.Background())
validForfeitTxs := make(map[domain.VtxoKey][]string)
for vtxoKey, ptxs := range forfeitTxsPtxs {
if len(ptxs) == 0 {
continue
}
var vtxo *domain.Vtxo
for _, v := range vtxos {
if v.VtxoKey == vtxoKey {
vtxo = &v
break
}
}
if vtxo == nil {
return nil, fmt.Errorf("missing vtxo %s", vtxoKey)
}
outputAmount := uint64(0)
// only take the first forfeit tx, as all forfeit must have the same output
firstForfeit := ptxs[0]
for _, output := range firstForfeit.UnsignedTx.TxOut {
outputAmount += uint64(output.Value)
}
inputAmount := vtxo.Amount + connectorAmount
feeAmount := inputAmount - outputAmount
if len(firstForfeit.Inputs[1].TaprootLeafScript) <= 0 {
return nil, fmt.Errorf("missing taproot leaf script for vtxo input, invalid forfeit tx")
}
vtxoTapscript := firstForfeit.Inputs[1].TaprootLeafScript[0]
ctrlBlock, err := txscript.ParseControlBlock(vtxoTapscript.ControlBlock)
if err != nil {
return nil, err
}
minFee, err := common.ComputeForfeitMinRelayFee(
minRate,
&waddrmgr.Tapscript{
RevealedScript: vtxoTapscript.Script,
ControlBlock: ctrlBlock,
},
64*2,
txscript.GetScriptClass(forfeitScript),
)
if err != nil {
return nil, err
}
dustAmount, err := b.wallet.GetDustAmount(context.Background())
if err != nil {
return nil, err
}
if inputAmount-feeAmount < dustAmount {
return nil, fmt.Errorf("forfeit tx output amount is dust, %d < %d", inputAmount-feeAmount, dustAmount)
}
if feeAmount < uint64(minFee) {
return nil, fmt.Errorf("forfeit tx fee is lower than the min relay fee, %d < %d", feeAmount, minFee)
}
feeThreshold := uint64(math.Ceil(float64(minFee) * 1.05))
if feeAmount > feeThreshold {
return nil, fmt.Errorf("forfeit tx fee is higher than 5%% of the min relay fee, %d > %d", feeAmount, feeThreshold)
}
vtxoChainhash, err := chainhash.NewHashFromStr(vtxoKey.Txid)
if err != nil {
return nil, err
}
vtxoInput := &wire.OutPoint{
Hash: *vtxoChainhash,
Index: vtxoKey.VOut,
}
vtxoTapKey, err := vtxo.TapKey()
if err != nil {
return nil, err
}
vtxoScript, err := common.P2TRScript(vtxoTapKey)
if err != nil {
return nil, err
}
rebuiltForfeits := make([]*psbt.Packet, 0)
for _, connector := range connectorsPtxs {
forfeits, err := bitcointree.BuildForfeitTxs(
connector,
vtxoInput,
vtxo.Amount,
connectorAmount,
feeAmount,
vtxoScript,
forfeitScript,
)
if err != nil {
return nil, err
}
rebuiltForfeits = append(rebuiltForfeits, forfeits...)
}
if len(rebuiltForfeits) != len(ptxs) {
return nil, fmt.Errorf("missing forfeits, expect %d, got %d", len(ptxs), len(rebuiltForfeits))
}
for _, forfeit := range rebuiltForfeits {
found := false
txid := forfeit.UnsignedTx.TxHash().String()
for _, ptx := range ptxs {
if txid == ptx.UnsignedTx.TxHash().String() {
found = true
break
}
}
if !found {
return nil, fmt.Errorf("missing forfeit tx %s", txid)
}
}
b64Txs := make([]string, 0, len(ptxs))
for _, forfeit := range ptxs {
b64, err := forfeit.B64Encode()
if err != nil {
return nil, err
}
b64Txs = append(b64Txs, b64)
}
validForfeitTxs[vtxoKey] = b64Txs
}
return validForfeitTxs, nil
}
func (b *txBuilder) BuildRoundTx(
@@ -266,22 +454,22 @@ func (b *txBuilder) BuildRoundTx(
boardingInputs []ports.BoardingInput,
sweptRounds []domain.Round,
cosigners ...*secp256k1.PublicKey,
) (roundTx string, congestionTree tree.CongestionTree, connectorAddress string, err error) {
) (roundTx string, congestionTree tree.CongestionTree, connectorAddress string, connectors []string, err error) {
var sharedOutputScript []byte
var sharedOutputAmount int64
if len(cosigners) == 0 {
return "", nil, "", fmt.Errorf("missing cosigners")
return "", nil, "", nil, fmt.Errorf("missing cosigners")
}
receivers, err := getOutputVtxosLeaves(payments)
if err != nil {
return "", nil, "", err
return "", nil, "", nil, err
}
feeAmount, err := b.minRelayFeeTreeTx()
if err != nil {
return "", nil, "", err
return
}
if !isOnchainOnly(payments) {
@@ -320,11 +508,43 @@ func (b *txBuilder) BuildRoundTx(
initialOutpoint, cosigners, aspPubkey, receivers, feeAmount, b.roundLifetime,
)
if err != nil {
return
return "", nil, "", nil, err
}
}
if countSpentVtxos(payments) <= 0 {
return
}
connectorAddr, err := btcutil.DecodeAddress(connectorAddress, b.onchainNetwork())
if err != nil {
return "", nil, "", nil, err
}
connectorPkScript, err := txscript.PayToAddrScript(connectorAddr)
if err != nil {
return "", nil, "", nil, err
}
minRelayFeeConnectorTx, err := b.minRelayFeeConnectorTx()
if err != nil {
return "", nil, "", nil, err
}
connectorsPsbts, err := b.createConnectors(roundTx, payments, connectorPkScript, minRelayFeeConnectorTx)
if err != nil {
return "", nil, "", nil, err
}
for _, ptx := range connectorsPsbts {
b64, err := ptx.B64Encode()
if err != nil {
return "", nil, "", nil, err
}
connectors = append(connectors, b64)
}
return roundTx, congestionTree, connectorAddress, connectors, nil
}
func (b *txBuilder) GetSweepInput(node tree.Node) (lifetime int64, sweepInput ports.SweepInput, err error) {
@@ -909,7 +1129,7 @@ func (b *txBuilder) VerifyAndCombinePartialTx(dest string, src string) (string,
}
partialSig := sourceInput.TaprootScriptSpendSig[0]
preimage, err := b.getTaprootPreimage(src, i, sourceInput.TaprootLeafScript[0].Script)
preimage, err := b.getTaprootPreimage(sourceTx, i, sourceInput.TaprootLeafScript[0].Script)
if err != nil {
return "", err
}
@@ -964,7 +1184,6 @@ func (b *txBuilder) createConnectors(
Hash: partialTx.UnsignedTx.TxHash(),
Index: 1,
}
if numberOfConnectors == 1 {
outputs := []*wire.TxOut{connectorOutput}
connectorTx, err := craftConnectorTx(previousInput, connectorScript, outputs, feeAmount)
@@ -1011,114 +1230,6 @@ func (b *txBuilder) minRelayFeeTreeTx() (uint64, error) {
return b.wallet.MinRelayFee(context.Background(), uint64(common.TreeTxSize))
}
func (b *txBuilder) createForfeitTxs(
payments []domain.Payment,
descriptors map[domain.VtxoKey]string,
connectors []*psbt.Packet,
minRelayFeeRate chainfee.SatPerKVByte,
) ([]string, error) {
forfeitAddress, err := b.wallet.GetForfeitAddress(context.Background())
if err != nil {
return nil, err
}
parsedAddr, err := btcutil.DecodeAddress(forfeitAddress, b.onchainNetwork())
if err != nil {
return nil, err
}
pkScript, err := txscript.PayToAddrScript(parsedAddr)
if err != nil {
return nil, err
}
scriptParsed, err := txscript.ParsePkScript(pkScript)
if err != nil {
return nil, err
}
forfeitTxs := make([]string, 0)
for _, payment := range payments {
for _, vtxo := range payment.Inputs {
desc, ok := descriptors[vtxo.VtxoKey]
if !ok {
return nil, err
}
offchainscript, err := bitcointree.ParseVtxoScript(desc)
if err != nil {
return nil, err
}
vtxoTaprootKey, tapTree, err := offchainscript.TapTree()
if err != nil {
return nil, err
}
connectorAmount, err := b.wallet.GetDustAmount(context.Background())
if err != nil {
return nil, err
}
vtxoScript, err := common.P2TRScript(vtxoTaprootKey)
if err != nil {
return nil, err
}
feeAmount, err := common.ComputeForfeitMinRelayFee(minRelayFeeRate, tapTree, scriptParsed.Class())
if err != nil {
return nil, err
}
vtxoTxHash, err := chainhash.NewHashFromStr(vtxo.Txid)
if err != nil {
return nil, err
}
for _, connector := range connectors {
txs, err := bitcointree.BuildForfeitTxs(
connector,
&wire.OutPoint{
Hash: *vtxoTxHash,
Index: vtxo.VOut,
},
vtxo.Amount,
connectorAmount,
feeAmount,
vtxoScript,
pkScript,
)
if err != nil {
return nil, err
}
for _, tx := range txs {
b64, err := tx.B64Encode()
if err != nil {
return nil, err
}
forfeitTxs = append(forfeitTxs, b64)
}
}
}
}
return forfeitTxs, nil
}
func (b *txBuilder) getConnectorPkScript(poolTx string) ([]byte, error) {
partialTx, err := psbt.NewFromRawBytes(strings.NewReader(poolTx), true)
if err != nil {
return nil, err
}
if len(partialTx.Outputs) < 1 {
return nil, fmt.Errorf("connector output not found in pool tx")
}
return partialTx.UnsignedTx.TxOut[1].PkScript, nil
}
func (b *txBuilder) selectUtxos(ctx context.Context, sweptRounds []domain.Round, amount uint64) ([]ports.TxInput, uint64, error) {
selectedConnectorsUtxos := make([]ports.TxInput, 0)
selectedConnectorsAmount := uint64(0)
@@ -1160,12 +1271,7 @@ func (b *txBuilder) selectUtxos(ctx context.Context, sweptRounds []domain.Round,
return append(selectedConnectorsUtxos, utxos...), change, nil
}
func (b *txBuilder) getTaprootPreimage(tx string, inputIndex int, leafScript []byte) ([]byte, error) {
partial, err := psbt.NewFromRawBytes(strings.NewReader(tx), true)
if err != nil {
return nil, err
}
func (b *txBuilder) getTaprootPreimage(partial *psbt.Packet, inputIndex int, leafScript []byte) ([]byte, error) {
prevouts := make(map[wire.OutPoint]*wire.TxOut)
for i, input := range partial.Inputs {

View File

@@ -5,7 +5,6 @@ import (
"encoding/hex"
"encoding/json"
"os"
"strings"
"testing"
"github.com/ark-network/ark/common"
@@ -13,7 +12,6 @@ import (
"github.com/ark-network/ark/server/internal/core/domain"
"github.com/ark-network/ark/server/internal/core/ports"
txbuilder "github.com/ark-network/ark/server/internal/infrastructure/tx-builder/covenantless"
"github.com/btcsuite/btcd/btcutil/psbt"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
@@ -77,7 +75,7 @@ func TestBuildPoolTx(t *testing.T) {
cosigners = append(cosigners, randKey.PubKey())
}
poolTx, congestionTree, connAddr, err := builder.BuildRoundTx(
poolTx, congestionTree, connAddr, _, err := builder.BuildRoundTx(
pubkey, f.Payments, []ports.BoardingInput{}, []domain.Round{}, cosigners...,
)
require.NoError(t, err)
@@ -98,7 +96,7 @@ func TestBuildPoolTx(t *testing.T) {
if len(fixtures.Invalid) > 0 {
t.Run("invalid", func(t *testing.T) {
for _, f := range fixtures.Invalid {
poolTx, congestionTree, connAddr, err := builder.BuildRoundTx(
poolTx, congestionTree, connAddr, _, err := builder.BuildRoundTx(
pubkey, f.Payments, []ports.BoardingInput{}, []domain.Round{},
)
require.EqualError(t, err, f.ExpectedErr)
@@ -110,67 +108,6 @@ func TestBuildPoolTx(t *testing.T) {
}
}
func TestBuildForfeitTxs(t *testing.T) {
builder := txbuilder.NewTxBuilder(
wallet, common.Bitcoin, 1209344, boardingExitDelay,
)
fixtures, err := parseForfeitTxsFixtures()
require.NoError(t, err)
require.NotEmpty(t, fixtures)
if len(fixtures.Valid) > 0 {
t.Run("valid", func(t *testing.T) {
for _, f := range fixtures.Valid {
connectors, forfeitTxs, err := builder.BuildForfeitTxs(
f.PoolTx, f.Payments, f.Descriptors, minRelayFeeRate,
)
require.NoError(t, err)
require.Len(t, connectors, f.ExpectedNumOfConnectors)
require.Len(t, forfeitTxs, f.ExpectedNumOfForfeitTxs)
expectedInputTxid := f.PoolTxid
// Verify the chain of connectors
for _, connector := range connectors {
tx, err := psbt.NewFromRawBytes(strings.NewReader(connector), true)
require.NoError(t, err)
require.NotNil(t, tx)
require.Len(t, tx.Inputs, 1)
require.Len(t, tx.Outputs, 2)
inputTxid := tx.UnsignedTx.TxIn[0].PreviousOutPoint.Hash.String()
require.Equal(t, expectedInputTxid, inputTxid)
require.Equal(t, 1, int(tx.UnsignedTx.TxIn[0].PreviousOutPoint.Index))
expectedInputTxid = tx.UnsignedTx.TxHash().String()
}
// decode and check forfeit txs
for _, forfeitTx := range forfeitTxs {
tx, err := psbt.NewFromRawBytes(strings.NewReader(forfeitTx), true)
require.NoError(t, err)
require.Len(t, tx.Inputs, 2)
require.Len(t, tx.Outputs, 1)
}
}
})
}
if len(fixtures.Invalid) > 0 {
t.Run("invalid", func(t *testing.T) {
for _, f := range fixtures.Invalid {
connectors, forfeitTxs, err := builder.BuildForfeitTxs(
f.PoolTx, f.Payments, f.Descriptors, minRelayFeeRate,
)
require.EqualError(t, err, f.ExpectedErr)
require.Empty(t, connectors)
require.Empty(t, forfeitTxs)
}
})
}
}
func randomInput() []ports.TxInput {
txid := randomHex(32)
input := &mockedInput{}
@@ -221,76 +158,3 @@ func parsePoolTxFixtures() (*poolTxFixtures, error) {
return &fixtures, nil
}
type forfeitTxsFixtures struct {
Valid []struct {
Payments []domain.Payment
Descriptors map[domain.VtxoKey]string
ExpectedNumOfConnectors int
ExpectedNumOfForfeitTxs int
PoolTx string
PoolTxid string
}
Invalid []struct {
Payments []domain.Payment
Descriptors map[domain.VtxoKey]string
ExpectedErr string
PoolTx string
}
}
func parseForfeitTxsFixtures() (*forfeitTxsFixtures, error) {
file, err := os.ReadFile("testdata/fixtures.json")
if err != nil {
return nil, err
}
v := map[string]interface{}{}
if err := json.Unmarshal(file, &v); err != nil {
return nil, err
}
vv := v["buildForfeitTxs"].(map[string]interface{})
file, _ = json.Marshal(vv)
var fixtures forfeitTxsFixtures
if err := json.Unmarshal(file, &fixtures); err != nil {
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
}