[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( func ComputeForfeitMinRelayFee(
feeRate chainfee.SatPerKVByte, feeRate chainfee.SatPerKVByte,
vtxoScriptTapTree TaprootTree, tapscript *waddrmgr.Tapscript,
witnessSize int,
aspScriptClass txscript.ScriptClass, aspScriptClass txscript.ScriptClass,
) (uint64, error) { ) (uint64, error) {
txWeightEstimator := &input.TxWeightEstimator{} 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.AddP2PKHInput() // connector input
txWeightEstimator.AddTapscriptInput( txWeightEstimator.AddTapscriptInput(
64*2, // forfeit witness = 2 signatures lntypes.WeightUnit(witnessSize),
&waddrmgr.Tapscript{ tapscript,
RevealedScript: biggestVtxoLeafProof.Script,
ControlBlock: ctrlBlock,
},
) )
switch aspScriptClass { switch aspScriptClass {

View File

@@ -21,6 +21,7 @@ import (
"github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcwallet/waddrmgr"
"github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/lightningnetwork/lnd/lnwallet/chainfee" "github.com/lightningnetwork/lnd/lnwallet/chainfee"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
@@ -1461,11 +1462,6 @@ func (a *covenantArkClient) createAndSignForfeits(
return nil, err return nil, err
} }
feeAmount, err := common.ComputeForfeitMinRelayFee(feeRate, vtxoTapTree, txscript.WitnessV0PubKeyHashTy)
if err != nil {
return nil, err
}
vtxoOutputScript, err := common.P2TRScript(vtxoTapKey) vtxoOutputScript, err := common.P2TRScript(vtxoTapKey)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -1477,6 +1473,7 @@ func (a *covenantArkClient) createAndSignForfeits(
} }
var forfeitClosure tree.Closure var forfeitClosure tree.Closure
var witnessSize int
switch s := vtxoScript.(type) { switch s := vtxoScript.(type) {
case *tree.DefaultVtxoScript: case *tree.DefaultVtxoScript:
@@ -1484,6 +1481,7 @@ func (a *covenantArkClient) createAndSignForfeits(
Pubkey: s.Owner, Pubkey: s.Owner,
AspPubkey: a.AspPubkey, AspPubkey: a.AspPubkey,
} }
witnessSize = 64 * 2
default: default:
return nil, fmt.Errorf("unsupported vtxo script: %T", s) return nil, fmt.Errorf("unsupported vtxo script: %T", s)
} }
@@ -1508,6 +1506,19 @@ func (a *covenantArkClient) createAndSignForfeits(
ControlBlock: *ctrlBlock, 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 { for _, connectorPset := range connectorsPsets {
forfeits, err := tree.BuildForfeitTxs( forfeits, err := tree.BuildForfeitTxs(
connectorPset, vtxoInput, vtxo.Amount, a.Dust, feeAmount, vtxoOutputScript, forfeitPkScript, 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/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcwallet/waddrmgr"
"github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/lightningnetwork/lnd/lnwallet/chainfee" "github.com/lightningnetwork/lnd/lnwallet/chainfee"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
@@ -2111,11 +2112,6 @@ func (a *covenantlessArkClient) createAndSignForfeits(
return nil, err return nil, err
} }
feeAmount, err := common.ComputeForfeitMinRelayFee(feeRate, vtxoTapTree, parsedScript.Class())
if err != nil {
return nil, err
}
vtxoOutputScript, err := common.P2TRScript(vtxoTapKey) vtxoOutputScript, err := common.P2TRScript(vtxoTapKey)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -2132,6 +2128,7 @@ func (a *covenantlessArkClient) createAndSignForfeits(
} }
var forfeitClosure bitcointree.Closure var forfeitClosure bitcointree.Closure
var witnessSize int
switch v := vtxoScript.(type) { switch v := vtxoScript.(type) {
case *bitcointree.DefaultVtxoScript: case *bitcointree.DefaultVtxoScript:
@@ -2139,6 +2136,7 @@ func (a *covenantlessArkClient) createAndSignForfeits(
Pubkey: v.Owner, Pubkey: v.Owner,
AspPubkey: a.AspPubkey, AspPubkey: a.AspPubkey,
} }
witnessSize = 64 * 2
default: default:
return nil, fmt.Errorf("unsupported vtxo script: %T", vtxoScript) return nil, fmt.Errorf("unsupported vtxo script: %T", vtxoScript)
} }
@@ -2159,6 +2157,24 @@ func (a *covenantlessArkClient) createAndSignForfeits(
LeafVersion: txscript.BaseLeafVersion, 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 { for _, connectorPset := range connectorsPsets {
forfeits, err := bitcointree.BuildForfeitTxs( forfeits, err := bitcointree.BuildForfeitTxs(
connectorPset, vtxoInput, vtxo.Amount, a.Dust, feeAmount, vtxoOutputScript, forfeitPkScript, 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 v1.1.5
github.com/btcsuite/btcd/btcutil/psbt v1.1.9 github.com/btcsuite/btcd/btcutil/psbt v1.1.9
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 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/decred/dcrd/dcrec/secp256k1/v4 v4.3.0
github.com/dgraph-io/badger/v4 v4.3.0 github.com/dgraph-io/badger/v4 v4.3.0
github.com/go-openapi/errors v0.22.0 github.com/go-openapi/errors v0.22.0
@@ -30,6 +31,7 @@ require (
github.com/aead/siphash v1.0.1 // indirect github.com/aead/siphash v1.0.1 // indirect
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f // 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/go-socks v0.0.0-20170105172521-4720035b7bfd // indirect
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 // indirect github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 // indirect
github.com/btcsuite/winsvc v1.0.0 // indirect github.com/btcsuite/winsvc v1.0.0 // indirect
@@ -59,7 +61,9 @@ require (
github.com/jrick/logrotate v1.0.0 // indirect github.com/jrick/logrotate v1.0.0 // indirect
github.com/kkdai/bstream v1.0.0 // indirect github.com/kkdai/bstream v1.0.0 // indirect
github.com/klauspost/compress v1.17.9 // 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/fn v1.2.1 // indirect
github.com/lightningnetwork/lnd/tlv v1.2.6 // indirect
github.com/mailru/easyjson v0.7.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/oklog/ulid v1.3.1 // indirect github.com/oklog/ulid v1.3.1 // indirect
@@ -71,6 +75,7 @@ require (
github.com/stretchr/objx v0.5.2 // indirect github.com/stretchr/objx v0.5.2 // indirect
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect
github.com/vulpemventures/fastsha256 v0.0.0-20160815193821-637e65642941 // 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.mongodb.org/mongo-driver v1.14.0 // indirect
go.opencensus.io v0.24.0 // indirect go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/otel v1.30.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 h1:bAs4lUbRJpnnkd9VhRV3jjAVU7DJVjMaK+IsvSeZvFo=
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= 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/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 h1:R/opQEbFEy9JGkIguV40SvRY1uliPX8ifOvi6ICsFCw=
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= 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= 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.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 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 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 h1:Qv4xQ2ka05vqzmdkFdISHCHP6CzHoYNVKfD18XPjHsM=
github.com/lightningnetwork/lnd v0.18.2-beta/go.mod h1:cGQR1cVEZFZQcCx2VBbDY8xwGjCz+SupSopU1HpjP2I= 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 h1:pPsVGrwi9QBwdLJzaEGK33wmiVKOxs/zc8H7+MamFf0=
github.com/lightningnetwork/lnd/fn v1.2.1/go.mod h1:SyFohpVrARPKH3XVAJZlXdVe+IwMYc4OMAvrDY32kw0= 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/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 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 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.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/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= 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 h1:P98w8egYRjYe3XDjxhYJagTokP/H6HzlsnojRgZRd80=
go.mongodb.org/mongo-driver v1.14.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c= 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= 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) { func (s *covenantService) SpendVtxos(ctx context.Context, inputs []ports.Input) (string, error) {
vtxosInputs := make([]domain.Vtxo, 0) vtxosInputs := make([]domain.Vtxo, 0)
boardingInputs := make([]ports.BoardingInput, 0) boardingInputs := make([]ports.BoardingInput, 0)
descriptors := make(map[domain.VtxoKey]string)
now := time.Now().Unix() 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) 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) vtxosInputs = append(vtxosInputs, vtxo)
descriptors[vtxo.VtxoKey] = input.Descriptor
} }
payment, err := domain.NewPayment(vtxosInputs) payment, err := domain.NewPayment(vtxosInputs)
if err != nil { if err != nil {
return "", err return "", err
} }
if err := s.paymentRequests.push(*payment, boardingInputs, descriptors); err != nil { if err := s.paymentRequests.push(*payment, boardingInputs); err != nil {
return "", err return "", err
} }
return payment.Id, nil return payment.Id, nil
@@ -519,7 +536,7 @@ func (s *covenantService) startFinalization() {
if num > paymentsThreshold { if num > paymentsThreshold {
num = paymentsThreshold num = paymentsThreshold
} }
payments, boardingInputs, descriptors, _, _ := s.paymentRequests.pop(num) payments, boardingInputs, _, _ := s.paymentRequests.pop(num)
if _, err := round.RegisterPayments(payments); err != nil { if _, err := round.RegisterPayments(payments); err != nil {
round.Fail(fmt.Errorf("failed to register payments: %s", err)) round.Fail(fmt.Errorf("failed to register payments: %s", err))
log.WithError(err).Warn("failed to register payments") log.WithError(err).Warn("failed to register payments")
@@ -533,7 +550,7 @@ func (s *covenantService) startFinalization() {
return 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 { if err != nil {
round.Fail(fmt.Errorf("failed to create pool tx: %s", err)) round.Fail(fmt.Errorf("failed to create pool tx: %s", err))
log.WithError(err).Warn("failed to create pool tx") 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) log.Debugf("pool tx created for round %s", round.Id)
needForfeits := false s.forfeitTxs.init(connectors, payments)
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
}
}
if _, err := round.StartFinalization( if _, err := round.StartFinalization(
connectorAddress, connectors, tree, unsignedPoolTx, connectorAddress, connectors, tree, unsignedPoolTx,
@@ -598,9 +588,8 @@ func (s *covenantService) finalizeRound() {
} }
}() }()
forfeitTxs, leftUnsigned := s.forfeitTxs.pop() forfeitTxs, err := s.forfeitTxs.pop()
if len(leftUnsigned) > 0 { if err != nil {
err := fmt.Errorf("%d forfeit txs left to sign", len(leftUnsigned))
changes = round.Fail(fmt.Errorf("failed to finalize round: %s", err)) changes = round.Fail(fmt.Errorf("failed to finalize round: %s", err))
log.WithError(err).Warn("failed to finalize round") log.WithError(err).Warn("failed to finalize round")
return return

View File

@@ -463,7 +463,7 @@ func (s *covenantlessService) SpendVtxos(ctx context.Context, inputs []ports.Inp
now := time.Now().Unix() now := time.Now().Unix()
boardingTxs := make(map[string]wire.MsgTx, 0) // txid -> txhex boardingTxs := make(map[string]wire.MsgTx, 0) // txid -> txhex
descriptors := make(map[domain.VtxoKey]string)
for _, input := range inputs { for _, input := range inputs {
vtxosResult, err := s.repoManager.Vtxos().GetVtxos(ctx, []domain.VtxoKey{input.VtxoKey}) vtxosResult, err := s.repoManager.Vtxos().GetVtxos(ctx, []domain.VtxoKey{input.VtxoKey})
if err != nil || len(vtxosResult) == 0 { 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) 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) vtxosInputs = append(vtxosInputs, vtxo)
} }
@@ -529,7 +546,7 @@ func (s *covenantlessService) SpendVtxos(ctx context.Context, inputs []ports.Inp
if err != nil { if err != nil {
return "", err return "", err
} }
if err := s.paymentRequests.push(*payment, boardingInputs, descriptors); err != nil { if err := s.paymentRequests.push(*payment, boardingInputs); err != nil {
return "", err return "", err
} }
return payment.Id, nil return payment.Id, nil
@@ -872,7 +889,7 @@ func (s *covenantlessService) startFinalization() {
if num > paymentsThreshold { if num > paymentsThreshold {
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) { if len(payments) > len(cosigners) {
err := fmt.Errorf("missing ephemeral key for payments") err := fmt.Errorf("missing ephemeral key for payments")
round.Fail(fmt.Errorf("round aborted: %s", err)) round.Fail(fmt.Errorf("round aborted: %s", err))
@@ -904,13 +921,21 @@ func (s *covenantlessService) startFinalization() {
cosigners = append(cosigners, ephemeralKey.PubKey()) 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 { if err != nil {
round.Fail(fmt.Errorf("failed to create pool tx: %s", err)) round.Fail(fmt.Errorf("failed to create round tx: %s", err))
log.WithError(err).Warn("failed to create pool tx") log.WithError(err).Warn("failed to create round tx")
return 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 { if len(tree) > 0 {
log.Debugf("signing congestion tree for round %s", round.Id) log.Debugf("signing congestion tree for round %s", round.Id)
@@ -1063,34 +1088,6 @@ func (s *covenantlessService) startFinalization() {
tree = signedTree 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( if _, err := round.StartFinalization(
connectorAddress, connectors, tree, unsignedRoundTx, connectorAddress, connectors, tree, unsignedRoundTx,
); err != nil { ); err != nil {
@@ -1143,9 +1140,8 @@ func (s *covenantlessService) finalizeRound(notes []note.Note) {
} }
}() }()
forfeitTxs, leftUnsigned := s.forfeitTxs.pop() forfeitTxs, err := s.forfeitTxs.pop()
if len(leftUnsigned) > 0 { if err != nil {
err := fmt.Errorf("%d forfeit txs left to sign", len(leftUnsigned))
changes = round.Fail(fmt.Errorf("failed to finalize round: %s", err)) changes = round.Fail(fmt.Errorf("failed to finalize round: %s", err))
log.WithError(err).Warn("failed to finalize round") log.WithError(err).Warn("failed to finalize round")
return return

View File

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

View File

@@ -6,6 +6,8 @@ import (
"fmt" "fmt"
"hash" "hash"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/google/uuid" "github.com/google/uuid"
) )
@@ -125,3 +127,11 @@ type Vtxo struct {
RedeemTx string // empty if in-round vtxo RedeemTx string // empty if in-round vtxo
CreatedAt int64 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/ark-network/ark/server/internal/core/domain"
"github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
) )
type SweepInput interface { type SweepInput interface {
@@ -28,16 +27,25 @@ type BoardingInput struct {
} }
type TxBuilder interface { 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( BuildRoundTx(
aspPubkey *secp256k1.PublicKey, payments []domain.Payment, boardingInputs []BoardingInput, sweptRounds []domain.Round, aspPubkey *secp256k1.PublicKey, payments []domain.Payment, boardingInputs []BoardingInput, sweptRounds []domain.Round,
cosigners ...*secp256k1.PublicKey, cosigners ...*secp256k1.PublicKey,
) (roundTx string, congestionTree tree.CongestionTree, connectorAddress string, err error) ) (
BuildForfeitTxs(
roundTx string, roundTx string,
payments []domain.Payment, congestionTree tree.CongestionTree,
descriptors map[domain.VtxoKey]string, connectorAddress string,
minRelayFeeRate chainfee.SatPerKVByte, connectors []string,
) (connectors []string, forfeitTxs []string, err error) 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) BuildSweepTx(inputs []SweepInput) (signedSweepTx string, err error)
GetSweepInput(node tree.Node) (lifetime int64, sweepInput SweepInput, err error) GetSweepInput(node tree.Node) (lifetime int64, sweepInput SweepInput, err error)
FinalizeAndExtract(tx string) (txhex string, err error) FinalizeAndExtract(tx string) (txhex string, err error)

View File

@@ -4,6 +4,7 @@ import (
"bytes" "bytes"
"context" "context"
"fmt" "fmt"
"math"
"github.com/ark-network/ark/common" "github.com/ark-network/ark/common"
"github.com/ark-network/ark/common/tree" "github.com/ark-network/ark/common/tree"
@@ -12,12 +13,11 @@ import (
"github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcwallet/waddrmgr"
"github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
"github.com/vulpemventures/go-elements/address" "github.com/vulpemventures/go-elements/address"
"github.com/vulpemventures/go-elements/elementsutil" "github.com/vulpemventures/go-elements/elementsutil"
"github.com/vulpemventures/go-elements/network" "github.com/vulpemventures/go-elements/network"
"github.com/vulpemventures/go-elements/payment"
"github.com/vulpemventures/go-elements/psetv2" "github.com/vulpemventures/go-elements/psetv2"
"github.com/vulpemventures/go-elements/taproot" "github.com/vulpemventures/go-elements/taproot"
"github.com/vulpemventures/go-elements/transaction" "github.com/vulpemventures/go-elements/transaction"
@@ -81,42 +81,235 @@ func (b *txBuilder) BuildSweepTx(inputs []ports.SweepInput) (signedSweepTx strin
return extractedTx.ToHex() return extractedTx.ToHex()
} }
func (b *txBuilder) BuildForfeitTxs( func (b *txBuilder) VerifyForfeitTxs(vtxos []domain.Vtxo, connectors []string, forfeitTxs []string) (map[domain.VtxoKey][]string, error) {
poolTx string, connectorsPsets := make([]*psetv2.Pset, 0, len(connectors))
payments []domain.Payment, var connectorAmount uint64
descriptors map[domain.VtxoKey]string,
minRelayFeeRate chainfee.SatPerKVByte, for i, connector := range connectors {
) (connectors []string, forfeitTxs []string, err error) { pset, err := psetv2.NewPsetFromBase64(connector)
connectorAddress, err := b.getConnectorAddress(poolTx)
if err != nil { if err != nil {
return nil, nil, err return nil, err
} }
connectorFeeAmount, err := b.minRelayFeeConnectorTx() if i == len(connectors)-1 {
if err != nil { var lastOutput *psetv2.Output
return nil, nil, err 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 lastOutput == nil {
if err != nil { return nil, fmt.Errorf("invalid connector tx")
return nil, nil, err
} }
connectorTxs, err := b.createConnectors(poolTx, payments, connectorAddress, connectorAmount, connectorFeeAmount) connectorAmount = uint64(lastOutput.Value)
if err != nil {
return nil, nil, err
} }
forfeitTxs, err = b.createForfeitTxs(payments, descriptors, connectorTxs, connectorAmount, minRelayFeeRate) connectorsPsets = append(connectorsPsets, pset)
if err != nil {
return nil, nil, err
} }
for _, tx := range connectorTxs { // decode forfeit txs, map by vtxo key
buf, _ := tx.ToBase64() forfeitTxsPsets := make(map[domain.VtxoKey][]*psetv2.Pset)
connectors = append(connectors, buf) 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( func (b *txBuilder) BuildRoundTx(
@@ -125,7 +318,7 @@ func (b *txBuilder) BuildRoundTx(
boardingInputs []ports.BoardingInput, boardingInputs []ports.BoardingInput,
sweptRounds []domain.Round, sweptRounds []domain.Round,
_ ...*secp256k1.PublicKey, // cosigners are not used in the covenant _ ...*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: // The creation of the tree and the pool tx are tightly coupled:
// - building the tree requires knowing the shared outpoint (txid:vout) // - building the tree requires knowing the shared outpoint (txid:vout)
// - building the pool tx requires knowing the shared output script and amount // - building the pool tx requires knowing the shared output script and amount
@@ -145,19 +338,19 @@ func (b *txBuilder) BuildRoundTx(
if !isOnchainOnly(payments) { if !isOnchainOnly(payments) {
feeSatsPerNode, err := b.wallet.MinRelayFee(context.Background(), uint64(common.CovenantTreeTxSize)) feeSatsPerNode, err := b.wallet.MinRelayFee(context.Background(), uint64(common.CovenantTreeTxSize))
if err != nil { if err != nil {
return "", nil, "", err return "", nil, "", nil, err
} }
vtxosLeaves, err := getOutputVtxosLeaves(payments) vtxosLeaves, err := getOutputVtxosLeaves(payments)
if err != nil { if err != nil {
return "", nil, "", err return "", nil, "", nil, err
} }
treeFactoryFn, sharedOutputScript, sharedOutputAmount, err = tree.CraftCongestionTree( treeFactoryFn, sharedOutputScript, sharedOutputAmount, err = tree.CraftCongestionTree(
b.onchainNetwork().AssetID, aspPubkey, vtxosLeaves, feeSatsPerNode, b.roundLifetime, b.onchainNetwork().AssetID, aspPubkey, vtxosLeaves, feeSatsPerNode, b.roundLifetime,
) )
if err != nil { 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 { if err != nil {
return 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 return
} }
@@ -242,13 +462,20 @@ func (b *txBuilder) GetSweepInput(node tree.Node) (lifetime int64, sweepInput po
return lifetime, sweepInput, nil return lifetime, sweepInput, nil
} }
func (b *txBuilder) VerifyTapscriptPartialSigs(tx string) (bool, string, error) { func (b *txBuilder) VerifyTapscriptPartialSigs(tx string) (bool, string, error) {
ptx, _ := psetv2.NewPsetFromBase64(tx) pset, err := psetv2.NewPsetFromBase64(tx)
utx, _ := ptx.UnsignedTx() 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() txid := utx.TxHash().String()
for index, input := range ptx.Inputs { for index, input := range pset.Inputs {
if len(input.TapLeafScript) == 0 { if len(input.TapLeafScript) == 0 {
continue continue
} }
@@ -274,7 +501,7 @@ func (b *txBuilder) VerifyTapscriptPartialSigs(tx string) (bool, string, error)
leafHash := taproot.NewBaseTapElementsLeaf(tapLeaf.Script).TapHash() leafHash := taproot.NewBaseTapElementsLeaf(tapLeaf.Script).TapHash()
preimage, err := b.getTaprootPreimage( preimage, err := b.getTaprootPreimage(
tx, pset,
index, index,
&leafHash, &leafHash,
) )
@@ -679,7 +906,7 @@ func (b *txBuilder) VerifyAndCombinePartialTx(dest string, src string) (string,
return "", err return "", err
} }
preimage, err := b.getTaprootPreimage(src, i, leafHash) preimage, err := b.getTaprootPreimage(sourcePset, i, leafHash)
if err != nil { if err != nil {
return "", err return "", err
} }
@@ -711,11 +938,11 @@ func (b *txBuilder) VerifyAndCombinePartialTx(dest string, src string) (string,
} }
func (b *txBuilder) createConnectors( func (b *txBuilder) createConnectors(
poolTx string, payments []domain.Payment, roundTx string, payments []domain.Payment,
connectorAddress string, connectorAddress string,
connectorAmount, feeAmount uint64, connectorAmount, feeAmount uint64,
) ([]*psetv2.Pset, error) { ) ([]*psetv2.Pset, error) {
txid, _ := getTxid(poolTx) txid, _ := getTxid(roundTx)
aspScript, err := address.ToOutputScript(connectorAddress) aspScript, err := address.ToOutputScript(connectorAddress)
if err != nil { if err != nil {
@@ -783,107 +1010,7 @@ func (b *txBuilder) createConnectors(
return connectors, nil return connectors, nil
} }
func (b *txBuilder) createForfeitTxs( func (b *txBuilder) getTaprootPreimage(pset *psetv2.Pset, inputIndex int, leafHash *chainhash.Hash) ([]byte, error) {
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
}
prevoutScripts := make([][]byte, 0) prevoutScripts := make([][]byte, 0)
prevoutAssets := make([][]byte, 0) prevoutAssets := make([][]byte, 0)
prevoutValues := 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/domain"
"github.com/ark-network/ark/server/internal/core/ports" "github.com/ark-network/ark/server/internal/core/ports"
txbuilder "github.com/ark-network/ark/server/internal/infrastructure/tx-builder/covenant" 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/decred/dcrd/dcrec/secp256k1/v4"
"github.com/stretchr/testify/mock" "github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/vulpemventures/go-elements/psetv2"
) )
const ( const (
@@ -67,7 +65,7 @@ func TestBuildPoolTx(t *testing.T) {
if len(fixtures.Valid) > 0 { if len(fixtures.Valid) > 0 {
t.Run("valid", func(t *testing.T) { t.Run("valid", func(t *testing.T) {
for _, f := range fixtures.Valid { for _, f := range fixtures.Valid {
poolTx, congestionTree, connAddr, err := builder.BuildRoundTx( poolTx, congestionTree, connAddr, _, err := builder.BuildRoundTx(
pubkey, f.Payments, []ports.BoardingInput{}, []domain.Round{}, pubkey, f.Payments, []ports.BoardingInput{}, []domain.Round{},
) )
require.NoError(t, err) require.NoError(t, err)
@@ -88,7 +86,7 @@ func TestBuildPoolTx(t *testing.T) {
if len(fixtures.Invalid) > 0 { if len(fixtures.Invalid) > 0 {
t.Run("invalid", func(t *testing.T) { t.Run("invalid", func(t *testing.T) {
for _, f := range fixtures.Invalid { for _, f := range fixtures.Invalid {
poolTx, congestionTree, connAddr, err := builder.BuildRoundTx( poolTx, congestionTree, connAddr, _, err := builder.BuildRoundTx(
pubkey, f.Payments, []ports.BoardingInput{}, []domain.Round{}, pubkey, f.Payments, []ports.BoardingInput{}, []domain.Round{},
) )
require.EqualError(t, err, f.ExpectedErr) 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 { func randomInput() []ports.TxInput {
txid := randomHex(32) txid := randomHex(32)
input := &mockedInput{} input := &mockedInput{}
@@ -211,81 +148,3 @@ func parsePoolTxFixtures() (*poolTxFixtures, error) {
return &fixtures, nil 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" "context"
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"math"
"strings" "strings"
"github.com/ark-network/ark/common" "github.com/ark-network/ark/common"
@@ -22,7 +23,6 @@ import (
"github.com/btcsuite/btcwallet/waddrmgr" "github.com/btcsuite/btcwallet/waddrmgr"
"github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
) )
type txBuilder struct { type txBuilder struct {
@@ -48,7 +48,15 @@ func (b *txBuilder) GetTxID(tx string) (string, error) {
} }
func (b *txBuilder) VerifyTapscriptPartialSigs(tx string) (bool, 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() txid := ptx.UnsignedTx.TxID()
for index, input := range ptx.Inputs { for index, input := range ptx.Inputs {
@@ -83,7 +91,7 @@ func (b *txBuilder) VerifyTapscriptPartialSigs(tx string) (bool, string, error)
} }
preimage, err := b.getTaprootPreimage( preimage, err := b.getTaprootPreimage(
tx, ptx,
index, index,
tapLeaf.Script, tapLeaf.Script,
) )
@@ -227,37 +235,217 @@ func (b *txBuilder) BuildSweepTx(inputs []ports.SweepInput) (signedSweepTx strin
return hex.EncodeToString(buf.Bytes()), nil return hex.EncodeToString(buf.Bytes()), nil
} }
func (b *txBuilder) BuildForfeitTxs( func (b *txBuilder) VerifyForfeitTxs(vtxos []domain.Vtxo, connectors []string, forfeitTxs []string) (map[domain.VtxoKey][]string, error) {
poolTx string, connectorsPtxs := make([]*psbt.Packet, 0, len(connectors))
payments []domain.Payment, var connectorAmount uint64
descriptors map[domain.VtxoKey]string,
minRelayFeeRate chainfee.SatPerKVByte, for i, connector := range connectors {
) (connectors []string, forfeitTxs []string, err error) { ptx, err := psbt.NewFromRawBytes(strings.NewReader(connector), true)
connectorPkScript, err := b.getConnectorPkScript(poolTx)
if err != nil { if err != nil {
return nil, nil, err return nil, err
} }
minRelayFeeConnectorTx, err := b.minRelayFeeConnectorTx() if i == len(connectors)-1 {
if err != nil { lastOutput := ptx.UnsignedTx.TxOut[len(ptx.UnsignedTx.TxOut)-1]
return nil, nil, err connectorAmount = uint64(lastOutput.Value)
} }
connectorTxs, err := b.createConnectors(poolTx, payments, connectorPkScript, minRelayFeeConnectorTx) connectorsPtxs = append(connectorsPtxs, ptx)
if err != nil {
return nil, nil, err
} }
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 { if err != nil {
return nil, nil, err return nil, err
} }
for _, tx := range connectorTxs { if len(ptx.Inputs) != 2 {
buf, _ := tx.B64Encode() return nil, fmt.Errorf("invalid forfeit tx, expect 2 inputs, got %d", len(ptx.Inputs))
connectors = append(connectors, buf)
} }
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( func (b *txBuilder) BuildRoundTx(
@@ -266,22 +454,22 @@ func (b *txBuilder) BuildRoundTx(
boardingInputs []ports.BoardingInput, boardingInputs []ports.BoardingInput,
sweptRounds []domain.Round, sweptRounds []domain.Round,
cosigners ...*secp256k1.PublicKey, 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 sharedOutputScript []byte
var sharedOutputAmount int64 var sharedOutputAmount int64
if len(cosigners) == 0 { if len(cosigners) == 0 {
return "", nil, "", fmt.Errorf("missing cosigners") return "", nil, "", nil, fmt.Errorf("missing cosigners")
} }
receivers, err := getOutputVtxosLeaves(payments) receivers, err := getOutputVtxosLeaves(payments)
if err != nil { if err != nil {
return "", nil, "", err return "", nil, "", nil, err
} }
feeAmount, err := b.minRelayFeeTreeTx() feeAmount, err := b.minRelayFeeTreeTx()
if err != nil { if err != nil {
return "", nil, "", err return
} }
if !isOnchainOnly(payments) { if !isOnchainOnly(payments) {
@@ -320,11 +508,43 @@ func (b *txBuilder) BuildRoundTx(
initialOutpoint, cosigners, aspPubkey, receivers, feeAmount, b.roundLifetime, initialOutpoint, cosigners, aspPubkey, receivers, feeAmount, b.roundLifetime,
) )
if err != nil { if err != nil {
return return "", nil, "", nil, err
} }
} }
if countSpentVtxos(payments) <= 0 {
return 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) { 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] 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 { if err != nil {
return "", err return "", err
} }
@@ -964,7 +1184,6 @@ func (b *txBuilder) createConnectors(
Hash: partialTx.UnsignedTx.TxHash(), Hash: partialTx.UnsignedTx.TxHash(),
Index: 1, Index: 1,
} }
if numberOfConnectors == 1 { if numberOfConnectors == 1 {
outputs := []*wire.TxOut{connectorOutput} outputs := []*wire.TxOut{connectorOutput}
connectorTx, err := craftConnectorTx(previousInput, connectorScript, outputs, feeAmount) 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)) 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) { func (b *txBuilder) selectUtxos(ctx context.Context, sweptRounds []domain.Round, amount uint64) ([]ports.TxInput, uint64, error) {
selectedConnectorsUtxos := make([]ports.TxInput, 0) selectedConnectorsUtxos := make([]ports.TxInput, 0)
selectedConnectorsAmount := uint64(0) selectedConnectorsAmount := uint64(0)
@@ -1160,12 +1271,7 @@ func (b *txBuilder) selectUtxos(ctx context.Context, sweptRounds []domain.Round,
return append(selectedConnectorsUtxos, utxos...), change, nil return append(selectedConnectorsUtxos, utxos...), change, nil
} }
func (b *txBuilder) getTaprootPreimage(tx string, inputIndex int, leafScript []byte) ([]byte, error) { func (b *txBuilder) getTaprootPreimage(partial *psbt.Packet, inputIndex int, leafScript []byte) ([]byte, error) {
partial, err := psbt.NewFromRawBytes(strings.NewReader(tx), true)
if err != nil {
return nil, err
}
prevouts := make(map[wire.OutPoint]*wire.TxOut) prevouts := make(map[wire.OutPoint]*wire.TxOut)
for i, input := range partial.Inputs { for i, input := range partial.Inputs {

View File

@@ -5,7 +5,6 @@ import (
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"os" "os"
"strings"
"testing" "testing"
"github.com/ark-network/ark/common" "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/domain"
"github.com/ark-network/ark/server/internal/core/ports" "github.com/ark-network/ark/server/internal/core/ports"
txbuilder "github.com/ark-network/ark/server/internal/infrastructure/tx-builder/covenantless" 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/decred/dcrd/dcrec/secp256k1/v4"
"github.com/stretchr/testify/mock" "github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@@ -77,7 +75,7 @@ func TestBuildPoolTx(t *testing.T) {
cosigners = append(cosigners, randKey.PubKey()) 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..., pubkey, f.Payments, []ports.BoardingInput{}, []domain.Round{}, cosigners...,
) )
require.NoError(t, err) require.NoError(t, err)
@@ -98,7 +96,7 @@ func TestBuildPoolTx(t *testing.T) {
if len(fixtures.Invalid) > 0 { if len(fixtures.Invalid) > 0 {
t.Run("invalid", func(t *testing.T) { t.Run("invalid", func(t *testing.T) {
for _, f := range fixtures.Invalid { for _, f := range fixtures.Invalid {
poolTx, congestionTree, connAddr, err := builder.BuildRoundTx( poolTx, congestionTree, connAddr, _, err := builder.BuildRoundTx(
pubkey, f.Payments, []ports.BoardingInput{}, []domain.Round{}, pubkey, f.Payments, []ports.BoardingInput{}, []domain.Round{},
) )
require.EqualError(t, err, f.ExpectedErr) 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 { func randomInput() []ports.TxInput {
txid := randomHex(32) txid := randomHex(32)
input := &mockedInput{} input := &mockedInput{}
@@ -221,76 +158,3 @@ func parsePoolTxFixtures() (*poolTxFixtures, error) {
return &fixtures, nil 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
}