mirror of
https://github.com/aljazceru/ark.git
synced 2025-12-17 12:14:21 +01:00
[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:
@@ -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 {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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=
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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()
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user