diff --git a/common/fees.go b/common/fees.go index 0b4934c37..cb530b0ca 100644 --- a/common/fees.go +++ b/common/fees.go @@ -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 { diff --git a/pkg/client-sdk/covenant_client.go b/pkg/client-sdk/covenant_client.go index 744538db3..58f25fabe 100644 --- a/pkg/client-sdk/covenant_client.go +++ b/pkg/client-sdk/covenant_client.go @@ -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, diff --git a/pkg/client-sdk/covenantless_client.go b/pkg/client-sdk/covenantless_client.go index b41518b6f..e1b11931f 100644 --- a/pkg/client-sdk/covenantless_client.go +++ b/pkg/client-sdk/covenantless_client.go @@ -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, diff --git a/pkg/client-sdk/go.mod b/pkg/client-sdk/go.mod index 0fb88c153..3c0c5300d 100644 --- a/pkg/client-sdk/go.mod +++ b/pkg/client-sdk/go.mod @@ -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 diff --git a/pkg/client-sdk/go.sum b/pkg/client-sdk/go.sum index 1b70b4c10..ce2cfcf9f 100644 --- a/pkg/client-sdk/go.sum +++ b/pkg/client-sdk/go.sum @@ -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= diff --git a/server/internal/core/application/covenant.go b/server/internal/core/application/covenant.go index 3ea30f6b2..a1c6adea9 100644 --- a/server/internal/core/application/covenant.go +++ b/server/internal/core/application/covenant.go @@ -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 diff --git a/server/internal/core/application/covenantless.go b/server/internal/core/application/covenantless.go index 3c39103fb..f7e34db65 100644 --- a/server/internal/core/application/covenantless.go +++ b/server/internal/core/application/covenantless.go @@ -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 diff --git a/server/internal/core/application/utils.go b/server/internal/core/application/utils.go index 1f1f3b0ec..78f16b7e2 100644 --- a/server/internal/core/application/utils.go +++ b/server/internal/core/application/utils.go @@ -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 + lock *sync.RWMutex + 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.forfeitTxs[txid] = &signedTx{tx, false} + m.vtxos = vtxosToSign + m.connectors = connectors + for _, vtxo := range vtxosToSign { + m.forfeitTxs[vtxo.VtxoKey] = make([]string, 0) } - - return nil } func (m *forfeitTxsMap) sign(txs []string) error { + if len(txs) == 0 { + return nil + } + + 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 + } + m.lock.Lock() defer m.lock.Unlock() - for _, tx := range txs { - valid, txid, err := m.builder.VerifyTapscriptPartialSigs(tx) - 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) - } - } + 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 diff --git a/server/internal/core/domain/payment.go b/server/internal/core/domain/payment.go index 9211aeb7b..6ca241f43 100644 --- a/server/internal/core/domain/payment.go +++ b/server/internal/core/domain/payment.go @@ -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) +} diff --git a/server/internal/core/ports/tx_builder.go b/server/internal/core/ports/tx_builder.go index d4fff63eb..9ce9049d6 100644 --- a/server/internal/core/ports/tx_builder.go +++ b/server/internal/core/ports/tx_builder.go @@ -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) diff --git a/server/internal/infrastructure/tx-builder/covenant/builder.go b/server/internal/infrastructure/tx-builder/covenant/builder.go index 3fa11c371..c159e5e32 100644 --- a/server/internal/infrastructure/tx-builder/covenant/builder.go +++ b/server/internal/infrastructure/tx-builder/covenant/builder.go @@ -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) - if err != nil { - return nil, nil, err - } +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 - connectorFeeAmount, err := b.minRelayFeeConnectorTx() - if err != nil { - return nil, nil, err + for i, connector := range connectors { + pset, err := psetv2.NewPsetFromBase64(connector) + if err != nil { + return 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 + } + } + + if lastOutput == nil { + return nil, fmt.Errorf("invalid connector tx") + } + + connectorAmount = uint64(lastOutput.Value) + } + + connectorsPsets = append(connectorsPsets, pset) } - connectorAmount, err := b.wallet.GetDustAmount(context.Background()) - if err != nil { - return nil, nil, err + // 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 + } + + 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) } - connectorTxs, err := b.createConnectors(poolTx, payments, connectorAddress, connectorAmount, connectorFeeAmount) + forfeitAddress, err := b.wallet.GetForfeitAddress(context.Background()) if err != nil { - return nil, nil, err + return nil, err } - forfeitTxs, err = b.createForfeitTxs(payments, descriptors, connectorTxs, connectorAmount, minRelayFeeRate) + forfeitScript, err := address.ToOutputScript(forfeitAddress) if err != nil { - return nil, nil, err + return nil, err } - for _, tx := range connectorTxs { - buf, _ := tx.ToBase64() - connectors = append(connectors, buf) + 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 connectors, forfeitTxs, nil + + 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) diff --git a/server/internal/infrastructure/tx-builder/covenant/builder_test.go b/server/internal/infrastructure/tx-builder/covenant/builder_test.go index 125439bb2..b6937566c 100644 --- a/server/internal/infrastructure/tx-builder/covenant/builder_test.go +++ b/server/internal/infrastructure/tx-builder/covenant/builder_test.go @@ -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() -} diff --git a/server/internal/infrastructure/tx-builder/covenantless/builder.go b/server/internal/infrastructure/tx-builder/covenantless/builder.go index 4536579cd..1b445778e 100644 --- a/server/internal/infrastructure/tx-builder/covenantless/builder.go +++ b/server/internal/infrastructure/tx-builder/covenantless/builder.go @@ -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) - if err != nil { - return nil, nil, err +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, err + } + + if i == len(connectors)-1 { + lastOutput := ptx.UnsignedTx.TxOut[len(ptx.UnsignedTx.TxOut)-1] + connectorAmount = uint64(lastOutput.Value) + } + + connectorsPtxs = append(connectorsPtxs, ptx) } - minRelayFeeConnectorTx, err := b.minRelayFeeConnectorTx() + // 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, err + } + + if len(ptx.Inputs) != 2 { + return nil, fmt.Errorf("invalid forfeit tx, expect 2 inputs, got %d", len(ptx.Inputs)) + } + + 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, nil, err + return nil, err } - connectorTxs, err := b.createConnectors(poolTx, payments, connectorPkScript, minRelayFeeConnectorTx) + addr, err := btcutil.DecodeAddress(forfeitAddress, nil) if err != nil { - return nil, nil, err + return nil, err } - forfeitTxs, err = b.createForfeitTxs(payments, descriptors, connectorTxs, minRelayFeeRate) + forfeitScript, err := txscript.PayToAddrScript(addr) if err != nil { - return nil, nil, err + return nil, err } - for _, tx := range connectorTxs { - buf, _ := tx.B64Encode() - connectors = append(connectors, buf) + 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 connectors, forfeitTxs, nil + + 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 + 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 { diff --git a/server/internal/infrastructure/tx-builder/covenantless/builder_test.go b/server/internal/infrastructure/tx-builder/covenantless/builder_test.go index da312d608..6121064ec 100644 --- a/server/internal/infrastructure/tx-builder/covenantless/builder_test.go +++ b/server/internal/infrastructure/tx-builder/covenantless/builder_test.go @@ -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 -}