Add reversible policy to pending vtxos (#311)

* [server] descriptor-based vtxo script

* [server] fix unit tests

* [sdk] descriptor based vtxo

* empty config check & version flag support

* fix: empty config check & version flag support (#309)

* fix

* [sdk] several fixes

* [sdk][server] several fixes

* [common][sdk] add reversible VtxoScript type, use it in async payment

* [common] improve parser

* [common] fix reversible vtxo parser

* [sdk] remove logs

* fix forfeit map

* remove debug log

* [sdk] do not allow reversible vtxo script in case of self-transfer

* remove signing pubkey

* remove signer public key, craft forfeit txs client side

* go work sync

* fix linter errors

* rename MakeForfeitTxs to BuildForfeitTxs

* fix conflicts

* fix tests

* comment VtxoScript type

* revert ROUND_INTERVAL value

---------

Co-authored-by: Pietralberto Mazza <18440657+altafan@users.noreply.github.com>
Co-authored-by: sekulicd <sekula87@gmail.com>
This commit is contained in:
Louis Singer
2024-09-19 10:01:33 +02:00
committed by GitHub
parent 7a83f9957e
commit 10ef0dbffa
82 changed files with 3440 additions and 2612 deletions

View File

@@ -475,21 +475,6 @@
} }
} }
}, },
"v1BoardingInput": {
"type": "object",
"properties": {
"txid": {
"type": "string"
},
"vout": {
"type": "integer",
"format": "int64"
},
"descriptor": {
"type": "string"
}
}
},
"v1ClaimPaymentRequest": { "v1ClaimPaymentRequest": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -668,11 +653,11 @@
"v1Input": { "v1Input": {
"type": "object", "type": "object",
"properties": { "properties": {
"vtxoInput": { "outpoint": {
"$ref": "#/definitions/v1VtxoInput" "$ref": "#/definitions/v1Outpoint"
}, },
"boardingInput": { "descriptor": {
"$ref": "#/definitions/v1BoardingInput" "type": "string"
} }
} }
}, },
@@ -709,12 +694,28 @@
} }
} }
}, },
"v1Outpoint": {
"type": "object",
"properties": {
"txid": {
"type": "string"
},
"vout": {
"type": "integer",
"format": "int64"
}
}
},
"v1Output": { "v1Output": {
"type": "object", "type": "object",
"properties": { "properties": {
"address": { "address": {
"type": "string", "type": "string",
"description": "Either the offchain or onchain address." "title": "onchain"
},
"descriptor": {
"type": "string",
"title": "offchain"
}, },
"amount": { "amount": {
"type": "string", "type": "string",
@@ -838,12 +839,6 @@
"poolTx": { "poolTx": {
"type": "string" "type": "string"
}, },
"forfeitTxs": {
"type": "array",
"items": {
"type": "string"
}
},
"congestionTree": { "congestionTree": {
"$ref": "#/definitions/v1Tree" "$ref": "#/definitions/v1Tree"
}, },
@@ -852,6 +847,10 @@
"items": { "items": {
"type": "string" "type": "string"
} }
},
"minRelayFeeRate": {
"type": "string",
"format": "int64"
} }
} }
}, },
@@ -970,10 +969,10 @@
"type": "object", "type": "object",
"properties": { "properties": {
"outpoint": { "outpoint": {
"$ref": "#/definitions/v1Input" "$ref": "#/definitions/v1Outpoint"
}, },
"receiver": { "descriptor": {
"$ref": "#/definitions/v1Output" "type": "string"
}, },
"spent": { "spent": {
"type": "boolean" "type": "boolean"
@@ -996,18 +995,10 @@
}, },
"pendingData": { "pendingData": {
"$ref": "#/definitions/v1PendingPayment" "$ref": "#/definitions/v1PendingPayment"
}
}
}, },
"v1VtxoInput": { "amount": {
"type": "object", "type": "string",
"properties": { "format": "uint64"
"txid": {
"type": "string"
},
"vout": {
"type": "integer",
"format": "int64"
} }
} }
} }

View File

@@ -196,9 +196,9 @@ message GetInfoResponse {
message RoundFinalizationEvent { message RoundFinalizationEvent {
string id = 1; string id = 1;
string pool_tx = 2; string pool_tx = 2;
repeated string forfeit_txs = 3; Tree congestion_tree = 3;
Tree congestion_tree = 4; repeated string connectors = 4;
repeated string connectors = 5; int64 min_relay_fee_rate = 5;
} }
message RoundFinalizedEvent { message RoundFinalizedEvent {
@@ -244,29 +244,23 @@ message Round {
RoundStage stage = 8; RoundStage stage = 8;
} }
message VtxoInput { message Outpoint {
string txid = 1; string txid = 1;
uint32 vout = 2; uint32 vout = 2;
} }
message BoardingInput {
string txid = 1;
uint32 vout = 2;
string descriptor = 3;
}
message Input { message Input {
oneof input { Outpoint outpoint = 1;
VtxoInput vtxo_input = 1; string descriptor = 2;
BoardingInput boarding_input = 2;
}
} }
message Output { message Output {
// Either the offchain or onchain address. oneof destination {
string address = 1; string address = 1; // onchain
string descriptor = 2; // offchain
}
// Amount to send in satoshis. // Amount to send in satoshis.
uint64 amount = 2; uint64 amount = 3;
} }
message Tree { message Tree {
@@ -284,8 +278,8 @@ message Node {
} }
message Vtxo { message Vtxo {
Input outpoint = 1; Outpoint outpoint = 1;
Output receiver = 2; string descriptor = 2;
bool spent = 3; bool spent = 3;
string pool_txid = 4; string pool_txid = 4;
string spent_by = 5; string spent_by = 5;
@@ -293,6 +287,7 @@ message Vtxo {
bool swept = 7; bool swept = 7;
bool pending = 8; bool pending = 8;
PendingPayment pending_data = 9; PendingPayment pending_data = 9;
uint64 amount = 10;
} }
message PendingPayment { message PendingPayment {

View File

@@ -131,6 +131,7 @@ func local_request_AdminService_GetRounds_0(ctx context.Context, marshaler runti
// UnaryRPC :call AdminServiceServer directly. // UnaryRPC :call AdminServiceServer directly.
// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. // StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906.
// Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterAdminServiceHandlerFromEndpoint instead. // Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterAdminServiceHandlerFromEndpoint instead.
// GRPC interceptors will not work for this type of registration. To use interceptors, you must use the "runtime.WithMiddlewares" option in the "runtime.NewServeMux" call.
func RegisterAdminServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux, server AdminServiceServer) error { func RegisterAdminServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux, server AdminServiceServer) error {
mux.Handle("GET", pattern_AdminService_GetScheduledSweep_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { mux.Handle("GET", pattern_AdminService_GetScheduledSweep_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
@@ -246,7 +247,7 @@ func RegisterAdminServiceHandler(ctx context.Context, mux *runtime.ServeMux, con
// to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "AdminServiceClient". // to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "AdminServiceClient".
// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "AdminServiceClient" // Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "AdminServiceClient"
// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in // doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in
// "AdminServiceClient" to call the correct interceptors. // "AdminServiceClient" to call the correct interceptors. This client ignores the HTTP middlewares.
func RegisterAdminServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux, client AdminServiceClient) error { func RegisterAdminServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux, client AdminServiceClient) error {
mux.Handle("GET", pattern_AdminService_GetScheduledSweep_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { mux.Handle("GET", pattern_AdminService_GetScheduledSweep_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {

File diff suppressed because it is too large Load Diff

View File

@@ -486,6 +486,7 @@ func local_request_ArkService_CompletePayment_0(ctx context.Context, marshaler r
// UnaryRPC :call ArkServiceServer directly. // UnaryRPC :call ArkServiceServer directly.
// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. // StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906.
// Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterArkServiceHandlerFromEndpoint instead. // Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterArkServiceHandlerFromEndpoint instead.
// GRPC interceptors will not work for this type of registration. To use interceptors, you must use the "runtime.WithMiddlewares" option in the "runtime.NewServeMux" call.
func RegisterArkServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux, server ArkServiceServer) error { func RegisterArkServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux, server ArkServiceServer) error {
mux.Handle("POST", pattern_ArkService_RegisterPayment_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { mux.Handle("POST", pattern_ArkService_RegisterPayment_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
@@ -858,7 +859,7 @@ func RegisterArkServiceHandler(ctx context.Context, mux *runtime.ServeMux, conn
// to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "ArkServiceClient". // to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "ArkServiceClient".
// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "ArkServiceClient" // Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "ArkServiceClient"
// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in // doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in
// "ArkServiceClient" to call the correct interceptors. // "ArkServiceClient" to call the correct interceptors. This client ignores the HTTP middlewares.
func RegisterArkServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux, client ArkServiceClient) error { func RegisterArkServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux, client ArkServiceClient) error {
mux.Handle("POST", pattern_ArkService_RegisterPayment_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { mux.Handle("POST", pattern_ArkService_RegisterPayment_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {

View File

@@ -211,6 +211,7 @@ func local_request_WalletService_GetBalance_0(ctx context.Context, marshaler run
// UnaryRPC :call WalletInitializerServiceServer directly. // UnaryRPC :call WalletInitializerServiceServer directly.
// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. // StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906.
// Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterWalletInitializerServiceHandlerFromEndpoint instead. // Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterWalletInitializerServiceHandlerFromEndpoint instead.
// GRPC interceptors will not work for this type of registration. To use interceptors, you must use the "runtime.WithMiddlewares" option in the "runtime.NewServeMux" call.
func RegisterWalletInitializerServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux, server WalletInitializerServiceServer) error { func RegisterWalletInitializerServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux, server WalletInitializerServiceServer) error {
mux.Handle("GET", pattern_WalletInitializerService_GenSeed_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { mux.Handle("GET", pattern_WalletInitializerService_GenSeed_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
@@ -345,6 +346,7 @@ func RegisterWalletInitializerServiceHandlerServer(ctx context.Context, mux *run
// UnaryRPC :call WalletServiceServer directly. // UnaryRPC :call WalletServiceServer directly.
// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. // StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906.
// Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterWalletServiceHandlerFromEndpoint instead. // Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterWalletServiceHandlerFromEndpoint instead.
// GRPC interceptors will not work for this type of registration. To use interceptors, you must use the "runtime.WithMiddlewares" option in the "runtime.NewServeMux" call.
func RegisterWalletServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux, server WalletServiceServer) error { func RegisterWalletServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux, server WalletServiceServer) error {
mux.Handle("POST", pattern_WalletService_Lock_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { mux.Handle("POST", pattern_WalletService_Lock_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
@@ -460,7 +462,7 @@ func RegisterWalletInitializerServiceHandler(ctx context.Context, mux *runtime.S
// to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "WalletInitializerServiceClient". // to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "WalletInitializerServiceClient".
// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "WalletInitializerServiceClient" // Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "WalletInitializerServiceClient"
// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in // doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in
// "WalletInitializerServiceClient" to call the correct interceptors. // "WalletInitializerServiceClient" to call the correct interceptors. This client ignores the HTTP middlewares.
func RegisterWalletInitializerServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux, client WalletInitializerServiceClient) error { func RegisterWalletInitializerServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux, client WalletInitializerServiceClient) error {
mux.Handle("GET", pattern_WalletInitializerService_GenSeed_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { mux.Handle("GET", pattern_WalletInitializerService_GenSeed_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
@@ -635,7 +637,7 @@ func RegisterWalletServiceHandler(ctx context.Context, mux *runtime.ServeMux, co
// to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "WalletServiceClient". // to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "WalletServiceClient".
// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "WalletServiceClient" // Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "WalletServiceClient"
// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in // doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in
// "WalletServiceClient" to call the correct interceptors. // "WalletServiceClient" to call the correct interceptors. This client ignores the HTTP middlewares.
func RegisterWalletServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux, client WalletServiceClient) error { func RegisterWalletServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux, client WalletServiceClient) error {
mux.Handle("POST", pattern_WalletService_Lock_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { mux.Handle("POST", pattern_WalletService_Lock_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {

View File

@@ -1,7 +1,6 @@
package bitcointree package bitcointree
import ( import (
"encoding/hex"
"fmt" "fmt"
"github.com/ark-network/ark/common/tree" "github.com/ark-network/ark/common/tree"
@@ -17,7 +16,7 @@ import (
// CraftSharedOutput returns the taproot script and the amount of the initial root output // CraftSharedOutput returns the taproot script and the amount of the initial root output
func CraftSharedOutput( func CraftSharedOutput(
cosigners []*secp256k1.PublicKey, aspPubkey *secp256k1.PublicKey, receivers []Receiver, cosigners []*secp256k1.PublicKey, aspPubkey *secp256k1.PublicKey, receivers []Receiver,
feeSatsPerNode uint64, roundLifetime, unilateralExitDelay int64, feeSatsPerNode uint64, roundLifetime int64,
) ([]byte, int64, error) { ) ([]byte, int64, error) {
aggregatedKey, _, err := createAggregatedKeyWithSweep( aggregatedKey, _, err := createAggregatedKeyWithSweep(
cosigners, aspPubkey, roundLifetime, cosigners, aspPubkey, roundLifetime,
@@ -26,7 +25,7 @@ func CraftSharedOutput(
return nil, 0, err return nil, 0, err
} }
root, err := createRootNode(aggregatedKey, cosigners, aspPubkey, receivers, feeSatsPerNode, unilateralExitDelay) root, err := createRootNode(aggregatedKey, cosigners, receivers, feeSatsPerNode)
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
} }
@@ -44,7 +43,7 @@ func CraftSharedOutput(
// CraftCongestionTree creates all the tree's transactions // CraftCongestionTree creates all the tree's transactions
func CraftCongestionTree( func CraftCongestionTree(
initialInput *wire.OutPoint, cosigners []*secp256k1.PublicKey, aspPubkey *secp256k1.PublicKey, receivers []Receiver, initialInput *wire.OutPoint, cosigners []*secp256k1.PublicKey, aspPubkey *secp256k1.PublicKey, receivers []Receiver,
feeSatsPerNode uint64, roundLifetime, unilateralExitDelay int64, feeSatsPerNode uint64, roundLifetime int64,
) (tree.CongestionTree, error) { ) (tree.CongestionTree, error) {
aggregatedKey, sweepTapLeaf, err := createAggregatedKeyWithSweep( aggregatedKey, sweepTapLeaf, err := createAggregatedKeyWithSweep(
cosigners, aspPubkey, roundLifetime, cosigners, aspPubkey, roundLifetime,
@@ -53,7 +52,7 @@ func CraftCongestionTree(
return nil, err return nil, err
} }
root, err := createRootNode(aggregatedKey, cosigners, aspPubkey, receivers, feeSatsPerNode, unilateralExitDelay) root, err := createRootNode(aggregatedKey, cosigners, receivers, feeSatsPerNode)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -109,9 +108,7 @@ type node interface {
} }
type leaf struct { type leaf struct {
aspKey *secp256k1.PublicKey vtxoScript VtxoScript
vtxoKey *secp256k1.PublicKey
exitDelay int64
amount int64 amount int64
} }
@@ -145,36 +142,11 @@ func (l *leaf) getAmount() int64 {
} }
func (l *leaf) getOutputs() ([]*wire.TxOut, error) { func (l *leaf) getOutputs() ([]*wire.TxOut, error) {
redeemClosure := &CSVSigClosure{ taprootKey, _, err := l.vtxoScript.TapTree()
Pubkey: l.vtxoKey,
Seconds: uint(l.exitDelay),
}
redeemLeaf, err := redeemClosure.Leaf()
if err != nil { if err != nil {
return nil, err return nil, err
} }
forfeitClosure := &MultisigClosure{
Pubkey: l.vtxoKey,
AspPubkey: l.aspKey,
}
forfeitLeaf, err := forfeitClosure.Leaf()
if err != nil {
return nil, err
}
leafTaprootTree := txscript.AssembleTaprootScriptTree(
*redeemLeaf, *forfeitLeaf,
)
root := leafTaprootTree.RootNode.TapHash()
taprootKey := txscript.ComputeTaprootOutputKey(
UnspendableKey(),
root[:],
)
script, err := taprootOutputScript(taprootKey) script, err := taprootOutputScript(taprootKey)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -272,9 +244,10 @@ func getTx(
} }
func createRootNode( func createRootNode(
aggregatedKey *musig2.AggregateKey, cosigners []*secp256k1.PublicKey, aggregatedKey *musig2.AggregateKey,
aspPubkey *secp256k1.PublicKey, receivers []Receiver, cosigners []*secp256k1.PublicKey,
feeSatsPerNode uint64, unilateralExitDelay int64, receivers []Receiver,
feeSatsPerNode uint64,
) (root node, err error) { ) (root node, err error) {
if len(receivers) == 0 { if len(receivers) == 0 {
return nil, fmt.Errorf("no receivers provided") return nil, fmt.Errorf("no receivers provided")
@@ -282,20 +255,8 @@ func createRootNode(
nodes := make([]node, 0, len(receivers)) nodes := make([]node, 0, len(receivers))
for _, r := range receivers { for _, r := range receivers {
pubkeyBytes, err := hex.DecodeString(r.Pubkey)
if err != nil {
return nil, err
}
receiverKey, err := secp256k1.ParsePubKey(pubkeyBytes)
if err != nil {
return nil, err
}
leafNode := &leaf{ leafNode := &leaf{
aspKey: aspPubkey, vtxoScript: r.Script,
vtxoKey: receiverKey,
exitDelay: unilateralExitDelay,
amount: int64(r.Amount), amount: int64(r.Amount),
} }
nodes = append(nodes, leafNode) nodes = append(nodes, leafNode)

View File

@@ -0,0 +1,85 @@
package bitcointree
import (
"github.com/ark-network/ark/common"
"github.com/btcsuite/btcd/btcutil/psbt"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
)
func BuildForfeitTxs(
connectorTx *psbt.Packet,
vtxoInput *wire.OutPoint,
vtxoAmount,
connectorAmount,
feeAmount uint64,
vtxoScript []byte,
aspPubKey *secp256k1.PublicKey,
) (forfeitTxs []*psbt.Packet, err error) {
aspScript, err := common.P2TRScript(aspPubKey)
if err != nil {
return nil, err
}
connectors, prevouts := getConnectorInputs(connectorTx, int64(connectorAmount))
for i, connectorInput := range connectors {
connectorPrevout := prevouts[i]
partialTx, err := psbt.New(
[]*wire.OutPoint{connectorInput, vtxoInput},
[]*wire.TxOut{{
Value: int64(vtxoAmount) + int64(connectorAmount) - int64(feeAmount),
PkScript: aspScript,
}},
2,
0,
[]uint32{wire.MaxTxInSequenceNum, wire.MaxTxInSequenceNum},
)
if err != nil {
return nil, err
}
updater, err := psbt.NewUpdater(partialTx)
if err != nil {
return nil, err
}
if err := updater.AddInWitnessUtxo(connectorPrevout, 0); err != nil {
return nil, err
}
if err := updater.AddInWitnessUtxo(&wire.TxOut{
Value: int64(vtxoAmount),
PkScript: vtxoScript,
}, 1); err != nil {
return nil, err
}
if err := updater.AddInSighashType(txscript.SigHashDefault, 1); err != nil {
return nil, err
}
forfeitTxs = append(forfeitTxs, partialTx)
}
return forfeitTxs, nil
}
func getConnectorInputs(partialTx *psbt.Packet, connectorAmount int64) ([]*wire.OutPoint, []*wire.TxOut) {
inputs := make([]*wire.OutPoint, 0)
witnessUtxos := make([]*wire.TxOut, 0)
for i, output := range partialTx.UnsignedTx.TxOut {
if output.Value == connectorAmount {
inputs = append(inputs, &wire.OutPoint{
Hash: partialTx.UnsignedTx.TxHash(),
Index: uint32(i),
})
witnessUtxos = append(witnessUtxos, output)
}
}
return inputs, witnessUtxos
}

View File

@@ -2,6 +2,7 @@ package bitcointree_test
import ( import (
"bytes" "bytes"
"encoding/hex"
"encoding/json" "encoding/json"
"fmt" "fmt"
"os" "os"
@@ -43,10 +44,9 @@ func TestRoundTripSignTree(t *testing.T) {
_, sharedOutputAmount, err := bitcointree.CraftSharedOutput( _, sharedOutputAmount, err := bitcointree.CraftSharedOutput(
cosigners, cosigners,
asp.PubKey(), asp.PubKey(),
f.Receivers, castReceivers(f.Receivers, asp.PubKey()),
minRelayFee, minRelayFee,
lifetime, lifetime,
exitDelay,
) )
require.NoError(t, err) require.NoError(t, err)
@@ -58,10 +58,9 @@ func TestRoundTripSignTree(t *testing.T) {
}, },
cosigners, cosigners,
asp.PubKey(), asp.PubKey(),
f.Receivers, castReceivers(f.Receivers, asp.PubKey()),
minRelayFee, minRelayFee,
lifetime, lifetime,
exitDelay,
) )
require.NoError(t, err) require.NoError(t, err)
@@ -218,9 +217,43 @@ func TestRoundTripSignTree(t *testing.T) {
} }
} }
type receiverFixture struct {
Amount int64 `json:"amount"`
Pubkey string `json:"pubkey"`
}
func (r receiverFixture) toVtxoScript(asp *secp256k1.PublicKey) bitcointree.VtxoScript {
bytesKey, err := hex.DecodeString(r.Pubkey)
if err != nil {
panic(err)
}
pubkey, err := secp256k1.ParsePubKey(bytesKey)
if err != nil {
panic(err)
}
return &bitcointree.DefaultVtxoScript{
Owner: pubkey,
Asp: asp,
ExitDelay: exitDelay,
}
}
func castReceivers(receivers []receiverFixture, asp *secp256k1.PublicKey) []bitcointree.Receiver {
receiversOut := make([]bitcointree.Receiver, 0, len(receivers))
for _, r := range receivers {
receiversOut = append(receiversOut, bitcointree.Receiver{
Script: r.toVtxoScript(asp),
Amount: uint64(r.Amount),
})
}
return receiversOut
}
type fixture struct { type fixture struct {
Valid []struct { Valid []struct {
Receivers []bitcointree.Receiver `json:"receivers"` Receivers []receiverFixture `json:"receivers"`
} `json:"valid"` } `json:"valid"`
} }

View File

@@ -141,44 +141,6 @@ func (d *CSVSigClosure) Decode(script []byte) (bool, error) {
return valid, nil return valid, nil
} }
func ComputeVtxoTaprootScript(
userPubkey, aspPubkey *secp256k1.PublicKey, exitDelay uint,
) (*secp256k1.PublicKey, *txscript.TapscriptProof, error) {
redeemClosure := &CSVSigClosure{
Pubkey: userPubkey,
Seconds: exitDelay,
}
forfeitClosure := &MultisigClosure{
Pubkey: userPubkey,
AspPubkey: aspPubkey,
}
redeemLeaf, err := redeemClosure.Leaf()
if err != nil {
return nil, nil, err
}
forfeitLeaf, err := forfeitClosure.Leaf()
if err != nil {
return nil, nil, err
}
vtxoTaprootTree := txscript.AssembleTaprootScriptTree(
*redeemLeaf, *forfeitLeaf,
)
root := vtxoTaprootTree.RootNode.TapHash()
unspendableKey := UnspendableKey()
vtxoTaprootKey := txscript.ComputeTaprootOutputKey(unspendableKey, root[:])
redeemLeafHash := redeemLeaf.TapHash()
proofIndex := vtxoTaprootTree.LeafProofIndex[redeemLeafHash]
proof := vtxoTaprootTree.LeafMerkleProofs[proofIndex]
return vtxoTaprootKey, &proof, nil
}
func decodeChecksigScript(script []byte) (bool, *secp256k1.PublicKey, error) { func decodeChecksigScript(script []byte) (bool, *secp256k1.PublicKey, error) {
data32Index := bytes.Index(script, []byte{txscript.OP_DATA_32}) data32Index := bytes.Index(script, []byte{txscript.OP_DATA_32})
if data32Index == -1 { if data32Index == -1 {

View File

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

236
common/bitcointree/vtxo.go Normal file
View File

@@ -0,0 +1,236 @@
package bitcointree
import (
"encoding/hex"
"fmt"
"github.com/ark-network/ark/common"
"github.com/ark-network/ark/common/descriptor"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
)
type VtxoScript common.VtxoScript[bitcoinTapTree]
func ParseVtxoScript(desc string) (VtxoScript, error) {
v := &DefaultVtxoScript{}
// TODO add other type
err := v.FromDescriptor(desc)
if err != nil {
v := &ReversibleVtxoScript{}
err = v.FromDescriptor(desc)
if err != nil {
return nil, fmt.Errorf("invalid vtxo descriptor: %s", desc)
}
return v, nil
}
return v, nil
}
/*
* DefaultVtxoScript is the default implementation of VTXO with 2 closures
* - Owner and ASP (forfeit)
* - Owner after t (unilateral exit)
*/
type DefaultVtxoScript struct {
Owner *secp256k1.PublicKey
Asp *secp256k1.PublicKey
ExitDelay uint
}
func (v *DefaultVtxoScript) ToDescriptor() string {
owner := hex.EncodeToString(schnorr.SerializePubKey(v.Owner))
return fmt.Sprintf(
descriptor.DefaultVtxoDescriptorTemplate,
hex.EncodeToString(UnspendableKey().SerializeCompressed()),
owner,
hex.EncodeToString(schnorr.SerializePubKey(v.Asp)),
v.ExitDelay,
owner,
)
}
func (v *DefaultVtxoScript) FromDescriptor(desc string) error {
taprootDesc, err := descriptor.ParseTaprootDescriptor(desc)
if err != nil {
return err
}
owner, asp, exitDelay, err := descriptor.ParseDefaultVtxoDescriptor(*taprootDesc)
if err != nil {
return err
}
v.Owner = owner
v.Asp = asp
v.ExitDelay = exitDelay
return nil
}
func (v *DefaultVtxoScript) TapTree() (*secp256k1.PublicKey, bitcoinTapTree, error) {
redeemClosure := &CSVSigClosure{
Pubkey: v.Owner,
Seconds: v.ExitDelay,
}
redeemLeaf, err := redeemClosure.Leaf()
if err != nil {
return nil, bitcoinTapTree{}, err
}
forfeitClosure := &MultisigClosure{
Pubkey: v.Owner,
AspPubkey: v.Asp,
}
forfeitLeaf, err := forfeitClosure.Leaf()
if err != nil {
return nil, bitcoinTapTree{}, err
}
tapTree := txscript.AssembleTaprootScriptTree(
*redeemLeaf, *forfeitLeaf,
)
root := tapTree.RootNode.TapHash()
taprootKey := txscript.ComputeTaprootOutputKey(
UnspendableKey(),
root[:],
)
return taprootKey, bitcoinTapTree{tapTree}, nil
}
/*
* ReversibleVtxoScript allows sender of the VTXO to revert the transaction
* unilateral exit is in favor of the sender
* - Owner and ASP (forfeit owner)
* - Sender and ASP (forfeit sender)
* - Sender after t (unilateral exit)
*/
type ReversibleVtxoScript struct {
Asp *secp256k1.PublicKey
Sender *secp256k1.PublicKey
Owner *secp256k1.PublicKey
ExitDelay uint
}
func (v *ReversibleVtxoScript) ToDescriptor() string {
owner := hex.EncodeToString(schnorr.SerializePubKey(v.Owner))
sender := hex.EncodeToString(schnorr.SerializePubKey(v.Sender))
asp := hex.EncodeToString(schnorr.SerializePubKey(v.Asp))
return fmt.Sprintf(
descriptor.ReversibleVtxoScriptTemplate,
hex.EncodeToString(UnspendableKey().SerializeCompressed()),
sender,
asp,
v.ExitDelay,
sender,
owner,
asp,
)
}
func (v *ReversibleVtxoScript) FromDescriptor(desc string) error {
taprootDesc, err := descriptor.ParseTaprootDescriptor(desc)
if err != nil {
return err
}
owner, sender, asp, exitDelay, err := descriptor.ParseReversibleVtxoDescriptor(*taprootDesc)
if err != nil {
return err
}
v.Owner = owner
v.Sender = sender
v.Asp = asp
v.ExitDelay = exitDelay
return nil
}
func (v *ReversibleVtxoScript) TapTree() (*secp256k1.PublicKey, bitcoinTapTree, error) {
redeemClosure := &CSVSigClosure{
Pubkey: v.Sender,
Seconds: v.ExitDelay,
}
redeemLeaf, err := redeemClosure.Leaf()
if err != nil {
return nil, bitcoinTapTree{}, err
}
forfeitClosure := &MultisigClosure{
Pubkey: v.Owner,
AspPubkey: v.Asp,
}
forfeitLeaf, err := forfeitClosure.Leaf()
if err != nil {
return nil, bitcoinTapTree{}, err
}
reverseForfeitClosure := &MultisigClosure{
Pubkey: v.Sender,
AspPubkey: v.Asp,
}
reverseForfeitLeaf, err := reverseForfeitClosure.Leaf()
if err != nil {
return nil, bitcoinTapTree{}, err
}
tapTree := txscript.AssembleTaprootScriptTree(
*redeemLeaf, *forfeitLeaf, *reverseForfeitLeaf,
)
root := tapTree.RootNode.TapHash()
taprootKey := txscript.ComputeTaprootOutputKey(
UnspendableKey(),
root[:],
)
return taprootKey, bitcoinTapTree{tapTree}, nil
}
// bitcoinTapTree is a wrapper around txscript.IndexedTapScriptTree to implement the common.TaprootTree interface
type bitcoinTapTree struct {
*txscript.IndexedTapScriptTree
}
func (b bitcoinTapTree) GetRoot() chainhash.Hash {
return b.RootNode.TapHash()
}
func (b bitcoinTapTree) GetTaprootMerkleProof(leafhash chainhash.Hash) (*common.TaprootMerkleProof, error) {
index, ok := b.LeafProofIndex[leafhash]
if !ok {
return nil, fmt.Errorf("leaf %s not found in tree", leafhash.String())
}
proof := b.LeafMerkleProofs[index]
controlBlock := proof.ToControlBlock(UnspendableKey())
controlBlockBytes, err := controlBlock.ToBytes()
if err != nil {
return nil, err
}
return &common.TaprootMerkleProof{
ControlBlock: controlBlockBytes,
Script: proof.Script,
}, nil
}
func (b bitcoinTapTree) GetLeaves() []chainhash.Hash {
leafHashes := make([]chainhash.Hash, 0)
for hash := range b.LeafProofIndex {
leafHashes = append(leafHashes, hash)
}
return leafHashes
}

View File

@@ -0,0 +1,67 @@
package bitcointree_test
import (
"encoding/hex"
"fmt"
"testing"
"github.com/ark-network/ark/common/bitcointree"
"github.com/ark-network/ark/common/descriptor"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/stretchr/testify/require"
)
func TestParseDescriptor(t *testing.T) {
aspKey, err := secp256k1.GeneratePrivateKey()
require.NoError(t, err)
aliceKey, err := secp256k1.GeneratePrivateKey()
require.NoError(t, err)
bobKey, err := secp256k1.GeneratePrivateKey()
require.NoError(t, err)
aspPubKey := hex.EncodeToString(schnorr.SerializePubKey(aspKey.PubKey()))
alicePubKey := hex.EncodeToString(schnorr.SerializePubKey(aliceKey.PubKey()))
bobPubKey := hex.EncodeToString(schnorr.SerializePubKey(bobKey.PubKey()))
unspendableKey := hex.EncodeToString(bitcointree.UnspendableKey().SerializeCompressed())
defaultScriptDescriptor := fmt.Sprintf(
descriptor.DefaultVtxoDescriptorTemplate,
unspendableKey,
alicePubKey,
aspPubKey,
512,
alicePubKey,
)
vtxo, err := bitcointree.ParseVtxoScript(defaultScriptDescriptor)
require.NoError(t, err)
require.IsType(t, &bitcointree.DefaultVtxoScript{}, vtxo)
require.Equal(t, defaultScriptDescriptor, vtxo.ToDescriptor())
require.Equal(t, alicePubKey, hex.EncodeToString(schnorr.SerializePubKey(vtxo.(*bitcointree.DefaultVtxoScript).Owner)))
require.Equal(t, aspPubKey, hex.EncodeToString(schnorr.SerializePubKey(vtxo.(*bitcointree.DefaultVtxoScript).Asp)))
reversibleScriptDescriptor := fmt.Sprintf(
descriptor.ReversibleVtxoScriptTemplate,
unspendableKey,
alicePubKey,
aspPubKey,
512,
alicePubKey,
bobPubKey,
aspPubKey,
)
vtxo, err = bitcointree.ParseVtxoScript(reversibleScriptDescriptor)
require.NoError(t, err)
require.IsType(t, &bitcointree.ReversibleVtxoScript{}, vtxo)
require.Equal(t, reversibleScriptDescriptor, vtxo.ToDescriptor())
require.Equal(t, alicePubKey, hex.EncodeToString(schnorr.SerializePubKey(vtxo.(*bitcointree.ReversibleVtxoScript).Sender)))
require.Equal(t, bobPubKey, hex.EncodeToString(schnorr.SerializePubKey(vtxo.(*bitcointree.ReversibleVtxoScript).Owner)))
require.Equal(t, aspPubKey, hex.EncodeToString(schnorr.SerializePubKey(vtxo.(*bitcointree.ReversibleVtxoScript).Asp)))
}

View File

@@ -8,37 +8,149 @@ import (
"github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/decred/dcrd/dcrec/secp256k1/v4"
) )
const BoardingDescriptorTemplate = "tr(%s,{ and(pk(%s), pk(%s)), and(older(%d), pk(%s)) })" // tr(unspendable, { and(pk(user), pk(asp)), and(older(timeout), pk(user)) })
const DefaultVtxoDescriptorTemplate = "tr(%s,{ and(pk(%s), pk(%s)), and(older(%d), pk(%s)) })"
func ParseBoardingDescriptor( // tr(unspendable, { { and(pk(sender), pk(asp)), and(older(timeout), pk(sender)) }, and(pk(receiver), pk(asp)) })
const ReversibleVtxoScriptTemplate = "tr(%s,{ { and(pk(%s), pk(%s)), and(older(%d), pk(%s)) }, and(pk(%s), pk(%s)) })"
func ParseReversibleVtxoDescriptor(
desc TaprootDescriptor, desc TaprootDescriptor,
) (user *secp256k1.PublicKey, timeout uint, err error) { ) (user, sender, asp *secp256k1.PublicKey, timeout uint, err error) {
if len(desc.ScriptTree) != 3 {
return nil, nil, nil, 0, errors.New("not a reversible vtxo script descriptor")
}
for _, leaf := range desc.ScriptTree { for _, leaf := range desc.ScriptTree {
if andLeaf, ok := leaf.(*And); ok { if andLeaf, ok := leaf.(*And); ok {
if first, ok := andLeaf.First.(*Older); ok { if first, ok := andLeaf.First.(*PK); ok {
timeout = first.Timeout
}
if second, ok := andLeaf.Second.(*PK); ok { if second, ok := andLeaf.Second.(*PK); ok {
keyBytes, err := hex.DecodeString(second.Key.Hex) keyBytes, err := hex.DecodeString(first.Key.Hex)
if err != nil { if err != nil {
return nil, 0, err return nil, nil, nil, 0, err
} }
if sender == nil {
sender, err = schnorr.ParsePubKey(keyBytes)
if err != nil {
return nil, nil, nil, 0, err
}
} else {
user, err = schnorr.ParsePubKey(keyBytes) user, err = schnorr.ParsePubKey(keyBytes)
if err != nil { if err != nil {
return nil, 0, err return nil, nil, nil, 0, err
}
}
if asp == nil {
keyBytes, err = hex.DecodeString(second.Key.Hex)
if err != nil {
return nil, nil, nil, 0, err
}
asp, err = schnorr.ParsePubKey(keyBytes)
if err != nil {
return nil, nil, nil, 0, err
}
}
}
}
if first, ok := andLeaf.First.(*Older); ok {
if second, ok := andLeaf.Second.(*PK); ok {
timeout = first.Timeout
keyBytes, err := hex.DecodeString(second.Key.Hex)
if err != nil {
return nil, nil, nil, 0, err
}
sender, err = schnorr.ParsePubKey(keyBytes)
if err != nil {
return nil, nil, nil, 0, err
}
} }
} }
} }
} }
if user == nil { if user == nil {
return nil, 0, errors.New("boarding descriptor is invalid") return nil, nil, nil, 0, errors.New("descriptor is invalid")
}
if asp == nil {
return nil, nil, nil, 0, errors.New("descriptor is invalid")
} }
if timeout == 0 { if timeout == 0 {
return nil, 0, errors.New("boarding descriptor is invalid") return nil, nil, nil, 0, errors.New("descriptor is invalid")
}
if sender == nil {
return nil, nil, nil, 0, errors.New("descriptor is invalid")
}
return
}
func ParseDefaultVtxoDescriptor(
desc TaprootDescriptor,
) (user, asp *secp256k1.PublicKey, timeout uint, err error) {
if len(desc.ScriptTree) != 2 {
return nil, nil, 0, errors.New("not a default vtxo script descriptor")
}
for _, leaf := range desc.ScriptTree {
if andLeaf, ok := leaf.(*And); ok {
if first, ok := andLeaf.First.(*PK); ok {
if second, ok := andLeaf.Second.(*PK); ok {
keyBytes, err := hex.DecodeString(first.Key.Hex)
if err != nil {
return nil, nil, 0, err
}
user, err = schnorr.ParsePubKey(keyBytes)
if err != nil {
return nil, nil, 0, err
}
keyBytes, err = hex.DecodeString(second.Key.Hex)
if err != nil {
return nil, nil, 0, err
}
asp, err = schnorr.ParsePubKey(keyBytes)
if err != nil {
return nil, nil, 0, err
}
}
}
if first, ok := andLeaf.First.(*Older); ok {
if second, ok := andLeaf.Second.(*PK); ok {
timeout = first.Timeout
keyBytes, err := hex.DecodeString(second.Key.Hex)
if err != nil {
return nil, nil, 0, err
}
user, err = schnorr.ParsePubKey(keyBytes)
if err != nil {
return nil, nil, 0, err
}
}
}
}
}
if user == nil {
return nil, nil, 0, errors.New("boarding descriptor is invalid")
}
if asp == nil {
return nil, nil, 0, errors.New("boarding descriptor is invalid")
}
if timeout == 0 {
return nil, nil, 0, errors.New("boarding descriptor is invalid")
} }
return return

View File

@@ -204,6 +204,9 @@ func (e *And) Script(verify bool) (string, error) {
func parseExpression(policy string) (Expression, error) { func parseExpression(policy string) (Expression, error) {
policy = strings.TrimSpace(policy) policy = strings.TrimSpace(policy)
if policy[0] == '{' {
policy = policy[1:]
}
expressions := make([]Expression, 0) expressions := make([]Expression, 0)
expressions = append(expressions, &PK{}) expressions = append(expressions, &PK{})
expressions = append(expressions, &Older{}) expressions = append(expressions, &Older{})

View File

@@ -41,6 +41,10 @@ func ParseTaprootDescriptor(desc string) (*TaprootDescriptor, error) {
return nil, err return nil, err
} }
for _, scriptStr := range scriptParts { for _, scriptStr := range scriptParts {
if scriptStr == "}" {
continue
}
leaf, err := parseExpression(scriptStr) leaf, err := parseExpression(scriptStr)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@@ -62,7 +62,7 @@ func TestParseTaprootDescriptor(t *testing.T) {
}, },
{ {
name: "Boarding", name: "Boarding",
desc: "tr(0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0,{ and(pk(873079a0091c9b16abd1f8c508320b07f0d50144d09ccd792ce9c915dac60465), pk(79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798)), and(older(604672), pk(79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798)) })", desc: "tr(0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0,{ and(pk(973079a0091c9b16abd1f8c508320b07f0d50144d09ccd792ce9c915dac60465), pk(79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798)), and(older(604672), pk(79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798)) })",
expected: descriptor.TaprootDescriptor{ expected: descriptor.TaprootDescriptor{
InternalKey: descriptor.Key{Hex: "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0"}, InternalKey: descriptor.Key{Hex: "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0"},
ScriptTree: []descriptor.Expression{ ScriptTree: []descriptor.Expression{
@@ -70,7 +70,7 @@ func TestParseTaprootDescriptor(t *testing.T) {
First: &descriptor.PK{ First: &descriptor.PK{
Key: descriptor.XOnlyKey{ Key: descriptor.XOnlyKey{
descriptor.Key{ descriptor.Key{
Hex: "873079a0091c9b16abd1f8c508320b07f0d50144d09ccd792ce9c915dac60465", Hex: "973079a0091c9b16abd1f8c508320b07f0d50144d09ccd792ce9c915dac60465",
}, },
}, },
}, },
@@ -125,16 +125,72 @@ func TestParseTaprootDescriptor(t *testing.T) {
}, },
wantErr: false, wantErr: false,
}, },
{
name: "Reversible VTXO",
desc: "tr(0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0,{ { and(pk(873079a0091c9b16abd1f8c508320b07f0d50144d09ccd792ce9c915dac60465), pk(79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798)), and(older(604672), pk(79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798)) }, {and(pk(873079a0091c9b16abd1f8c508320b07f0d50144d09ccd792ce9c915dac60465), pk(79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798))}})",
expected: descriptor.TaprootDescriptor{
InternalKey: descriptor.Key{Hex: "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0"},
ScriptTree: []descriptor.Expression{
&descriptor.And{
First: &descriptor.PK{
Key: descriptor.XOnlyKey{
descriptor.Key{
Hex: "873079a0091c9b16abd1f8c508320b07f0d50144d09ccd792ce9c915dac60465",
},
},
},
Second: &descriptor.PK{
Key: descriptor.XOnlyKey{
descriptor.Key{
Hex: "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
},
},
},
},
&descriptor.And{
Second: &descriptor.PK{
Key: descriptor.XOnlyKey{
descriptor.Key{
Hex: "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
},
},
},
First: &descriptor.Older{
Timeout: 604672,
},
},
&descriptor.And{
First: &descriptor.PK{
Key: descriptor.XOnlyKey{
descriptor.Key{
Hex: "873079a0091c9b16abd1f8c508320b07f0d50144d09ccd792ce9c915dac60465",
},
},
},
Second: &descriptor.PK{
Key: descriptor.XOnlyKey{
descriptor.Key{
Hex: "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
},
},
},
},
},
},
wantErr: false,
},
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
got, err := descriptor.ParseTaprootDescriptor(tt.desc) got, err := descriptor.ParseTaprootDescriptor(tt.desc)
if (err != nil) != tt.wantErr { if tt.wantErr {
require.Equal(t, tt.wantErr, err != nil, err) require.Error(t, err)
return return
} }
require.Equal(t, tt.expected, got) require.NoError(t, err)
require.NotNil(t, got)
require.Equal(t, tt.expected, *got)
}) })
} }
} }

View File

@@ -1,8 +1,12 @@
package common package common
import ( import (
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcwallet/waddrmgr"
"github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
) )
var TreeTxSize = (&input.TxWeightEstimator{}). var TreeTxSize = (&input.TxWeightEstimator{}).
@@ -19,3 +23,29 @@ var ConnectorTxSize = (&input.TxWeightEstimator{}).
AddP2WKHOutput(). AddP2WKHOutput().
AddP2WKHOutput(). AddP2WKHOutput().
VSize() VSize()
func ComputeForfeitMinRelayFee(feeRate chainfee.SatPerKVByte, vtxoScriptTapTree TaprootTree) (uint64, error) {
txWeightEstimator := &input.TxWeightEstimator{}
biggestVtxoLeafProof, err := BiggestLeafMerkleProof(vtxoScriptTapTree)
if err != nil {
return 0, err
}
ctrlBlock, err := txscript.ParseControlBlock(biggestVtxoLeafProof.ControlBlock)
if err != nil {
return 0, err
}
txWeightEstimator.AddP2PKHInput() // connector input
txWeightEstimator.AddTapscriptInput(
64*2, // forfeit witness = 2 signatures
&waddrmgr.Tapscript{
RevealedScript: biggestVtxoLeafProof.Script,
ControlBlock: ctrlBlock,
},
)
txWeightEstimator.AddP2TROutput() // asp output
return uint64(feeRate.FeeForVSize(lntypes.VByte(txWeightEstimator.VSize())).ToUnit(btcutil.AmountSatoshi)), nil
}

View File

@@ -8,6 +8,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/lightningnetwork/lnd v0.18.2-beta github.com/lightningnetwork/lnd v0.18.2-beta
github.com/stretchr/testify v1.9.0 github.com/stretchr/testify v1.9.0
@@ -17,7 +18,6 @@ require (
dario.cat/mergo v1.0.1 // indirect dario.cat/mergo v1.0.1 // indirect
github.com/aead/siphash v1.0.1 // indirect github.com/aead/siphash v1.0.1 // 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 v0.16.10-0.20240718224643-db3a4a2543bd // indirect
github.com/btcsuite/btcwallet/wallet/txauthor v1.3.4 // indirect github.com/btcsuite/btcwallet/wallet/txauthor v1.3.4 // indirect
github.com/btcsuite/btcwallet/wallet/txrules v1.2.1 // indirect github.com/btcsuite/btcwallet/wallet/txrules v1.2.1 // indirect
github.com/btcsuite/btcwallet/wallet/txsizes v1.2.4 // indirect github.com/btcsuite/btcwallet/wallet/txsizes v1.2.4 // indirect
@@ -25,16 +25,19 @@ require (
github.com/btcsuite/btcwallet/wtxmgr v1.5.3 // indirect github.com/btcsuite/btcwallet/wtxmgr v1.5.3 // 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/coreos/go-semver v0.3.1 // indirect github.com/coreos/go-semver v0.3.1 // indirect
github.com/coreos/go-systemd/v22 v22.5.0 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect
github.com/decred/dcrd/crypto/blake256 v1.0.1 // indirect github.com/decred/dcrd/crypto/blake256 v1.0.1 // indirect
github.com/decred/dcrd/lru v1.1.3 // indirect github.com/decred/dcrd/lru v1.1.3 // indirect
github.com/docker/cli v27.1.1+incompatible // indirect github.com/docker/cli v27.1.1+incompatible // indirect
github.com/fergusstrange/embedded-postgres v1.28.0 // indirect github.com/fergusstrange/embedded-postgres v1.28.0 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/go-errors/errors v1.5.1 // indirect github.com/go-errors/errors v1.5.1 // indirect
github.com/go-viper/mapstructure/v2 v2.1.0 // indirect github.com/go-viper/mapstructure/v2 v2.1.0 // indirect
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
github.com/golang-migrate/migrate/v4 v4.17.1 // indirect github.com/golang-migrate/migrate/v4 v4.17.1 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/btree v1.1.2 // indirect github.com/google/btree v1.1.2 // indirect
github.com/gorilla/websocket v1.5.3 // indirect github.com/gorilla/websocket v1.5.3 // indirect
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect
@@ -46,6 +49,7 @@ require (
github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jessevdk/go-flags v1.6.1 // indirect github.com/jessevdk/go-flags v1.6.1 // indirect
github.com/jonboulle/clockwork v0.4.0 // indirect github.com/jonboulle/clockwork v0.4.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/cpuid/v2 v2.2.8 // indirect github.com/klauspost/cpuid/v2 v2.2.8 // indirect
github.com/kr/pretty v0.3.1 // indirect github.com/kr/pretty v0.3.1 // indirect
@@ -64,6 +68,7 @@ require (
github.com/lightningnetwork/lnd/tor v1.1.3 // indirect github.com/lightningnetwork/lnd/tor v1.1.3 // indirect
github.com/ltcsuite/ltcd v0.23.5 // indirect github.com/ltcsuite/ltcd v0.23.5 // indirect
github.com/miekg/dns v1.1.61 // indirect github.com/miekg/dns v1.1.61 // indirect
github.com/onsi/ginkgo v1.16.4 // indirect
github.com/ory/dockertest/v3 v3.11.0 // indirect github.com/ory/dockertest/v3 v3.11.0 // indirect
github.com/prometheus/client_golang v1.19.1 // indirect github.com/prometheus/client_golang v1.19.1 // indirect
github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/client_model v0.6.1 // indirect
@@ -71,6 +76,7 @@ require (
github.com/prometheus/procfs v0.15.1 // indirect github.com/prometheus/procfs v0.15.1 // indirect
github.com/rogpeppe/go-internal v1.12.0 // indirect github.com/rogpeppe/go-internal v1.12.0 // indirect
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/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75 // indirect github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75 // indirect
github.com/vulpemventures/fastsha256 v0.0.0-20160815193821-637e65642941 // indirect github.com/vulpemventures/fastsha256 v0.0.0-20160815193821-637e65642941 // indirect
github.com/xiang90/probing v0.0.0-20221125231312-a49e3df8f510 // indirect github.com/xiang90/probing v0.0.0-20221125231312-a49e3df8f510 // indirect

View File

@@ -35,6 +35,7 @@ github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/fergusstrange/embedded-postgres v1.28.0 h1:Atixd24HCuBHBavnG4eiZAjRizOViwUahKGSjJdz1SU= github.com/fergusstrange/embedded-postgres v1.28.0 h1:Atixd24HCuBHBavnG4eiZAjRizOViwUahKGSjJdz1SU=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk= github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
@@ -45,6 +46,7 @@ github.com/golang-migrate/migrate/v4 v4.17.1 h1:4zQ6iqL6t6AiItphxJctQb3cFqWiSpMn
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
@@ -101,6 +103,9 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc=
github.com/onsi/gomega v1.26.0 h1:03cDLK28U6hWvCAns6NeydX3zIm4SF3ci69ulidS32Q=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
github.com/opencontainers/runc v1.1.13 h1:98S2srgG9vw0zWcDpFMn5TRrh8kLxa/5OFUstuUhmRs= github.com/opencontainers/runc v1.1.13 h1:98S2srgG9vw0zWcDpFMn5TRrh8kLxa/5OFUstuUhmRs=
@@ -164,6 +169,7 @@ google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
lukechampine.com/blake3 v1.3.0 h1:sJ3XhFINmHSrYCgl958hscfIa3bw8x4DqMP3u1YvoYE= lukechampine.com/blake3 v1.3.0 h1:sJ3XhFINmHSrYCgl958hscfIa3bw8x4DqMP3u1YvoYE=

View File

@@ -1,9 +1,9 @@
package tree package tree
import ( import (
"encoding/hex"
"fmt" "fmt"
"github.com/ark-network/ark/common"
"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"
@@ -14,13 +14,13 @@ import (
func CraftCongestionTree( func CraftCongestionTree(
asset string, aspPubkey *secp256k1.PublicKey, receivers []Receiver, asset string, aspPubkey *secp256k1.PublicKey, receivers []Receiver,
feeSatsPerNode uint64, roundLifetime, unilateralExitDelay int64, feeSatsPerNode uint64, roundLifetime int64,
) ( ) (
buildCongestionTree TreeFactory, buildCongestionTree TreeFactory,
sharedOutputScript []byte, sharedOutputAmount uint64, err error, sharedOutputScript []byte, sharedOutputAmount uint64, err error,
) { ) {
root, err := createPartialCongestionTree( root, err := createPartialCongestionTree(
asset, aspPubkey, receivers, feeSatsPerNode, roundLifetime, unilateralExitDelay, asset, aspPubkey, receivers, feeSatsPerNode, roundLifetime,
) )
if err != nil { if err != nil {
return return
@@ -49,7 +49,6 @@ type node struct {
asset string asset string
feeSats uint64 feeSats uint64
roundLifetime int64 roundLifetime int64
unilateralExitDelay int64
_inputTaprootKey *secp256k1.PublicKey _inputTaprootKey *secp256k1.PublicKey
_inputTaprootTree *taproot.IndexedElementsTapScriptTree _inputTaprootTree *taproot.IndexedElementsTapScriptTree
@@ -242,53 +241,15 @@ func (n *node) getWitnessData() (
} }
func (n *node) getVtxoWitnessData() ( func (n *node) getVtxoWitnessData() (
*secp256k1.PublicKey, *taproot.IndexedElementsTapScriptTree, error, *secp256k1.PublicKey, common.TaprootTree, error,
) { ) {
if !n.isLeaf() { if !n.isLeaf() {
return nil, nil, fmt.Errorf("cannot call vtxoWitness on a non-leaf node") return nil, nil, fmt.Errorf("cannot call vtxoWitness on a non-leaf node")
} }
key, err := hex.DecodeString(n.receivers[0].Pubkey) receiver := n.receivers[0]
if err != nil {
return nil, nil, err
}
pubkey, err := secp256k1.ParsePubKey(key) return receiver.Script.TapTree()
if err != nil {
return nil, nil, err
}
redeemClosure := &CSVSigClosure{
Pubkey: pubkey,
Seconds: uint(n.unilateralExitDelay),
}
redeemLeaf, err := redeemClosure.Leaf()
if err != nil {
return nil, nil, err
}
forfeitClosure := &ForfeitClosure{
Pubkey: pubkey,
AspPubkey: n.sweepKey,
}
forfeitLeaf, err := forfeitClosure.Leaf()
if err != nil {
return nil, nil, err
}
leafTaprootTree := taproot.AssembleTaprootScriptTree(
*redeemLeaf, *forfeitLeaf,
)
root := leafTaprootTree.RootNode.TapHash()
taprootKey := taproot.ComputeTaprootOutputKey(
UnspendableKey(),
root[:],
)
return taprootKey, leafTaprootTree, nil
} }
func (n *node) getTreeNode( func (n *node) getTreeNode(
@@ -413,7 +374,7 @@ func (n *node) createFinalCongestionTree() TreeFactory {
func createPartialCongestionTree( func createPartialCongestionTree(
asset string, aspPubkey *secp256k1.PublicKey, receivers []Receiver, asset string, aspPubkey *secp256k1.PublicKey, receivers []Receiver,
feeSatsPerNode uint64, roundLifetime, unilateralExitDelay int64, feeSatsPerNode uint64, roundLifetime int64,
) (root *node, err error) { ) (root *node, err error) {
if len(receivers) == 0 { if len(receivers) == 0 {
return nil, fmt.Errorf("no receivers provided") return nil, fmt.Errorf("no receivers provided")
@@ -427,7 +388,6 @@ func createPartialCongestionTree(
asset: asset, asset: asset,
feeSats: feeSatsPerNode, feeSats: feeSatsPerNode,
roundLifetime: roundLifetime, roundLifetime: roundLifetime,
unilateralExitDelay: unilateralExitDelay,
} }
nodes = append(nodes, leafNode) nodes = append(nodes, leafNode)
} }

View File

@@ -1,31 +1,26 @@
package txbuilder package tree
import ( import (
"context" "github.com/ark-network/ark/common"
"github.com/ark-network/ark/common/tree"
"github.com/ark-network/ark/server/internal/core/domain"
"github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcwallet/waddrmgr" "github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/lightningnetwork/lnd/input"
"github.com/vulpemventures/go-elements/elementsutil" "github.com/vulpemventures/go-elements/elementsutil"
"github.com/vulpemventures/go-elements/psetv2" "github.com/vulpemventures/go-elements/psetv2"
"github.com/vulpemventures/go-elements/taproot"
"github.com/vulpemventures/go-elements/transaction" "github.com/vulpemventures/go-elements/transaction"
) )
func (b *txBuilder) craftForfeitTxs( func BuildForfeitTxs(
connectorTx *psetv2.Pset, connectorTx *psetv2.Pset,
connectorAmount uint64, vtxoInput psetv2.InputArgs,
vtxo domain.Vtxo, vtxoAmount,
vtxoForfeitTapleaf taproot.TapscriptElementsProof, connectorAmount,
vtxoScript, aspScript []byte, feeAmount uint64,
) (forfeitTxs []string, err error) { vtxoScript []byte,
aspPubKey *secp256k1.PublicKey,
) (forfeitTxs []*psetv2.Pset, err error) {
connectors, prevouts := getConnectorInputs(connectorTx, connectorAmount) connectors, prevouts := getConnectorInputs(connectorTx, connectorAmount)
for i, connectorInput := range connectors { for i, connectorInput := range connectors {
weightEstimator := &input.TxWeightEstimator{}
connectorPrevout := prevouts[i] connectorPrevout := prevouts[i]
asset := elementsutil.AssetHashFromBytes(connectorPrevout.Asset) asset := elementsutil.AssetHashFromBytes(connectorPrevout.Asset)
@@ -39,13 +34,6 @@ func (b *txBuilder) craftForfeitTxs(
return nil, err return nil, err
} }
vtxoInput := psetv2.InputArgs{
Txid: vtxo.Txid,
TxIndex: vtxo.VOut,
}
vtxoAmount, _ := elementsutil.ValueToBytes(vtxo.Amount)
if err := updater.AddInputs([]psetv2.InputArgs{connectorInput, vtxoInput}); err != nil { if err := updater.AddInputs([]psetv2.InputArgs{connectorInput, vtxoInput}); err != nil {
return nil, err return nil, err
} }
@@ -58,9 +46,12 @@ func (b *txBuilder) craftForfeitTxs(
return nil, err return nil, err
} }
weightEstimator.AddP2WKHInput() amountBytes, err := elementsutil.ValueToBytes(vtxoAmount)
if err != nil {
return nil, err
}
vtxoPrevout := transaction.NewTxOutput(connectorPrevout.Asset, vtxoAmount, vtxoScript) vtxoPrevout := transaction.NewTxOutput(connectorPrevout.Asset, amountBytes, vtxoScript)
if err = updater.AddInWitnessUtxo(1, vtxoPrevout); err != nil { if err = updater.AddInWitnessUtxo(1, vtxoPrevout); err != nil {
return nil, err return nil, err
@@ -70,27 +61,7 @@ func (b *txBuilder) craftForfeitTxs(
return nil, err return nil, err
} }
unspendableKey := tree.UnspendableKey() aspScript, err := common.P2TRScript(aspPubKey)
tapScript := psetv2.NewTapLeafScript(vtxoForfeitTapleaf, unspendableKey)
if err := updater.AddInTapLeafScript(1, tapScript); err != nil {
return nil, err
}
weightEstimator.AddTapscriptInput(64*2, &waddrmgr.Tapscript{
ControlBlock: &tapScript.ControlBlock.ControlBlock,
RevealedScript: tapScript.TapLeaf.Script,
})
connectorAmount, err := elementsutil.ValueFromBytes(connectorPrevout.Value)
if err != nil {
return nil, err
}
weightEstimator.AddP2WKHOutput()
weightEstimator.AddP2WKHOutput()
feeAmount, err := b.wallet.MinRelayFee(context.Background(), uint64(weightEstimator.VSize()))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -98,7 +69,7 @@ func (b *txBuilder) craftForfeitTxs(
err = updater.AddOutputs([]psetv2.OutputArgs{ err = updater.AddOutputs([]psetv2.OutputArgs{
{ {
Asset: asset, Asset: asset,
Amount: vtxo.Amount + connectorAmount - feeAmount, Amount: vtxoAmount + connectorAmount - feeAmount,
Script: aspScript, Script: aspScript,
}, },
{ {
@@ -110,12 +81,28 @@ func (b *txBuilder) craftForfeitTxs(
return nil, err return nil, err
} }
tx, err := pset.ToBase64() forfeitTxs = append(forfeitTxs, pset)
if err != nil {
return nil, err
}
forfeitTxs = append(forfeitTxs, tx)
} }
return forfeitTxs, nil return forfeitTxs, nil
} }
func getConnectorInputs(pset *psetv2.Pset, connectorAmount uint64) ([]psetv2.InputArgs, []*transaction.TxOutput) {
txID, _ := getPsetId(pset)
inputs := make([]psetv2.InputArgs, 0, len(pset.Outputs))
witnessUtxos := make([]*transaction.TxOutput, 0, len(pset.Outputs))
for i, output := range pset.Outputs {
utx, _ := pset.UnsignedTx()
if output.Value == connectorAmount && len(output.Script) > 0 {
inputs = append(inputs, psetv2.InputArgs{
Txid: txID,
TxIndex: uint32(i),
})
witnessUtxos = append(witnessUtxos, utx.Outputs[i])
}
}
return inputs, witnessUtxos
}

View File

@@ -10,9 +10,6 @@ import (
"github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/txscript"
"github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/vulpemventures/go-elements/address"
"github.com/vulpemventures/go-elements/network"
"github.com/vulpemventures/go-elements/payment"
"github.com/vulpemventures/go-elements/taproot" "github.com/vulpemventures/go-elements/taproot"
) )
@@ -40,7 +37,7 @@ type CSVSigClosure struct {
Seconds uint Seconds uint
} }
type ForfeitClosure struct { type MultisigClosure struct {
Pubkey *secp256k1.PublicKey Pubkey *secp256k1.PublicKey
AspPubkey *secp256k1.PublicKey AspPubkey *secp256k1.PublicKey
} }
@@ -58,7 +55,7 @@ func DecodeClosure(script []byte) (Closure, error) {
return closure, nil return closure, nil
} }
closure = &ForfeitClosure{} closure = &MultisigClosure{}
if valid, err := closure.Decode(script); err == nil && valid { if valid, err := closure.Decode(script); err == nil && valid {
return closure, nil return closure, nil
} }
@@ -66,7 +63,7 @@ func DecodeClosure(script []byte) (Closure, error) {
return nil, fmt.Errorf("invalid closure script %s", hex.EncodeToString(script)) return nil, fmt.Errorf("invalid closure script %s", hex.EncodeToString(script))
} }
func (f *ForfeitClosure) Leaf() (*taproot.TapElementsLeaf, error) { func (f *MultisigClosure) Leaf() (*taproot.TapElementsLeaf, error) {
aspKeyBytes := schnorr.SerializePubKey(f.AspPubkey) aspKeyBytes := schnorr.SerializePubKey(f.AspPubkey)
userKeyBytes := schnorr.SerializePubKey(f.Pubkey) userKeyBytes := schnorr.SerializePubKey(f.Pubkey)
@@ -81,7 +78,7 @@ func (f *ForfeitClosure) Leaf() (*taproot.TapElementsLeaf, error) {
return &tapLeaf, nil return &tapLeaf, nil
} }
func (f *ForfeitClosure) Decode(script []byte) (bool, error) { func (f *MultisigClosure) Decode(script []byte) (bool, error) {
valid, aspPubKey, err := decodeChecksigScript(script) valid, aspPubKey, err := decodeChecksigScript(script)
if err != nil { if err != nil {
return false, err return false, err
@@ -284,59 +281,6 @@ func (c *UnrollClosure) Decode(script []byte) (valid bool, err error) {
return true, nil return true, nil
} }
func ComputeVtxoTaprootScript(
userPubkey, aspPubkey *secp256k1.PublicKey, exitDelay uint, net network.Network,
) (*secp256k1.PublicKey, *taproot.TapscriptElementsProof, []byte, string, error) {
redeemClosure := &CSVSigClosure{
Pubkey: userPubkey,
Seconds: exitDelay,
}
forfeitClosure := &ForfeitClosure{
Pubkey: userPubkey,
AspPubkey: aspPubkey,
}
redeemLeaf, err := redeemClosure.Leaf()
if err != nil {
return nil, nil, nil, "", err
}
forfeitLeaf, err := forfeitClosure.Leaf()
if err != nil {
return nil, nil, nil, "", err
}
vtxoTaprootTree := taproot.AssembleTaprootScriptTree(
*redeemLeaf, *forfeitLeaf,
)
root := vtxoTaprootTree.RootNode.TapHash()
unspendableKey := UnspendableKey()
vtxoTaprootKey := taproot.ComputeTaprootOutputKey(unspendableKey, root[:])
redeemLeafHash := redeemLeaf.TapHash()
proofIndex := vtxoTaprootTree.LeafProofIndex[redeemLeafHash]
proof := vtxoTaprootTree.LeafMerkleProofs[proofIndex]
pay, err := payment.FromTweakedKey(vtxoTaprootKey, &net, nil)
if err != nil {
return nil, nil, nil, "", err
}
addr, err := pay.TaprootAddress()
if err != nil {
return nil, nil, nil, "", err
}
script, err := address.ToOutputScript(addr)
if err != nil {
return nil, nil, nil, "", err
}
return vtxoTaprootKey, &proof, script, addr, nil
}
func decodeIntrospectionScript( func decodeIntrospectionScript(
script []byte, expectedIndex byte, isVerify bool, script []byte, expectedIndex byte, isVerify bool,
) (bool, *secp256k1.PublicKey, uint64, error) { ) (bool, *secp256k1.PublicKey, uint64, error) {

View File

@@ -1,10 +1,12 @@
package tree package tree
import "github.com/vulpemventures/go-elements/psetv2" import (
"github.com/vulpemventures/go-elements/psetv2"
)
type TreeFactory func(outpoint psetv2.InputArgs) (CongestionTree, error) type TreeFactory func(outpoint psetv2.InputArgs) (CongestionTree, error)
type Receiver struct { type Receiver struct {
Pubkey string Script VtxoScript
Amount uint64 Amount uint64
} }

131
common/tree/vtxo.go Normal file
View File

@@ -0,0 +1,131 @@
package tree
import (
"encoding/hex"
"fmt"
"github.com/ark-network/ark/common"
"github.com/ark-network/ark/common/descriptor"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/vulpemventures/go-elements/taproot"
)
type VtxoScript common.VtxoScript[elementsTapTree]
func ParseVtxoScript(desc string) (VtxoScript, error) {
v := &DefaultVtxoScript{}
// TODO add other type
err := v.FromDescriptor(desc)
return v, err
}
/*
* DefaultVtxoScript is the default implementation of VTXO with 2 closures
* - Owner and ASP (forfeit)
* - Owner after t (unilateral exit)
*/
type DefaultVtxoScript struct {
Owner *secp256k1.PublicKey
Asp *secp256k1.PublicKey
ExitDelay uint
}
func (v *DefaultVtxoScript) ToDescriptor() string {
owner := hex.EncodeToString(schnorr.SerializePubKey(v.Owner))
return fmt.Sprintf(
descriptor.DefaultVtxoDescriptorTemplate,
hex.EncodeToString(UnspendableKey().SerializeCompressed()),
owner,
hex.EncodeToString(schnorr.SerializePubKey(v.Asp)),
v.ExitDelay,
owner,
)
}
func (v *DefaultVtxoScript) FromDescriptor(desc string) error {
taprootDesc, err := descriptor.ParseTaprootDescriptor(desc)
if err != nil {
return err
}
owner, asp, exitDelay, err := descriptor.ParseDefaultVtxoDescriptor(*taprootDesc)
if err != nil {
return err
}
v.Owner = owner
v.Asp = asp
v.ExitDelay = exitDelay
return nil
}
func (v *DefaultVtxoScript) TapTree() (*secp256k1.PublicKey, elementsTapTree, error) {
redeemClosure := &CSVSigClosure{
Pubkey: v.Owner,
Seconds: v.ExitDelay,
}
redeemLeaf, err := redeemClosure.Leaf()
if err != nil {
return nil, elementsTapTree{}, err
}
forfeitClosure := &MultisigClosure{
Pubkey: v.Owner,
AspPubkey: v.Asp,
}
forfeitLeaf, err := forfeitClosure.Leaf()
if err != nil {
return nil, elementsTapTree{}, err
}
tapTree := taproot.AssembleTaprootScriptTree(
*redeemLeaf, *forfeitLeaf,
)
root := tapTree.RootNode.TapHash()
taprootKey := taproot.ComputeTaprootOutputKey(UnspendableKey(), root[:])
return taprootKey, elementsTapTree{tapTree}, nil
}
// elementsTapTree wraps the IndexedElementsTapScriptTree to implement the common.TaprootTree interface
type elementsTapTree struct {
*taproot.IndexedElementsTapScriptTree
}
func (b elementsTapTree) GetRoot() chainhash.Hash {
return b.RootNode.TapHash()
}
func (b elementsTapTree) GetTaprootMerkleProof(leafhash chainhash.Hash) (*common.TaprootMerkleProof, error) {
index, ok := b.LeafProofIndex[leafhash]
if !ok {
return nil, fmt.Errorf("leaf %s not found in taproot tree", leafhash.String())
}
proof := b.LeafMerkleProofs[index]
controlBlock := proof.ToControlBlock(UnspendableKey())
controlBlockBytes, err := controlBlock.ToBytes()
if err != nil {
return nil, err
}
return &common.TaprootMerkleProof{
ControlBlock: controlBlockBytes,
Script: proof.Script,
}, nil
}
func (b elementsTapTree) GetLeaves() []chainhash.Hash {
hashes := make([]chainhash.Hash, 0)
for h := range b.LeafProofIndex {
hashes = append(hashes, h)
}
return hashes
}

11
common/utils.go Normal file
View File

@@ -0,0 +1,11 @@
package common
import (
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/txscript"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
)
func P2TRScript(taprootKey *secp256k1.PublicKey) ([]byte, error) {
return txscript.NewScriptBuilder().AddOp(txscript.OP_1).AddData(schnorr.SerializePubKey(taprootKey)).Script()
}

62
common/vtxo.go Normal file
View File

@@ -0,0 +1,62 @@
package common
import (
"errors"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
)
var (
ErrWrongDescriptor = errors.New("wrong descriptor, cannot parse vtxo script")
)
type TaprootMerkleProof struct {
ControlBlock []byte
Script []byte
}
// TaprootTree is an interface wrapping the methods needed to spend a vtxo taproot contract
// the implementation depends on the chain (liquid or bitcoin)
type TaprootTree interface {
GetLeaves() []chainhash.Hash
GetTaprootMerkleProof(leafhash chainhash.Hash) (*TaprootMerkleProof, error)
GetRoot() chainhash.Hash
}
/*
A vtxo script is defined as a taproot contract with at least 1 forfeit closure (User && ASP) and 1 exit closure (A after t).
It may also contain others closures implementing specific use cases.
VtxoScript abstracts the taproot complexity behind vtxo contracts.
it is compiled, transferred and parsed using descriptor string.
default vtxo script = tr(_,{ and(pk(USER), pk(ASP)), and(older(T), pk(USER)) })
reversible vtxo script = tr(_,{ { and(pk(SENDER), pk(ASP)), and(older(T), pk(SENDER)) }, { and(pk(RECEIVER), pk(ASP) } })
*/
type VtxoScript[T TaprootTree] interface {
TapTree() (taprootKey *secp256k1.PublicKey, taprootScriptTree T, err error)
ToDescriptor() string
FromDescriptor(descriptor string) error
}
// BiggestLeafMerkleProof returns the leaf with the biggest witness size (for fee estimation)
// we need this to estimate the fee without knowning the exact leaf that will be spent
func BiggestLeafMerkleProof(t TaprootTree) (*TaprootMerkleProof, error) {
var biggest *TaprootMerkleProof
var biggestSize int
for _, leaf := range t.GetLeaves() {
proof, err := t.GetTaprootMerkleProof(leaf)
if err != nil {
return nil, err
}
if len(proof.ControlBlock)+len(proof.Script) > biggestSize {
biggest = proof
biggestSize = len(proof.ControlBlock) + len(proof.Script)
}
}
return biggest, nil
}

View File

@@ -418,6 +418,7 @@ github.com/ark-network/ark/common v0.0.0-20240812222508-b097e943fb45/go.mod h1:8
github.com/ark-network/ark/common v0.0.0-20240812233307-18e343b31899/go.mod h1:8DYeb06Dl8onmrV09xfsdDMGv5HoVtWoKhLBLXOYHew= github.com/ark-network/ark/common v0.0.0-20240812233307-18e343b31899/go.mod h1:8DYeb06Dl8onmrV09xfsdDMGv5HoVtWoKhLBLXOYHew=
github.com/ark-network/ark/common v0.0.0-20240815203029-edc4534dfc87/go.mod h1:aYAGDfoeBLofnZt9n85wusFyCkrS7hvwdo5TynBlkuY= github.com/ark-network/ark/common v0.0.0-20240815203029-edc4534dfc87/go.mod h1:aYAGDfoeBLofnZt9n85wusFyCkrS7hvwdo5TynBlkuY=
github.com/ark-network/ark/pkg/client-sdk v0.0.0-20240812230256-910716f72d1a/go.mod h1:avKeK73ezowttW3PaycYB4mChaqigAxr4q8pFwIuHww= github.com/ark-network/ark/pkg/client-sdk v0.0.0-20240812230256-910716f72d1a/go.mod h1:avKeK73ezowttW3PaycYB4mChaqigAxr4q8pFwIuHww=
github.com/ark-network/ark/pkg/client-sdk v0.0.0-20240913171921-2174e4b04d86/go.mod h1:CRN5aL3u3Q/3tCQLp/ND7NT34/GRsG1ccLk5aX2r7mQ=
github.com/ark-network/ark/server/pkg/kvdb v0.0.0-20240812230256-910716f72d1a/go.mod h1:ivr4Qm16kbJMTovsdscYiV1s1vPOYmEBtp9EgrHFGi4= github.com/ark-network/ark/server/pkg/kvdb v0.0.0-20240812230256-910716f72d1a/go.mod h1:ivr4Qm16kbJMTovsdscYiV1s1vPOYmEBtp9EgrHFGi4=
github.com/ark-network/ark/server/pkg/macaroons v0.0.0-20240812230256-910716f72d1a/go.mod h1:OtZoQaSumPsVKWq/OkduHZdpAutQYaB2yVf1rlm6vI4= github.com/ark-network/ark/server/pkg/macaroons v0.0.0-20240812230256-910716f72d1a/go.mod h1:OtZoQaSumPsVKWq/OkduHZdpAutQYaB2yVf1rlm6vI4=
github.com/ark-network/ark/server/pkg/macaroons v0.0.0-20240812233307-18e343b31899/go.mod h1:OtZoQaSumPsVKWq/OkduHZdpAutQYaB2yVf1rlm6vI4= github.com/ark-network/ark/server/pkg/macaroons v0.0.0-20240812233307-18e343b31899/go.mod h1:OtZoQaSumPsVKWq/OkduHZdpAutQYaB2yVf1rlm6vI4=
@@ -562,6 +563,7 @@ github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KE
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/gobuffalo/here v0.6.0 h1:hYrd0a6gDmWxBM4TnrGw8mQg24iSVoIkHEk7FodQcBI= github.com/gobuffalo/here v0.6.0 h1:hYrd0a6gDmWxBM4TnrGw8mQg24iSVoIkHEk7FodQcBI=
github.com/gobuffalo/here v0.6.0/go.mod h1:wAG085dHOYqUpf+Ap+WOdrPTp5IYcDAs/x7PLa8Y5fM= github.com/gobuffalo/here v0.6.0/go.mod h1:wAG085dHOYqUpf+Ap+WOdrPTp5IYcDAs/x7PLa8Y5fM=
github.com/goccy/go-json v0.9.11 h1:/pAaQDLHEoCq/5FFmSKBswWmK6H0e8g4159Kc/X/nqk= github.com/goccy/go-json v0.9.11 h1:/pAaQDLHEoCq/5FFmSKBswWmK6H0e8g4159Kc/X/nqk=
@@ -1040,6 +1042,7 @@ golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -1083,6 +1086,7 @@ golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=

View File

@@ -7,6 +7,7 @@ import (
"github.com/ark-network/ark/common/bitcointree" "github.com/ark-network/ark/common/bitcointree"
"github.com/ark-network/ark/common/tree" "github.com/ark-network/ark/common/tree"
"github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
) )
const ( const (
@@ -37,7 +38,7 @@ type ASPClient interface {
ctx context.Context, signedForfeitTxs []string, signedRoundTx string, ctx context.Context, signedForfeitTxs []string, signedRoundTx string,
) error ) error
CreatePayment( CreatePayment(
ctx context.Context, inputs []VtxoKey, outputs []Output, ctx context.Context, inputs []Input, outputs []Output,
) (string, []string, error) ) (string, []string, error)
CompletePayment( CompletePayment(
ctx context.Context, signedRedeemTx string, signedUnconditionalForfeitTxs []string, ctx context.Context, signedRedeemTx string, signedUnconditionalForfeitTxs []string,
@@ -67,40 +68,19 @@ type RoundEventChannel struct {
Err error Err error
} }
type Input interface { type Outpoint struct {
GetTxID() string
GetVOut() uint32
GetDescriptor() string
}
type VtxoKey struct {
Txid string Txid string
VOut uint32 VOut uint32
} }
func (k VtxoKey) GetTxID() string { type Input struct {
return k.Txid Outpoint
}
func (k VtxoKey) GetVOut() uint32 {
return k.VOut
}
func (k VtxoKey) GetDescriptor() string {
return ""
}
type BoardingInput struct {
VtxoKey
Descriptor string Descriptor string
} }
func (k BoardingInput) GetDescriptor() string {
return k.Descriptor
}
type Vtxo struct { type Vtxo struct {
VtxoKey Outpoint
Descriptor string
Amount uint64 Amount uint64
RoundTxid string RoundTxid string
ExpiresAt *time.Time ExpiresAt *time.Time
@@ -111,7 +91,8 @@ type Vtxo struct {
} }
type Output struct { type Output struct {
Address string Address string // onchain output address
Descriptor string // offchain vtxo descriptor
Amount uint64 Amount uint64
} }
@@ -154,9 +135,9 @@ type Round struct {
type RoundFinalizationEvent struct { type RoundFinalizationEvent struct {
ID string ID string
Tx string Tx string
ForfeitTxs []string
Tree tree.CongestionTree Tree tree.CongestionTree
Connectors []string Connectors []string
MinRelayFeeRate chainfee.SatPerKVByte
} }
func (e RoundFinalizationEvent) isRoundEvent() {} func (e RoundFinalizationEvent) isRoundEvent() {}

View File

@@ -14,6 +14,7 @@ import (
"github.com/ark-network/ark/pkg/client-sdk/client" "github.com/ark-network/ark/pkg/client-sdk/client"
"github.com/ark-network/ark/pkg/client-sdk/internal/utils" "github.com/ark-network/ark/pkg/client-sdk/internal/utils"
"github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
"google.golang.org/grpc" "google.golang.org/grpc"
"google.golang.org/grpc/credentials" "google.golang.org/grpc/credentials"
"google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/credentials/insecure"
@@ -202,15 +203,10 @@ func (a *grpcClient) FinalizePayment(
} }
func (a *grpcClient) CreatePayment( func (a *grpcClient) CreatePayment(
ctx context.Context, inputs []client.VtxoKey, outputs []client.Output, ctx context.Context, inputs []client.Input, outputs []client.Output,
) (string, []string, error) { ) (string, []string, error) {
insCast := make([]client.Input, 0, len(inputs))
for _, in := range inputs {
insCast = append(insCast, in)
}
req := &arkv1.CreatePaymentRequest{ req := &arkv1.CreatePaymentRequest{
Inputs: ins(insCast).toProto(), Inputs: ins(inputs).toProto(),
Outputs: outs(outputs).toProto(), Outputs: outs(outputs).toProto(),
} }
resp, err := a.svc.CreatePayment(ctx, req) resp, err := a.svc.CreatePayment(ctx, req)
@@ -323,8 +319,19 @@ func (a *grpcClient) SendTreeSignatures(
type out client.Output type out client.Output
func (o out) toProto() *arkv1.Output { func (o out) toProto() *arkv1.Output {
if len(o.Address) > 0 {
return &arkv1.Output{ return &arkv1.Output{
Destination: &arkv1.Output_Address{
Address: o.Address, Address: o.Address,
},
Amount: o.Amount,
}
}
return &arkv1.Output{
Destination: &arkv1.Output_Descriptor_{
Descriptor_: o.Descriptor,
},
Amount: o.Amount, Amount: o.Amount,
} }
} }
@@ -364,9 +371,9 @@ func (e event) toRoundEvent() (client.RoundEvent, error) {
return client.RoundFinalizationEvent{ return client.RoundFinalizationEvent{
ID: ee.GetId(), ID: ee.GetId(),
Tx: ee.GetPoolTx(), Tx: ee.GetPoolTx(),
ForfeitTxs: ee.GetForfeitTxs(),
Tree: tree, Tree: tree,
Connectors: ee.GetConnectors(), Connectors: ee.GetConnectors(),
MinRelayFeeRate: chainfee.SatPerKVByte(ee.MinRelayFeeRate),
}, nil }, nil
} }
@@ -430,17 +437,18 @@ func (v vtxo) toVtxo() client.Vtxo {
uncondForfeitTxs = v.GetPendingData().GetUnconditionalForfeitTxs() uncondForfeitTxs = v.GetPendingData().GetUnconditionalForfeitTxs()
} }
return client.Vtxo{ return client.Vtxo{
VtxoKey: client.VtxoKey{ Outpoint: client.Outpoint{
Txid: v.GetOutpoint().GetVtxoInput().GetTxid(), Txid: v.GetOutpoint().GetTxid(),
VOut: v.GetOutpoint().GetVtxoInput().GetVout(), VOut: v.GetOutpoint().GetVout(),
}, },
Amount: v.GetReceiver().GetAmount(), Amount: v.GetAmount(),
RoundTxid: v.GetPoolTxid(), RoundTxid: v.GetPoolTxid(),
ExpiresAt: expiresAt, ExpiresAt: expiresAt,
Pending: v.GetPending(), Pending: v.GetPending(),
RedeemTx: redeemTx, RedeemTx: redeemTx,
UnconditionalForfeitTxs: uncondForfeitTxs, UnconditionalForfeitTxs: uncondForfeitTxs,
SpentBy: v.GetSpentBy(), SpentBy: v.GetSpentBy(),
Descriptor: v.GetDescriptor_(),
} }
} }
@@ -455,25 +463,12 @@ func (v vtxos) toVtxos() []client.Vtxo {
} }
func toProtoInput(i client.Input) *arkv1.Input { func toProtoInput(i client.Input) *arkv1.Input {
if len(i.GetDescriptor()) > 0 {
return &arkv1.Input{ return &arkv1.Input{
Input: &arkv1.Input_BoardingInput{ Outpoint: &arkv1.Outpoint{
BoardingInput: &arkv1.BoardingInput{ Txid: i.Txid,
Txid: i.GetTxID(), Vout: i.VOut,
Vout: i.GetVOut(),
Descriptor_: i.GetDescriptor(),
},
},
}
}
return &arkv1.Input{
Input: &arkv1.Input_VtxoInput{
VtxoInput: &arkv1.VtxoInput{
Txid: i.GetTxID(),
Vout: i.GetVOut(),
},
}, },
Descriptor_: i.Descriptor,
} }
} }

View File

@@ -21,6 +21,7 @@ import (
"github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/decred/dcrd/dcrec/secp256k1/v4"
httptransport "github.com/go-openapi/runtime/client" httptransport "github.com/go-openapi/runtime/client"
"github.com/go-openapi/strfmt" "github.com/go-openapi/strfmt"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
) )
type restClient struct { type restClient struct {
@@ -146,7 +147,7 @@ func (a *restClient) ListVtxos(
expiresAt = &t expiresAt = &t
} }
amount, err := strconv.Atoi(v.Receiver.Amount) amount, err := strconv.Atoi(v.Amount)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@@ -159,9 +160,9 @@ func (a *restClient) ListVtxos(
} }
spendableVtxos = append(spendableVtxos, client.Vtxo{ spendableVtxos = append(spendableVtxos, client.Vtxo{
VtxoKey: client.VtxoKey{ Outpoint: client.Outpoint{
Txid: v.Outpoint.VtxoInput.Txid, Txid: v.Outpoint.Txid,
VOut: uint32(v.Outpoint.VtxoInput.Vout), VOut: uint32(v.Outpoint.Vout),
}, },
Amount: uint64(amount), Amount: uint64(amount),
RoundTxid: v.PoolTxid, RoundTxid: v.PoolTxid,
@@ -170,6 +171,7 @@ func (a *restClient) ListVtxos(
RedeemTx: redeemTx, RedeemTx: redeemTx,
UnconditionalForfeitTxs: uncondForfeitTxs, UnconditionalForfeitTxs: uncondForfeitTxs,
SpentBy: v.SpentBy, SpentBy: v.SpentBy,
Descriptor: v.Descriptor,
}) })
} }
@@ -185,20 +187,21 @@ func (a *restClient) ListVtxos(
expiresAt = &t expiresAt = &t
} }
amount, err := strconv.Atoi(v.Receiver.Amount) amount, err := strconv.Atoi(v.Amount)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
spentVtxos = append(spentVtxos, client.Vtxo{ spentVtxos = append(spentVtxos, client.Vtxo{
VtxoKey: client.VtxoKey{ Outpoint: client.Outpoint{
Txid: v.Outpoint.VtxoInput.Txid, Txid: v.Outpoint.Txid,
VOut: uint32(v.Outpoint.VtxoInput.Vout), VOut: uint32(v.Outpoint.Vout),
}, },
Amount: uint64(amount), Amount: uint64(amount),
RoundTxid: v.PoolTxid, RoundTxid: v.PoolTxid,
ExpiresAt: expiresAt, ExpiresAt: expiresAt,
SpentBy: v.SpentBy, SpentBy: v.SpentBy,
Descriptor: v.Descriptor,
}) })
} }
@@ -249,26 +252,13 @@ func (a *restClient) RegisterPayment(
) (string, error) { ) (string, error) {
ins := make([]*models.V1Input, 0, len(inputs)) ins := make([]*models.V1Input, 0, len(inputs))
for _, i := range inputs { for _, i := range inputs {
var input *models.V1Input ins = append(ins, &models.V1Input{
Outpoint: &models.V1Outpoint{
if len(i.GetDescriptor()) > 0 { Txid: i.Txid,
input = &models.V1Input{ Vout: int64(i.VOut),
BoardingInput: &models.V1BoardingInput{
Txid: i.GetTxID(),
Vout: int64(i.GetVOut()),
Descriptor: i.GetDescriptor(),
}, },
} Descriptor: i.Descriptor,
} else { })
input = &models.V1Input{
VtxoInput: &models.V1VtxoInput{
Txid: i.GetTxID(),
Vout: int64(i.GetVOut()),
},
}
}
ins = append(ins, input)
} }
body := &models.V1RegisterPaymentRequest{ body := &models.V1RegisterPaymentRequest{
Inputs: ins, Inputs: ins,
@@ -328,12 +318,18 @@ func (a *restClient) Ping(
} }
if e := payload.RoundFinalization; e != nil { if e := payload.RoundFinalization; e != nil {
tree := treeFromProto{e.CongestionTree}.parse() tree := treeFromProto{e.CongestionTree}.parse()
minRelayFeeRate, err := strconv.Atoi(e.MinRelayFeeRate)
if err != nil {
return nil, err
}
return client.RoundFinalizationEvent{ return client.RoundFinalizationEvent{
ID: e.ID, ID: e.ID,
Tx: e.PoolTx, Tx: e.PoolTx,
ForfeitTxs: e.ForfeitTxs,
Tree: tree, Tree: tree,
Connectors: e.Connectors, Connectors: e.Connectors,
MinRelayFeeRate: chainfee.SatPerKVByte(minRelayFeeRate),
}, nil }, nil
} }
@@ -394,19 +390,16 @@ func (a *restClient) FinalizePayment(
} }
func (a *restClient) CreatePayment( func (a *restClient) CreatePayment(
ctx context.Context, inputs []client.VtxoKey, outputs []client.Output, ctx context.Context, inputs []client.Input, outputs []client.Output,
) (string, []string, error) { ) (string, []string, error) {
ins := make([]*models.V1Input, 0, len(inputs)) ins := make([]*models.V1Input, 0, len(inputs))
for _, i := range inputs { for _, i := range inputs {
if len(i.GetDescriptor()) > 0 {
return "", nil, fmt.Errorf("boarding inputs are not allowed in create payment")
}
ins = append(ins, &models.V1Input{ ins = append(ins, &models.V1Input{
VtxoInput: &models.V1VtxoInput{ Outpoint: &models.V1Outpoint{
Txid: i.Txid, Txid: i.Txid,
Vout: int64(i.VOut), Vout: int64(i.VOut),
}, },
Descriptor: i.Descriptor,
}) })
} }
outs := make([]*models.V1Output, 0, len(outputs)) outs := make([]*models.V1Output, 0, len(outputs))
@@ -414,6 +407,7 @@ func (a *restClient) CreatePayment(
outs = append(outs, &models.V1Output{ outs = append(outs, &models.V1Output{
Address: o.Address, Address: o.Address,
Amount: strconv.Itoa(int(o.Amount)), Amount: strconv.Itoa(int(o.Amount)),
Descriptor: o.Descriptor,
}) })
} }
body := models.V1CreatePaymentRequest{ body := models.V1CreatePaymentRequest{

View File

@@ -18,22 +18,18 @@ import (
// swagger:model v1Input // swagger:model v1Input
type V1Input struct { type V1Input struct {
// boarding input // descriptor
BoardingInput *V1BoardingInput `json:"boardingInput,omitempty"` Descriptor string `json:"descriptor,omitempty"`
// vtxo input // outpoint
VtxoInput *V1VtxoInput `json:"vtxoInput,omitempty"` Outpoint *V1Outpoint `json:"outpoint,omitempty"`
} }
// Validate validates this v1 input // Validate validates this v1 input
func (m *V1Input) Validate(formats strfmt.Registry) error { func (m *V1Input) Validate(formats strfmt.Registry) error {
var res []error var res []error
if err := m.validateBoardingInput(formats); err != nil { if err := m.validateOutpoint(formats); err != nil {
res = append(res, err)
}
if err := m.validateVtxoInput(formats); err != nil {
res = append(res, err) res = append(res, err)
} }
@@ -43,36 +39,17 @@ func (m *V1Input) Validate(formats strfmt.Registry) error {
return nil return nil
} }
func (m *V1Input) validateBoardingInput(formats strfmt.Registry) error { func (m *V1Input) validateOutpoint(formats strfmt.Registry) error {
if swag.IsZero(m.BoardingInput) { // not required if swag.IsZero(m.Outpoint) { // not required
return nil return nil
} }
if m.BoardingInput != nil { if m.Outpoint != nil {
if err := m.BoardingInput.Validate(formats); err != nil { if err := m.Outpoint.Validate(formats); err != nil {
if ve, ok := err.(*errors.Validation); ok { if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("boardingInput") return ve.ValidateName("outpoint")
} else if ce, ok := err.(*errors.CompositeError); ok { } else if ce, ok := err.(*errors.CompositeError); ok {
return ce.ValidateName("boardingInput") return ce.ValidateName("outpoint")
}
return err
}
}
return nil
}
func (m *V1Input) validateVtxoInput(formats strfmt.Registry) error {
if swag.IsZero(m.VtxoInput) { // not required
return nil
}
if m.VtxoInput != nil {
if err := m.VtxoInput.Validate(formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("vtxoInput")
} else if ce, ok := err.(*errors.CompositeError); ok {
return ce.ValidateName("vtxoInput")
} }
return err return err
} }
@@ -85,11 +62,7 @@ func (m *V1Input) validateVtxoInput(formats strfmt.Registry) error {
func (m *V1Input) ContextValidate(ctx context.Context, formats strfmt.Registry) error { func (m *V1Input) ContextValidate(ctx context.Context, formats strfmt.Registry) error {
var res []error var res []error
if err := m.contextValidateBoardingInput(ctx, formats); err != nil { if err := m.contextValidateOutpoint(ctx, formats); err != nil {
res = append(res, err)
}
if err := m.contextValidateVtxoInput(ctx, formats); err != nil {
res = append(res, err) res = append(res, err)
} }
@@ -99,40 +72,19 @@ func (m *V1Input) ContextValidate(ctx context.Context, formats strfmt.Registry)
return nil return nil
} }
func (m *V1Input) contextValidateBoardingInput(ctx context.Context, formats strfmt.Registry) error { func (m *V1Input) contextValidateOutpoint(ctx context.Context, formats strfmt.Registry) error {
if m.BoardingInput != nil { if m.Outpoint != nil {
if swag.IsZero(m.BoardingInput) { // not required if swag.IsZero(m.Outpoint) { // not required
return nil return nil
} }
if err := m.BoardingInput.ContextValidate(ctx, formats); err != nil { if err := m.Outpoint.ContextValidate(ctx, formats); err != nil {
if ve, ok := err.(*errors.Validation); ok { if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("boardingInput") return ve.ValidateName("outpoint")
} else if ce, ok := err.(*errors.CompositeError); ok { } else if ce, ok := err.(*errors.CompositeError); ok {
return ce.ValidateName("boardingInput") return ce.ValidateName("outpoint")
}
return err
}
}
return nil
}
func (m *V1Input) contextValidateVtxoInput(ctx context.Context, formats strfmt.Registry) error {
if m.VtxoInput != nil {
if swag.IsZero(m.VtxoInput) { // not required
return nil
}
if err := m.VtxoInput.ContextValidate(ctx, formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("vtxoInput")
} else if ce, ok := err.(*errors.CompositeError); ok {
return ce.ValidateName("vtxoInput")
} }
return err return err
} }

View File

@@ -0,0 +1,53 @@
// Code generated by go-swagger; DO NOT EDIT.
package models
// This file was generated by the swagger tool.
// Editing this file might prove futile when you re-run the swagger generate command
import (
"context"
"github.com/go-openapi/strfmt"
"github.com/go-openapi/swag"
)
// V1Outpoint v1 outpoint
//
// swagger:model v1Outpoint
type V1Outpoint struct {
// txid
Txid string `json:"txid,omitempty"`
// vout
Vout int64 `json:"vout,omitempty"`
}
// Validate validates this v1 outpoint
func (m *V1Outpoint) Validate(formats strfmt.Registry) error {
return nil
}
// ContextValidate validates this v1 outpoint based on context it is used
func (m *V1Outpoint) ContextValidate(ctx context.Context, formats strfmt.Registry) error {
return nil
}
// MarshalBinary interface implementation
func (m *V1Outpoint) MarshalBinary() ([]byte, error) {
if m == nil {
return nil, nil
}
return swag.WriteJSON(m)
}
// UnmarshalBinary interface implementation
func (m *V1Outpoint) UnmarshalBinary(b []byte) error {
var res V1Outpoint
if err := swag.ReadJSON(b, &res); err != nil {
return err
}
*m = res
return nil
}

View File

@@ -17,11 +17,14 @@ import (
// swagger:model v1Output // swagger:model v1Output
type V1Output struct { type V1Output struct {
// Either the offchain or onchain address. // onchain
Address string `json:"address,omitempty"` Address string `json:"address,omitempty"`
// Amount to send in satoshis. // Amount to send in satoshis.
Amount string `json:"amount,omitempty"` Amount string `json:"amount,omitempty"`
// offchain
Descriptor string `json:"descriptor,omitempty"`
} }
// Validate validates this v1 output // Validate validates this v1 output

View File

@@ -24,12 +24,12 @@ type V1RoundFinalizationEvent struct {
// connectors // connectors
Connectors []string `json:"connectors"` Connectors []string `json:"connectors"`
// forfeit txs
ForfeitTxs []string `json:"forfeitTxs"`
// id // id
ID string `json:"id,omitempty"` ID string `json:"id,omitempty"`
// min relay fee rate
MinRelayFeeRate string `json:"minRelayFeeRate,omitempty"`
// pool tx // pool tx
PoolTx string `json:"poolTx,omitempty"` PoolTx string `json:"poolTx,omitempty"`
} }

View File

@@ -18,11 +18,17 @@ import (
// swagger:model v1Vtxo // swagger:model v1Vtxo
type V1Vtxo struct { type V1Vtxo struct {
// amount
Amount string `json:"amount,omitempty"`
// descriptor
Descriptor string `json:"descriptor,omitempty"`
// expire at // expire at
ExpireAt string `json:"expireAt,omitempty"` ExpireAt string `json:"expireAt,omitempty"`
// outpoint // outpoint
Outpoint *V1Input `json:"outpoint,omitempty"` Outpoint *V1Outpoint `json:"outpoint,omitempty"`
// pending // pending
Pending bool `json:"pending,omitempty"` Pending bool `json:"pending,omitempty"`
@@ -33,9 +39,6 @@ type V1Vtxo struct {
// pool txid // pool txid
PoolTxid string `json:"poolTxid,omitempty"` PoolTxid string `json:"poolTxid,omitempty"`
// receiver
Receiver *V1Output `json:"receiver,omitempty"`
// spent // spent
Spent bool `json:"spent,omitempty"` Spent bool `json:"spent,omitempty"`
@@ -58,10 +61,6 @@ func (m *V1Vtxo) Validate(formats strfmt.Registry) error {
res = append(res, err) res = append(res, err)
} }
if err := m.validateReceiver(formats); err != nil {
res = append(res, err)
}
if len(res) > 0 { if len(res) > 0 {
return errors.CompositeValidationError(res...) return errors.CompositeValidationError(res...)
} }
@@ -106,25 +105,6 @@ func (m *V1Vtxo) validatePendingData(formats strfmt.Registry) error {
return nil return nil
} }
func (m *V1Vtxo) validateReceiver(formats strfmt.Registry) error {
if swag.IsZero(m.Receiver) { // not required
return nil
}
if m.Receiver != nil {
if err := m.Receiver.Validate(formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("receiver")
} else if ce, ok := err.(*errors.CompositeError); ok {
return ce.ValidateName("receiver")
}
return err
}
}
return nil
}
// ContextValidate validate this v1 vtxo based on the context it is used // ContextValidate validate this v1 vtxo based on the context it is used
func (m *V1Vtxo) ContextValidate(ctx context.Context, formats strfmt.Registry) error { func (m *V1Vtxo) ContextValidate(ctx context.Context, formats strfmt.Registry) error {
var res []error var res []error
@@ -137,10 +117,6 @@ func (m *V1Vtxo) ContextValidate(ctx context.Context, formats strfmt.Registry) e
res = append(res, err) res = append(res, err)
} }
if err := m.contextValidateReceiver(ctx, formats); err != nil {
res = append(res, err)
}
if len(res) > 0 { if len(res) > 0 {
return errors.CompositeValidationError(res...) return errors.CompositeValidationError(res...)
} }
@@ -189,27 +165,6 @@ func (m *V1Vtxo) contextValidatePendingData(ctx context.Context, formats strfmt.
return nil return nil
} }
func (m *V1Vtxo) contextValidateReceiver(ctx context.Context, formats strfmt.Registry) error {
if m.Receiver != nil {
if swag.IsZero(m.Receiver) { // not required
return nil
}
if err := m.Receiver.ContextValidate(ctx, formats); err != nil {
if ve, ok := err.(*errors.Validation); ok {
return ve.ValidateName("receiver")
} else if ce, ok := err.(*errors.CompositeError); ok {
return ce.ValidateName("receiver")
}
return err
}
}
return nil
}
// MarshalBinary interface implementation // MarshalBinary interface implementation
func (m *V1Vtxo) MarshalBinary() ([]byte, error) { func (m *V1Vtxo) MarshalBinary() ([]byte, error) {
if m == nil { if m == nil {

View File

@@ -226,7 +226,7 @@ func loadFixtures(jsonStr string) (vtxos, []Transaction, error) {
return vtxos{}, nil, err return vtxos{}, nil, err
} }
spendable[i] = client.Vtxo{ spendable[i] = client.Vtxo{
VtxoKey: client.VtxoKey{ Outpoint: client.Outpoint{
Txid: vtxo.Outpoint.Txid, Txid: vtxo.Outpoint.Txid,
VOut: vtxo.Outpoint.Vout, VOut: vtxo.Outpoint.Vout,
}, },
@@ -251,7 +251,7 @@ func loadFixtures(jsonStr string) (vtxos, []Transaction, error) {
return vtxos{}, nil, err return vtxos{}, nil, err
} }
spent[i] = client.Vtxo{ spent[i] = client.Vtxo{
VtxoKey: client.VtxoKey{ Outpoint: client.Outpoint{
Txid: vtxo.Outpoint.Txid, Txid: vtxo.Outpoint.Txid,
VOut: vtxo.Outpoint.Vout, VOut: vtxo.Outpoint.Vout,
}, },

View File

@@ -12,7 +12,6 @@ import (
"time" "time"
"github.com/ark-network/ark/common" "github.com/ark-network/ark/common"
"github.com/ark-network/ark/common/descriptor"
"github.com/ark-network/ark/common/tree" "github.com/ark-network/ark/common/tree"
"github.com/ark-network/ark/pkg/client-sdk/client" "github.com/ark-network/ark/pkg/client-sdk/client"
"github.com/ark-network/ark/pkg/client-sdk/explorer" "github.com/ark-network/ark/pkg/client-sdk/explorer"
@@ -23,9 +22,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/decred/dcrd/dcrec/secp256k1/v4" "github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/vulpemventures/go-elements/address" "github.com/vulpemventures/go-elements/address"
"github.com/vulpemventures/go-elements/psetv2" "github.com/vulpemventures/go-elements/psetv2"
"github.com/vulpemventures/go-elements/taproot"
) )
type liquidReceiver struct { type liquidReceiver struct {
@@ -435,8 +436,14 @@ func (a *covenantArkClient) CollaborativeRedeem(
if err != nil { if err != nil {
return "", err return "", err
} }
desc, err := a.offchainAddressToDefaultVtxoDescriptor(offchainAddr)
if err != nil {
return "", err
}
receivers = append(receivers, client.Output{ receivers = append(receivers, client.Output{
Address: offchainAddr, Descriptor: desc,
Amount: changeAmount, Amount: changeAmount,
}) })
} }
@@ -444,9 +451,12 @@ func (a *covenantArkClient) CollaborativeRedeem(
inputs := make([]client.Input, 0, len(selectedCoins)) inputs := make([]client.Input, 0, len(selectedCoins))
for _, coin := range selectedCoins { for _, coin := range selectedCoins {
inputs = append(inputs, client.VtxoKey{ inputs = append(inputs, client.Input{
Outpoint: client.Outpoint{
Txid: coin.Txid, Txid: coin.Txid,
VOut: coin.VOut, VOut: coin.VOut,
},
Descriptor: coin.Descriptor,
}) })
} }
@@ -460,7 +470,7 @@ func (a *covenantArkClient) CollaborativeRedeem(
} }
poolTxID, err := a.handleRoundStream( poolTxID, err := a.handleRoundStream(
ctx, paymentID, selectedCoins, false, receivers, ctx, paymentID, selectedCoins, nil, "", receivers,
) )
if err != nil { if err != nil {
return "", err return "", err
@@ -500,14 +510,22 @@ func (a *covenantArkClient) Claim(ctx context.Context) (string, error) {
return "", fmt.Errorf("no funds to claim") return "", fmt.Errorf("no funds to claim")
} }
desc, err := a.offchainAddressToDefaultVtxoDescriptor(myselfOffchain)
if err != nil {
return "", err
}
receiver := client.Output{ receiver := client.Output{
Address: myselfOffchain, Descriptor: desc,
Amount: pendingBalance, Amount: pendingBalance,
} }
desc := strings.ReplaceAll(a.BoardingDescriptorTemplate, "USER", hex.EncodeToString(schnorr.SerializePubKey(mypubkey))) return a.selfTransferAllPendingPayments(
ctx,
return a.selfTransferAllPendingPayments(ctx, boardingUtxos, receiver, desc) boardingUtxos,
receiver,
hex.EncodeToString(mypubkey.SerializeCompressed()),
)
} }
func (a *covenantArkClient) GetTransactionHistory(ctx context.Context) ([]Transaction, error) { func (a *covenantArkClient) GetTransactionHistory(ctx context.Context) ([]Transaction, error) {
@@ -577,14 +595,18 @@ func (a *covenantArkClient) getClaimableBoardingUtxos(ctx context.Context) ([]ex
descriptorStr := strings.ReplaceAll( descriptorStr := strings.ReplaceAll(
a.BoardingDescriptorTemplate, "USER", myPubkeyStr, a.BoardingDescriptorTemplate, "USER", myPubkeyStr,
) )
desc, err := descriptor.ParseTaprootDescriptor(descriptorStr)
boardingScript, err := tree.ParseVtxoScript(descriptorStr)
if err != nil { if err != nil {
return nil, err return nil, err
} }
_, boardingTimeout, err := descriptor.ParseBoardingDescriptor(*desc) var boardingTimeout uint
if err != nil {
return nil, err if defaultVtxo, ok := boardingScript.(*tree.DefaultVtxoScript); ok {
boardingTimeout = defaultVtxo.ExitDelay
} else {
return nil, fmt.Errorf("unsupported boarding descriptor: %s", descriptorStr)
} }
for _, addr := range boardingAddrs { for _, addr := range boardingAddrs {
@@ -800,8 +822,13 @@ func (a *covenantArkClient) sendOffchain(
return "", fmt.Errorf("invalid amount (%d), must be greater than dust %d", receiver.Amount(), a.Dust) return "", fmt.Errorf("invalid amount (%d), must be greater than dust %d", receiver.Amount(), a.Dust)
} }
desc, err := a.offchainAddressToDefaultVtxoDescriptor(receiver.To())
if err != nil {
return "", err
}
receiversOutput = append(receiversOutput, client.Output{ receiversOutput = append(receiversOutput, client.Output{
Address: receiver.To(), Descriptor: desc,
Amount: receiver.Amount(), Amount: receiver.Amount(),
}) })
sumOfReceivers += receiver.Amount() sumOfReceivers += receiver.Amount()
@@ -828,8 +855,14 @@ func (a *covenantArkClient) sendOffchain(
if err != nil { if err != nil {
return "", err return "", err
} }
desc, err := a.offchainAddressToDefaultVtxoDescriptor(offchainAddr)
if err != nil {
return "", err
}
changeReceiver := client.Output{ changeReceiver := client.Output{
Address: offchainAddr, Descriptor: desc,
Amount: changeAmount, Amount: changeAmount,
} }
receiversOutput = append(receiversOutput, changeReceiver) receiversOutput = append(receiversOutput, changeReceiver)
@@ -837,9 +870,12 @@ func (a *covenantArkClient) sendOffchain(
inputs := make([]client.Input, 0, len(selectedCoins)) inputs := make([]client.Input, 0, len(selectedCoins))
for _, coin := range selectedCoins { for _, coin := range selectedCoins {
inputs = append(inputs, client.VtxoKey{ inputs = append(inputs, client.Input{
Outpoint: client.Outpoint{
Txid: coin.Txid, Txid: coin.Txid,
VOut: coin.VOut, VOut: coin.VOut,
},
Descriptor: coin.Descriptor,
}) })
} }
@@ -859,7 +895,7 @@ func (a *covenantArkClient) sendOffchain(
log.Infof("payment registered with id: %s", paymentID) log.Infof("payment registered with id: %s", paymentID)
poolTxID, err := a.handleRoundStream( poolTxID, err := a.handleRoundStream(
ctx, paymentID, selectedCoins, false, receiversOutput, ctx, paymentID, selectedCoins, nil, "", receiversOutput,
) )
if err != nil { if err != nil {
return "", err return "", err
@@ -900,16 +936,46 @@ func (a *covenantArkClient) addInputs(
return err return err
} }
_, leafProof, _, _, err := tree.ComputeVtxoTaprootScript( vtxoScript := &tree.DefaultVtxoScript{
userPubkey, aspPubkey, utxo.Delay, utils.ToElementsNetwork(a.Network), Owner: userPubkey,
) Asp: aspPubkey,
ExitDelay: utxo.Delay,
}
forfeitClosure := &tree.MultisigClosure{
Pubkey: userPubkey,
AspPubkey: aspPubkey,
}
forfeitLeaf, err := forfeitClosure.Leaf()
if err != nil {
return err
}
_, taprootTree, err := vtxoScript.TapTree()
if err != nil {
return err
}
leafProof, err := taprootTree.GetTaprootMerkleProof(forfeitLeaf.TapHash())
if err != nil {
return err
}
controlBlock, err := taproot.ParseControlBlock(leafProof.Script)
if err != nil { if err != nil {
return err return err
} }
inputIndex := len(updater.Pset.Inputs) - 1 inputIndex := len(updater.Pset.Inputs) - 1
if err := updater.AddInTapLeafScript(inputIndex, psetv2.NewTapLeafScript(*leafProof, tree.UnspendableKey())); err != nil { if err := updater.AddInTapLeafScript(
inputIndex,
psetv2.TapLeafScript{
TapElementsLeaf: taproot.NewBaseTapElementsLeaf(leafProof.Script),
ControlBlock: *controlBlock,
},
); err != nil {
return err return err
} }
} }
@@ -921,7 +987,8 @@ func (a *covenantArkClient) handleRoundStream(
ctx context.Context, ctx context.Context,
paymentID string, paymentID string,
vtxosToSign []client.Vtxo, vtxosToSign []client.Vtxo,
mustSignRoundTx bool, boardingUtxos []explorer.Utxo,
boardingDescriptor string,
receivers []client.Output, receivers []client.Output,
) (string, error) { ) (string, error) {
eventsCh, err := a.client.GetEventStream(ctx, paymentID) eventsCh, err := a.client.GetEventStream(ctx, paymentID)
@@ -955,7 +1022,7 @@ func (a *covenantArkClient) handleRoundStream(
log.Info("a round finalization started") log.Info("a round finalization started")
signedForfeitTxs, signedRoundTx, err := a.handleRoundFinalization( signedForfeitTxs, signedRoundTx, err := a.handleRoundFinalization(
ctx, event.(client.RoundFinalizationEvent), vtxosToSign, mustSignRoundTx, receivers, ctx, event.(client.RoundFinalizationEvent), vtxosToSign, boardingUtxos, boardingDescriptor, receivers,
) )
if err != nil { if err != nil {
return "", err return "", err
@@ -979,30 +1046,104 @@ func (a *covenantArkClient) handleRoundStream(
} }
func (a *covenantArkClient) handleRoundFinalization( func (a *covenantArkClient) handleRoundFinalization(
ctx context.Context, event client.RoundFinalizationEvent, ctx context.Context,
vtxos []client.Vtxo, mustSignRoundTx bool, receivers []client.Output, event client.RoundFinalizationEvent,
vtxos []client.Vtxo,
boardingUtxos []explorer.Utxo,
boardingDescriptor string,
receivers []client.Output,
) (signedForfeits []string, signedRoundTx string, err error) { ) (signedForfeits []string, signedRoundTx string, err error) {
if err = a.validateCongestionTree(event, receivers); err != nil { if err = a.validateCongestionTree(event, receivers); err != nil {
return return
} }
offchainAddr, _, err := a.wallet.NewAddress(ctx, false)
if err != nil {
return
}
_, myPubkey, _, err := common.DecodeAddress(offchainAddr)
if err != nil {
return
}
if len(vtxos) > 0 { if len(vtxos) > 0 {
signedForfeits, err = a.loopAndSign( signedForfeits, err = a.createAndSignForfeits(ctx, vtxos, event.Connectors, event.MinRelayFeeRate, myPubkey)
ctx, event.ForfeitTxs, vtxos, event.Connectors,
)
if err != nil { if err != nil {
return return
} }
} }
if mustSignRoundTx { if len(boardingUtxos) > 0 {
signedRoundTx, err = a.wallet.SignTransaction(ctx, a.explorer, event.Tx) boardingVtxoScript, err := tree.ParseVtxoScript(boardingDescriptor)
if err != nil { if err != nil {
return return nil, "", err
}
roundPtx, err := psetv2.NewPsetFromBase64(event.Tx)
if err != nil {
return nil, "", err
}
// add tapscript leaf
forfeitClosure := &tree.MultisigClosure{
Pubkey: myPubkey,
AspPubkey: a.AspPubkey,
}
forfeitLeaf, err := forfeitClosure.Leaf()
if err != nil {
return nil, "", err
}
_, taprootTree, err := boardingVtxoScript.TapTree()
if err != nil {
return nil, "", err
}
forfeitProof, err := taprootTree.GetTaprootMerkleProof(forfeitLeaf.TapHash())
if err != nil {
return nil, "", err
}
ctrlBlock, err := taproot.ParseControlBlock(forfeitProof.ControlBlock)
if err != nil {
return nil, "", err
}
tapscript := psetv2.TapLeafScript{
TapElementsLeaf: taproot.NewBaseTapElementsLeaf(forfeitProof.Script),
ControlBlock: *ctrlBlock,
}
updater, err := psetv2.NewUpdater(roundPtx)
if err != nil {
return nil, "", err
}
for i, input := range updater.Pset.Inputs {
for _, boardingUtxo := range boardingUtxos {
if chainhash.Hash(input.PreviousTxid).String() == boardingUtxo.Txid && boardingUtxo.Vout == input.PreviousTxIndex {
if err := updater.AddInTapLeafScript(i, tapscript); err != nil {
return nil, "", err
}
break
}
} }
} }
return b64, err := updater.Pset.ToBase64()
if err != nil {
return nil, "", err
}
signedRoundTx, err = a.wallet.SignTransaction(ctx, a.explorer, b64)
if err != nil {
return nil, "", err
}
}
return signedForfeits, signedRoundTx, nil
} }
func (a *covenantArkClient) validateCongestionTree( func (a *covenantArkClient) validateCongestionTree(
@@ -1016,7 +1157,7 @@ func (a *covenantArkClient) validateCongestionTree(
connectors := event.Connectors connectors := event.Connectors
if !utils.IsLiquidOnchainOnly(receivers) { if !utils.IsOnchainOnly(receivers) {
if err := tree.ValidateCongestionTree( if err := tree.ValidateCongestionTree(
event.Tree, poolTx, a.StoreData.AspPubkey, a.RoundLifetime, event.Tree, poolTx, a.StoreData.AspPubkey, a.RoundLifetime,
); err != nil { ); err != nil {
@@ -1029,7 +1170,7 @@ func (a *covenantArkClient) validateCongestionTree(
} }
if err := a.validateReceivers( if err := a.validateReceivers(
ptx, receivers, event.Tree, a.StoreData.AspPubkey, ptx, receivers, event.Tree,
); err != nil { ); err != nil {
return err return err
} }
@@ -1043,14 +1184,13 @@ func (a *covenantArkClient) validateReceivers(
ptx *psetv2.Pset, ptx *psetv2.Pset,
receivers []client.Output, receivers []client.Output,
congestionTree tree.CongestionTree, congestionTree tree.CongestionTree,
aspPubkey *secp256k1.PublicKey,
) error { ) error {
for _, receiver := range receivers { for _, receiver := range receivers {
isOnChain, onchainScript, userPubkey, err := utils.ParseLiquidAddress( isOnChain, onchainScript, err := utils.ParseLiquidAddress(
receiver.Address, receiver.Address,
) )
if err != nil { if err != nil {
return err return fmt.Errorf("invalid receiver address: %s err = %s", receiver.Address, err)
} }
if isOnChain { if isOnChain {
@@ -1059,7 +1199,7 @@ func (a *covenantArkClient) validateReceivers(
} }
} else { } else {
if err := a.validateOffChainReceiver( if err := a.validateOffChainReceiver(
congestionTree, receiver, userPubkey, aspPubkey, congestionTree, receiver,
); err != nil { ); err != nil {
return err return err
} }
@@ -1095,13 +1235,15 @@ func (a *covenantArkClient) validateOnChainReceiver(
func (a *covenantArkClient) validateOffChainReceiver( func (a *covenantArkClient) validateOffChainReceiver(
congestionTree tree.CongestionTree, congestionTree tree.CongestionTree,
receiver client.Output, receiver client.Output,
userPubkey, aspPubkey *secp256k1.PublicKey,
) error { ) error {
found := false found := false
net := utils.ToElementsNetwork(a.Network)
outputTapKey, _, _, _, err := tree.ComputeVtxoTaprootScript( receiverVtxoScript, err := tree.ParseVtxoScript(receiver.Descriptor)
userPubkey, aspPubkey, uint(a.UnilateralExitDelay), net, if err != nil {
) return err
}
outputTapKey, _, err := receiverVtxoScript.TapTree()
if err != nil { if err != nil {
return err return err
} }
@@ -1136,36 +1278,105 @@ func (a *covenantArkClient) validateOffChainReceiver(
return nil return nil
} }
func (a *covenantArkClient) loopAndSign( func (a *covenantArkClient) createAndSignForfeits(
ctx context.Context, ctx context.Context,
forfeitTxs []string, vtxosToSign []client.Vtxo, connectors []string, vtxosToSign []client.Vtxo,
connectors []string,
feeRate chainfee.SatPerKVByte,
myPubKey *secp256k1.PublicKey,
) ([]string, error) { ) ([]string, error) {
signedForfeits := make([]string, 0) signedForfeits := make([]string, 0)
connectorsPsets := make([]*psetv2.Pset, 0, len(connectors))
connectorsTxids := make([]string, 0, len(connectors))
for _, connector := range connectors { for _, connector := range connectors {
p, _ := psetv2.NewPsetFromBase64(connector) p, err := psetv2.NewPsetFromBase64(connector)
utx, _ := p.UnsignedTx()
txid := utx.TxHash().String()
connectorsTxids = append(connectorsTxids, txid)
}
for _, forfeitTx := range forfeitTxs {
pset, err := psetv2.NewPsetFromBase64(forfeitTx)
if err != nil { if err != nil {
return nil, err return nil, err
} }
for _, input := range pset.Inputs { connectorsPsets = append(connectorsPsets, p)
inputTxid := chainhash.Hash(input.PreviousTxid).String() }
for _, coin := range vtxosToSign {
if inputTxid == coin.Txid { for _, vtxo := range vtxosToSign {
signedPset, err := a.signForfeitTx(ctx, forfeitTx, pset, connectorsTxids) vtxoScript, err := tree.ParseVtxoScript(vtxo.Descriptor)
if err != nil { if err != nil {
return nil, err return nil, err
} }
signedForfeits = append(signedForfeits, signedPset)
vtxoTapKey, vtxoTapTree, err := vtxoScript.TapTree()
if err != nil {
return nil, err
} }
feeAmount, err := common.ComputeForfeitMinRelayFee(feeRate, vtxoTapTree)
if err != nil {
return nil, err
}
vtxoOutputScript, err := common.P2TRScript(vtxoTapKey)
if err != nil {
return nil, err
}
vtxoInput := psetv2.InputArgs{
Txid: vtxo.Txid,
TxIndex: vtxo.VOut,
}
forfeitClosure := &tree.MultisigClosure{
Pubkey: myPubKey,
AspPubkey: a.AspPubkey,
}
forfeitLeaf, err := forfeitClosure.Leaf()
if err != nil {
return nil, err
}
leafProof, err := vtxoTapTree.GetTaprootMerkleProof(forfeitLeaf.TapHash())
if err != nil {
return nil, err
}
ctrlBlock, err := taproot.ParseControlBlock(leafProof.ControlBlock)
if err != nil {
return nil, err
}
tapscript := psetv2.TapLeafScript{
TapElementsLeaf: taproot.NewBaseTapElementsLeaf(leafProof.Script),
ControlBlock: *ctrlBlock,
}
for _, connectorPset := range connectorsPsets {
forfeits, err := tree.BuildForfeitTxs(
connectorPset, vtxoInput, vtxo.Amount, a.Dust, feeAmount, vtxoOutputScript, a.AspPubkey,
)
if err != nil {
return nil, err
}
for _, forfeit := range forfeits {
updater, err := psetv2.NewUpdater(forfeit)
if err != nil {
return nil, err
}
if err := updater.AddInTapLeafScript(1, tapscript); err != nil {
return nil, err
}
b64, err := updater.Pset.ToBase64()
if err != nil {
return nil, err
}
signedForfeit, err := a.wallet.SignTransaction(ctx, a.explorer, b64)
if err != nil {
return nil, err
}
signedForfeits = append(signedForfeits, signedForfeit)
} }
} }
} }
@@ -1173,24 +1384,6 @@ func (a *covenantArkClient) loopAndSign(
return signedForfeits, nil return signedForfeits, nil
} }
func (a *covenantArkClient) signForfeitTx(
ctx context.Context, txStr string, tx *psetv2.Pset, connectorsTxids []string,
) (string, error) {
connectorTxid := chainhash.Hash(tx.Inputs[0].PreviousTxid).String()
connectorFound := false
for _, id := range connectorsTxids {
if id == connectorTxid {
connectorFound = true
break
}
}
if !connectorFound {
return "", fmt.Errorf("connector txid %s not found in the connectors list", connectorTxid)
}
return a.wallet.SignTransaction(ctx, a.explorer, txStr)
}
func (a *covenantArkClient) coinSelectOnchain( func (a *covenantArkClient) coinSelectOnchain(
ctx context.Context, targetAmount uint64, exclude []explorer.Utxo, ctx context.Context, targetAmount uint64, exclude []explorer.Utxo,
) ([]explorer.Utxo, uint64, error) { ) ([]explorer.Utxo, uint64, error) {
@@ -1208,14 +1401,17 @@ func (a *covenantArkClient) coinSelectOnchain(
descriptorStr := strings.ReplaceAll( descriptorStr := strings.ReplaceAll(
a.BoardingDescriptorTemplate, "USER", myPubkeyStr, a.BoardingDescriptorTemplate, "USER", myPubkeyStr,
) )
desc, err := descriptor.ParseTaprootDescriptor(descriptorStr) boardingScript, err := tree.ParseVtxoScript(descriptorStr)
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
} }
_, boardingTimeout, err := descriptor.ParseBoardingDescriptor(*desc) var boardingTimeout uint
if err != nil {
return nil, 0, err if defaultVtxo, ok := boardingScript.(*tree.DefaultVtxoScript); ok {
boardingTimeout = defaultVtxo.ExitDelay
} else {
return nil, 0, fmt.Errorf("unsupported boarding descriptor: %s", descriptorStr)
} }
now := time.Now() now := time.Now()
@@ -1387,13 +1583,17 @@ func (a *covenantArkClient) getVtxos(
} }
func (a *covenantArkClient) selfTransferAllPendingPayments( func (a *covenantArkClient) selfTransferAllPendingPayments(
ctx context.Context, boardingUtxo []explorer.Utxo, myself client.Output, boardingDescriptor string, ctx context.Context, boardingUtxos []explorer.Utxo, myself client.Output, mypubkey string,
) (string, error) { ) (string, error) {
inputs := make([]client.Input, 0, len(boardingUtxo)) inputs := make([]client.Input, 0, len(boardingUtxos))
for _, utxo := range boardingUtxo { boardingDescriptor := strings.ReplaceAll(
inputs = append(inputs, client.BoardingInput{ a.BoardingDescriptorTemplate, "USER", mypubkey[2:],
VtxoKey: client.VtxoKey{ )
for _, utxo := range boardingUtxos {
inputs = append(inputs, client.Input{
Outpoint: client.Outpoint{
Txid: utxo.Txid, Txid: utxo.Txid,
VOut: utxo.Vout, VOut: utxo.Vout,
}, },
@@ -1413,7 +1613,7 @@ func (a *covenantArkClient) selfTransferAllPendingPayments(
} }
roundTxid, err := a.handleRoundStream( roundTxid, err := a.handleRoundStream(
ctx, paymentID, make([]client.Vtxo, 0), len(boardingUtxo) > 0, outputs, ctx, paymentID, make([]client.Vtxo, 0), boardingUtxos, boardingDescriptor, outputs,
) )
if err != nil { if err != nil {
return "", err return "", err
@@ -1422,6 +1622,21 @@ func (a *covenantArkClient) selfTransferAllPendingPayments(
return roundTxid, nil return roundTxid, nil
} }
func (a *covenantArkClient) offchainAddressToDefaultVtxoDescriptor(addr string) (string, error) {
_, userPubKey, aspPubkey, err := common.DecodeAddress(addr)
if err != nil {
return "", err
}
vtxoScript := tree.DefaultVtxoScript{
Owner: userPubKey,
Asp: aspPubkey,
ExitDelay: uint(a.UnilateralExitDelay),
}
return vtxoScript.ToDescriptor(), nil
}
func (a *covenantArkClient) getBoardingTxs(ctx context.Context) (transactions []Transaction) { func (a *covenantArkClient) getBoardingTxs(ctx context.Context) (transactions []Transaction) {
utxos, err := a.getClaimableBoardingUtxos(ctx) utxos, err := a.getClaimableBoardingUtxos(ctx)
if err != nil { if err != nil {

View File

@@ -13,7 +13,6 @@ import (
"github.com/ark-network/ark/common" "github.com/ark-network/ark/common"
"github.com/ark-network/ark/common/bitcointree" "github.com/ark-network/ark/common/bitcointree"
"github.com/ark-network/ark/common/descriptor"
"github.com/ark-network/ark/common/tree" "github.com/ark-network/ark/common/tree"
"github.com/ark-network/ark/pkg/client-sdk/client" "github.com/ark-network/ark/pkg/client-sdk/client"
"github.com/ark-network/ark/pkg/client-sdk/explorer" "github.com/ark-network/ark/pkg/client-sdk/explorer"
@@ -28,6 +27,7 @@ import (
"github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
"github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
@@ -425,8 +425,14 @@ func (a *covenantlessArkClient) CollaborativeRedeem(
if err != nil { if err != nil {
return "", err return "", err
} }
desc, err := a.offchainAddressToDefaultVtxoDescriptor(offchainAddr)
if err != nil {
return "", err
}
receivers = append(receivers, client.Output{ receivers = append(receivers, client.Output{
Address: offchainAddr, Descriptor: desc,
Amount: changeAmount, Amount: changeAmount,
}) })
} }
@@ -434,9 +440,12 @@ func (a *covenantlessArkClient) CollaborativeRedeem(
inputs := make([]client.Input, 0, len(selectedCoins)) inputs := make([]client.Input, 0, len(selectedCoins))
for _, coin := range selectedCoins { for _, coin := range selectedCoins {
inputs = append(inputs, client.VtxoKey{ inputs = append(inputs, client.Input{
Outpoint: client.Outpoint{
Txid: coin.Txid, Txid: coin.Txid,
VOut: coin.VOut, VOut: coin.VOut,
},
Descriptor: coin.Descriptor,
}) })
} }
@@ -459,7 +468,7 @@ func (a *covenantlessArkClient) CollaborativeRedeem(
} }
poolTxID, err := a.handleRoundStream( poolTxID, err := a.handleRoundStream(
ctx, paymentID, selectedCoins, false, receivers, roundEphemeralKey, ctx, paymentID, selectedCoins, nil, "", receivers, roundEphemeralKey,
) )
if err != nil { if err != nil {
return "", err return "", err
@@ -478,7 +487,7 @@ func (a *covenantlessArkClient) SendAsync(
netParams := utils.ToBitcoinNetwork(a.Network) netParams := utils.ToBitcoinNetwork(a.Network)
for _, receiver := range receivers { for _, receiver := range receivers {
isOnchain, _, _, err := utils.ParseBitcoinAddress(receiver.To(), netParams) isOnchain, _, err := utils.ParseBitcoinAddress(receiver.To(), netParams)
if err != nil { if err != nil {
return "", err return "", err
} }
@@ -516,8 +525,26 @@ func (a *covenantlessArkClient) SendAsync(
return "", fmt.Errorf("invalid amount (%d), must be greater than dust %d", receiver.Amount(), a.Dust) return "", fmt.Errorf("invalid amount (%d), must be greater than dust %d", receiver.Amount(), a.Dust)
} }
isSelfTransfer := offchainAddrs[0] == receiver.To()
var desc string
// reversible vtxo does not make sense for self transfer
// if the receiver is the same as the sender, handle the output like the change
if !isSelfTransfer {
desc, err = a.offchainAddressToReversibleVtxoDescriptor(offchainAddrs[0], receiver.To())
if err != nil {
return "", err
}
} else {
desc, err = a.offchainAddressToDefaultVtxoDescriptor(receiver.To())
if err != nil {
return "", err
}
}
receiversOutput = append(receiversOutput, client.Output{ receiversOutput = append(receiversOutput, client.Output{
Address: receiver.To(), Descriptor: desc,
Amount: receiver.Amount(), Amount: receiver.Amount(),
}) })
sumOfReceivers += receiver.Amount() sumOfReceivers += receiver.Amount()
@@ -535,17 +562,28 @@ func (a *covenantlessArkClient) SendAsync(
} }
if changeAmount > 0 { if changeAmount > 0 {
changeDesc, err := a.offchainAddressToDefaultVtxoDescriptor(offchainAddrs[0])
if err != nil {
return "", err
}
changeReceiver := client.Output{ changeReceiver := client.Output{
Address: offchainAddrs[0], Descriptor: changeDesc,
Amount: changeAmount, Amount: changeAmount,
} }
receiversOutput = append(receiversOutput, changeReceiver) receiversOutput = append(receiversOutput, changeReceiver)
} }
inputs := make([]client.VtxoKey, 0, len(selectedCoins)) inputs := make([]client.Input, 0, len(selectedCoins))
for _, coin := range selectedCoins { for _, coin := range selectedCoins {
inputs = append(inputs, coin.VtxoKey) inputs = append(inputs, client.Input{
Outpoint: client.Outpoint{
Txid: coin.Txid,
VOut: coin.VOut,
},
Descriptor: coin.Descriptor,
})
} }
redeemTx, unconditionalForfeitTxs, err := a.client.CreatePayment( redeemTx, unconditionalForfeitTxs, err := a.client.CreatePayment(
@@ -556,23 +594,13 @@ func (a *covenantlessArkClient) SendAsync(
// TODO verify the redeem tx signature // TODO verify the redeem tx signature
signedUnconditionalForfeitTxs := make([]string, 0, len(unconditionalForfeitTxs))
for _, tx := range unconditionalForfeitTxs {
signedForfeitTx, err := a.wallet.SignTransaction(ctx, a.explorer, tx)
if err != nil {
return "", err
}
signedUnconditionalForfeitTxs = append(signedUnconditionalForfeitTxs, signedForfeitTx)
}
signedRedeemTx, err := a.wallet.SignTransaction(ctx, a.explorer, redeemTx) signedRedeemTx, err := a.wallet.SignTransaction(ctx, a.explorer, redeemTx)
if err != nil { if err != nil {
return "", err return "", err
} }
if err = a.client.CompletePayment( if err = a.client.CompletePayment(
ctx, signedRedeemTx, signedUnconditionalForfeitTxs, ctx, signedRedeemTx, unconditionalForfeitTxs,
); err != nil { ); err != nil {
return "", err return "", err
} }
@@ -612,13 +640,23 @@ func (a *covenantlessArkClient) Claim(ctx context.Context) (string, error) {
return "", nil return "", nil
} }
desc, err := a.offchainAddressToDefaultVtxoDescriptor(myselfOffchain)
if err != nil {
return "", err
}
receiver := client.Output{ receiver := client.Output{
Address: myselfOffchain, Descriptor: desc,
Amount: pendingBalance, Amount: pendingBalance,
} }
desc := strings.ReplaceAll(a.BoardingDescriptorTemplate, "USER", hex.EncodeToString(schnorr.SerializePubKey(mypubkey))) return a.selfTransferAllPendingPayments(
return a.selfTransferAllPendingPayments(ctx, pendingVtxos, boardingUtxos, receiver, desc) ctx,
pendingVtxos,
boardingUtxos,
receiver,
hex.EncodeToString(mypubkey.SerializeCompressed()),
)
} }
func (a *covenantlessArkClient) GetTransactionHistory(ctx context.Context) ([]Transaction, error) { func (a *covenantlessArkClient) GetTransactionHistory(ctx context.Context) ([]Transaction, error) {
@@ -818,8 +856,13 @@ func (a *covenantlessArkClient) sendOffchain(
return "", fmt.Errorf("invalid amount (%d), must be greater than dust %d", receiver.Amount(), a.Dust) return "", fmt.Errorf("invalid amount (%d), must be greater than dust %d", receiver.Amount(), a.Dust)
} }
desc, err := a.offchainAddressToDefaultVtxoDescriptor(receiver.To())
if err != nil {
return "", err
}
receiversOutput = append(receiversOutput, client.Output{ receiversOutput = append(receiversOutput, client.Output{
Address: receiver.To(), Descriptor: desc,
Amount: receiver.Amount(), Amount: receiver.Amount(),
}) })
sumOfReceivers += receiver.Amount() sumOfReceivers += receiver.Amount()
@@ -846,8 +889,14 @@ func (a *covenantlessArkClient) sendOffchain(
if err != nil { if err != nil {
return "", err return "", err
} }
desc, err := a.offchainAddressToDefaultVtxoDescriptor(offchainAddr)
if err != nil {
return "", err
}
changeReceiver := client.Output{ changeReceiver := client.Output{
Address: offchainAddr, Descriptor: desc,
Amount: changeAmount, Amount: changeAmount,
} }
receiversOutput = append(receiversOutput, changeReceiver) receiversOutput = append(receiversOutput, changeReceiver)
@@ -855,9 +904,12 @@ func (a *covenantlessArkClient) sendOffchain(
inputs := make([]client.Input, 0, len(selectedCoins)) inputs := make([]client.Input, 0, len(selectedCoins))
for _, coin := range selectedCoins { for _, coin := range selectedCoins {
inputs = append(inputs, client.VtxoKey{ inputs = append(inputs, client.Input{
Outpoint: client.Outpoint{
Txid: coin.Txid, Txid: coin.Txid,
VOut: coin.VOut, VOut: coin.VOut,
},
Descriptor: coin.Descriptor,
}) })
} }
@@ -882,7 +934,7 @@ func (a *covenantlessArkClient) sendOffchain(
log.Infof("payment registered with id: %s", paymentID) log.Infof("payment registered with id: %s", paymentID)
poolTxID, err := a.handleRoundStream( poolTxID, err := a.handleRoundStream(
ctx, paymentID, selectedCoins, false, receiversOutput, roundEphemeralKey, ctx, paymentID, selectedCoins, nil, "", receiversOutput, roundEphemeralKey,
) )
if err != nil { if err != nil {
return "", err return "", err
@@ -926,25 +978,38 @@ func (a *covenantlessArkClient) addInputs(
Sequence: sequence, Sequence: sequence,
}) })
_, leafProof, err := bitcointree.ComputeVtxoTaprootScript( vtxoScript := &bitcointree.DefaultVtxoScript{
userPubkey, aspPubkey, utxo.Delay, Owner: userPubkey,
) Asp: aspPubkey,
ExitDelay: utxo.Delay,
}
exitClosure := &bitcointree.CSVSigClosure{
Pubkey: userPubkey,
Seconds: uint(utxo.Delay),
}
exitLeaf, err := exitClosure.Leaf()
if err != nil { if err != nil {
return err return err
} }
controlBlock := leafProof.ToControlBlock(bitcointree.UnspendableKey()) _, taprootTree, err := vtxoScript.TapTree()
controlBlockBytes, err := controlBlock.ToBytes()
if err != nil { if err != nil {
return err return err
} }
leafProof, err := taprootTree.GetTaprootMerkleProof(exitLeaf.TapHash())
if err != nil {
return fmt.Errorf("failed to get taproot merkle proof: %s", err)
}
updater.Upsbt.Inputs = append(updater.Upsbt.Inputs, psbt.PInput{ updater.Upsbt.Inputs = append(updater.Upsbt.Inputs, psbt.PInput{
TaprootLeafScript: []*psbt.TaprootTapLeafScript{ TaprootLeafScript: []*psbt.TaprootTapLeafScript{
{ {
ControlBlock: controlBlockBytes, ControlBlock: leafProof.ControlBlock,
Script: leafProof.Script, Script: leafProof.Script,
LeafVersion: leafProof.LeafVersion, LeafVersion: txscript.BaseLeafVersion,
}, },
}, },
}) })
@@ -957,7 +1022,8 @@ func (a *covenantlessArkClient) handleRoundStream(
ctx context.Context, ctx context.Context,
paymentID string, paymentID string,
vtxosToSign []client.Vtxo, vtxosToSign []client.Vtxo,
mustSignRoundTx bool, boardingUtxos []explorer.Utxo,
boardingDescriptor string,
receivers []client.Output, receivers []client.Output,
roundEphemeralKey *secp256k1.PrivateKey, roundEphemeralKey *secp256k1.PrivateKey,
) (string, error) { ) (string, error) {
@@ -1033,7 +1099,7 @@ func (a *covenantlessArkClient) handleRoundStream(
log.Info("a round finalization started") log.Info("a round finalization started")
signedForfeitTxs, signedRoundTx, err := a.handleRoundFinalization( signedForfeitTxs, signedRoundTx, err := a.handleRoundFinalization(
ctx, event.(client.RoundFinalizationEvent), vtxosToSign, mustSignRoundTx, receivers, ctx, event.(client.RoundFinalizationEvent), vtxosToSign, boardingUtxos, boardingDescriptor, receivers,
) )
if err != nil { if err != nil {
return "", err return "", err
@@ -1134,30 +1200,103 @@ func (a *covenantlessArkClient) handleRoundSigningNoncesGenerated(
} }
func (a *covenantlessArkClient) handleRoundFinalization( func (a *covenantlessArkClient) handleRoundFinalization(
ctx context.Context, event client.RoundFinalizationEvent, ctx context.Context,
vtxos []client.Vtxo, mustSignRoundTx bool, receivers []client.Output, event client.RoundFinalizationEvent,
) (signedForfeits []string, signedRoundTx string, err error) { vtxos []client.Vtxo,
boardingUtxos []explorer.Utxo,
boardingDescriptor string,
receivers []client.Output,
) ([]string, string, error) {
if err := a.validateCongestionTree(event, receivers); err != nil { if err := a.validateCongestionTree(event, receivers); err != nil {
return nil, "", fmt.Errorf("failed to verify congestion tree: %s", err) return nil, "", fmt.Errorf("failed to verify congestion tree: %s", err)
} }
offchainAddr, _, err := a.wallet.NewAddress(ctx, false)
if err != nil {
return nil, "", err
}
_, myPubkey, _, err := common.DecodeAddress(offchainAddr)
if err != nil {
return nil, "", err
}
var forfeits []string
if len(vtxos) > 0 { if len(vtxos) > 0 {
signedForfeits, err = a.loopAndSign( signedForfeits, err := a.createAndSignForfeits(
ctx, event.ForfeitTxs, vtxos, event.Connectors, ctx, vtxos, event.Connectors, event.MinRelayFeeRate, myPubkey,
) )
if err != nil { if err != nil {
return return nil, "", err
}
} }
if mustSignRoundTx { forfeits = signedForfeits
signedRoundTx, err = a.wallet.SignTransaction(ctx, a.explorer, event.Tx) }
if len(boardingUtxos) > 0 {
boardingVtxoScript, err := bitcointree.ParseVtxoScript(boardingDescriptor)
if err != nil { if err != nil {
return return nil, "", err
}
roundPtx, err := psbt.NewFromRawBytes(strings.NewReader(event.Tx), true)
if err != nil {
return nil, "", err
}
// add tapscript leaf
forfeitClosure := &bitcointree.MultisigClosure{
Pubkey: myPubkey,
AspPubkey: a.AspPubkey,
}
forfeitLeaf, err := forfeitClosure.Leaf()
if err != nil {
return nil, "", err
}
_, taprootTree, err := boardingVtxoScript.TapTree()
if err != nil {
return nil, "", err
}
forfeitProof, err := taprootTree.GetTaprootMerkleProof(forfeitLeaf.TapHash())
if err != nil {
return nil, "", fmt.Errorf("failed to get taproot merkle proof for boarding utxo: %s", err)
}
tapscript := &psbt.TaprootTapLeafScript{
ControlBlock: forfeitProof.ControlBlock,
Script: forfeitProof.Script,
LeafVersion: txscript.BaseLeafVersion,
}
for i := range roundPtx.Inputs {
previousOutpoint := roundPtx.UnsignedTx.TxIn[i].PreviousOutPoint
for _, boardingUtxo := range boardingUtxos {
if boardingUtxo.Txid == previousOutpoint.Hash.String() && boardingUtxo.Vout == previousOutpoint.Index {
roundPtx.Inputs[i].TaprootLeafScript = []*psbt.TaprootTapLeafScript{tapscript}
break
}
} }
} }
return b64, err := roundPtx.B64Encode()
if err != nil {
return nil, "", err
}
signedRoundTx, err := a.wallet.SignTransaction(ctx, a.explorer, b64)
if err != nil {
return nil, "", err
}
return forfeits, signedRoundTx, nil
}
return forfeits, "", nil
} }
func (a *covenantlessArkClient) validateCongestionTree( func (a *covenantlessArkClient) validateCongestionTree(
@@ -1169,8 +1308,7 @@ func (a *covenantlessArkClient) validateCongestionTree(
return err return err
} }
netParams := utils.ToBitcoinNetwork(a.Network) if !utils.IsOnchainOnly(receivers) {
if !utils.IsBitcoinOnchainOnly(receivers, netParams) {
if err := bitcointree.ValidateCongestionTree( if err := bitcointree.ValidateCongestionTree(
event.Tree, poolTx, a.StoreData.AspPubkey, a.RoundLifetime, event.Tree, poolTx, a.StoreData.AspPubkey, a.RoundLifetime,
); err != nil { ); err != nil {
@@ -1183,7 +1321,7 @@ func (a *covenantlessArkClient) validateCongestionTree(
// } // }
if err := a.validateReceivers( if err := a.validateReceivers(
ptx, receivers, event.Tree, a.StoreData.AspPubkey, ptx, receivers, event.Tree,
); err != nil { ); err != nil {
return err return err
} }
@@ -1197,15 +1335,14 @@ func (a *covenantlessArkClient) validateReceivers(
ptx *psbt.Packet, ptx *psbt.Packet,
receivers []client.Output, receivers []client.Output,
congestionTree tree.CongestionTree, congestionTree tree.CongestionTree,
aspPubkey *secp256k1.PublicKey,
) error { ) error {
netParams := utils.ToBitcoinNetwork(a.Network) netParams := utils.ToBitcoinNetwork(a.Network)
for _, receiver := range receivers { for _, receiver := range receivers {
isOnChain, onchainScript, userPubkey, err := utils.ParseBitcoinAddress( isOnChain, onchainScript, err := utils.ParseBitcoinAddress(
receiver.Address, netParams, receiver.Address, netParams,
) )
if err != nil { if err != nil {
return err return fmt.Errorf("invalid receiver address: %s err = %s", receiver.Address, err)
} }
if isOnChain { if isOnChain {
@@ -1214,7 +1351,7 @@ func (a *covenantlessArkClient) validateReceivers(
} }
} else { } else {
if err := a.validateOffChainReceiver( if err := a.validateOffChainReceiver(
congestionTree, receiver, userPubkey, aspPubkey, congestionTree, receiver,
); err != nil { ); err != nil {
return err return err
} }
@@ -1250,12 +1387,15 @@ func (a *covenantlessArkClient) validateOnChainReceiver(
func (a *covenantlessArkClient) validateOffChainReceiver( func (a *covenantlessArkClient) validateOffChainReceiver(
congestionTree tree.CongestionTree, congestionTree tree.CongestionTree,
receiver client.Output, receiver client.Output,
userPubkey, aspPubkey *secp256k1.PublicKey,
) error { ) error {
found := false found := false
outputTapKey, _, err := bitcointree.ComputeVtxoTaprootScript(
userPubkey, aspPubkey, uint(a.UnilateralExitDelay), receiverVtxoScript, err := bitcointree.ParseVtxoScript(receiver.Descriptor)
) if err != nil {
return err
}
outputTapKey, _, err := receiverVtxoScript.TapTree()
if err != nil { if err != nil {
return err return err
} }
@@ -1298,57 +1438,107 @@ func (a *covenantlessArkClient) validateOffChainReceiver(
return nil return nil
} }
func (a *covenantlessArkClient) loopAndSign( func (a *covenantlessArkClient) createAndSignForfeits(
ctx context.Context, ctx context.Context,
forfeitTxs []string, vtxosToSign []client.Vtxo, connectors []string, vtxosToSign []client.Vtxo,
connectors []string,
feeRate chainfee.SatPerKVByte,
myPubkey *secp256k1.PublicKey,
) ([]string, error) { ) ([]string, error) {
signedForfeits := make([]string, 0) signedForfeits := make([]string, 0)
connectorsPsets := make([]*psbt.Packet, 0, len(connectors))
connectorsTxids := make([]string, 0, len(connectors))
for _, connector := range connectors { for _, connector := range connectors {
ptx, err := psbt.NewFromRawBytes(strings.NewReader(connector), true) p, err := psbt.NewFromRawBytes(strings.NewReader(connector), true)
if err != nil {
return nil, err
}
txid := ptx.UnsignedTx.TxHash().String()
connectorsTxids = append(connectorsTxids, txid)
}
for _, forfeitTx := range forfeitTxs {
ptx, err := psbt.NewFromRawBytes(strings.NewReader(forfeitTx), true)
if err != nil { if err != nil {
return nil, err return nil, err
} }
for _, input := range ptx.UnsignedTx.TxIn { connectorsPsets = append(connectorsPsets, p)
inputTxid := input.PreviousOutPoint.Hash.String()
for _, coin := range vtxosToSign {
// check if it contains one of the input to sign
if inputTxid == coin.Txid {
// verify that the connector is in the connectors list
connectorTxid := ptx.UnsignedTx.TxIn[0].PreviousOutPoint.Hash.String()
connectorFound := false
for _, txid := range connectorsTxids {
if txid == connectorTxid {
connectorFound = true
break
}
} }
if !connectorFound { for _, vtxo := range vtxosToSign {
return nil, fmt.Errorf("connector txid %s not found in the connectors list", connectorTxid) vtxoScript, err := bitcointree.ParseVtxoScript(vtxo.Descriptor)
}
signedForfeitTx, err := a.wallet.SignTransaction(ctx, a.explorer, forfeitTx)
if err != nil { if err != nil {
return nil, err return nil, err
} }
signedForfeits = append(signedForfeits, signedForfeitTx) vtxoTapKey, vtxoTapTree, err := vtxoScript.TapTree()
} if err != nil {
return nil, err
}
feeAmount, err := common.ComputeForfeitMinRelayFee(feeRate, vtxoTapTree)
if err != nil {
return nil, err
}
vtxoOutputScript, err := common.P2TRScript(vtxoTapKey)
if err != nil {
return nil, err
}
vtxoTxHash, err := chainhash.NewHashFromStr(vtxo.Txid)
if err != nil {
return nil, err
}
vtxoInput := &wire.OutPoint{
Hash: *vtxoTxHash,
Index: vtxo.VOut,
}
forfeitClosure := &bitcointree.MultisigClosure{
Pubkey: myPubkey,
AspPubkey: a.AspPubkey,
}
forfeitLeaf, err := forfeitClosure.Leaf()
if err != nil {
return nil, err
}
leafProof, err := vtxoTapTree.GetTaprootMerkleProof(forfeitLeaf.TapHash())
if err != nil {
return nil, err
}
tapscript := psbt.TaprootTapLeafScript{
ControlBlock: leafProof.ControlBlock,
Script: leafProof.Script,
LeafVersion: txscript.BaseLeafVersion,
}
for _, connectorPset := range connectorsPsets {
forfeits, err := bitcointree.BuildForfeitTxs(
connectorPset, vtxoInput, vtxo.Amount, a.Dust, feeAmount, vtxoOutputScript, a.AspPubkey,
)
if err != nil {
return nil, err
}
if len(forfeits) <= 0 {
return nil, fmt.Errorf("no forfeit txs created dust = %d", a.Dust)
}
for _, forfeit := range forfeits {
forfeit.Inputs[1].TaprootLeafScript = []*psbt.TaprootTapLeafScript{&tapscript}
b64, err := forfeit.B64Encode()
if err != nil {
return nil, err
}
signedForfeit, err := a.wallet.SignTransaction(ctx, a.explorer, b64)
if err != nil {
return nil, err
}
signedForfeits = append(signedForfeits, signedForfeit)
} }
} }
} }
return signedForfeits, nil return signedForfeits, nil
@@ -1371,14 +1561,18 @@ func (a *covenantlessArkClient) coinSelectOnchain(
descriptorStr := strings.ReplaceAll( descriptorStr := strings.ReplaceAll(
a.BoardingDescriptorTemplate, "USER", myPubkeyStr, a.BoardingDescriptorTemplate, "USER", myPubkeyStr,
) )
desc, err := descriptor.ParseTaprootDescriptor(descriptorStr)
boardingScript, err := bitcointree.ParseVtxoScript(descriptorStr)
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
} }
_, boardingTimeout, err := descriptor.ParseBoardingDescriptor(*desc) var boardingTimeout uint
if err != nil {
return nil, 0, err if defaultVtxo, ok := boardingScript.(*bitcointree.DefaultVtxoScript); ok {
boardingTimeout = defaultVtxo.ExitDelay
} else {
return nil, 0, fmt.Errorf("unsupported boarding descriptor: %s", descriptorStr)
} }
now := time.Now() now := time.Now()
@@ -1567,14 +1761,18 @@ func (a *covenantlessArkClient) getClaimableBoardingUtxos(ctx context.Context) (
descriptorStr := strings.ReplaceAll( descriptorStr := strings.ReplaceAll(
a.BoardingDescriptorTemplate, "USER", myPubkeyStr, a.BoardingDescriptorTemplate, "USER", myPubkeyStr,
) )
desc, err := descriptor.ParseTaprootDescriptor(descriptorStr)
boardingScript, err := bitcointree.ParseVtxoScript(descriptorStr)
if err != nil { if err != nil {
return nil, err return nil, err
} }
_, boardingTimeout, err := descriptor.ParseBoardingDescriptor(*desc) var boardingTimeout uint
if err != nil {
return nil, err if defaultVtxo, ok := boardingScript.(*bitcointree.DefaultVtxoScript); ok {
boardingTimeout = defaultVtxo.ExitDelay
} else {
return nil, fmt.Errorf("unsupported boarding descriptor: %s", descriptorStr)
} }
claimable := make([]explorer.Utxo, 0) claimable := make([]explorer.Utxo, 0)
@@ -1644,17 +1842,27 @@ func (a *covenantlessArkClient) getVtxos(
} }
func (a *covenantlessArkClient) selfTransferAllPendingPayments( func (a *covenantlessArkClient) selfTransferAllPendingPayments(
ctx context.Context, pendingVtxos []client.Vtxo, boardingUtxo []explorer.Utxo, myself client.Output, boardingDescriptor string, ctx context.Context, pendingVtxos []client.Vtxo, boardingUtxos []explorer.Utxo, myself client.Output, mypubkey string,
) (string, error) { ) (string, error) {
inputs := make([]client.Input, 0, len(pendingVtxos)+len(boardingUtxo)) inputs := make([]client.Input, 0, len(pendingVtxos)+len(boardingUtxos))
boardingDescriptor := strings.ReplaceAll(
a.BoardingDescriptorTemplate, "USER", mypubkey[2:],
)
for _, coin := range pendingVtxos { for _, coin := range pendingVtxos {
inputs = append(inputs, coin.VtxoKey) inputs = append(inputs, client.Input{
Outpoint: client.Outpoint{
Txid: coin.Txid,
VOut: coin.VOut,
},
Descriptor: coin.Descriptor,
})
} }
for _, utxo := range boardingUtxo { for _, utxo := range boardingUtxos {
inputs = append(inputs, client.BoardingInput{ inputs = append(inputs, client.Input{
VtxoKey: client.VtxoKey{ Outpoint: client.Outpoint{
Txid: utxo.Txid, Txid: utxo.Txid,
VOut: utxo.Vout, VOut: utxo.Vout,
}, },
@@ -1682,7 +1890,7 @@ func (a *covenantlessArkClient) selfTransferAllPendingPayments(
} }
roundTxid, err := a.handleRoundStream( roundTxid, err := a.handleRoundStream(
ctx, paymentID, pendingVtxos, len(boardingUtxo) > 0, outputs, roundEphemeralKey, ctx, paymentID, pendingVtxos, boardingUtxos, boardingDescriptor, outputs, roundEphemeralKey,
) )
if err != nil { if err != nil {
return "", err return "", err
@@ -1691,6 +1899,42 @@ func (a *covenantlessArkClient) selfTransferAllPendingPayments(
return roundTxid, nil return roundTxid, nil
} }
func (a *covenantlessArkClient) offchainAddressToReversibleVtxoDescriptor(myaddr string, receiveraddr string) (string, error) {
_, receiverPubkey, aspPubkey, err := common.DecodeAddress(receiveraddr)
if err != nil {
return "", err
}
_, userPubKey, _, err := common.DecodeAddress(myaddr)
if err != nil {
return "", err
}
vtxoScript := bitcointree.ReversibleVtxoScript{
Owner: receiverPubkey,
Sender: userPubKey,
Asp: aspPubkey,
ExitDelay: uint(a.UnilateralExitDelay),
}
return vtxoScript.ToDescriptor(), nil
}
func (a *covenantlessArkClient) offchainAddressToDefaultVtxoDescriptor(addr string) (string, error) {
_, userPubKey, aspPubkey, err := common.DecodeAddress(addr)
if err != nil {
return "", err
}
vtxoScript := bitcointree.DefaultVtxoScript{
Owner: userPubKey,
Asp: aspPubkey,
ExitDelay: uint(a.UnilateralExitDelay),
}
return vtxoScript.ToDescriptor(), nil
}
func (a *covenantlessArkClient) getBoardingTxs(ctx context.Context) (transactions []Transaction) { func (a *covenantlessArkClient) getBoardingTxs(ctx context.Context) (transactions []Transaction) {
utxos, err := a.getClaimableBoardingUtxos(ctx) utxos, err := a.getClaimableBoardingUtxos(ctx)
if err != nil { if err != nil {

View File

@@ -18,6 +18,7 @@ require (
github.com/go-openapi/strfmt v0.23.0 github.com/go-openapi/strfmt v0.23.0
github.com/go-openapi/swag v0.23.0 github.com/go-openapi/swag v0.23.0
github.com/go-openapi/validate v0.24.0 github.com/go-openapi/validate v0.24.0
github.com/lightningnetwork/lnd v0.18.2-beta
github.com/sirupsen/logrus v1.9.3 github.com/sirupsen/logrus v1.9.3
github.com/stretchr/testify v1.9.0 github.com/stretchr/testify v1.9.0
github.com/vulpemventures/go-elements v0.5.4 github.com/vulpemventures/go-elements v0.5.4
@@ -38,6 +39,7 @@ require (
github.com/btcsuite/btcwallet/wtxmgr v1.5.3 // indirect github.com/btcsuite/btcwallet/wtxmgr v1.5.3 // 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/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/containerd/continuity v0.4.3 // indirect github.com/containerd/continuity v0.4.3 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
@@ -45,6 +47,7 @@ require (
github.com/decred/dcrd/lru v1.1.3 // indirect github.com/decred/dcrd/lru v1.1.3 // indirect
github.com/docker/docker v27.1.1+incompatible // indirect github.com/docker/docker v27.1.1+incompatible // indirect
github.com/docker/go-connections v0.5.0 // indirect github.com/docker/go-connections v0.5.0 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/analysis v0.23.0 // indirect github.com/go-openapi/analysis v0.23.0 // indirect
@@ -52,16 +55,18 @@ require (
github.com/go-openapi/jsonreference v0.21.0 // indirect github.com/go-openapi/jsonreference v0.21.0 // indirect
github.com/go-openapi/loads v0.22.0 // indirect github.com/go-openapi/loads v0.22.0 // indirect
github.com/go-openapi/spec v0.21.0 // indirect github.com/go-openapi/spec v0.21.0 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/uuid v1.6.0 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.21.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.21.0 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jessevdk/go-flags v1.6.1 // indirect
github.com/josharian/intern v1.0.0 // indirect github.com/josharian/intern v1.0.0 // indirect
github.com/jrick/logrotate v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/kkdai/bstream v1.0.0 // indirect github.com/kkdai/bstream v1.0.0 // indirect
github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf // indirect github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf // indirect
github.com/lightninglabs/neutrino v0.16.1-0.20240425105051-602843d34ffd // indirect github.com/lightninglabs/neutrino v0.16.1-0.20240425105051-602843d34ffd // indirect
github.com/lightninglabs/neutrino/cache v1.1.2 // indirect github.com/lightninglabs/neutrino/cache v1.1.2 // indirect
github.com/lightningnetwork/lnd v0.18.2-beta // indirect
github.com/lightningnetwork/lnd/clock v1.1.1 // indirect github.com/lightningnetwork/lnd/clock v1.1.1 // indirect
github.com/lightningnetwork/lnd/fn v1.2.1 // indirect github.com/lightningnetwork/lnd/fn v1.2.1 // indirect
github.com/lightningnetwork/lnd/queue v1.1.1 // indirect github.com/lightningnetwork/lnd/queue v1.1.1 // indirect
@@ -74,11 +79,13 @@ require (
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/oklog/ulid v1.3.1 // indirect github.com/oklog/ulid v1.3.1 // indirect
github.com/onsi/ginkgo v1.16.4 // indirect
github.com/opencontainers/image-spec v1.1.0 // indirect github.com/opencontainers/image-spec v1.1.0 // indirect
github.com/opencontainers/runc v1.1.13 // indirect github.com/opencontainers/runc v1.1.13 // indirect
github.com/opentracing/opentracing-go v1.2.0 // indirect github.com/opentracing/opentracing-go v1.2.0 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
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/vulpemventures/fastsha256 v0.0.0-20160815193821-637e65642941 // indirect github.com/vulpemventures/fastsha256 v0.0.0-20160815193821-637e65642941 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
go.etcd.io/etcd/client/v2 v2.305.15 // indirect go.etcd.io/etcd/client/v2 v2.305.15 // indirect

View File

@@ -93,6 +93,7 @@ github.com/fergusstrange/embedded-postgres v1.28.0 h1:Atixd24HCuBHBavnG4eiZAjRiz
github.com/fergusstrange/embedded-postgres v1.28.0/go.mod h1:t/MLs0h9ukYM6FSt99R7InCHs1nW0ordoVCcnzmpTYw= github.com/fergusstrange/embedded-postgres v1.28.0/go.mod h1:t/MLs0h9ukYM6FSt99R7InCHs1nW0ordoVCcnzmpTYw=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk= github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk=
github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
@@ -263,16 +264,19 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8m
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc=
github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.26.0 h1:03cDLK28U6hWvCAns6NeydX3zIm4SF3ci69ulidS32Q=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
@@ -432,6 +436,7 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

View File

@@ -67,60 +67,34 @@ func CoinSelect(
} }
func ParseLiquidAddress(addr string) ( func ParseLiquidAddress(addr string) (
bool, []byte, *secp256k1.PublicKey, error, bool, []byte, error,
) { ) {
outputScript, err := address.ToOutputScript(addr) outputScript, err := address.ToOutputScript(addr)
if err != nil { if err != nil {
_, userPubkey, _, err := common.DecodeAddress(addr) return false, nil, nil
if err != nil {
return false, nil, nil, err
}
return false, nil, userPubkey, nil
} }
return true, outputScript, nil, nil return true, outputScript, nil
} }
func ParseBitcoinAddress(addr string, net chaincfg.Params) ( func ParseBitcoinAddress(addr string, net chaincfg.Params) (
bool, []byte, *secp256k1.PublicKey, error, bool, []byte, error,
) { ) {
btcAddr, err := btcutil.DecodeAddress(addr, &net) btcAddr, err := btcutil.DecodeAddress(addr, &net)
if err != nil { if err != nil {
_, userPubkey, _, err := common.DecodeAddress(addr) return false, nil, nil
if err != nil {
return false, nil, nil, err
}
return false, nil, userPubkey, nil
} }
onchainScript, err := txscript.PayToAddrScript(btcAddr) onchainScript, err := txscript.PayToAddrScript(btcAddr)
if err != nil { if err != nil {
return false, nil, nil, err return false, nil, err
} }
return true, onchainScript, nil, nil return true, onchainScript, nil
} }
func IsBitcoinOnchainOnly(receivers []client.Output, net chaincfg.Params) bool { func IsOnchainOnly(receivers []client.Output) bool {
for _, receiver := range receivers { for _, receiver := range receivers {
isOnChain, _, _, err := ParseBitcoinAddress(receiver.Address, net) isOnChain := len(receiver.Address) > 0
if err != nil {
continue
}
if !isOnChain {
return false
}
}
return true
}
func IsLiquidOnchainOnly(receivers []client.Output) bool {
for _, receiver := range receivers {
isOnChain, _, _, err := ParseLiquidAddress(receiver.Address)
if err != nil {
continue
}
if !isOnChain { if !isOnChain {
return false return false

View File

@@ -9,7 +9,6 @@ import (
"github.com/ark-network/ark/common" "github.com/ark-network/ark/common"
"github.com/ark-network/ark/common/bitcointree" "github.com/ark-network/ark/common/bitcointree"
"github.com/ark-network/ark/common/descriptor"
"github.com/ark-network/ark/pkg/client-sdk/explorer" "github.com/ark-network/ark/pkg/client-sdk/explorer"
"github.com/ark-network/ark/pkg/client-sdk/internal/utils" "github.com/ark-network/ark/pkg/client-sdk/internal/utils"
"github.com/ark-network/ark/pkg/client-sdk/store" "github.com/ark-network/ark/pkg/client-sdk/store"
@@ -219,9 +218,13 @@ func (w *bitcoinWallet) getAddress(
netParams := utils.ToBitcoinNetwork(data.Network) netParams := utils.ToBitcoinNetwork(data.Network)
vtxoTapKey, _, err := bitcointree.ComputeVtxoTaprootScript( defaultVtxoScript := &bitcointree.DefaultVtxoScript{
w.walletData.Pubkey, data.AspPubkey, uint(data.UnilateralExitDelay), Asp: data.AspPubkey,
) Owner: w.walletData.Pubkey,
ExitDelay: uint(data.UnilateralExitDelay),
}
vtxoTapKey, _, err := defaultVtxoScript.TapTree()
if err != nil { if err != nil {
return "", "", "", err return "", "", "", err
} }
@@ -239,19 +242,12 @@ func (w *bitcoinWallet) getAddress(
data.BoardingDescriptorTemplate, "USER", myPubkeyStr, data.BoardingDescriptorTemplate, "USER", myPubkeyStr,
) )
desc, err := descriptor.ParseTaprootDescriptor(descriptorStr) boardingVtxoScript, err := bitcointree.ParseVtxoScript(descriptorStr)
if err != nil { if err != nil {
return "", "", "", err return "", "", "", err
} }
_, boardingTimeout, err := descriptor.ParseBoardingDescriptor(*desc) boardingTapKey, _, err := boardingVtxoScript.TapTree()
if err != nil {
return "", "", "", err
}
boardingTapKey, _, err := bitcointree.ComputeVtxoTaprootScript(
w.walletData.Pubkey, data.AspPubkey, boardingTimeout,
)
if err != nil { if err != nil {
return "", "", "", err return "", "", "", err
} }

View File

@@ -8,7 +8,6 @@ import (
"strings" "strings"
"github.com/ark-network/ark/common" "github.com/ark-network/ark/common"
"github.com/ark-network/ark/common/descriptor"
"github.com/ark-network/ark/common/tree" "github.com/ark-network/ark/common/tree"
"github.com/ark-network/ark/pkg/client-sdk/explorer" "github.com/ark-network/ark/pkg/client-sdk/explorer"
"github.com/ark-network/ark/pkg/client-sdk/internal/utils" "github.com/ark-network/ark/pkg/client-sdk/internal/utils"
@@ -18,6 +17,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/vulpemventures/go-elements/payment"
"github.com/vulpemventures/go-elements/psetv2" "github.com/vulpemventures/go-elements/psetv2"
"github.com/vulpemventures/go-elements/transaction" "github.com/vulpemventures/go-elements/transaction"
) )
@@ -165,7 +165,7 @@ func (s *liquidWallet) SignTransaction(
switch c := closure.(type) { switch c := closure.(type) {
case *tree.CSVSigClosure: case *tree.CSVSigClosure:
sign = bytes.Equal(c.Pubkey.SerializeCompressed()[1:], serializedPubKey[1:]) sign = bytes.Equal(c.Pubkey.SerializeCompressed()[1:], serializedPubKey[1:])
case *tree.ForfeitClosure: case *tree.MultisigClosure:
sign = bytes.Equal(c.Pubkey.SerializeCompressed()[1:], serializedPubKey[1:]) sign = bytes.Equal(c.Pubkey.SerializeCompressed()[1:], serializedPubKey[1:])
} }
@@ -242,9 +242,23 @@ func (w *liquidWallet) getAddress(
liquidNet := utils.ToElementsNetwork(data.Network) liquidNet := utils.ToElementsNetwork(data.Network)
_, _, _, redemptionAddr, err := tree.ComputeVtxoTaprootScript( vtxoScript := &tree.DefaultVtxoScript{
w.walletData.Pubkey, data.AspPubkey, uint(data.UnilateralExitDelay), liquidNet, Owner: w.walletData.Pubkey,
) Asp: data.AspPubkey,
ExitDelay: uint(data.UnilateralExitDelay),
}
vtxoTapKey, _, err := vtxoScript.TapTree()
if err != nil {
return "", "", "", err
}
vtxoP2tr, err := payment.FromTweakedKey(vtxoTapKey, &liquidNet, nil)
if err != nil {
return "", "", "", err
}
redemptionAddr, err := vtxoP2tr.TaprootAddress()
if err != nil { if err != nil {
return "", "", "", err return "", "", "", err
} }
@@ -254,19 +268,22 @@ func (w *liquidWallet) getAddress(
data.BoardingDescriptorTemplate, "USER", myPubkeyStr, data.BoardingDescriptorTemplate, "USER", myPubkeyStr,
) )
desc, err := descriptor.ParseTaprootDescriptor(descriptorStr) onboardingScript, err := tree.ParseVtxoScript(descriptorStr)
if err != nil { if err != nil {
return "", "", "", err return "", "", "", err
} }
_, boardingTimeout, err := descriptor.ParseBoardingDescriptor(*desc) tapKey, _, err := onboardingScript.TapTree()
if err != nil { if err != nil {
return "", "", "", err return "", "", "", err
} }
_, _, _, boardingAddr, err := tree.ComputeVtxoTaprootScript( p2tr, err := payment.FromTweakedKey(tapKey, &liquidNet, nil)
w.walletData.Pubkey, data.AspPubkey, boardingTimeout, liquidNet, if err != nil {
) return "", "", "", err
}
boardingAddr, err := p2tr.TaprootAddress()
if err != nil { if err != nil {
return "", "", "", err return "", "", "", err
} }

View File

@@ -277,11 +277,11 @@ func (c *Config) txBuilderService() error {
switch c.TxBuilderType { switch c.TxBuilderType {
case "covenant": case "covenant":
svc = txbuilder.NewTxBuilder( svc = txbuilder.NewTxBuilder(
c.wallet, c.Network, c.RoundLifetime, c.UnilateralExitDelay, c.BoardingExitDelay, c.wallet, c.Network, c.RoundLifetime, c.BoardingExitDelay,
) )
case "covenantless": case "covenantless":
svc = cltxbuilder.NewTxBuilder( svc = cltxbuilder.NewTxBuilder(
c.wallet, c.Network, c.RoundLifetime, c.UnilateralExitDelay, c.BoardingExitDelay, c.wallet, c.Network, c.RoundLifetime, c.BoardingExitDelay,
) )
default: default:
err = fmt.Errorf("unknown tx builder type") err = fmt.Errorf("unknown tx builder type")

View File

@@ -18,6 +18,8 @@ import (
"github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/decred/dcrd/dcrec/secp256k1/v4"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/vulpemventures/go-elements/elementsutil" "github.com/vulpemventures/go-elements/elementsutil"
"github.com/vulpemventures/go-elements/network"
"github.com/vulpemventures/go-elements/payment"
"github.com/vulpemventures/go-elements/psetv2" "github.com/vulpemventures/go-elements/psetv2"
"github.com/vulpemventures/go-elements/transaction" "github.com/vulpemventures/go-elements/transaction"
) )
@@ -118,159 +120,158 @@ func (s *covenantService) Stop() {
close(s.eventsCh) close(s.eventsCh)
} }
func (s *covenantService) GetBoardingAddress(ctx context.Context, userPubkey *secp256k1.PublicKey) (string, error) { func (s *covenantService) GetBoardingAddress(ctx context.Context, userPubkey *secp256k1.PublicKey) (string, string, error) {
addr, _, err := s.builder.GetBoardingScript(userPubkey, s.pubkey) vtxoScript := &tree.DefaultVtxoScript{
if err != nil { Asp: s.pubkey,
return "", err Owner: userPubkey,
ExitDelay: uint(s.boardingExitDelay),
} }
return addr, nil
tapKey, _, err := vtxoScript.TapTree()
if err != nil {
return "", "", fmt.Errorf("failed to get taproot key: %s", err)
}
p2tr, err := payment.FromTweakedKey(tapKey, s.onchainNetwork(), nil)
if err != nil {
return "", "", err
}
addr, err := p2tr.TaprootAddress()
if err != nil {
return "", "", err
}
return addr, vtxoScript.ToDescriptor(), nil
} }
func (s *covenantService) SpendVtxos(ctx context.Context, inputs []Input) (string, error) { func (s *covenantService) SpendVtxos(ctx context.Context, inputs []ports.Input) (string, error) {
vtxosInputs := make([]domain.VtxoKey, 0) vtxosInputs := make([]domain.Vtxo, 0)
boardingInputs := make([]Input, 0) boardingInputs := make([]ports.BoardingInput, 0)
for _, in := range inputs {
if in.IsVtxo() {
vtxosInputs = append(vtxosInputs, in.VtxoKey())
continue
}
boardingInputs = append(boardingInputs, in)
}
vtxos := make([]domain.Vtxo, 0)
if len(vtxosInputs) > 0 {
var err error
vtxos, err = s.repoManager.Vtxos().GetVtxos(ctx, vtxosInputs)
if err != nil {
return "", err
}
for _, v := range vtxos {
if v.Spent {
return "", fmt.Errorf("input %s:%d already spent", v.Txid, v.VOut)
}
if v.Redeemed {
return "", fmt.Errorf("input %s:%d already redeemed", v.Txid, v.VOut)
}
if v.Spent {
return "", fmt.Errorf("input %s:%d already spent", v.Txid, v.VOut)
}
}
}
boardingTxs := make(map[string]string, 0) // txid -> txhex
now := time.Now().Unix() now := time.Now().Unix()
for _, in := range boardingInputs { boardingTxs := make(map[string]*transaction.Transaction, 0) // txid -> txhex
if _, ok := boardingTxs[in.Txid]; !ok {
txhex, err := s.wallet.GetTransaction(ctx, in.Txid) for _, input := range inputs {
vtxosResult, err := s.repoManager.Vtxos().GetVtxos(ctx, []domain.VtxoKey{input.VtxoKey})
if err != nil || len(vtxosResult) == 0 {
// vtxo not found in db, check if it exists on-chain
if _, ok := boardingTxs[input.Txid]; !ok {
// check if the tx exists and is confirmed
txhex, err := s.wallet.GetTransaction(ctx, input.Txid)
if err != nil { if err != nil {
return "", fmt.Errorf("failed to get tx %s: %s", in.Txid, err) return "", fmt.Errorf("failed to get tx %s: %s", input.Txid, err)
} }
confirmed, blocktime, err := s.wallet.IsTransactionConfirmed(ctx, in.Txid) tx, err := transaction.NewTxFromHex(txhex)
if err != nil { if err != nil {
return "", fmt.Errorf("failed to check tx %s: %s", in.Txid, err) return "", fmt.Errorf("failed to parse tx %s: %s", input.Txid, err)
}
confirmed, blocktime, err := s.wallet.IsTransactionConfirmed(ctx, input.Txid)
if err != nil {
return "", fmt.Errorf("failed to check tx %s: %s", input.Txid, err)
} }
if !confirmed { if !confirmed {
return "", fmt.Errorf("tx %s not confirmed", in.Txid) return "", fmt.Errorf("tx %s not confirmed", input.Txid)
} }
// if the exit path is available, forbid registering the boarding utxo
if blocktime+int64(s.boardingExitDelay) < now { if blocktime+int64(s.boardingExitDelay) < now {
return "", fmt.Errorf("tx %s expired", in.Txid) return "", fmt.Errorf("tx %s expired", input.Txid)
} }
boardingTxs[in.Txid] = txhex boardingTxs[input.Txid] = tx
}
} }
utxos := make([]ports.BoardingInput, 0, len(boardingInputs)) tx := boardingTxs[input.Txid]
boardingInput, err := s.newBoardingInput(tx, input)
for _, in := range boardingInputs {
desc, err := in.GetDescriptor()
if err != nil {
log.WithError(err).Debugf("failed to parse boarding input descriptor")
return "", fmt.Errorf("failed to parse descriptor %s for input %s:%d", in.Descriptor, in.Txid, in.Index)
}
input, err := s.newBoardingInput(boardingTxs[in.Txid], in.Index, *desc)
if err != nil {
log.WithError(err).Debugf("failed to create boarding input")
return "", fmt.Errorf("input %s:%d is not a valid boarding input", in.Txid, in.Index)
}
utxos = append(utxos, input)
}
payment, err := domain.NewPayment(vtxos)
if err != nil { if err != nil {
return "", err return "", err
} }
if err := s.paymentRequests.push(*payment, utxos); err != nil {
boardingInputs = append(boardingInputs, *boardingInput)
continue
}
vtxo := vtxosResult[0]
if vtxo.Spent {
return "", fmt.Errorf("input %s:%d already spent", vtxo.Txid, vtxo.VOut)
}
if vtxo.Redeemed {
return "", fmt.Errorf("input %s:%d already redeemed", vtxo.Txid, vtxo.VOut)
}
if vtxo.Swept {
return "", fmt.Errorf("input %s:%d already swept", vtxo.Txid, vtxo.VOut)
}
vtxosInputs = append(vtxosInputs, vtxo)
}
payment, err := domain.NewPayment(vtxosInputs)
if err != nil {
return "", err
}
if err := s.paymentRequests.push(*payment, boardingInputs); err != nil {
return "", err return "", err
} }
return payment.Id, nil return payment.Id, nil
} }
func (s *covenantService) newBoardingInput( func (s *covenantService) newBoardingInput(tx *transaction.Transaction, input ports.Input) (*ports.BoardingInput, error) {
txhex string, vout uint32, desc descriptor.TaprootDescriptor, if len(tx.Outputs) <= int(input.VtxoKey.VOut) {
) (ports.BoardingInput, error) {
tx, err := transaction.NewTxFromHex(txhex)
if err != nil {
return nil, fmt.Errorf("failed to parse tx: %s", err)
}
if len(tx.Outputs) <= int(vout) {
return nil, fmt.Errorf("output not found") return nil, fmt.Errorf("output not found")
} }
out := tx.Outputs[vout] output := tx.Outputs[input.VtxoKey.VOut]
script := out.Script
if len(out.RangeProof) > 0 || len(out.SurjectionProof) > 0 { if len(output.RangeProof) > 0 || len(output.SurjectionProof) > 0 {
return nil, fmt.Errorf("output is confidential") return nil, fmt.Errorf("output is confidential")
} }
scriptFromDescriptor, err := tree.ComputeOutputScript(desc) amount, err := elementsutil.ValueFromBytes(output.Value)
if err != nil {
return nil, fmt.Errorf("failed to compute output script: %s", err)
}
if !bytes.Equal(script, scriptFromDescriptor) {
return nil, fmt.Errorf("descriptor does not match script in transaction output")
}
pubkey, timeout, err := descriptor.ParseBoardingDescriptor(desc)
if err != nil {
return nil, fmt.Errorf("failed to parse boarding descriptor: %s", err)
}
if timeout != uint(s.boardingExitDelay) {
return nil, fmt.Errorf("invalid boarding descriptor, timeout mismatch")
}
_, expectedScript, err := s.builder.GetBoardingScript(pubkey, s.pubkey)
if err != nil {
return nil, fmt.Errorf("failed to compute boarding script: %s", err)
}
if !bytes.Equal(script, expectedScript) {
return nil, fmt.Errorf("output script mismatch expected script")
}
value, err := elementsutil.ValueFromBytes(out.Value)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to parse value: %s", err) return nil, fmt.Errorf("failed to parse value: %s", err)
} }
return &boardingInput{ boardingScript, err := tree.ParseVtxoScript(input.Descriptor)
txId: tx.TxHash(), if err != nil {
vout: vout, return nil, fmt.Errorf("failed to parse boarding descriptor: %s", err)
boardingPubKey: pubkey, }
amount: value,
tapKey, _, err := boardingScript.TapTree()
if err != nil {
return nil, fmt.Errorf("failed to get taproot key: %s", err)
}
expectedScriptPubKey, err := common.P2TRScript(tapKey)
if err != nil {
return nil, fmt.Errorf("failed to get script pubkey: %s", err)
}
if !bytes.Equal(output.Script, expectedScriptPubKey) {
return nil, fmt.Errorf("descriptor does not match script in transaction output")
}
if defaultVtxoScript, ok := boardingScript.(*tree.DefaultVtxoScript); ok {
if !bytes.Equal(schnorr.SerializePubKey(defaultVtxoScript.Asp), schnorr.SerializePubKey(s.pubkey)) {
return nil, fmt.Errorf("invalid boarding descriptor, ASP mismatch")
}
if defaultVtxoScript.ExitDelay != uint(s.boardingExitDelay) {
return nil, fmt.Errorf("invalid boarding descriptor, timeout mismatch")
}
} else {
return nil, fmt.Errorf("only default vtxo script is supported for boarding")
}
return &ports.BoardingInput{
Amount: amount,
Input: input,
}, nil }, nil
} }
@@ -315,7 +316,7 @@ func (s *covenantService) CompleteAsyncPayment(ctx context.Context, redeemTx str
return fmt.Errorf("unimplemented") return fmt.Errorf("unimplemented")
} }
func (s *covenantService) CreateAsyncPayment(ctx context.Context, inputs []domain.VtxoKey, receivers []domain.Receiver) (string, []string, error) { func (s *covenantService) CreateAsyncPayment(ctx context.Context, inputs []ports.Input, receivers []domain.Receiver) (string, []string, error) {
return "", nil, fmt.Errorf("unimplemented") return "", nil, fmt.Errorf("unimplemented")
} }
@@ -373,10 +374,10 @@ func (s *covenantService) GetInfo(ctx context.Context) (*ServiceInfo, error) {
Network: s.network.Name, Network: s.network.Name,
Dust: dust, Dust: dust,
BoardingDescriptorTemplate: fmt.Sprintf( BoardingDescriptorTemplate: fmt.Sprintf(
descriptor.BoardingDescriptorTemplate, descriptor.DefaultVtxoDescriptorTemplate,
hex.EncodeToString(tree.UnspendableKey().SerializeCompressed()), hex.EncodeToString(tree.UnspendableKey().SerializeCompressed()),
hex.EncodeToString(schnorr.SerializePubKey(s.pubkey)),
"USER", "USER",
hex.EncodeToString(schnorr.SerializePubKey(s.pubkey)),
s.boardingExitDelay, s.boardingExitDelay,
"USER", "USER",
), ),
@@ -496,8 +497,10 @@ func (s *covenantService) startFinalization() {
var forfeitTxs, connectors []string var forfeitTxs, connectors []string
minRelayFeeRate := s.wallet.MinRelayFeeRate(ctx)
if needForfeits { if needForfeits {
connectors, forfeitTxs, err = s.builder.BuildForfeitTxs(s.pubkey, unsignedPoolTx, payments) connectors, forfeitTxs, err = s.builder.BuildForfeitTxs(s.pubkey, unsignedPoolTx, payments, minRelayFeeRate)
if err != nil { if err != nil {
round.Fail(fmt.Errorf("failed to create connectors and forfeit txs: %s", err)) round.Fail(fmt.Errorf("failed to create connectors and forfeit txs: %s", err))
log.WithError(err).Warn("failed to create connectors and forfeit txs") log.WithError(err).Warn("failed to create connectors and forfeit txs")
@@ -505,6 +508,12 @@ func (s *covenantService) startFinalization() {
} }
log.Debugf("forfeit transactions created for round %s", round.Id) 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(
@@ -515,8 +524,6 @@ func (s *covenantService) startFinalization() {
return return
} }
s.forfeitTxs.push(forfeitTxs)
log.Debugf("started finalization stage for round: %s", round.Id) log.Debugf("started finalization stage for round: %s", round.Id)
} }
@@ -845,13 +852,12 @@ func (s *covenantService) propagateEvents(round *domain.Round) {
lastEvent := round.Events()[len(round.Events())-1] lastEvent := round.Events()[len(round.Events())-1]
switch e := lastEvent.(type) { switch e := lastEvent.(type) {
case domain.RoundFinalizationStarted: case domain.RoundFinalizationStarted:
forfeitTxs := s.forfeitTxs.view()
ev := domain.RoundFinalizationStarted{ ev := domain.RoundFinalizationStarted{
Id: e.Id, Id: e.Id,
CongestionTree: e.CongestionTree, CongestionTree: e.CongestionTree,
Connectors: e.Connectors, Connectors: e.Connectors,
PoolTx: e.PoolTx, PoolTx: e.PoolTx,
UnsignedForfeitTxs: forfeitTxs, MinRelayFeeRate: int64(s.wallet.MinRelayFeeRate(context.Background())),
} }
s.lastEvent = ev s.lastEvent = ev
s.eventsCh <- ev s.eventsCh <- ev
@@ -888,34 +894,59 @@ func (s *covenantService) getNewVtxos(round *domain.Round) []domain.Vtxo {
for _, node := range leaves { for _, node := range leaves {
tx, _ := psetv2.NewPsetFromBase64(node.Tx) tx, _ := psetv2.NewPsetFromBase64(node.Tx)
for i, out := range tx.Outputs { for i, out := range tx.Outputs {
for _, p := range round.Payments { if len(out.Script) <= 0 {
var pubkey string continue // skip fee outputs
}
desc := ""
found := false found := false
for _, p := range round.Payments {
if found {
break
}
for _, r := range p.Receivers { for _, r := range p.Receivers {
if r.IsOnchain() { if r.IsOnchain() {
continue continue
} }
buf, _ := hex.DecodeString(r.Pubkey) vtxoScript, err := tree.ParseVtxoScript(r.Descriptor)
pk, _ := secp256k1.ParsePubKey(buf) if err != nil {
script, _ := s.builder.GetVtxoScript(pk, s.pubkey) log.WithError(err).Warn("failed to parse vtxo descriptor")
continue
}
tapKey, _, err := vtxoScript.TapTree()
if err != nil {
log.WithError(err).Warn("failed to compute vtxo tap key")
continue
}
script, err := common.P2TRScript(tapKey)
if err != nil {
log.WithError(err).Warn("failed to create vtxo scriptpubkey")
continue
}
if bytes.Equal(script, out.Script) { if bytes.Equal(script, out.Script) {
found = true found = true
pubkey = r.Pubkey desc = r.Descriptor
break break
} }
} }
}
if found { if found {
vtxos = append(vtxos, domain.Vtxo{ vtxos = append(vtxos, domain.Vtxo{
VtxoKey: domain.VtxoKey{Txid: node.Txid, VOut: uint32(i)}, VtxoKey: domain.VtxoKey{Txid: node.Txid, VOut: uint32(i)},
Receiver: domain.Receiver{Pubkey: pubkey, Amount: out.Value}, Receiver: domain.Receiver{Descriptor: desc, Amount: uint64(out.Value)},
PoolTx: round.Txid, PoolTx: round.Txid,
}) })
break break
} }
} }
} }
}
return vtxos return vtxos
} }
@@ -984,15 +1015,17 @@ func (s *covenantService) restoreWatchingVtxos() error {
func (s *covenantService) extractVtxosScripts(vtxos []domain.Vtxo) ([]string, error) { func (s *covenantService) extractVtxosScripts(vtxos []domain.Vtxo) ([]string, error) {
indexedScripts := make(map[string]struct{}) indexedScripts := make(map[string]struct{})
for _, vtxo := range vtxos { for _, vtxo := range vtxos {
buf, err := hex.DecodeString(vtxo.Pubkey) vtxoScript, err := tree.ParseVtxoScript(vtxo.Receiver.Descriptor)
if err != nil { if err != nil {
return nil, err return nil, err
} }
userPubkey, err := secp256k1.ParsePubKey(buf)
tapKey, _, err := vtxoScript.TapTree()
if err != nil { if err != nil {
return nil, err return nil, err
} }
script, err := s.builder.GetVtxoScript(userPubkey, s.pubkey)
script, err := common.P2TRScript(tapKey)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -1019,6 +1052,19 @@ func (s *covenantService) saveEvents(
return s.repoManager.Rounds().AddOrUpdateRound(ctx, *round) return s.repoManager.Rounds().AddOrUpdateRound(ctx, *round)
} }
func (s *covenantService) onchainNetwork() *network.Network {
switch s.network {
case common.Liquid:
return &network.Liquid
case common.LiquidTestNet:
return &network.Testnet
case common.LiquidRegTest:
return &network.Regtest
default:
return nil
}
}
func findForfeitTxLiquid( func findForfeitTxLiquid(
forfeits []string, connectorTxid string, connectorVout uint32, vtxoTxid string, forfeits []string, connectorTxid string, connectorVout uint32, vtxoTxid string,
) (string, error) { ) (string, error) {

View File

@@ -16,7 +16,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"
"github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/btcutil/psbt" "github.com/btcsuite/btcd/btcutil/psbt"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
"github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/decred/dcrd/dcrec/secp256k1/v4"
@@ -153,10 +155,9 @@ func (s *covenantlessService) CompleteAsyncPayment(
return fmt.Errorf("async payment not found") return fmt.Errorf("async payment not found")
} }
txs := append([]string{redeemTx}, unconditionalForfeitTxs...)
vtxoRepo := s.repoManager.Vtxos() vtxoRepo := s.repoManager.Vtxos()
for _, tx := range txs { for _, tx := range []string{redeemTx} {
ptx, err := psbt.NewFromRawBytes(strings.NewReader(tx), true) ptx, err := psbt.NewFromRawBytes(strings.NewReader(tx), true)
if err != nil { if err != nil {
return fmt.Errorf("failed to parse tx: %s", err) return fmt.Errorf("failed to parse tx: %s", err)
@@ -200,39 +201,40 @@ func (s *covenantlessService) CompleteAsyncPayment(
return fmt.Errorf("vtxo already swept") return fmt.Errorf("vtxo already swept")
} }
// verify that the user signs the tx using the right public key vtxoScript, err := bitcointree.ParseVtxoScript(vtxo[0].Descriptor)
vtxoPublicKey, err := hex.DecodeString(vtxo[0].Pubkey)
if err != nil { if err != nil {
return fmt.Errorf("failed to decode pubkey: %s", err) return fmt.Errorf("failed to parse vtxo script: %s", err)
} }
pubkey, err := secp256k1.ParsePubKey(vtxoPublicKey) vtxoTapKey, _, err := vtxoScript.TapTree()
if err != nil {
return fmt.Errorf("failed to get taproot key: %s", err)
}
// verify that the user signs a forfeit closure
var userPubKey *secp256k1.PublicKey
aspXOnlyPubKey := schnorr.SerializePubKey(s.pubkey)
for _, sig := range input.TaprootScriptSpendSig {
if !bytes.Equal(sig.XOnlyPubKey, aspXOnlyPubKey) {
parsed, err := schnorr.ParsePubKey(sig.XOnlyPubKey)
if err != nil { if err != nil {
return fmt.Errorf("failed to parse pubkey: %s", err) return fmt.Errorf("failed to parse pubkey: %s", err)
} }
userPubKey = parsed
xonlyPubkey := schnorr.SerializePubKey(pubkey)
// find signature belonging to the pubkey
found := false
for _, sig := range input.TaprootScriptSpendSig {
if bytes.Equal(sig.XOnlyPubKey, xonlyPubkey) {
found = true
break break
} }
} }
if !found { if userPubKey == nil {
return fmt.Errorf("signature not found for pubkey") return fmt.Errorf("redeem transaction is not signed")
} }
// verify witness utxo // verify witness utxo
pkscript, err := common.P2TRScript(vtxoTapKey)
pkscript, err := s.builder.GetVtxoScript(pubkey, s.pubkey)
if err != nil { if err != nil {
return fmt.Errorf("failed to get vtxo script: %s", err) return fmt.Errorf("failed to get pkscript: %s", err)
} }
if !bytes.Equal(input.WitnessUtxo.PkScript, pkscript) { if !bytes.Equal(input.WitnessUtxo.PkScript, pkscript) {
@@ -250,7 +252,7 @@ func (s *covenantlessService) CompleteAsyncPayment(
} }
} }
spentVtxos := make([]domain.VtxoKey, 0, len(unconditionalForfeitTxs)) spentVtxos := make([]domain.VtxoKey, 0)
for _, in := range redeemPtx.UnsignedTx.TxIn { for _, in := range redeemPtx.UnsignedTx.TxIn {
spentVtxos = append(spentVtxos, domain.VtxoKey{ spentVtxos = append(spentVtxos, domain.VtxoKey{
Txid: in.PreviousOutPoint.Hash.String(), Txid: in.PreviousOutPoint.Hash.String(),
@@ -267,7 +269,7 @@ func (s *covenantlessService) CompleteAsyncPayment(
VOut: uint32(outIndex), VOut: uint32(outIndex),
}, },
Receiver: domain.Receiver{ Receiver: domain.Receiver{
Pubkey: asyncPayData.receivers[outIndex].Pubkey, Descriptor: asyncPayData.receivers[outIndex].Descriptor,
Amount: uint64(out.Value), Amount: uint64(out.Value),
}, },
ExpireAt: asyncPayData.expireAt, ExpireAt: asyncPayData.expireAt,
@@ -300,9 +302,14 @@ func (s *covenantlessService) CompleteAsyncPayment(
} }
func (s *covenantlessService) CreateAsyncPayment( func (s *covenantlessService) CreateAsyncPayment(
ctx context.Context, inputs []domain.VtxoKey, receivers []domain.Receiver, ctx context.Context, inputs []ports.Input, receivers []domain.Receiver,
) (string, []string, error) { ) (string, []string, error) {
vtxos, err := s.repoManager.Vtxos().GetVtxos(ctx, inputs) vtxosKeys := make([]domain.VtxoKey, 0, len(inputs))
for _, in := range inputs {
vtxosKeys = append(vtxosKeys, in.VtxoKey)
}
vtxos, err := s.repoManager.Vtxos().GetVtxos(ctx, vtxosKeys)
if err != nil { if err != nil {
return "", nil, err return "", nil, err
} }
@@ -310,6 +317,8 @@ func (s *covenantlessService) CreateAsyncPayment(
return "", nil, fmt.Errorf("vtxos not found") return "", nil, fmt.Errorf("vtxos not found")
} }
vtxosInputs := make([]domain.Vtxo, 0, len(inputs))
expiration := vtxos[0].ExpireAt expiration := vtxos[0].ExpireAt
for _, vtxo := range vtxos { for _, vtxo := range vtxos {
if vtxo.Spent { if vtxo.Spent {
@@ -327,10 +336,12 @@ func (s *covenantlessService) CreateAsyncPayment(
if vtxo.ExpireAt < expiration { if vtxo.ExpireAt < expiration {
expiration = vtxo.ExpireAt expiration = vtxo.ExpireAt
} }
vtxosInputs = append(vtxosInputs, vtxo)
} }
res, err := s.builder.BuildAsyncPaymentTransactions( res, err := s.builder.BuildAsyncPaymentTransactions(
vtxos, s.pubkey, receivers, vtxosInputs, s.pubkey, receivers,
) )
if err != nil { if err != nil {
return "", nil, fmt.Errorf("failed to build async payment txs: %s", err) return "", nil, fmt.Errorf("failed to build async payment txs: %s", err)
@@ -352,144 +363,148 @@ func (s *covenantlessService) CreateAsyncPayment(
return res.RedeemTx, res.UnconditionalForfeitTxs, nil return res.RedeemTx, res.UnconditionalForfeitTxs, nil
} }
func (s *covenantlessService) SpendVtxos(ctx context.Context, inputs []Input) (string, error) { func (s *covenantlessService) GetBoardingAddress(
vtxosInputs := make([]domain.VtxoKey, 0) ctx context.Context, userPubkey *secp256k1.PublicKey,
boardingInputs := make([]Input, 0) ) (address string, descriptor string, err error) {
vtxoScript := &bitcointree.DefaultVtxoScript{
for _, input := range inputs { Asp: s.pubkey,
if input.IsVtxo() { Owner: userPubkey,
vtxosInputs = append(vtxosInputs, input.VtxoKey()) ExitDelay: uint(s.boardingExitDelay),
continue
} }
boardingInputs = append(boardingInputs, input) tapKey, _, err := vtxoScript.TapTree()
}
vtxos := make([]domain.Vtxo, 0)
if len(vtxosInputs) > 0 {
var err error
vtxos, err = s.repoManager.Vtxos().GetVtxos(ctx, vtxosInputs)
if err != nil { if err != nil {
return "", err return "", "", fmt.Errorf("failed to get taproot key: %s", err)
}
for _, v := range vtxos {
if v.Spent {
return "", fmt.Errorf("input %s:%d already spent", v.Txid, v.VOut)
} }
if v.Redeemed { addr, err := btcutil.NewAddressTaproot(
return "", fmt.Errorf("input %s:%d already redeemed", v.Txid, v.VOut) schnorr.SerializePubKey(tapKey), s.chainParams(),
)
if err != nil {
return "", "", fmt.Errorf("failed to get address: %s", err)
} }
if v.Spent { return addr.EncodeAddress(), vtxoScript.ToDescriptor(), nil
return "", fmt.Errorf("input %s:%d already spent", v.Txid, v.VOut) }
}
} func (s *covenantlessService) SpendVtxos(ctx context.Context, inputs []ports.Input) (string, error) {
} vtxosInputs := make([]domain.Vtxo, 0)
boardingInputs := make([]ports.BoardingInput, 0)
boardingTxs := make(map[string]string, 0) // txid -> txhex
now := time.Now().Unix() now := time.Now().Unix()
for _, in := range boardingInputs { boardingTxs := make(map[string]wire.MsgTx, 0) // txid -> txhex
if _, ok := boardingTxs[in.Txid]; !ok {
for _, input := range inputs {
vtxosResult, err := s.repoManager.Vtxos().GetVtxos(ctx, []domain.VtxoKey{input.VtxoKey})
if err != nil || len(vtxosResult) == 0 {
// vtxo not found in db, check if it exists on-chain
if _, ok := boardingTxs[input.Txid]; !ok {
// check if the tx exists and is confirmed // check if the tx exists and is confirmed
txhex, err := s.wallet.GetTransaction(ctx, in.Txid) txhex, err := s.wallet.GetTransaction(ctx, input.Txid)
if err != nil { if err != nil {
return "", fmt.Errorf("failed to get tx %s: %s", in.Txid, err) return "", fmt.Errorf("failed to get tx %s: %s", input.Txid, err)
} }
confirmed, blocktime, err := s.wallet.IsTransactionConfirmed(ctx, in.Txid) var tx wire.MsgTx
if err := tx.Deserialize(hex.NewDecoder(strings.NewReader(txhex))); err != nil {
return "", fmt.Errorf("failed to deserialize tx %s: %s", input.Txid, err)
}
confirmed, blocktime, err := s.wallet.IsTransactionConfirmed(ctx, input.Txid)
if err != nil { if err != nil {
return "", fmt.Errorf("failed to check tx %s: %s", in.Txid, err) return "", fmt.Errorf("failed to check tx %s: %s", input.Txid, err)
} }
if !confirmed { if !confirmed {
return "", fmt.Errorf("tx %s not confirmed", in.Txid) return "", fmt.Errorf("tx %s not confirmed", input.Txid)
} }
// if the exit path is available, forbid registering the boarding utxo // if the exit path is available, forbid registering the boarding utxo
if blocktime+int64(s.boardingExitDelay) < now { if blocktime+int64(s.boardingExitDelay) < now {
return "", fmt.Errorf("tx %s expired", in.Txid) return "", fmt.Errorf("tx %s expired", input.Txid)
} }
boardingTxs[in.Txid] = txhex boardingTxs[input.Txid] = tx
}
} }
utxos := make([]ports.BoardingInput, 0, len(boardingInputs)) tx := boardingTxs[input.Txid]
boardingInput, err := s.newBoardingInput(tx, input)
for _, in := range boardingInputs {
desc, err := in.GetDescriptor()
if err != nil {
log.WithError(err).Debugf("failed to parse boarding input descriptor")
return "", fmt.Errorf("failed to parse descriptor %s for input %s:%d", in.Descriptor, in.Txid, in.Index)
}
input, err := s.newBoardingInput(boardingTxs[in.Txid], in.Index, *desc)
if err != nil {
log.WithError(err).Debugf("failed to create boarding input")
return "", fmt.Errorf("input %s:%d is not a valid boarding input", in.Txid, in.Index)
}
utxos = append(utxos, input)
}
payment, err := domain.NewPayment(vtxos)
if err != nil { if err != nil {
return "", err return "", err
} }
if err := s.paymentRequests.push(*payment, utxos); err != nil {
boardingInputs = append(boardingInputs, *boardingInput)
continue
}
vtxo := vtxosResult[0]
if vtxo.Spent {
return "", fmt.Errorf("input %s:%d already spent", vtxo.Txid, vtxo.VOut)
}
if vtxo.Redeemed {
return "", fmt.Errorf("input %s:%d already redeemed", vtxo.Txid, vtxo.VOut)
}
if vtxo.Swept {
return "", fmt.Errorf("input %s:%d already swept", vtxo.Txid, vtxo.VOut)
}
vtxosInputs = append(vtxosInputs, vtxo)
}
payment, err := domain.NewPayment(vtxosInputs)
if err != nil {
return "", err
}
if err := s.paymentRequests.push(*payment, boardingInputs); err != nil {
return "", err return "", err
} }
return payment.Id, nil return payment.Id, nil
} }
func (s *covenantlessService) newBoardingInput(txhex string, vout uint32, desc descriptor.TaprootDescriptor) (ports.BoardingInput, error) { func (s *covenantlessService) newBoardingInput(tx wire.MsgTx, input ports.Input) (*ports.BoardingInput, error) {
var tx wire.MsgTx if len(tx.TxOut) <= int(input.VtxoKey.VOut) {
if err := tx.Deserialize(hex.NewDecoder(strings.NewReader(txhex))); err != nil {
return nil, fmt.Errorf("failed to deserialize tx: %s", err)
}
if len(tx.TxOut) <= int(vout) {
return nil, fmt.Errorf("output not found") return nil, fmt.Errorf("output not found")
} }
out := tx.TxOut[vout] output := tx.TxOut[input.VtxoKey.VOut]
script := out.PkScript
scriptFromDescriptor, err := bitcointree.ComputeOutputScript(desc) boardingScript, err := bitcointree.ParseVtxoScript(input.Descriptor)
if err != nil {
return nil, fmt.Errorf("failed to compute output script: %s", err)
}
if !bytes.Equal(script, scriptFromDescriptor) {
return nil, fmt.Errorf("descriptor does not match script in transaction output")
}
pubkey, timeout, err := descriptor.ParseBoardingDescriptor(desc)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to parse boarding descriptor: %s", err) return nil, fmt.Errorf("failed to parse boarding descriptor: %s", err)
} }
if timeout != uint(s.boardingExitDelay) { tapKey, _, err := boardingScript.TapTree()
if err != nil {
return nil, fmt.Errorf("failed to get taproot key: %s", err)
}
expectedScriptPubKey, err := common.P2TRScript(tapKey)
if err != nil {
return nil, fmt.Errorf("failed to get script pubkey: %s", err)
}
if !bytes.Equal(output.PkScript, expectedScriptPubKey) {
return nil, fmt.Errorf("descriptor does not match script in transaction output")
}
if defaultVtxoScript, ok := boardingScript.(*bitcointree.DefaultVtxoScript); ok {
if !bytes.Equal(schnorr.SerializePubKey(defaultVtxoScript.Asp), schnorr.SerializePubKey(s.pubkey)) {
return nil, fmt.Errorf("invalid boarding descriptor, ASP mismatch")
}
if defaultVtxoScript.ExitDelay != uint(s.boardingExitDelay) {
return nil, fmt.Errorf("invalid boarding descriptor, timeout mismatch") return nil, fmt.Errorf("invalid boarding descriptor, timeout mismatch")
} }
} else {
_, expectedScript, err := s.builder.GetBoardingScript(pubkey, s.pubkey) return nil, fmt.Errorf("only default vtxo script is supported for boarding")
if err != nil {
return nil, fmt.Errorf("failed to get boarding script: %s", err)
} }
if !bytes.Equal(script, expectedScript) { return &ports.BoardingInput{
return nil, fmt.Errorf("invalid boarding input output script") Amount: uint64(output.Value),
} Input: input,
return &boardingInput{
txId: tx.TxHash(),
vout: vout,
boardingPubKey: pubkey,
amount: uint64(out.Value),
}, nil }, nil
} }
@@ -584,27 +599,16 @@ func (s *covenantlessService) GetInfo(ctx context.Context) (*ServiceInfo, error)
Network: s.network.Name, Network: s.network.Name,
Dust: dust, Dust: dust,
BoardingDescriptorTemplate: fmt.Sprintf( BoardingDescriptorTemplate: fmt.Sprintf(
descriptor.BoardingDescriptorTemplate, descriptor.DefaultVtxoDescriptorTemplate,
hex.EncodeToString(bitcointree.UnspendableKey().SerializeCompressed()), hex.EncodeToString(bitcointree.UnspendableKey().SerializeCompressed()),
hex.EncodeToString(schnorr.SerializePubKey(s.pubkey)),
"USER", "USER",
hex.EncodeToString(schnorr.SerializePubKey(s.pubkey)),
s.boardingExitDelay, s.boardingExitDelay,
"USER", "USER",
), ),
}, nil }, nil
} }
func (s *covenantlessService) GetBoardingAddress(
ctx context.Context, userPubkey *secp256k1.PublicKey,
) (string, error) {
addr, _, err := s.builder.GetBoardingScript(userPubkey, s.pubkey)
if err != nil {
return "", fmt.Errorf("failed to compute boarding script: %s", err)
}
return addr, nil
}
func (s *covenantlessService) RegisterCosignerPubkey(ctx context.Context, paymentId string, pubkey string) error { func (s *covenantlessService) RegisterCosignerPubkey(ctx context.Context, paymentId string, pubkey string) error {
pubkeyBytes, err := hex.DecodeString(pubkey) pubkeyBytes, err := hex.DecodeString(pubkey)
if err != nil { if err != nil {
@@ -944,14 +948,22 @@ func (s *covenantlessService) startFinalization() {
var forfeitTxs, connectors []string var forfeitTxs, connectors []string
minRelayFeeRate := s.wallet.MinRelayFeeRate(ctx)
if needForfeits { if needForfeits {
connectors, forfeitTxs, err = s.builder.BuildForfeitTxs(s.pubkey, unsignedRoundTx, payments) connectors, forfeitTxs, err = s.builder.BuildForfeitTxs(s.pubkey, unsignedRoundTx, payments, minRelayFeeRate)
if err != nil { if err != nil {
round.Fail(fmt.Errorf("failed to create connectors and forfeit txs: %s", err)) round.Fail(fmt.Errorf("failed to create connectors and forfeit txs: %s", err))
log.WithError(err).Warn("failed to create connectors and forfeit txs") log.WithError(err).Warn("failed to create connectors and forfeit txs")
return return
} }
log.Debugf("forfeit transactions created for round %s", round.Id) 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(
@@ -962,8 +974,6 @@ func (s *covenantlessService) startFinalization() {
return return
} }
s.forfeitTxs.push(forfeitTxs)
log.Debugf("started finalization stage for round: %s", round.Id) log.Debugf("started finalization stage for round: %s", round.Id)
} }
@@ -1244,13 +1254,12 @@ func (s *covenantlessService) propagateEvents(round *domain.Round) {
lastEvent := round.Events()[len(round.Events())-1] lastEvent := round.Events()[len(round.Events())-1]
switch e := lastEvent.(type) { switch e := lastEvent.(type) {
case domain.RoundFinalizationStarted: case domain.RoundFinalizationStarted:
forfeitTxs := s.forfeitTxs.view()
ev := domain.RoundFinalizationStarted{ ev := domain.RoundFinalizationStarted{
Id: e.Id, Id: e.Id,
CongestionTree: e.CongestionTree, CongestionTree: e.CongestionTree,
Connectors: e.Connectors, Connectors: e.Connectors,
PoolTx: e.PoolTx, PoolTx: e.PoolTx,
UnsignedForfeitTxs: forfeitTxs, MinRelayFeeRate: int64(s.wallet.MinRelayFeeRate(context.Background())),
} }
s.lastEvent = ev s.lastEvent = ev
s.eventsCh <- ev s.eventsCh <- ev
@@ -1291,39 +1300,55 @@ func (s *covenantlessService) getNewVtxos(round *domain.Round) []domain.Vtxo {
continue continue
} }
for i, out := range tx.UnsignedTx.TxOut { for i, out := range tx.UnsignedTx.TxOut {
for _, p := range round.Payments { desc := ""
var pubkey string
found := false found := false
for _, p := range round.Payments {
if found {
break
}
for _, r := range p.Receivers { for _, r := range p.Receivers {
if r.IsOnchain() { if r.IsOnchain() {
continue continue
} }
buf, _ := hex.DecodeString(r.Pubkey) vtxoScript, err := bitcointree.ParseVtxoScript(r.Descriptor)
pk, _ := secp256k1.ParsePubKey(buf)
script, err := s.builder.GetVtxoScript(pk, s.pubkey)
if err != nil { if err != nil {
log.WithError(err).Warn("failed to get vtxo script") log.WithError(err).Warn("failed to parse vtxo descriptor")
continue
}
tapKey, _, err := vtxoScript.TapTree()
if err != nil {
log.WithError(err).Warn("failed to compute vtxo tap key")
continue
}
script, err := common.P2TRScript(tapKey)
if err != nil {
log.WithError(err).Warn("failed to create vtxo scriptpubkey")
continue continue
} }
if bytes.Equal(script, out.PkScript) { if bytes.Equal(script, out.PkScript) {
found = true found = true
pubkey = r.Pubkey desc = r.Descriptor
break break
} }
} }
}
if found { if found {
vtxos = append(vtxos, domain.Vtxo{ vtxos = append(vtxos, domain.Vtxo{
VtxoKey: domain.VtxoKey{Txid: node.Txid, VOut: uint32(i)}, VtxoKey: domain.VtxoKey{Txid: node.Txid, VOut: uint32(i)},
Receiver: domain.Receiver{Pubkey: pubkey, Amount: uint64(out.Value)}, Receiver: domain.Receiver{Descriptor: desc, Amount: uint64(out.Value)},
PoolTx: round.Txid, PoolTx: round.Txid,
}) })
break break
} }
} }
} }
}
return vtxos return vtxos
} }
@@ -1391,16 +1416,19 @@ func (s *covenantlessService) restoreWatchingVtxos() error {
func (s *covenantlessService) extractVtxosScripts(vtxos []domain.Vtxo) ([]string, error) { func (s *covenantlessService) extractVtxosScripts(vtxos []domain.Vtxo) ([]string, error) {
indexedScripts := make(map[string]struct{}) indexedScripts := make(map[string]struct{})
for _, vtxo := range vtxos { for _, vtxo := range vtxos {
buf, err := hex.DecodeString(vtxo.Pubkey) vtxoScript, err := bitcointree.ParseVtxoScript(vtxo.Receiver.Descriptor)
if err != nil { if err != nil {
return nil, err return nil, err
} }
userPubkey, err := secp256k1.ParsePubKey(buf)
tapKey, _, err := vtxoScript.TapTree()
if err != nil { if err != nil {
return nil, err return nil, err
} }
script, err := s.builder.GetVtxoScript(userPubkey, s.pubkey)
script, err := common.P2TRScript(tapKey)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -1427,6 +1455,19 @@ func (s *covenantlessService) saveEvents(
return s.repoManager.Rounds().AddOrUpdateRound(ctx, *round) return s.repoManager.Rounds().AddOrUpdateRound(ctx, *round)
} }
func (s *covenantlessService) chainParams() *chaincfg.Params {
switch s.network.Name {
case common.Bitcoin.Name:
return &chaincfg.MainNetParams
case common.BitcoinTestNet.Name:
return &chaincfg.TestNet3Params
case common.BitcoinRegTest.Name:
return &chaincfg.RegressionNetParams
default:
return nil
}
}
func (s *covenantlessService) reactToFraud(ctx context.Context, vtxo domain.Vtxo, mutx *sync.Mutex) error { func (s *covenantlessService) reactToFraud(ctx context.Context, vtxo domain.Vtxo, mutx *sync.Mutex) error {
mutx.Lock() mutx.Lock()
defer mutx.Unlock() defer mutx.Unlock()

View File

@@ -2,10 +2,9 @@ package application
import ( import (
"context" "context"
"fmt"
"github.com/ark-network/ark/common/descriptor"
"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/decred/dcrd/dcrec/secp256k1/v4" "github.com/decred/dcrd/dcrec/secp256k1/v4"
) )
@@ -16,7 +15,7 @@ var (
type Service interface { type Service interface {
Start() error Start() error
Stop() Stop()
SpendVtxos(ctx context.Context, inputs []Input) (string, error) SpendVtxos(ctx context.Context, inputs []ports.Input) (string, error)
ClaimVtxos(ctx context.Context, creds string, receivers []domain.Receiver) error ClaimVtxos(ctx context.Context, creds string, receivers []domain.Receiver) error
SignVtxos(ctx context.Context, forfeitTxs []string) error SignVtxos(ctx context.Context, forfeitTxs []string) error
SignRoundTx(ctx context.Context, roundTx string) error SignRoundTx(ctx context.Context, roundTx string) error
@@ -33,12 +32,14 @@ type Service interface {
GetInfo(ctx context.Context) (*ServiceInfo, error) GetInfo(ctx context.Context) (*ServiceInfo, error)
// Async payments // Async payments
CreateAsyncPayment( CreateAsyncPayment(
ctx context.Context, inputs []domain.VtxoKey, receivers []domain.Receiver, ctx context.Context, inputs []ports.Input, receivers []domain.Receiver,
) (string, []string, error) ) (string, []string, error)
CompleteAsyncPayment( CompleteAsyncPayment(
ctx context.Context, redeemTx string, unconditionalForfeitTxs []string, ctx context.Context, redeemTx string, unconditionalForfeitTxs []string,
) error ) error
GetBoardingAddress(ctx context.Context, userPubkey *secp256k1.PublicKey) (string, error) GetBoardingAddress(
ctx context.Context, userPubkey *secp256k1.PublicKey,
) (address string, descriptor string, err error)
// Tree signing methods // Tree signing methods
RegisterCosignerPubkey(ctx context.Context, paymentId string, ephemeralPublicKey string) error RegisterCosignerPubkey(ctx context.Context, paymentId string, ephemeralPublicKey string) error
RegisterCosignerNonces( RegisterCosignerNonces(
@@ -67,30 +68,6 @@ type WalletStatus struct {
IsSynced bool IsSynced bool
} }
type Input struct {
Txid string
Index uint32
Descriptor string
}
func (i Input) IsVtxo() bool {
return len(i.Descriptor) <= 0
}
func (i Input) VtxoKey() domain.VtxoKey {
return domain.VtxoKey{
Txid: i.Txid,
VOut: i.Index,
}
}
func (i Input) GetDescriptor() (*descriptor.TaprootDescriptor, error) {
if i.IsVtxo() {
return nil, fmt.Errorf("input is not a boarding input")
}
return descriptor.ParseTaprootDescriptor(i.Descriptor)
}
type txOutpoint struct { type txOutpoint struct {
txid string txid string
vout uint32 vout uint32

View File

@@ -10,7 +10,6 @@ import (
"github.com/ark-network/ark/common/tree" "github.com/ark-network/ark/common/tree"
"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"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
@@ -180,14 +179,19 @@ func newForfeitTxsMap(txBuilder ports.TxBuilder) *forfeitTxsMap {
return &forfeitTxsMap{&sync.RWMutex{}, make(map[string]*signedTx), txBuilder} return &forfeitTxsMap{&sync.RWMutex{}, make(map[string]*signedTx), txBuilder}
} }
func (m *forfeitTxsMap) push(txs []string) { func (m *forfeitTxsMap) push(txs []string) error {
m.lock.Lock() m.lock.Lock()
defer m.lock.Unlock() defer m.lock.Unlock()
for _, tx := range txs { for _, tx := range txs {
signed, txid, _ := m.builder.VerifyTapscriptPartialSigs(tx) txid, err := m.builder.GetTxID(tx)
m.forfeitTxs[txid] = &signedTx{tx, signed} if err != nil {
return err
} }
m.forfeitTxs[txid] = &signedTx{tx, false}
}
return nil
} }
func (m *forfeitTxsMap) sign(txs []string) error { func (m *forfeitTxsMap) sign(txs []string) error {
@@ -229,17 +233,6 @@ func (m *forfeitTxsMap) pop() (signed, unsigned []string) {
return signed, unsigned return signed, unsigned
} }
func (m *forfeitTxsMap) view() []string {
m.lock.RLock()
defer m.lock.RUnlock()
txs := make([]string, 0, len(m.forfeitTxs))
for _, tx := range m.forfeitTxs {
txs = append(txs, tx.tx)
}
return txs
}
// 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
// returns the sweepable outputs as ports.SweepInput mapped by their expiration time // returns the sweepable outputs as ports.SweepInput mapped by their expiration time
func findSweepableOutputs( func findSweepableOutputs(
@@ -313,26 +306,3 @@ func getSpentVtxos(payments map[string]domain.Payment) []domain.VtxoKey {
} }
return vtxos return vtxos
} }
type boardingInput struct {
txId chainhash.Hash
vout uint32
boardingPubKey *secp256k1.PublicKey
amount uint64
}
func (b boardingInput) GetHash() chainhash.Hash {
return b.txId
}
func (b boardingInput) GetIndex() uint32 {
return b.vout
}
func (b boardingInput) GetAmount() uint64 {
return b.amount
}
func (b boardingInput) GetBoardingPubkey() *secp256k1.PublicKey {
return b.boardingPubKey
}

View File

@@ -22,8 +22,8 @@ type RoundFinalizationStarted struct {
CongestionTree tree.CongestionTree // BTC: signed CongestionTree tree.CongestionTree // BTC: signed
Connectors []string Connectors []string
ConnectorAddress string ConnectorAddress string
UnsignedForfeitTxs []string
PoolTx string PoolTx string
MinRelayFeeRate int64
} }
type RoundFinalized struct { type RoundFinalized struct {

View File

@@ -68,7 +68,7 @@ func (p Payment) validate(ignoreOuts bool) error {
return fmt.Errorf("missing outputs") return fmt.Errorf("missing outputs")
} }
for _, r := range p.Receivers { for _, r := range p.Receivers {
if len(r.OnchainAddress) <= 0 && len(r.Pubkey) <= 0 { if len(r.OnchainAddress) <= 0 && len(r.Descriptor) <= 0 {
return fmt.Errorf("missing receiver destination") return fmt.Errorf("missing receiver destination")
} }
} }
@@ -96,7 +96,7 @@ func (k VtxoKey) Hash() string {
} }
type Receiver struct { type Receiver struct {
Pubkey string Descriptor string
Amount uint64 Amount uint64
OnchainAddress string OnchainAddress string
} }

View File

@@ -1,12 +1,23 @@
package domain_test package domain_test
import ( import (
"fmt"
"testing" "testing"
"github.com/ark-network/ark/common/descriptor"
"github.com/ark-network/ark/server/internal/core/domain" "github.com/ark-network/ark/server/internal/core/domain"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
var desc = fmt.Sprintf(
descriptor.DefaultVtxoDescriptorTemplate,
"030000000000000000000000000000000000000000000000000000000000000001",
"0000000000000000000000000000000000000000000000000000000000000001",
"0000000000000000000000000000000000000000000000000000000000000001",
512,
"0000000000000000000000000000000000000000000000000000000000000001",
)
var inputs = []domain.Vtxo{ var inputs = []domain.Vtxo{
{ {
VtxoKey: domain.VtxoKey{ VtxoKey: domain.VtxoKey{
@@ -14,7 +25,7 @@ var inputs = []domain.Vtxo{
VOut: 0, VOut: 0,
}, },
Receiver: domain.Receiver{ Receiver: domain.Receiver{
Pubkey: "030000000000000000000000000000000000000000000000000000000000000001", Descriptor: desc,
Amount: 1000, Amount: 1000,
}, },
}, },
@@ -40,11 +51,11 @@ func TestPayment(t *testing.T) {
err = payment.AddReceivers([]domain.Receiver{ err = payment.AddReceivers([]domain.Receiver{
{ {
Pubkey: "030000000000000000000000000000000000000000000000000000000000000001", Descriptor: desc,
Amount: 450, Amount: 450,
}, },
{ {
Pubkey: "020000000000000000000000000000000000000000000000000000000000000002", Descriptor: desc,
Amount: 550, Amount: 550,
}, },
}) })

View File

@@ -14,27 +14,29 @@ var (
payments = []domain.Payment{ payments = []domain.Payment{
{ {
Id: "0", Id: "0",
Inputs: []domain.Vtxo{{ Inputs: []domain.Vtxo{
{
VtxoKey: domain.VtxoKey{ VtxoKey: domain.VtxoKey{
Txid: txid, Txid: txid,
VOut: 0, VOut: 0,
}, },
Receiver: domain.Receiver{ Receiver: domain.Receiver{
Pubkey: pubkey, Descriptor: desc,
Amount: 2000, Amount: 2000,
}, },
}}, },
},
Receivers: []domain.Receiver{ Receivers: []domain.Receiver{
{ {
Pubkey: pubkey, Descriptor: desc,
Amount: 700, Amount: 700,
}, },
{ {
Pubkey: pubkey, Descriptor: desc,
Amount: 700, Amount: 700,
}, },
{ {
Pubkey: pubkey, Descriptor: desc,
Amount: 600, Amount: 600,
}, },
}, },
@@ -48,7 +50,7 @@ var (
VOut: 0, VOut: 0,
}, },
Receiver: domain.Receiver{ Receiver: domain.Receiver{
Pubkey: pubkey, Descriptor: desc,
Amount: 1000, Amount: 1000,
}, },
}, },
@@ -58,13 +60,13 @@ var (
VOut: 0, VOut: 0,
}, },
Receiver: domain.Receiver{ Receiver: domain.Receiver{
Pubkey: pubkey, Descriptor: desc,
Amount: 1000, Amount: 1000,
}, },
}, },
}, },
Receivers: []domain.Receiver{{ Receivers: []domain.Receiver{{
Pubkey: pubkey, Descriptor: desc,
Amount: 2000, Amount: 2000,
}}, }},
}, },
@@ -72,7 +74,6 @@ var (
emptyPtx = "cHNldP8BAgQCAAAAAQQBAAEFAQABBgEDAfsEAgAAAAA=" emptyPtx = "cHNldP8BAgQCAAAAAQQBAAEFAQABBgEDAfsEAgAAAAA="
emptyTx = "0200000000000000000000" emptyTx = "0200000000000000000000"
txid = "0000000000000000000000000000000000000000000000000000000000000000" txid = "0000000000000000000000000000000000000000000000000000000000000000"
pubkey = "030000000000000000000000000000000000000000000000000000000000000001"
congestionTree = tree.CongestionTree{ congestionTree = tree.CongestionTree{
{ {
{ {

View File

@@ -5,6 +5,7 @@ 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 {
@@ -16,11 +17,14 @@ type SweepInput interface {
GetInternalKey() *secp256k1.PublicKey GetInternalKey() *secp256k1.PublicKey
} }
type BoardingInput interface { type Input struct {
GetAmount() uint64 domain.VtxoKey
GetIndex() uint32 Descriptor string
GetHash() chainhash.Hash }
GetBoardingPubkey() *secp256k1.PublicKey
type BoardingInput struct {
Input
Amount uint64
} }
type TxBuilder interface { type TxBuilder interface {
@@ -28,9 +32,8 @@ type TxBuilder interface {
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,
) (poolTx string, congestionTree tree.CongestionTree, connectorAddress string, err error) ) (poolTx string, congestionTree tree.CongestionTree, connectorAddress string, err error)
BuildForfeitTxs(aspPubkey *secp256k1.PublicKey, poolTx string, payments []domain.Payment) (connectors []string, forfeitTxs []string, err error) BuildForfeitTxs(aspPubkey *secp256k1.PublicKey, poolTx string, payments []domain.Payment, minRelayFeeRate chainfee.SatPerKVByte) (connectors []string, forfeitTxs []string, err error)
BuildSweepTx(inputs []SweepInput) (signedSweepTx string, err error) BuildSweepTx(inputs []SweepInput) (signedSweepTx string, err error)
GetVtxoScript(userPubkey, aspPubkey *secp256k1.PublicKey) ([]byte, error)
GetSweepInput(parentblocktime int64, node tree.Node) (expirationtime int64, sweepInput SweepInput, err error) GetSweepInput(parentblocktime int64, node tree.Node) (expirationtime int64, sweepInput SweepInput, err error)
FinalizeAndExtract(tx string) (txhex string, err error) FinalizeAndExtract(tx string) (txhex string, err error)
VerifyTapscriptPartialSigs(tx string) (valid bool, txid string, err error) VerifyTapscriptPartialSigs(tx string) (valid bool, txid string, err error)
@@ -40,6 +43,6 @@ type TxBuilder interface {
vtxosToSpend []domain.Vtxo, vtxosToSpend []domain.Vtxo,
aspPubKey *secp256k1.PublicKey, receivers []domain.Receiver, aspPubKey *secp256k1.PublicKey, receivers []domain.Receiver,
) (*domain.AsyncPaymentTxs, error) ) (*domain.AsyncPaymentTxs, error)
GetBoardingScript(userPubkey, aspPubkey *secp256k1.PublicKey) (addr string, script []byte, err error)
VerifyAndCombinePartialTx(dest string, src string) (string, error) VerifyAndCombinePartialTx(dest string, src string) (string, error)
GetTxID(tx string) (string, error)
} }

View File

@@ -5,6 +5,7 @@ import (
"errors" "errors"
"github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
) )
// ErrNonFinalBIP68 is returned when a transaction spending a CSV-locked output is not final. // ErrNonFinalBIP68 is returned when a transaction spending a CSV-locked output is not final.
@@ -30,6 +31,7 @@ type WalletService interface {
WaitForSync(ctx context.Context, txid string) error WaitForSync(ctx context.Context, txid string) error
EstimateFees(ctx context.Context, psbt string) (uint64, error) EstimateFees(ctx context.Context, psbt string) (uint64, error)
MinRelayFee(ctx context.Context, vbytes uint64) (uint64, error) MinRelayFee(ctx context.Context, vbytes uint64) (uint64, error)
MinRelayFeeRate(ctx context.Context) chainfee.SatPerKVByte
ListConnectorUtxos(ctx context.Context, connectorAddress string) ([]TxInput, error) ListConnectorUtxos(ctx context.Context, connectorAddress string) ([]TxInput, error)
MainAccountBalance(ctx context.Context) (uint64, uint64, error) MainAccountBalance(ctx context.Context) (uint64, uint64, error)
ConnectorsAccountBalance(ctx context.Context) (uint64, uint64, error) ConnectorsAccountBalance(ctx context.Context) (uint64, uint64, error)

View File

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

View File

@@ -4,12 +4,14 @@ import (
"context" "context"
"crypto/rand" "crypto/rand"
"encoding/hex" "encoding/hex"
"fmt"
"os" "os"
"reflect" "reflect"
"sort" "sort"
"testing" "testing"
"time" "time"
"github.com/ark-network/ark/common/descriptor"
"github.com/ark-network/ark/common/tree" "github.com/ark-network/ark/common/tree"
"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"
@@ -22,8 +24,26 @@ import (
const ( const (
emptyPtx = "cHNldP8BAgQCAAAAAQQBAAEFAQABBgEDAfsEAgAAAAA=" emptyPtx = "cHNldP8BAgQCAAAAAQQBAAEFAQABBgEDAfsEAgAAAAA="
emptyTx = "0200000000000000000000" emptyTx = "0200000000000000000000"
pubkey1 = "0300000000000000000000000000000000000000000000000000000000000000001" pubkey1 = "00000000000000000000000000000000000000000000000000000000000000001"
pubkey2 = "0200000000000000000000000000000000000000000000000000000000000000002" pubkey2 = "00000000000000000000000000000000000000000000000000000000000000002"
)
var desc1 = fmt.Sprintf(
descriptor.DefaultVtxoDescriptorTemplate,
randomString(66),
pubkey1,
pubkey1,
512,
pubkey1,
)
var desc2 = fmt.Sprintf(
descriptor.DefaultVtxoDescriptorTemplate,
randomString(66),
pubkey2,
pubkey2,
512,
pubkey2,
) )
var congestionTree = [][]tree.Node{ var congestionTree = [][]tree.Node{
@@ -251,19 +271,20 @@ func testRoundRepository(t *testing.T, svc ports.RepoManager) {
PoolTx: randomString(32), PoolTx: randomString(32),
ExpireAt: 7980322, ExpireAt: 7980322,
Receiver: domain.Receiver{ Receiver: domain.Receiver{
Pubkey: randomString(36), Descriptor: randomString(120),
Amount: 300, Amount: 300,
}, },
}, },
}, },
Receivers: []domain.Receiver{{ Receivers: []domain.Receiver{{
Pubkey: randomString(36), Descriptor: randomString(120),
Amount: 300, Amount: 300,
}}, }},
}, },
{ {
Id: uuid.New().String(), Id: uuid.New().String(),
Inputs: []domain.Vtxo{ Inputs: []domain.Vtxo{
{ {
VtxoKey: domain.VtxoKey{ VtxoKey: domain.VtxoKey{
Txid: randomString(32), Txid: randomString(32),
@@ -272,18 +293,18 @@ func testRoundRepository(t *testing.T, svc ports.RepoManager) {
PoolTx: randomString(32), PoolTx: randomString(32),
ExpireAt: 7980322, ExpireAt: 7980322,
Receiver: domain.Receiver{ Receiver: domain.Receiver{
Pubkey: randomString(36), Descriptor: randomString(120),
Amount: 600, Amount: 600,
}, },
}, },
}, },
Receivers: []domain.Receiver{ Receivers: []domain.Receiver{
{ {
Pubkey: randomString(36), Descriptor: randomString(120),
Amount: 400, Amount: 400,
}, },
{ {
Pubkey: randomString(34), Descriptor: randomString(120),
Amount: 200, Amount: 200,
}, },
}, },
@@ -350,7 +371,7 @@ func testVtxoRepository(t *testing.T, svc ports.RepoManager) {
VOut: 0, VOut: 0,
}, },
Receiver: domain.Receiver{ Receiver: domain.Receiver{
Pubkey: pubkey1, Descriptor: desc1,
Amount: 1000, Amount: 1000,
}, },
}, },
@@ -360,7 +381,7 @@ func testVtxoRepository(t *testing.T, svc ports.RepoManager) {
VOut: 1, VOut: 1,
}, },
Receiver: domain.Receiver{ Receiver: domain.Receiver{
Pubkey: pubkey1, Descriptor: desc1,
Amount: 2000, Amount: 2000,
}, },
}, },
@@ -371,7 +392,7 @@ func testVtxoRepository(t *testing.T, svc ports.RepoManager) {
VOut: 1, VOut: 1,
}, },
Receiver: domain.Receiver{ Receiver: domain.Receiver{
Pubkey: pubkey2, Descriptor: desc2,
Amount: 2000, Amount: 2000,
}, },
}) })
@@ -531,7 +552,7 @@ type sortReceivers []domain.Receiver
func (a sortReceivers) Len() int { return len(a) } func (a sortReceivers) Len() int { return len(a) }
func (a sortReceivers) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func (a sortReceivers) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a sortReceivers) Less(i, j int) bool { return a[i].Pubkey < a[j].Pubkey } func (a sortReceivers) Less(i, j int) bool { return a[i].Amount < a[j].Amount }
type sortStrings []string type sortStrings []string

View File

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

View File

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

View File

@@ -165,7 +165,7 @@ func (r *roundRepository) AddOrUpdateRound(ctx context.Context, round domain.Rou
ctx, ctx,
queries.UpsertReceiverParams{ queries.UpsertReceiverParams{
PaymentID: payment.Id, PaymentID: payment.Id,
Pubkey: receiver.Pubkey, Descriptor: receiver.Descriptor,
Amount: int64(receiver.Amount), Amount: int64(receiver.Amount),
OnchainAddress: receiver.OnchainAddress, OnchainAddress: receiver.OnchainAddress,
}, },
@@ -320,7 +320,7 @@ func (r *roundRepository) GetSweptRounds(ctx context.Context) ([]domain.Round, e
func rowToReceiver(row queries.PaymentReceiverVw) domain.Receiver { func rowToReceiver(row queries.PaymentReceiverVw) domain.Receiver {
return domain.Receiver{ return domain.Receiver{
Pubkey: row.Pubkey.String, Descriptor: row.Descriptor.String,
Amount: uint64(row.Amount.Int64), Amount: uint64(row.Amount.Int64),
OnchainAddress: row.OnchainAddress.String, OnchainAddress: row.OnchainAddress.String,
} }
@@ -413,8 +413,8 @@ func readRoundRows(rows []roundPaymentTxReceiverVtxoRow) ([]*domain.Round, error
found := false found := false
for _, rcv := range payment.Receivers { for _, rcv := range payment.Receivers {
if v.receiver.Pubkey.Valid && v.receiver.Amount.Valid { if v.receiver.Descriptor.Valid && v.receiver.Amount.Valid {
if rcv.Pubkey == v.receiver.Pubkey.String && int64(rcv.Amount) == v.receiver.Amount.Int64 { if rcv.Descriptor == v.receiver.Descriptor.String && int64(rcv.Amount) == v.receiver.Amount.Int64 {
found = true found = true
break break
} }
@@ -470,7 +470,7 @@ func rowToPaymentVtxoVw(row queries.PaymentVtxoVw) domain.Vtxo {
VOut: uint32(row.Vout.Int64), VOut: uint32(row.Vout.Int64),
}, },
Receiver: domain.Receiver{ Receiver: domain.Receiver{
Pubkey: row.Pubkey.String, Descriptor: row.Descriptor.String,
Amount: uint64(row.Amount.Int64), Amount: uint64(row.Amount.Int64),
}, },
PoolTx: row.PoolTx.String, PoolTx: row.PoolTx.String,

View File

@@ -15,7 +15,7 @@ type Payment struct {
type PaymentReceiverVw struct { type PaymentReceiverVw struct {
PaymentID sql.NullString PaymentID sql.NullString
Pubkey sql.NullString Descriptor sql.NullString
Amount sql.NullInt64 Amount sql.NullInt64
OnchainAddress sql.NullString OnchainAddress sql.NullString
} }
@@ -23,7 +23,6 @@ type PaymentReceiverVw struct {
type PaymentVtxoVw struct { type PaymentVtxoVw struct {
Txid sql.NullString Txid sql.NullString
Vout sql.NullInt64 Vout sql.NullInt64
Pubkey sql.NullString
Amount sql.NullInt64 Amount sql.NullInt64
PoolTx sql.NullString PoolTx sql.NullString
SpentBy sql.NullString SpentBy sql.NullString
@@ -33,11 +32,12 @@ type PaymentVtxoVw struct {
ExpireAt sql.NullInt64 ExpireAt sql.NullInt64
PaymentID sql.NullString PaymentID sql.NullString
RedeemTx sql.NullString RedeemTx sql.NullString
Descriptor sql.NullString
} }
type Receiver struct { type Receiver struct {
PaymentID string PaymentID string
Pubkey string Descriptor string
Amount int64 Amount int64
OnchainAddress string OnchainAddress string
} }
@@ -105,7 +105,6 @@ type UncondForfeitTxVw struct {
type Vtxo struct { type Vtxo struct {
Txid string Txid string
Vout int64 Vout int64
Pubkey string
Amount int64 Amount int64
PoolTx string PoolTx string
SpentBy string SpentBy string
@@ -115,4 +114,5 @@ type Vtxo struct {
ExpireAt int64 ExpireAt int64
PaymentID sql.NullString PaymentID sql.NullString
RedeemTx sql.NullString RedeemTx sql.NullString
Descriptor sql.NullString
} }

View File

@@ -54,7 +54,7 @@ func (q *Queries) MarkVtxoAsSwept(ctx context.Context, arg MarkVtxoAsSweptParams
} }
const selectNotRedeemedVtxos = `-- name: SelectNotRedeemedVtxos :many const selectNotRedeemedVtxos = `-- name: SelectNotRedeemedVtxos :many
SELECT vtxo.txid, vtxo.vout, vtxo.pubkey, vtxo.amount, vtxo.pool_tx, vtxo.spent_by, vtxo.spent, vtxo.redeemed, vtxo.swept, vtxo.expire_at, vtxo.payment_id, vtxo.redeem_tx, SELECT vtxo.txid, vtxo.vout, vtxo.amount, vtxo.pool_tx, vtxo.spent_by, vtxo.spent, vtxo.redeemed, vtxo.swept, vtxo.expire_at, vtxo.payment_id, vtxo.redeem_tx, vtxo.descriptor,
uncond_forfeit_tx_vw.id, uncond_forfeit_tx_vw.tx, uncond_forfeit_tx_vw.vtxo_txid, uncond_forfeit_tx_vw.vtxo_vout, uncond_forfeit_tx_vw.position uncond_forfeit_tx_vw.id, uncond_forfeit_tx_vw.tx, uncond_forfeit_tx_vw.vtxo_txid, uncond_forfeit_tx_vw.vtxo_vout, uncond_forfeit_tx_vw.position
FROM vtxo FROM vtxo
LEFT OUTER JOIN uncond_forfeit_tx_vw ON vtxo.txid=uncond_forfeit_tx_vw.vtxo_txid AND vtxo.vout=uncond_forfeit_tx_vw.vtxo_vout LEFT OUTER JOIN uncond_forfeit_tx_vw ON vtxo.txid=uncond_forfeit_tx_vw.vtxo_txid AND vtxo.vout=uncond_forfeit_tx_vw.vtxo_vout
@@ -78,7 +78,6 @@ func (q *Queries) SelectNotRedeemedVtxos(ctx context.Context) ([]SelectNotRedeem
if err := rows.Scan( if err := rows.Scan(
&i.Vtxo.Txid, &i.Vtxo.Txid,
&i.Vtxo.Vout, &i.Vtxo.Vout,
&i.Vtxo.Pubkey,
&i.Vtxo.Amount, &i.Vtxo.Amount,
&i.Vtxo.PoolTx, &i.Vtxo.PoolTx,
&i.Vtxo.SpentBy, &i.Vtxo.SpentBy,
@@ -88,6 +87,7 @@ func (q *Queries) SelectNotRedeemedVtxos(ctx context.Context) ([]SelectNotRedeem
&i.Vtxo.ExpireAt, &i.Vtxo.ExpireAt,
&i.Vtxo.PaymentID, &i.Vtxo.PaymentID,
&i.Vtxo.RedeemTx, &i.Vtxo.RedeemTx,
&i.Vtxo.Descriptor,
&i.UncondForfeitTxVw.ID, &i.UncondForfeitTxVw.ID,
&i.UncondForfeitTxVw.Tx, &i.UncondForfeitTxVw.Tx,
&i.UncondForfeitTxVw.VtxoTxid, &i.UncondForfeitTxVw.VtxoTxid,
@@ -108,11 +108,11 @@ func (q *Queries) SelectNotRedeemedVtxos(ctx context.Context) ([]SelectNotRedeem
} }
const selectNotRedeemedVtxosWithPubkey = `-- name: SelectNotRedeemedVtxosWithPubkey :many const selectNotRedeemedVtxosWithPubkey = `-- name: SelectNotRedeemedVtxosWithPubkey :many
SELECT vtxo.txid, vtxo.vout, vtxo.pubkey, vtxo.amount, vtxo.pool_tx, vtxo.spent_by, vtxo.spent, vtxo.redeemed, vtxo.swept, vtxo.expire_at, vtxo.payment_id, vtxo.redeem_tx, SELECT vtxo.txid, vtxo.vout, vtxo.amount, vtxo.pool_tx, vtxo.spent_by, vtxo.spent, vtxo.redeemed, vtxo.swept, vtxo.expire_at, vtxo.payment_id, vtxo.redeem_tx, vtxo.descriptor,
uncond_forfeit_tx_vw.id, uncond_forfeit_tx_vw.tx, uncond_forfeit_tx_vw.vtxo_txid, uncond_forfeit_tx_vw.vtxo_vout, uncond_forfeit_tx_vw.position uncond_forfeit_tx_vw.id, uncond_forfeit_tx_vw.tx, uncond_forfeit_tx_vw.vtxo_txid, uncond_forfeit_tx_vw.vtxo_vout, uncond_forfeit_tx_vw.position
FROM vtxo FROM vtxo
LEFT OUTER JOIN uncond_forfeit_tx_vw ON vtxo.txid=uncond_forfeit_tx_vw.vtxo_txid AND vtxo.vout=uncond_forfeit_tx_vw.vtxo_vout LEFT OUTER JOIN uncond_forfeit_tx_vw ON vtxo.txid=uncond_forfeit_tx_vw.vtxo_txid AND vtxo.vout=uncond_forfeit_tx_vw.vtxo_vout
WHERE redeemed = false AND pubkey = ? WHERE redeemed = false AND INSTR(descriptor, ?) > 0
` `
type SelectNotRedeemedVtxosWithPubkeyRow struct { type SelectNotRedeemedVtxosWithPubkeyRow struct {
@@ -120,8 +120,8 @@ type SelectNotRedeemedVtxosWithPubkeyRow struct {
UncondForfeitTxVw UncondForfeitTxVw UncondForfeitTxVw UncondForfeitTxVw
} }
func (q *Queries) SelectNotRedeemedVtxosWithPubkey(ctx context.Context, pubkey string) ([]SelectNotRedeemedVtxosWithPubkeyRow, error) { func (q *Queries) SelectNotRedeemedVtxosWithPubkey(ctx context.Context, instr string) ([]SelectNotRedeemedVtxosWithPubkeyRow, error) {
rows, err := q.db.QueryContext(ctx, selectNotRedeemedVtxosWithPubkey, pubkey) rows, err := q.db.QueryContext(ctx, selectNotRedeemedVtxosWithPubkey, instr)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -132,7 +132,6 @@ func (q *Queries) SelectNotRedeemedVtxosWithPubkey(ctx context.Context, pubkey s
if err := rows.Scan( if err := rows.Scan(
&i.Vtxo.Txid, &i.Vtxo.Txid,
&i.Vtxo.Vout, &i.Vtxo.Vout,
&i.Vtxo.Pubkey,
&i.Vtxo.Amount, &i.Vtxo.Amount,
&i.Vtxo.PoolTx, &i.Vtxo.PoolTx,
&i.Vtxo.SpentBy, &i.Vtxo.SpentBy,
@@ -142,6 +141,7 @@ func (q *Queries) SelectNotRedeemedVtxosWithPubkey(ctx context.Context, pubkey s
&i.Vtxo.ExpireAt, &i.Vtxo.ExpireAt,
&i.Vtxo.PaymentID, &i.Vtxo.PaymentID,
&i.Vtxo.RedeemTx, &i.Vtxo.RedeemTx,
&i.Vtxo.Descriptor,
&i.UncondForfeitTxVw.ID, &i.UncondForfeitTxVw.ID,
&i.UncondForfeitTxVw.Tx, &i.UncondForfeitTxVw.Tx,
&i.UncondForfeitTxVw.VtxoTxid, &i.UncondForfeitTxVw.VtxoTxid,
@@ -224,8 +224,8 @@ const selectRoundWithRoundId = `-- name: SelectRoundWithRoundId :many
SELECT round.id, round.starting_timestamp, round.ending_timestamp, round.ended, round.failed, round.stage_code, round.txid, round.unsigned_tx, round.connector_address, round.dust_amount, round.version, round.swept, SELECT round.id, round.starting_timestamp, round.ending_timestamp, round.ended, round.failed, round.stage_code, round.txid, round.unsigned_tx, round.connector_address, round.dust_amount, round.version, round.swept,
round_payment_vw.id, round_payment_vw.round_id, round_payment_vw.id, round_payment_vw.round_id,
round_tx_vw.id, round_tx_vw.tx, round_tx_vw.round_id, round_tx_vw.type, round_tx_vw.position, round_tx_vw.txid, round_tx_vw.tree_level, round_tx_vw.parent_txid, round_tx_vw.is_leaf, round_tx_vw.id, round_tx_vw.tx, round_tx_vw.round_id, round_tx_vw.type, round_tx_vw.position, round_tx_vw.txid, round_tx_vw.tree_level, round_tx_vw.parent_txid, round_tx_vw.is_leaf,
payment_receiver_vw.payment_id, payment_receiver_vw.pubkey, payment_receiver_vw.amount, payment_receiver_vw.onchain_address, payment_receiver_vw.payment_id, payment_receiver_vw.descriptor, payment_receiver_vw.amount, payment_receiver_vw.onchain_address,
payment_vtxo_vw.txid, payment_vtxo_vw.vout, payment_vtxo_vw.pubkey, payment_vtxo_vw.amount, payment_vtxo_vw.pool_tx, payment_vtxo_vw.spent_by, payment_vtxo_vw.spent, payment_vtxo_vw.redeemed, payment_vtxo_vw.swept, payment_vtxo_vw.expire_at, payment_vtxo_vw.payment_id, payment_vtxo_vw.redeem_tx payment_vtxo_vw.txid, payment_vtxo_vw.vout, payment_vtxo_vw.amount, payment_vtxo_vw.pool_tx, payment_vtxo_vw.spent_by, payment_vtxo_vw.spent, payment_vtxo_vw.redeemed, payment_vtxo_vw.swept, payment_vtxo_vw.expire_at, payment_vtxo_vw.payment_id, payment_vtxo_vw.redeem_tx, payment_vtxo_vw.descriptor
FROM round FROM round
LEFT OUTER JOIN round_payment_vw ON round.id=round_payment_vw.round_id LEFT OUTER JOIN round_payment_vw ON round.id=round_payment_vw.round_id
LEFT OUTER JOIN round_tx_vw ON round.id=round_tx_vw.round_id LEFT OUTER JOIN round_tx_vw ON round.id=round_tx_vw.round_id
@@ -276,12 +276,11 @@ func (q *Queries) SelectRoundWithRoundId(ctx context.Context, id string) ([]Sele
&i.RoundTxVw.ParentTxid, &i.RoundTxVw.ParentTxid,
&i.RoundTxVw.IsLeaf, &i.RoundTxVw.IsLeaf,
&i.PaymentReceiverVw.PaymentID, &i.PaymentReceiverVw.PaymentID,
&i.PaymentReceiverVw.Pubkey, &i.PaymentReceiverVw.Descriptor,
&i.PaymentReceiverVw.Amount, &i.PaymentReceiverVw.Amount,
&i.PaymentReceiverVw.OnchainAddress, &i.PaymentReceiverVw.OnchainAddress,
&i.PaymentVtxoVw.Txid, &i.PaymentVtxoVw.Txid,
&i.PaymentVtxoVw.Vout, &i.PaymentVtxoVw.Vout,
&i.PaymentVtxoVw.Pubkey,
&i.PaymentVtxoVw.Amount, &i.PaymentVtxoVw.Amount,
&i.PaymentVtxoVw.PoolTx, &i.PaymentVtxoVw.PoolTx,
&i.PaymentVtxoVw.SpentBy, &i.PaymentVtxoVw.SpentBy,
@@ -291,6 +290,7 @@ func (q *Queries) SelectRoundWithRoundId(ctx context.Context, id string) ([]Sele
&i.PaymentVtxoVw.ExpireAt, &i.PaymentVtxoVw.ExpireAt,
&i.PaymentVtxoVw.PaymentID, &i.PaymentVtxoVw.PaymentID,
&i.PaymentVtxoVw.RedeemTx, &i.PaymentVtxoVw.RedeemTx,
&i.PaymentVtxoVw.Descriptor,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@@ -309,8 +309,8 @@ const selectRoundWithRoundTxId = `-- name: SelectRoundWithRoundTxId :many
SELECT round.id, round.starting_timestamp, round.ending_timestamp, round.ended, round.failed, round.stage_code, round.txid, round.unsigned_tx, round.connector_address, round.dust_amount, round.version, round.swept, SELECT round.id, round.starting_timestamp, round.ending_timestamp, round.ended, round.failed, round.stage_code, round.txid, round.unsigned_tx, round.connector_address, round.dust_amount, round.version, round.swept,
round_payment_vw.id, round_payment_vw.round_id, round_payment_vw.id, round_payment_vw.round_id,
round_tx_vw.id, round_tx_vw.tx, round_tx_vw.round_id, round_tx_vw.type, round_tx_vw.position, round_tx_vw.txid, round_tx_vw.tree_level, round_tx_vw.parent_txid, round_tx_vw.is_leaf, round_tx_vw.id, round_tx_vw.tx, round_tx_vw.round_id, round_tx_vw.type, round_tx_vw.position, round_tx_vw.txid, round_tx_vw.tree_level, round_tx_vw.parent_txid, round_tx_vw.is_leaf,
payment_receiver_vw.payment_id, payment_receiver_vw.pubkey, payment_receiver_vw.amount, payment_receiver_vw.onchain_address, payment_receiver_vw.payment_id, payment_receiver_vw.descriptor, payment_receiver_vw.amount, payment_receiver_vw.onchain_address,
payment_vtxo_vw.txid, payment_vtxo_vw.vout, payment_vtxo_vw.pubkey, payment_vtxo_vw.amount, payment_vtxo_vw.pool_tx, payment_vtxo_vw.spent_by, payment_vtxo_vw.spent, payment_vtxo_vw.redeemed, payment_vtxo_vw.swept, payment_vtxo_vw.expire_at, payment_vtxo_vw.payment_id, payment_vtxo_vw.redeem_tx payment_vtxo_vw.txid, payment_vtxo_vw.vout, payment_vtxo_vw.amount, payment_vtxo_vw.pool_tx, payment_vtxo_vw.spent_by, payment_vtxo_vw.spent, payment_vtxo_vw.redeemed, payment_vtxo_vw.swept, payment_vtxo_vw.expire_at, payment_vtxo_vw.payment_id, payment_vtxo_vw.redeem_tx, payment_vtxo_vw.descriptor
FROM round FROM round
LEFT OUTER JOIN round_payment_vw ON round.id=round_payment_vw.round_id LEFT OUTER JOIN round_payment_vw ON round.id=round_payment_vw.round_id
LEFT OUTER JOIN round_tx_vw ON round.id=round_tx_vw.round_id LEFT OUTER JOIN round_tx_vw ON round.id=round_tx_vw.round_id
@@ -361,12 +361,11 @@ func (q *Queries) SelectRoundWithRoundTxId(ctx context.Context, txid string) ([]
&i.RoundTxVw.ParentTxid, &i.RoundTxVw.ParentTxid,
&i.RoundTxVw.IsLeaf, &i.RoundTxVw.IsLeaf,
&i.PaymentReceiverVw.PaymentID, &i.PaymentReceiverVw.PaymentID,
&i.PaymentReceiverVw.Pubkey, &i.PaymentReceiverVw.Descriptor,
&i.PaymentReceiverVw.Amount, &i.PaymentReceiverVw.Amount,
&i.PaymentReceiverVw.OnchainAddress, &i.PaymentReceiverVw.OnchainAddress,
&i.PaymentVtxoVw.Txid, &i.PaymentVtxoVw.Txid,
&i.PaymentVtxoVw.Vout, &i.PaymentVtxoVw.Vout,
&i.PaymentVtxoVw.Pubkey,
&i.PaymentVtxoVw.Amount, &i.PaymentVtxoVw.Amount,
&i.PaymentVtxoVw.PoolTx, &i.PaymentVtxoVw.PoolTx,
&i.PaymentVtxoVw.SpentBy, &i.PaymentVtxoVw.SpentBy,
@@ -376,6 +375,7 @@ func (q *Queries) SelectRoundWithRoundTxId(ctx context.Context, txid string) ([]
&i.PaymentVtxoVw.ExpireAt, &i.PaymentVtxoVw.ExpireAt,
&i.PaymentVtxoVw.PaymentID, &i.PaymentVtxoVw.PaymentID,
&i.PaymentVtxoVw.RedeemTx, &i.PaymentVtxoVw.RedeemTx,
&i.PaymentVtxoVw.Descriptor,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@@ -394,8 +394,8 @@ const selectSweepableRounds = `-- name: SelectSweepableRounds :many
SELECT round.id, round.starting_timestamp, round.ending_timestamp, round.ended, round.failed, round.stage_code, round.txid, round.unsigned_tx, round.connector_address, round.dust_amount, round.version, round.swept, SELECT round.id, round.starting_timestamp, round.ending_timestamp, round.ended, round.failed, round.stage_code, round.txid, round.unsigned_tx, round.connector_address, round.dust_amount, round.version, round.swept,
round_payment_vw.id, round_payment_vw.round_id, round_payment_vw.id, round_payment_vw.round_id,
round_tx_vw.id, round_tx_vw.tx, round_tx_vw.round_id, round_tx_vw.type, round_tx_vw.position, round_tx_vw.txid, round_tx_vw.tree_level, round_tx_vw.parent_txid, round_tx_vw.is_leaf, round_tx_vw.id, round_tx_vw.tx, round_tx_vw.round_id, round_tx_vw.type, round_tx_vw.position, round_tx_vw.txid, round_tx_vw.tree_level, round_tx_vw.parent_txid, round_tx_vw.is_leaf,
payment_receiver_vw.payment_id, payment_receiver_vw.pubkey, payment_receiver_vw.amount, payment_receiver_vw.onchain_address, payment_receiver_vw.payment_id, payment_receiver_vw.descriptor, payment_receiver_vw.amount, payment_receiver_vw.onchain_address,
payment_vtxo_vw.txid, payment_vtxo_vw.vout, payment_vtxo_vw.pubkey, payment_vtxo_vw.amount, payment_vtxo_vw.pool_tx, payment_vtxo_vw.spent_by, payment_vtxo_vw.spent, payment_vtxo_vw.redeemed, payment_vtxo_vw.swept, payment_vtxo_vw.expire_at, payment_vtxo_vw.payment_id, payment_vtxo_vw.redeem_tx payment_vtxo_vw.txid, payment_vtxo_vw.vout, payment_vtxo_vw.amount, payment_vtxo_vw.pool_tx, payment_vtxo_vw.spent_by, payment_vtxo_vw.spent, payment_vtxo_vw.redeemed, payment_vtxo_vw.swept, payment_vtxo_vw.expire_at, payment_vtxo_vw.payment_id, payment_vtxo_vw.redeem_tx, payment_vtxo_vw.descriptor
FROM round FROM round
LEFT OUTER JOIN round_payment_vw ON round.id=round_payment_vw.round_id LEFT OUTER JOIN round_payment_vw ON round.id=round_payment_vw.round_id
LEFT OUTER JOIN round_tx_vw ON round.id=round_tx_vw.round_id LEFT OUTER JOIN round_tx_vw ON round.id=round_tx_vw.round_id
@@ -446,12 +446,11 @@ func (q *Queries) SelectSweepableRounds(ctx context.Context) ([]SelectSweepableR
&i.RoundTxVw.ParentTxid, &i.RoundTxVw.ParentTxid,
&i.RoundTxVw.IsLeaf, &i.RoundTxVw.IsLeaf,
&i.PaymentReceiverVw.PaymentID, &i.PaymentReceiverVw.PaymentID,
&i.PaymentReceiverVw.Pubkey, &i.PaymentReceiverVw.Descriptor,
&i.PaymentReceiverVw.Amount, &i.PaymentReceiverVw.Amount,
&i.PaymentReceiverVw.OnchainAddress, &i.PaymentReceiverVw.OnchainAddress,
&i.PaymentVtxoVw.Txid, &i.PaymentVtxoVw.Txid,
&i.PaymentVtxoVw.Vout, &i.PaymentVtxoVw.Vout,
&i.PaymentVtxoVw.Pubkey,
&i.PaymentVtxoVw.Amount, &i.PaymentVtxoVw.Amount,
&i.PaymentVtxoVw.PoolTx, &i.PaymentVtxoVw.PoolTx,
&i.PaymentVtxoVw.SpentBy, &i.PaymentVtxoVw.SpentBy,
@@ -461,6 +460,7 @@ func (q *Queries) SelectSweepableRounds(ctx context.Context) ([]SelectSweepableR
&i.PaymentVtxoVw.ExpireAt, &i.PaymentVtxoVw.ExpireAt,
&i.PaymentVtxoVw.PaymentID, &i.PaymentVtxoVw.PaymentID,
&i.PaymentVtxoVw.RedeemTx, &i.PaymentVtxoVw.RedeemTx,
&i.PaymentVtxoVw.Descriptor,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@@ -476,7 +476,7 @@ func (q *Queries) SelectSweepableRounds(ctx context.Context) ([]SelectSweepableR
} }
const selectSweepableVtxos = `-- name: SelectSweepableVtxos :many const selectSweepableVtxos = `-- name: SelectSweepableVtxos :many
SELECT vtxo.txid, vtxo.vout, vtxo.pubkey, vtxo.amount, vtxo.pool_tx, vtxo.spent_by, vtxo.spent, vtxo.redeemed, vtxo.swept, vtxo.expire_at, vtxo.payment_id, vtxo.redeem_tx, SELECT vtxo.txid, vtxo.vout, vtxo.amount, vtxo.pool_tx, vtxo.spent_by, vtxo.spent, vtxo.redeemed, vtxo.swept, vtxo.expire_at, vtxo.payment_id, vtxo.redeem_tx, vtxo.descriptor,
uncond_forfeit_tx_vw.id, uncond_forfeit_tx_vw.tx, uncond_forfeit_tx_vw.vtxo_txid, uncond_forfeit_tx_vw.vtxo_vout, uncond_forfeit_tx_vw.position uncond_forfeit_tx_vw.id, uncond_forfeit_tx_vw.tx, uncond_forfeit_tx_vw.vtxo_txid, uncond_forfeit_tx_vw.vtxo_vout, uncond_forfeit_tx_vw.position
FROM vtxo FROM vtxo
LEFT OUTER JOIN uncond_forfeit_tx_vw ON vtxo.txid=uncond_forfeit_tx_vw.vtxo_txid AND vtxo.vout=uncond_forfeit_tx_vw.vtxo_vout LEFT OUTER JOIN uncond_forfeit_tx_vw ON vtxo.txid=uncond_forfeit_tx_vw.vtxo_txid AND vtxo.vout=uncond_forfeit_tx_vw.vtxo_vout
@@ -500,7 +500,6 @@ func (q *Queries) SelectSweepableVtxos(ctx context.Context) ([]SelectSweepableVt
if err := rows.Scan( if err := rows.Scan(
&i.Vtxo.Txid, &i.Vtxo.Txid,
&i.Vtxo.Vout, &i.Vtxo.Vout,
&i.Vtxo.Pubkey,
&i.Vtxo.Amount, &i.Vtxo.Amount,
&i.Vtxo.PoolTx, &i.Vtxo.PoolTx,
&i.Vtxo.SpentBy, &i.Vtxo.SpentBy,
@@ -510,6 +509,7 @@ func (q *Queries) SelectSweepableVtxos(ctx context.Context) ([]SelectSweepableVt
&i.Vtxo.ExpireAt, &i.Vtxo.ExpireAt,
&i.Vtxo.PaymentID, &i.Vtxo.PaymentID,
&i.Vtxo.RedeemTx, &i.Vtxo.RedeemTx,
&i.Vtxo.Descriptor,
&i.UncondForfeitTxVw.ID, &i.UncondForfeitTxVw.ID,
&i.UncondForfeitTxVw.Tx, &i.UncondForfeitTxVw.Tx,
&i.UncondForfeitTxVw.VtxoTxid, &i.UncondForfeitTxVw.VtxoTxid,
@@ -533,8 +533,8 @@ const selectSweptRounds = `-- name: SelectSweptRounds :many
SELECT round.id, round.starting_timestamp, round.ending_timestamp, round.ended, round.failed, round.stage_code, round.txid, round.unsigned_tx, round.connector_address, round.dust_amount, round.version, round.swept, SELECT round.id, round.starting_timestamp, round.ending_timestamp, round.ended, round.failed, round.stage_code, round.txid, round.unsigned_tx, round.connector_address, round.dust_amount, round.version, round.swept,
round_payment_vw.id, round_payment_vw.round_id, round_payment_vw.id, round_payment_vw.round_id,
round_tx_vw.id, round_tx_vw.tx, round_tx_vw.round_id, round_tx_vw.type, round_tx_vw.position, round_tx_vw.txid, round_tx_vw.tree_level, round_tx_vw.parent_txid, round_tx_vw.is_leaf, round_tx_vw.id, round_tx_vw.tx, round_tx_vw.round_id, round_tx_vw.type, round_tx_vw.position, round_tx_vw.txid, round_tx_vw.tree_level, round_tx_vw.parent_txid, round_tx_vw.is_leaf,
payment_receiver_vw.payment_id, payment_receiver_vw.pubkey, payment_receiver_vw.amount, payment_receiver_vw.onchain_address, payment_receiver_vw.payment_id, payment_receiver_vw.descriptor, payment_receiver_vw.amount, payment_receiver_vw.onchain_address,
payment_vtxo_vw.txid, payment_vtxo_vw.vout, payment_vtxo_vw.pubkey, payment_vtxo_vw.amount, payment_vtxo_vw.pool_tx, payment_vtxo_vw.spent_by, payment_vtxo_vw.spent, payment_vtxo_vw.redeemed, payment_vtxo_vw.swept, payment_vtxo_vw.expire_at, payment_vtxo_vw.payment_id, payment_vtxo_vw.redeem_tx payment_vtxo_vw.txid, payment_vtxo_vw.vout, payment_vtxo_vw.amount, payment_vtxo_vw.pool_tx, payment_vtxo_vw.spent_by, payment_vtxo_vw.spent, payment_vtxo_vw.redeemed, payment_vtxo_vw.swept, payment_vtxo_vw.expire_at, payment_vtxo_vw.payment_id, payment_vtxo_vw.redeem_tx, payment_vtxo_vw.descriptor
FROM round FROM round
LEFT OUTER JOIN round_payment_vw ON round.id=round_payment_vw.round_id LEFT OUTER JOIN round_payment_vw ON round.id=round_payment_vw.round_id
LEFT OUTER JOIN round_tx_vw ON round.id=round_tx_vw.round_id LEFT OUTER JOIN round_tx_vw ON round.id=round_tx_vw.round_id
@@ -585,12 +585,11 @@ func (q *Queries) SelectSweptRounds(ctx context.Context) ([]SelectSweptRoundsRow
&i.RoundTxVw.ParentTxid, &i.RoundTxVw.ParentTxid,
&i.RoundTxVw.IsLeaf, &i.RoundTxVw.IsLeaf,
&i.PaymentReceiverVw.PaymentID, &i.PaymentReceiverVw.PaymentID,
&i.PaymentReceiverVw.Pubkey, &i.PaymentReceiverVw.Descriptor,
&i.PaymentReceiverVw.Amount, &i.PaymentReceiverVw.Amount,
&i.PaymentReceiverVw.OnchainAddress, &i.PaymentReceiverVw.OnchainAddress,
&i.PaymentVtxoVw.Txid, &i.PaymentVtxoVw.Txid,
&i.PaymentVtxoVw.Vout, &i.PaymentVtxoVw.Vout,
&i.PaymentVtxoVw.Pubkey,
&i.PaymentVtxoVw.Amount, &i.PaymentVtxoVw.Amount,
&i.PaymentVtxoVw.PoolTx, &i.PaymentVtxoVw.PoolTx,
&i.PaymentVtxoVw.SpentBy, &i.PaymentVtxoVw.SpentBy,
@@ -600,6 +599,7 @@ func (q *Queries) SelectSweptRounds(ctx context.Context) ([]SelectSweptRoundsRow
&i.PaymentVtxoVw.ExpireAt, &i.PaymentVtxoVw.ExpireAt,
&i.PaymentVtxoVw.PaymentID, &i.PaymentVtxoVw.PaymentID,
&i.PaymentVtxoVw.RedeemTx, &i.PaymentVtxoVw.RedeemTx,
&i.PaymentVtxoVw.Descriptor,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@@ -615,7 +615,7 @@ func (q *Queries) SelectSweptRounds(ctx context.Context) ([]SelectSweptRoundsRow
} }
const selectVtxoByOutpoint = `-- name: SelectVtxoByOutpoint :one const selectVtxoByOutpoint = `-- name: SelectVtxoByOutpoint :one
SELECT vtxo.txid, vtxo.vout, vtxo.pubkey, vtxo.amount, vtxo.pool_tx, vtxo.spent_by, vtxo.spent, vtxo.redeemed, vtxo.swept, vtxo.expire_at, vtxo.payment_id, vtxo.redeem_tx, SELECT vtxo.txid, vtxo.vout, vtxo.amount, vtxo.pool_tx, vtxo.spent_by, vtxo.spent, vtxo.redeemed, vtxo.swept, vtxo.expire_at, vtxo.payment_id, vtxo.redeem_tx, vtxo.descriptor,
uncond_forfeit_tx_vw.id, uncond_forfeit_tx_vw.tx, uncond_forfeit_tx_vw.vtxo_txid, uncond_forfeit_tx_vw.vtxo_vout, uncond_forfeit_tx_vw.position uncond_forfeit_tx_vw.id, uncond_forfeit_tx_vw.tx, uncond_forfeit_tx_vw.vtxo_txid, uncond_forfeit_tx_vw.vtxo_vout, uncond_forfeit_tx_vw.position
FROM vtxo FROM vtxo
LEFT OUTER JOIN uncond_forfeit_tx_vw ON vtxo.txid=uncond_forfeit_tx_vw.vtxo_txid AND vtxo.vout=uncond_forfeit_tx_vw.vtxo_vout LEFT OUTER JOIN uncond_forfeit_tx_vw ON vtxo.txid=uncond_forfeit_tx_vw.vtxo_txid AND vtxo.vout=uncond_forfeit_tx_vw.vtxo_vout
@@ -638,7 +638,6 @@ func (q *Queries) SelectVtxoByOutpoint(ctx context.Context, arg SelectVtxoByOutp
err := row.Scan( err := row.Scan(
&i.Vtxo.Txid, &i.Vtxo.Txid,
&i.Vtxo.Vout, &i.Vtxo.Vout,
&i.Vtxo.Pubkey,
&i.Vtxo.Amount, &i.Vtxo.Amount,
&i.Vtxo.PoolTx, &i.Vtxo.PoolTx,
&i.Vtxo.SpentBy, &i.Vtxo.SpentBy,
@@ -648,6 +647,7 @@ func (q *Queries) SelectVtxoByOutpoint(ctx context.Context, arg SelectVtxoByOutp
&i.Vtxo.ExpireAt, &i.Vtxo.ExpireAt,
&i.Vtxo.PaymentID, &i.Vtxo.PaymentID,
&i.Vtxo.RedeemTx, &i.Vtxo.RedeemTx,
&i.Vtxo.Descriptor,
&i.UncondForfeitTxVw.ID, &i.UncondForfeitTxVw.ID,
&i.UncondForfeitTxVw.Tx, &i.UncondForfeitTxVw.Tx,
&i.UncondForfeitTxVw.VtxoTxid, &i.UncondForfeitTxVw.VtxoTxid,
@@ -658,7 +658,7 @@ func (q *Queries) SelectVtxoByOutpoint(ctx context.Context, arg SelectVtxoByOutp
} }
const selectVtxosByPoolTxid = `-- name: SelectVtxosByPoolTxid :many const selectVtxosByPoolTxid = `-- name: SelectVtxosByPoolTxid :many
SELECT vtxo.txid, vtxo.vout, vtxo.pubkey, vtxo.amount, vtxo.pool_tx, vtxo.spent_by, vtxo.spent, vtxo.redeemed, vtxo.swept, vtxo.expire_at, vtxo.payment_id, vtxo.redeem_tx, SELECT vtxo.txid, vtxo.vout, vtxo.amount, vtxo.pool_tx, vtxo.spent_by, vtxo.spent, vtxo.redeemed, vtxo.swept, vtxo.expire_at, vtxo.payment_id, vtxo.redeem_tx, vtxo.descriptor,
uncond_forfeit_tx_vw.id, uncond_forfeit_tx_vw.tx, uncond_forfeit_tx_vw.vtxo_txid, uncond_forfeit_tx_vw.vtxo_vout, uncond_forfeit_tx_vw.position uncond_forfeit_tx_vw.id, uncond_forfeit_tx_vw.tx, uncond_forfeit_tx_vw.vtxo_txid, uncond_forfeit_tx_vw.vtxo_vout, uncond_forfeit_tx_vw.position
FROM vtxo FROM vtxo
LEFT OUTER JOIN uncond_forfeit_tx_vw ON vtxo.txid=uncond_forfeit_tx_vw.vtxo_txid AND vtxo.vout=uncond_forfeit_tx_vw.vtxo_vout LEFT OUTER JOIN uncond_forfeit_tx_vw ON vtxo.txid=uncond_forfeit_tx_vw.vtxo_txid AND vtxo.vout=uncond_forfeit_tx_vw.vtxo_vout
@@ -682,7 +682,6 @@ func (q *Queries) SelectVtxosByPoolTxid(ctx context.Context, poolTx string) ([]S
if err := rows.Scan( if err := rows.Scan(
&i.Vtxo.Txid, &i.Vtxo.Txid,
&i.Vtxo.Vout, &i.Vtxo.Vout,
&i.Vtxo.Pubkey,
&i.Vtxo.Amount, &i.Vtxo.Amount,
&i.Vtxo.PoolTx, &i.Vtxo.PoolTx,
&i.Vtxo.SpentBy, &i.Vtxo.SpentBy,
@@ -692,6 +691,7 @@ func (q *Queries) SelectVtxosByPoolTxid(ctx context.Context, poolTx string) ([]S
&i.Vtxo.ExpireAt, &i.Vtxo.ExpireAt,
&i.Vtxo.PaymentID, &i.Vtxo.PaymentID,
&i.Vtxo.RedeemTx, &i.Vtxo.RedeemTx,
&i.Vtxo.Descriptor,
&i.UncondForfeitTxVw.ID, &i.UncondForfeitTxVw.ID,
&i.UncondForfeitTxVw.Tx, &i.UncondForfeitTxVw.Tx,
&i.UncondForfeitTxVw.VtxoTxid, &i.UncondForfeitTxVw.VtxoTxid,
@@ -757,16 +757,16 @@ func (q *Queries) UpsertPayment(ctx context.Context, arg UpsertPaymentParams) er
} }
const upsertReceiver = `-- name: UpsertReceiver :exec const upsertReceiver = `-- name: UpsertReceiver :exec
INSERT INTO receiver (payment_id, pubkey, amount, onchain_address) VALUES (?, ?, ?, ?) INSERT INTO receiver (payment_id, descriptor, amount, onchain_address) VALUES (?, ?, ?, ?)
ON CONFLICT(payment_id, pubkey) DO UPDATE SET ON CONFLICT(payment_id, descriptor) DO UPDATE SET
amount = EXCLUDED.amount, amount = EXCLUDED.amount,
onchain_address = EXCLUDED.onchain_address, onchain_address = EXCLUDED.onchain_address,
pubkey = EXCLUDED.pubkey descriptor = EXCLUDED.descriptor
` `
type UpsertReceiverParams struct { type UpsertReceiverParams struct {
PaymentID string PaymentID string
Pubkey string Descriptor string
Amount int64 Amount int64
OnchainAddress string OnchainAddress string
} }
@@ -774,7 +774,7 @@ type UpsertReceiverParams struct {
func (q *Queries) UpsertReceiver(ctx context.Context, arg UpsertReceiverParams) error { func (q *Queries) UpsertReceiver(ctx context.Context, arg UpsertReceiverParams) error {
_, err := q.db.ExecContext(ctx, upsertReceiver, _, err := q.db.ExecContext(ctx, upsertReceiver,
arg.PaymentID, arg.PaymentID,
arg.Pubkey, arg.Descriptor,
arg.Amount, arg.Amount,
arg.OnchainAddress, arg.OnchainAddress,
) )
@@ -909,9 +909,9 @@ func (q *Queries) UpsertUnconditionalForfeitTx(ctx context.Context, arg UpsertUn
} }
const upsertVtxo = `-- name: UpsertVtxo :exec const upsertVtxo = `-- name: UpsertVtxo :exec
INSERT INTO vtxo (txid, vout, pubkey, amount, pool_tx, spent_by, spent, redeemed, swept, expire_at, redeem_tx) INSERT INTO vtxo (txid, vout, descriptor, amount, pool_tx, spent_by, spent, redeemed, swept, expire_at, redeem_tx)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(txid, vout) DO UPDATE SET VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(txid, vout) DO UPDATE SET
pubkey = EXCLUDED.pubkey, descriptor = EXCLUDED.descriptor,
amount = EXCLUDED.amount, amount = EXCLUDED.amount,
pool_tx = EXCLUDED.pool_tx, pool_tx = EXCLUDED.pool_tx,
spent_by = EXCLUDED.spent_by, spent_by = EXCLUDED.spent_by,
@@ -925,7 +925,7 @@ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(txid, vout) DO UPDATE SET
type UpsertVtxoParams struct { type UpsertVtxoParams struct {
Txid string Txid string
Vout int64 Vout int64
Pubkey string Descriptor sql.NullString
Amount int64 Amount int64
PoolTx string PoolTx string
SpentBy string SpentBy string
@@ -940,7 +940,7 @@ func (q *Queries) UpsertVtxo(ctx context.Context, arg UpsertVtxoParams) error {
_, err := q.db.ExecContext(ctx, upsertVtxo, _, err := q.db.ExecContext(ctx, upsertVtxo,
arg.Txid, arg.Txid,
arg.Vout, arg.Vout,
arg.Pubkey, arg.Descriptor,
arg.Amount, arg.Amount,
arg.PoolTx, arg.PoolTx,
arg.SpentBy, arg.SpentBy,

View File

@@ -44,11 +44,11 @@ INSERT INTO payment (id, round_id) VALUES (?, ?)
ON CONFLICT(id) DO UPDATE SET round_id = EXCLUDED.round_id; ON CONFLICT(id) DO UPDATE SET round_id = EXCLUDED.round_id;
-- name: UpsertReceiver :exec -- name: UpsertReceiver :exec
INSERT INTO receiver (payment_id, pubkey, amount, onchain_address) VALUES (?, ?, ?, ?) INSERT INTO receiver (payment_id, descriptor, amount, onchain_address) VALUES (?, ?, ?, ?)
ON CONFLICT(payment_id, pubkey) DO UPDATE SET ON CONFLICT(payment_id, descriptor) DO UPDATE SET
amount = EXCLUDED.amount, amount = EXCLUDED.amount,
onchain_address = EXCLUDED.onchain_address, onchain_address = EXCLUDED.onchain_address,
pubkey = EXCLUDED.pubkey; descriptor = EXCLUDED.descriptor;
-- name: UpdateVtxoPaymentId :exec -- name: UpdateVtxoPaymentId :exec
UPDATE vtxo SET payment_id = ? WHERE txid = ? AND vout = ?; UPDATE vtxo SET payment_id = ? WHERE txid = ? AND vout = ?;
@@ -120,9 +120,9 @@ VALUES (?, ?, ?, ?) ON CONFLICT(id) DO UPDATE SET
position = EXCLUDED.position; position = EXCLUDED.position;
-- name: UpsertVtxo :exec -- name: UpsertVtxo :exec
INSERT INTO vtxo (txid, vout, pubkey, amount, pool_tx, spent_by, spent, redeemed, swept, expire_at, redeem_tx) INSERT INTO vtxo (txid, vout, descriptor, amount, pool_tx, spent_by, spent, redeemed, swept, expire_at, redeem_tx)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(txid, vout) DO UPDATE SET VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(txid, vout) DO UPDATE SET
pubkey = EXCLUDED.pubkey, descriptor = EXCLUDED.descriptor,
amount = EXCLUDED.amount, amount = EXCLUDED.amount,
pool_tx = EXCLUDED.pool_tx, pool_tx = EXCLUDED.pool_tx,
spent_by = EXCLUDED.spent_by, spent_by = EXCLUDED.spent_by,
@@ -151,7 +151,7 @@ SELECT sqlc.embed(vtxo),
sqlc.embed(uncond_forfeit_tx_vw) sqlc.embed(uncond_forfeit_tx_vw)
FROM vtxo FROM vtxo
LEFT OUTER JOIN uncond_forfeit_tx_vw ON vtxo.txid=uncond_forfeit_tx_vw.vtxo_txid AND vtxo.vout=uncond_forfeit_tx_vw.vtxo_vout LEFT OUTER JOIN uncond_forfeit_tx_vw ON vtxo.txid=uncond_forfeit_tx_vw.vtxo_txid AND vtxo.vout=uncond_forfeit_tx_vw.vtxo_vout
WHERE redeemed = false AND pubkey = ?; WHERE redeemed = false AND INSTR(descriptor, ?) > 0;
-- name: SelectVtxoByOutpoint :one -- name: SelectVtxoByOutpoint :one
SELECT sqlc.embed(vtxo), SELECT sqlc.embed(vtxo),

View File

@@ -45,7 +45,7 @@ func (v *vxtoRepository) AddVtxos(ctx context.Context, vtxos []domain.Vtxo) erro
ctx, queries.UpsertVtxoParams{ ctx, queries.UpsertVtxoParams{
Txid: vtxo.Txid, Txid: vtxo.Txid,
Vout: int64(vtxo.VOut), Vout: int64(vtxo.VOut),
Pubkey: vtxo.Pubkey, Descriptor: sql.NullString{String: vtxo.Descriptor, Valid: true},
Amount: int64(vtxo.Amount), Amount: int64(vtxo.Amount),
PoolTx: vtxo.PoolTx, PoolTx: vtxo.PoolTx,
SpentBy: vtxo.SpentBy, SpentBy: vtxo.SpentBy,
@@ -100,6 +100,10 @@ func (v *vxtoRepository) GetAllVtxos(ctx context.Context, pubkey string) ([]doma
var rows []vtxoWithUnconditionalForfeitTxs var rows []vtxoWithUnconditionalForfeitTxs
if withPubkey { if withPubkey {
if len(pubkey) == 66 {
pubkey = pubkey[2:]
}
res, err := v.querier.SelectNotRedeemedVtxosWithPubkey(ctx, pubkey) res, err := v.querier.SelectNotRedeemedVtxosWithPubkey(ctx, pubkey)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
@@ -294,7 +298,7 @@ func rowToVtxo(row queries.Vtxo, uncondForfeitTxs []queries.UncondForfeitTxVw) d
VOut: uint32(row.Vout), VOut: uint32(row.Vout),
}, },
Receiver: domain.Receiver{ Receiver: domain.Receiver{
Pubkey: row.Pubkey, Descriptor: row.Descriptor.String,
Amount: uint64(row.Amount), Amount: uint64(row.Amount),
}, },
PoolTx: row.PoolTx, PoolTx: row.PoolTx,

View File

@@ -3,7 +3,6 @@ package txbuilder
import ( import (
"bytes" "bytes"
"context" "context"
"encoding/hex"
"fmt" "fmt"
"github.com/ark-network/ark/common" "github.com/ark-network/ark/common"
@@ -14,6 +13,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/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"
@@ -27,7 +27,6 @@ type txBuilder struct {
wallet ports.WalletService wallet ports.WalletService
net common.Network net common.Network
roundLifetime int64 // in seconds roundLifetime int64 // in seconds
exitDelay int64 // in seconds
boardingExitDelay int64 // in seconds boardingExitDelay int64 // in seconds
} }
@@ -35,27 +34,13 @@ func NewTxBuilder(
wallet ports.WalletService, wallet ports.WalletService,
net common.Network, net common.Network,
roundLifetime int64, roundLifetime int64,
exitDelay int64,
boardingExitDelay int64, boardingExitDelay int64,
) ports.TxBuilder { ) ports.TxBuilder {
return &txBuilder{wallet, net, roundLifetime, exitDelay, boardingExitDelay} return &txBuilder{wallet, net, roundLifetime, boardingExitDelay}
} }
func (b *txBuilder) GetBoardingScript(owner, asp *secp256k1.PublicKey) (string, []byte, error) { func (b *txBuilder) GetTxID(tx string) (string, error) {
addr, script, _, err := b.getBoardingTaproot(owner, asp) return getTxid(tx)
if err != nil {
return "", nil, err
}
return addr, script, nil
}
func (b *txBuilder) GetVtxoScript(userPubkey, aspPubkey *secp256k1.PublicKey) ([]byte, error) {
outputScript, _, err := b.getLeafScriptAndTree(userPubkey, aspPubkey)
if err != nil {
return nil, err
}
return outputScript, nil
} }
func (b *txBuilder) BuildSweepTx(inputs []ports.SweepInput) (signedSweepTx string, err error) { func (b *txBuilder) BuildSweepTx(inputs []ports.SweepInput) (signedSweepTx string, err error) {
@@ -97,7 +82,11 @@ func (b *txBuilder) BuildSweepTx(inputs []ports.SweepInput) (signedSweepTx strin
} }
func (b *txBuilder) BuildForfeitTxs( func (b *txBuilder) BuildForfeitTxs(
aspPubkey *secp256k1.PublicKey, poolTx string, payments []domain.Payment) (connectors []string, forfeitTxs []string, err error) { aspPubkey *secp256k1.PublicKey,
poolTx string,
payments []domain.Payment,
minRelayFeeRate chainfee.SatPerKVByte,
) (connectors []string, forfeitTxs []string, err error) {
connectorAddress, err := b.getConnectorAddress(poolTx) connectorAddress, err := b.getConnectorAddress(poolTx)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
@@ -118,7 +107,7 @@ func (b *txBuilder) BuildForfeitTxs(
return nil, nil, err return nil, nil, err
} }
forfeitTxs, err = b.createForfeitTxs(aspPubkey, payments, connectorTxs, connectorAmount) forfeitTxs, err = b.createForfeitTxs(aspPubkey, payments, connectorTxs, connectorAmount, minRelayFeeRate)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@@ -159,8 +148,13 @@ func (b *txBuilder) BuildPoolTx(
return "", nil, "", err return "", nil, "", err
} }
receivers, err := getOffchainReceivers(payments)
if err != nil {
return "", nil, "", err
}
treeFactoryFn, sharedOutputScript, sharedOutputAmount, err = tree.CraftCongestionTree( treeFactoryFn, sharedOutputScript, sharedOutputAmount, err = tree.CraftCongestionTree(
b.onchainNetwork().AssetID, aspPubkey, getOffchainReceivers(payments), feeSatsPerNode, b.roundLifetime, b.exitDelay, b.onchainNetwork().AssetID, aspPubkey, receivers, feeSatsPerNode, b.roundLifetime,
) )
if err != nil { if err != nil {
return "", nil, "", err return "", nil, "", err
@@ -270,7 +264,7 @@ func (b *txBuilder) VerifyTapscriptPartialSigs(tx string) (bool, string, error)
rootHash := tapLeaf.ControlBlock.RootHash(tapLeaf.Script) rootHash := tapLeaf.ControlBlock.RootHash(tapLeaf.Script)
tapKeyFromControlBlock := taproot.ComputeTaprootOutputKey(tree.UnspendableKey(), rootHash[:]) tapKeyFromControlBlock := taproot.ComputeTaprootOutputKey(tree.UnspendableKey(), rootHash[:])
pkscript, err := p2trScript(tapKeyFromControlBlock) pkscript, err := common.P2TRScript(tapKeyFromControlBlock)
if err != nil { if err != nil {
return false, txid, err return false, txid, err
} }
@@ -376,51 +370,13 @@ func (b *txBuilder) BuildAsyncPaymentTransactions(
return nil, fmt.Errorf("not implemented") return nil, fmt.Errorf("not implemented")
} }
func (b *txBuilder) getLeafScriptAndTree(
userPubkey, aspPubkey *secp256k1.PublicKey,
) ([]byte, *taproot.IndexedElementsTapScriptTree, error) {
redeemClosure := &tree.CSVSigClosure{
Pubkey: userPubkey,
Seconds: uint(b.exitDelay),
}
redeemLeaf, err := redeemClosure.Leaf()
if err != nil {
return nil, nil, err
}
forfeitClosure := &tree.ForfeitClosure{
Pubkey: userPubkey,
AspPubkey: aspPubkey,
}
forfeitLeaf, err := forfeitClosure.Leaf()
if err != nil {
return nil, nil, err
}
taprootTree := taproot.AssembleTaprootScriptTree(
*redeemLeaf, *forfeitLeaf,
)
root := taprootTree.RootNode.TapHash()
unspendableKey := tree.UnspendableKey()
taprootKey := taproot.ComputeTaprootOutputKey(unspendableKey, root[:])
outputScript, err := p2trScript(taprootKey)
if err != nil {
return nil, nil, err
}
return outputScript, taprootTree, nil
}
func (b *txBuilder) createPoolTx( func (b *txBuilder) createPoolTx(
sharedOutputAmount uint64, sharedOutputAmount uint64,
sharedOutputScript []byte, sharedOutputScript []byte,
payments []domain.Payment, payments []domain.Payment,
boardingInputs []ports.BoardingInput, boardingInputs []ports.BoardingInput,
aspPubKey *secp256k1.PublicKey, connectorAddress string, aspPubKey *secp256k1.PublicKey,
connectorAddress string,
sweptRounds []domain.Round, sweptRounds []domain.Round,
) (*psetv2.Pset, error) { ) (*psetv2.Pset, error) {
aspScript, err := p2wpkhScript(aspPubKey, b.onchainNetwork()) aspScript, err := p2wpkhScript(aspPubKey, b.onchainNetwork())
@@ -487,7 +443,7 @@ func (b *txBuilder) createPoolTx(
} }
for _, in := range boardingInputs { for _, in := range boardingInputs {
targetAmount -= in.GetAmount() targetAmount -= in.Amount
} }
ctx := context.Background() ctx := context.Background()
@@ -529,8 +485,8 @@ func (b *txBuilder) createPoolTx(
if err := updater.AddInputs( if err := updater.AddInputs(
[]psetv2.InputArgs{ []psetv2.InputArgs{
{ {
Txid: in.GetHash().String(), Txid: in.Txid,
TxIndex: in.GetIndex(), TxIndex: in.VtxoKey.VOut,
}, },
}, },
); err != nil { ); err != nil {
@@ -544,21 +500,27 @@ func (b *txBuilder) createPoolTx(
return nil, fmt.Errorf("failed to convert asset to bytes: %s", err) return nil, fmt.Errorf("failed to convert asset to bytes: %s", err)
} }
valueBytes, err := elementsutil.ValueToBytes(in.GetAmount()) valueBytes, err := elementsutil.ValueToBytes(in.Amount)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to convert value to bytes: %s", err) return nil, fmt.Errorf("failed to convert value to bytes: %s", err)
} }
_, script, tapLeafProof, err := b.getBoardingTaproot(in.GetBoardingPubkey(), aspPubKey) boardingVtxoScript, err := tree.ParseVtxoScript(in.Descriptor)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if err := updater.AddInWitnessUtxo(index, transaction.NewTxOutput(assetBytes, valueBytes, script)); err != nil { boardingTapKey, _, err := boardingVtxoScript.TapTree()
if err != nil {
return nil, err return nil, err
} }
if err := updater.AddInTapLeafScript(index, psetv2.NewTapLeafScript(*tapLeafProof, tree.UnspendableKey())); err != nil { boardingOutputScript, err := common.P2TRScript(boardingTapKey)
if err != nil {
return nil, err
}
if err := updater.AddInWitnessUtxo(index, transaction.NewTxOutput(assetBytes, valueBytes, boardingOutputScript)); err != nil {
return nil, err return nil, err
} }
@@ -740,10 +702,13 @@ func (b *txBuilder) VerifyAndCombinePartialTx(dest string, src string) (string,
return "", fmt.Errorf("invalid signature") return "", fmt.Errorf("invalid signature")
} }
if err := roundSigner.SignTaprootInputTapscriptSig(i, partialSig); err != nil { if err := roundSigner.AddInTapLeafScript(i, input.TapLeafScript[0]); err != nil {
return "", err return "", err
} }
if err := roundSigner.SignTaprootInputTapscriptSig(i, partialSig); err != nil {
return "", err
}
} }
return roundSigner.Pset.ToBase64() return roundSigner.Pset.ToBase64()
@@ -823,56 +788,59 @@ func (b *txBuilder) createConnectors(
} }
func (b *txBuilder) createForfeitTxs( func (b *txBuilder) createForfeitTxs(
aspPubkey *secp256k1.PublicKey, payments []domain.Payment, connectors []*psetv2.Pset, connectorAmount uint64, aspPubkey *secp256k1.PublicKey,
payments []domain.Payment,
connectors []*psetv2.Pset,
connectorAmount uint64,
minRelayFeeRate chainfee.SatPerKVByte,
) ([]string, error) { ) ([]string, error) {
aspScript, err := p2wpkhScript(aspPubkey, b.onchainNetwork())
if err != nil {
return nil, err
}
forfeitTxs := make([]string, 0) forfeitTxs := make([]string, 0)
for _, payment := range payments { for _, payment := range payments {
for _, vtxo := range payment.Inputs { for _, vtxo := range payment.Inputs {
pubkeyBytes, err := hex.DecodeString(vtxo.Pubkey) offchainScript, err := tree.ParseVtxoScript(vtxo.Descriptor)
if err != nil {
return nil, fmt.Errorf("failed to decode pubkey: %s", err)
}
vtxoPubkey, err := secp256k1.ParsePubKey(pubkeyBytes)
if err != nil { if err != nil {
return nil, err return nil, err
} }
vtxoScript, vtxoTaprootTree, err := b.getLeafScriptAndTree(vtxoPubkey, aspPubkey) vtxoTapKey, vtxoTree, err := offchainScript.TapTree()
if err != nil { if err != nil {
return nil, err return nil, err
} }
var forfeitProof *taproot.TapscriptElementsProof vtxoScript, err := common.P2TRScript(vtxoTapKey)
if err != nil {
for _, proof := range vtxoTaprootTree.LeafMerkleProofs { return nil, err
isForfeit, err := (&tree.ForfeitClosure{}).Decode(proof.Script)
if !isForfeit || err != nil {
continue
} }
forfeitProof = &proof feeAmount, err := common.ComputeForfeitMinRelayFee(minRelayFeeRate, vtxoTree)
break if err != nil {
} return nil, err
if forfeitProof == nil {
return nil, fmt.Errorf("forfeit proof not found")
} }
for _, connector := range connectors { for _, connector := range connectors {
txs, err := b.craftForfeitTxs( txs, err := tree.BuildForfeitTxs(
connector, connectorAmount, vtxo, *forfeitProof, vtxoScript, aspScript, connector,
psetv2.InputArgs{
Txid: vtxo.Txid,
TxIndex: vtxo.VOut,
},
vtxo.Amount,
connectorAmount,
feeAmount,
vtxoScript,
aspPubkey,
) )
if err != nil { if err != nil {
return nil, err return nil, err
} }
forfeitTxs = append(forfeitTxs, txs...) for _, tx := range txs {
b64, err := tx.ToBase64()
if err != nil {
return nil, err
}
forfeitTxs = append(forfeitTxs, b64)
}
} }
} }
} }
@@ -946,52 +914,6 @@ func (b *txBuilder) onchainNetwork() *network.Network {
} }
} }
func (b *txBuilder) getBoardingTaproot(owner, asp *secp256k1.PublicKey) (string, []byte, *taproot.TapscriptElementsProof, error) {
multisigClosure := tree.ForfeitClosure{
Pubkey: owner,
AspPubkey: asp,
}
csvClosure := tree.CSVSigClosure{
Pubkey: owner,
Seconds: uint(b.boardingExitDelay),
}
multisigLeaf, err := multisigClosure.Leaf()
if err != nil {
return "", nil, nil, err
}
csvLeaf, err := csvClosure.Leaf()
if err != nil {
return "", nil, nil, err
}
tapTree := taproot.AssembleTaprootScriptTree(*multisigLeaf, *csvLeaf)
root := tapTree.RootNode.TapHash()
tapKey := taproot.ComputeTaprootOutputKey(tree.UnspendableKey(), root[:])
p2tr, err := payment.FromTweakedKey(tapKey, b.onchainNetwork(), nil)
if err != nil {
return "", nil, nil, err
}
addr, err := p2tr.TaprootAddress()
if err != nil {
return "", nil, nil, err
}
tapLeaf, err := multisigClosure.Leaf()
if err != nil {
return "", nil, nil, err
}
leafProofIndex := tapTree.LeafProofIndex[tapLeaf.TapHash()]
leafProof := tapTree.LeafMerkleProofs[leafProofIndex]
return addr, p2tr.Script, &leafProof, nil
}
func extractSweepLeaf(input psetv2.Input) (sweepLeaf *psetv2.TapLeafScript, lifetime int64, err error) { func extractSweepLeaf(input psetv2.Input) (sweepLeaf *psetv2.TapLeafScript, lifetime int64, err error) {
for _, leaf := range input.TapLeafScript { for _, leaf := range input.TapLeafScript {
closure := &tree.CSVSigClosure{} closure := &tree.CSVSigClosure{}

View File

@@ -20,12 +20,12 @@ import (
) )
const ( const (
testingKey = "0218d5ca8b58797b7dbd65c075dd7ba7784b3f38ab71b1a5a8e3f94ba0257654a6" testingKey = "020000000000000000000000000000000000000000000000000000000000000001"
connectorAddress = "tex1qekd5u0qj8jl07vy60830xy7n9qtmcx9u3s0cqc" connectorAddress = "tex1qekd5u0qj8jl07vy60830xy7n9qtmcx9u3s0cqc"
minRelayFee = uint64(30) minRelayFee = uint64(30)
roundLifetime = int64(1209344) roundLifetime = int64(1209344)
unilateralExitDelay = int64(512)
boardingExitDelay = int64(512) boardingExitDelay = int64(512)
minRelayFeeRate = 3
) )
var ( var (
@@ -54,7 +54,7 @@ func TestMain(m *testing.M) {
func TestBuildPoolTx(t *testing.T) { func TestBuildPoolTx(t *testing.T) {
builder := txbuilder.NewTxBuilder( builder := txbuilder.NewTxBuilder(
wallet, common.Liquid, roundLifetime, unilateralExitDelay, boardingExitDelay, wallet, common.Liquid, roundLifetime, boardingExitDelay,
) )
fixtures, err := parsePoolTxFixtures() fixtures, err := parsePoolTxFixtures()
@@ -99,7 +99,7 @@ func TestBuildPoolTx(t *testing.T) {
func TestBuildForfeitTxs(t *testing.T) { func TestBuildForfeitTxs(t *testing.T) {
builder := txbuilder.NewTxBuilder( builder := txbuilder.NewTxBuilder(
wallet, common.Liquid, 1209344, unilateralExitDelay, boardingExitDelay, wallet, common.Liquid, 1209344, boardingExitDelay,
) )
fixtures, err := parseForfeitTxsFixtures() fixtures, err := parseForfeitTxsFixtures()
@@ -110,7 +110,7 @@ func TestBuildForfeitTxs(t *testing.T) {
t.Run("valid", func(t *testing.T) { t.Run("valid", func(t *testing.T) {
for _, f := range fixtures.Valid { for _, f := range fixtures.Valid {
connectors, forfeitTxs, err := builder.BuildForfeitTxs( connectors, forfeitTxs, err := builder.BuildForfeitTxs(
pubkey, f.PoolTx, f.Payments, pubkey, f.PoolTx, f.Payments, minRelayFeeRate,
) )
require.NoError(t, err) require.NoError(t, err)
require.Len(t, connectors, f.ExpectedNumOfConnectors) require.Len(t, connectors, f.ExpectedNumOfConnectors)
@@ -148,7 +148,7 @@ func TestBuildForfeitTxs(t *testing.T) {
t.Run("invalid", func(t *testing.T) { t.Run("invalid", func(t *testing.T) {
for _, f := range fixtures.Invalid { for _, f := range fixtures.Invalid {
connectors, forfeitTxs, err := builder.BuildForfeitTxs( connectors, forfeitTxs, err := builder.BuildForfeitTxs(
pubkey, f.PoolTx, f.Payments, pubkey, f.PoolTx, f.Payments, minRelayFeeRate,
) )
require.EqualError(t, err, f.ExpectedErr) require.EqualError(t, err, f.ExpectedErr)
require.Empty(t, connectors) require.Empty(t, connectors)

View File

@@ -51,24 +51,3 @@ func craftConnectorTx(
return ptx, nil return ptx, nil
} }
func getConnectorInputs(pset *psetv2.Pset, connectorAmount uint64) ([]psetv2.InputArgs, []*transaction.TxOutput) {
txID, _ := getPsetId(pset)
inputs := make([]psetv2.InputArgs, 0, len(pset.Outputs))
witnessUtxos := make([]*transaction.TxOutput, 0, len(pset.Outputs))
for i, output := range pset.Outputs {
utx, _ := pset.UnsignedTx()
if output.Value == connectorAmount && len(output.Script) > 0 {
inputs = append(inputs, psetv2.InputArgs{
Txid: txID,
TxIndex: uint32(i),
})
witnessUtxos = append(witnessUtxos, utx.Outputs[i])
}
}
return inputs, witnessUtxos
}

View File

@@ -5,6 +5,7 @@ import (
"github.com/ark-network/ark/server/internal/core/ports" "github.com/ark-network/ark/server/internal/core/ports"
"github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
"github.com/stretchr/testify/mock" "github.com/stretchr/testify/mock"
) )
@@ -259,6 +260,16 @@ func (m *mockedWallet) GetTransaction(ctx context.Context, txid string) (string,
return res, args.Error(1) return res, args.Error(1)
} }
func (m *mockedWallet) MinRelayFeeRate(ctx context.Context) chainfee.SatPerKVByte {
args := m.Called(ctx)
var res chainfee.SatPerKVByte
if a := args.Get(0); a != nil {
res = a.(chainfee.SatPerKVByte)
}
return res
}
type mockedInput struct { type mockedInput struct {
mock.Mock mock.Mock
} }

View File

@@ -79,7 +79,7 @@ func sweepTransaction(
root := leaf.ControlBlock.RootHash(leaf.Script) root := leaf.ControlBlock.RootHash(leaf.Script)
taprootKey := taproot.ComputeTaprootOutputKey(leaf.ControlBlock.InternalKey, root) taprootKey := taproot.ComputeTaprootOutputKey(leaf.ControlBlock.InternalKey, root)
script, err := p2trScript(taprootKey) script, err := common.P2TRScript(taprootKey)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -9,13 +9,14 @@
{ {
"txid": "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6", "txid": "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6",
"vout": 0, "vout": 0,
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002", "descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"signerPubkey": "020000000000000000000000000000000000000000000000000000000000000001",
"amount": 1100 "amount": 1100
} }
], ],
"receivers": [ "receivers": [
{ {
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002", "descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"amount": 1100 "amount": 1100
} }
] ]
@@ -32,17 +33,18 @@
{ {
"txid": "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6", "txid": "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6",
"vout": 0, "vout": 0,
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002", "descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"signerPubkey": "020000000000000000000000000000000000000000000000000000000000000001",
"amount": 1100 "amount": 1100
} }
], ],
"receivers": [ "receivers": [
{ {
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002", "descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"amount": 600 "amount": 600
}, },
{ {
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002", "descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"amount": 500 "amount": 500
} }
] ]
@@ -59,17 +61,18 @@
{ {
"txid": "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6", "txid": "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6",
"vout": 0, "vout": 0,
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002", "descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"signerPubkey": "020000000000000000000000000000000000000000000000000000000000000001",
"amount": 1100 "amount": 1100
} }
], ],
"receivers": [ "receivers": [
{ {
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002", "descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"amount": 600 "amount": 600
}, },
{ {
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002", "descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"amount": 500 "amount": 500
} }
] ]
@@ -80,17 +83,18 @@
{ {
"txid": "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6", "txid": "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6",
"vout": 0, "vout": 0,
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002", "descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"signerPubkey": "020000000000000000000000000000000000000000000000000000000000000001",
"amount": 1100 "amount": 1100
} }
], ],
"receivers": [ "receivers": [
{ {
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002", "descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"amount": 600 "amount": 600
}, },
{ {
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002", "descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"amount": 500 "amount": 500
} }
] ]
@@ -101,17 +105,18 @@
{ {
"txid": "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6", "txid": "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6",
"vout": 0, "vout": 0,
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002", "descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"signerPubkey": "020000000000000000000000000000000000000000000000000000000000000001",
"amount": 1100 "amount": 1100
} }
], ],
"receivers": [ "receivers": [
{ {
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002", "descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"amount": 600 "amount": 600
}, },
{ {
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002", "descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"amount": 500 "amount": 500
} }
] ]
@@ -128,53 +133,58 @@
{ {
"txid": "755c820771284d85ea4bbcc246565b4eddadc44237a7e57a0f9cb78a840d1d41", "txid": "755c820771284d85ea4bbcc246565b4eddadc44237a7e57a0f9cb78a840d1d41",
"vout": 0, "vout": 0,
"pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5", "descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"signerPubkey": "020000000000000000000000000000000000000000000000000000000000000001",
"amount": 1000 "amount": 1000
}, },
{ {
"txid": "66a0df86fcdeb84b8877adfe0b2c556dba30305d72ddbd4c49355f6930355357", "txid": "66a0df86fcdeb84b8877adfe0b2c556dba30305d72ddbd4c49355f6930355357",
"vout": 0, "vout": 0,
"pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5", "descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"signerPubkey": "020000000000000000000000000000000000000000000000000000000000000001",
"amount": 1000 "amount": 1000
}, },
{ {
"txid": "9913159bc7aa493ca53cbb9cbc88f97ba01137c814009dc7ef520c3fafc67909", "txid": "9913159bc7aa493ca53cbb9cbc88f97ba01137c814009dc7ef520c3fafc67909",
"vout": 1, "vout": 1,
"pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5", "descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"signerPubkey": "020000000000000000000000000000000000000000000000000000000000000001",
"amount": 500 "amount": 500
}, },
{ {
"txid": "5e10e77a7cdedc153be5193a4b6055a7802706ded4f2a9efefe86ed2f9a6ae60", "txid": "5e10e77a7cdedc153be5193a4b6055a7802706ded4f2a9efefe86ed2f9a6ae60",
"vout": 0, "vout": 0,
"pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5", "descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"signerPubkey": "020000000000000000000000000000000000000000000000000000000000000001",
"amount": 1000 "amount": 1000
}, },
{ {
"txid": "5e10e77a7cdedc153be5193a4b6055a7802706ded4f2a9efefe86ed2f9a6ae60", "txid": "5e10e77a7cdedc153be5193a4b6055a7802706ded4f2a9efefe86ed2f9a6ae60",
"vout": 1, "vout": 1,
"pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5", "descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"signerPubkey": "020000000000000000000000000000000000000000000000000000000000000001",
"amount": 1000 "amount": 1000
} }
], ],
"receivers": [ "receivers": [
{ {
"pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5", "descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"amount": 1000 "amount": 1000
}, },
{ {
"pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5", "descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"amount": 1000 "amount": 1000
}, },
{ {
"pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5", "descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"amount": 1000 "amount": 1000
}, },
{ {
"pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5", "descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"amount": 1000 "amount": 1000
}, },
{ {
"pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5", "descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"amount": 500 "amount": 500
} }
] ]
@@ -196,23 +206,25 @@
{ {
"txid": "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6", "txid": "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6",
"vout": 0, "vout": 0,
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002", "descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"signerPubkey": "020000000000000000000000000000000000000000000000000000000000000001",
"amount": 600 "amount": 600
}, },
{ {
"txid": "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6", "txid": "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6",
"vout": 1, "vout": 1,
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002", "descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"signerPubkey": "020000000000000000000000000000000000000000000000000000000000000001",
"amount": 500 "amount": 500
} }
], ],
"receivers": [ "receivers": [
{ {
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002", "descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"amount": 600 "amount": 600
}, },
{ {
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002", "descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"amount": 500 "amount": 500
} }
] ]

View File

@@ -7,7 +7,6 @@ import (
"github.com/ark-network/ark/common/tree" "github.com/ark-network/ark/common/tree"
"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"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/txscript"
"github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/vulpemventures/go-elements/address" "github.com/vulpemventures/go-elements/address"
@@ -62,19 +61,24 @@ func getOnchainReceivers(
func getOffchainReceivers( func getOffchainReceivers(
payments []domain.Payment, payments []domain.Payment,
) []tree.Receiver { ) ([]tree.Receiver, error) {
receivers := make([]tree.Receiver, 0) receivers := make([]tree.Receiver, 0)
for _, payment := range payments { for _, payment := range payments {
for _, receiver := range payment.Receivers { for _, receiver := range payment.Receivers {
if !receiver.IsOnchain() { if !receiver.IsOnchain() {
vtxoScript, err := tree.ParseVtxoScript(receiver.Descriptor)
if err != nil {
return nil, err
}
receivers = append(receivers, tree.Receiver{ receivers = append(receivers, tree.Receiver{
Pubkey: receiver.Pubkey, Script: vtxoScript,
Amount: receiver.Amount, Amount: receiver.Amount,
}) })
} }
} }
} }
return receivers return receivers, nil
} }
func toWitnessUtxo(in ports.TxInput) (*transaction.TxOutput, error) { func toWitnessUtxo(in ports.TxInput) (*transaction.TxOutput, error) {
@@ -136,10 +140,6 @@ func addInputs(
return nil return nil
} }
func p2trScript(taprootKey *secp256k1.PublicKey) ([]byte, error) {
return txscript.NewScriptBuilder().AddOp(txscript.OP_1).AddData(schnorr.SerializePubKey(taprootKey)).Script()
}
func isOnchainOnly(payments []domain.Payment) bool { func isOnchainOnly(payments []domain.Payment) bool {
for _, p := range payments { for _, p := range payments {
for _, r := range p.Receivers { for _, r := range p.Receivers {

View File

@@ -22,20 +22,29 @@ 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 {
wallet ports.WalletService wallet ports.WalletService
net common.Network net common.Network
roundLifetime int64 // in seconds roundLifetime int64 // in seconds
exitDelay int64 // in seconds
boardingExitDelay int64 // in seconds boardingExitDelay int64 // in seconds
} }
func NewTxBuilder( func NewTxBuilder(
wallet ports.WalletService, net common.Network, roundLifetime, exitDelay, boardingExitDelay int64, wallet ports.WalletService, net common.Network, roundLifetime, boardingExitDelay int64,
) ports.TxBuilder { ) ports.TxBuilder {
return &txBuilder{wallet, net, roundLifetime, exitDelay, boardingExitDelay} return &txBuilder{wallet, net, roundLifetime, boardingExitDelay}
}
func (b *txBuilder) GetTxID(tx string) (string, error) {
ptx, err := psbt.NewFromRawBytes(strings.NewReader(tx), true)
if err != nil {
return "", err
}
return ptx.UnsignedTx.TxHash().String(), nil
} }
func (b *txBuilder) VerifyTapscriptPartialSigs(tx string) (bool, string, error) { func (b *txBuilder) VerifyTapscriptPartialSigs(tx string) (bool, string, error) {
@@ -64,7 +73,7 @@ func (b *txBuilder) VerifyTapscriptPartialSigs(tx string) (bool, string, error)
rootHash := controlBlock.RootHash(tapLeaf.Script) rootHash := controlBlock.RootHash(tapLeaf.Script)
tapKeyFromControlBlock := txscript.ComputeTaprootOutputKey(bitcointree.UnspendableKey(), rootHash[:]) tapKeyFromControlBlock := txscript.ComputeTaprootOutputKey(bitcointree.UnspendableKey(), rootHash[:])
pkscript, err := p2trScript(tapKeyFromControlBlock) pkscript, err := common.P2TRScript(tapKeyFromControlBlock)
if err != nil { if err != nil {
return false, txid, err return false, txid, err
} }
@@ -173,14 +182,6 @@ func (b *txBuilder) FinalizeAndExtract(tx string) (string, error) {
return hex.EncodeToString(serialized.Bytes()), nil return hex.EncodeToString(serialized.Bytes()), nil
} }
func (b *txBuilder) GetVtxoScript(userPubkey, aspPubkey *secp256k1.PublicKey) ([]byte, error) {
outputScript, _, err := b.getLeafScriptAndTree(userPubkey, aspPubkey)
if err != nil {
return nil, err
}
return outputScript, nil
}
func (b *txBuilder) BuildSweepTx(inputs []ports.SweepInput) (signedSweepTx string, err error) { func (b *txBuilder) BuildSweepTx(inputs []ports.SweepInput) (signedSweepTx string, err error) {
sweepPsbt, err := sweepTransaction( sweepPsbt, err := sweepTransaction(
b.wallet, b.wallet,
@@ -227,7 +228,7 @@ func (b *txBuilder) BuildSweepTx(inputs []ports.SweepInput) (signedSweepTx strin
} }
func (b *txBuilder) BuildForfeitTxs( func (b *txBuilder) BuildForfeitTxs(
aspPubkey *secp256k1.PublicKey, poolTx string, payments []domain.Payment, aspPubkey *secp256k1.PublicKey, poolTx string, payments []domain.Payment, minRelayFeeRate chainfee.SatPerKVByte,
) (connectors []string, forfeitTxs []string, err error) { ) (connectors []string, forfeitTxs []string, err error) {
connectorPkScript, err := b.getConnectorPkScript(poolTx) connectorPkScript, err := b.getConnectorPkScript(poolTx)
if err != nil { if err != nil {
@@ -244,12 +245,7 @@ func (b *txBuilder) BuildForfeitTxs(
return nil, nil, err return nil, nil, err
} }
minRelayFeeForfeitTx, err := b.minRelayFeeForfeitTx() forfeitTxs, err = b.createForfeitTxs(aspPubkey, payments, connectorTxs, minRelayFeeRate)
if err != nil {
return nil, nil, err
}
forfeitTxs, err = b.createForfeitTxs(aspPubkey, payments, connectorTxs, minRelayFeeForfeitTx)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@@ -275,7 +271,10 @@ func (b *txBuilder) BuildPoolTx(
return "", nil, "", fmt.Errorf("missing cosigners") return "", nil, "", fmt.Errorf("missing cosigners")
} }
receivers := getOffchainReceivers(payments) receivers, err := getOffchainReceivers(payments)
if err != nil {
return "", nil, "", err
}
feeAmount, err := b.minRelayFeeTreeTx() feeAmount, err := b.minRelayFeeTreeTx()
if err != nil { if err != nil {
@@ -284,7 +283,7 @@ func (b *txBuilder) BuildPoolTx(
if !isOnchainOnly(payments) { if !isOnchainOnly(payments) {
sharedOutputScript, sharedOutputAmount, err = bitcointree.CraftSharedOutput( sharedOutputScript, sharedOutputAmount, err = bitcointree.CraftSharedOutput(
cosigners, aspPubkey, receivers, feeAmount, b.roundLifetime, b.exitDelay, cosigners, aspPubkey, receivers, feeAmount, b.roundLifetime,
) )
if err != nil { if err != nil {
return return
@@ -297,7 +296,7 @@ func (b *txBuilder) BuildPoolTx(
} }
ptx, err := b.createPoolTx( ptx, err := b.createPoolTx(
aspPubkey, sharedOutputAmount, sharedOutputScript, payments, boardingInputs, connectorAddress, sweptRounds, sharedOutputAmount, sharedOutputScript, payments, boardingInputs, connectorAddress, sweptRounds,
) )
if err != nil { if err != nil {
return return
@@ -315,7 +314,7 @@ func (b *txBuilder) BuildPoolTx(
} }
congestionTree, err = bitcointree.CraftCongestionTree( congestionTree, err = bitcointree.CraftCongestionTree(
initialOutpoint, cosigners, aspPubkey, receivers, feeAmount, b.roundLifetime, b.exitDelay, initialOutpoint, cosigners, aspPubkey, receivers, feeAmount, b.roundLifetime,
) )
if err != nil { if err != nil {
return return
@@ -401,7 +400,6 @@ func (b *txBuilder) FindLeaves(congestionTree tree.CongestionTree, fromtxid stri
return foundLeaves, nil return foundLeaves, nil
} }
// TODO add locktimes to txs
func (b *txBuilder) BuildAsyncPaymentTransactions( func (b *txBuilder) BuildAsyncPaymentTransactions(
vtxos []domain.Vtxo, aspPubKey *secp256k1.PublicKey, receivers []domain.Receiver, vtxos []domain.Vtxo, aspPubKey *secp256k1.PublicKey, receivers []domain.Receiver,
) (*domain.AsyncPaymentTxs, error) { ) (*domain.AsyncPaymentTxs, error) {
@@ -410,6 +408,7 @@ func (b *txBuilder) BuildAsyncPaymentTransactions(
} }
for _, vtxo := range vtxos { for _, vtxo := range vtxos {
// TODO allow to chain async payment ?
if vtxo.AsyncPayment != nil { if vtxo.AsyncPayment != nil {
return nil, fmt.Errorf("vtxo %s is an async payment", vtxo.Txid) return nil, fmt.Errorf("vtxo %s is an async payment", vtxo.Txid)
} }
@@ -420,21 +419,11 @@ func (b *txBuilder) BuildAsyncPaymentTransactions(
unconditionalForfeitTxs := make([]string, 0, len(vtxos)) unconditionalForfeitTxs := make([]string, 0, len(vtxos))
redeemTxWeightEstimator := &input.TxWeightEstimator{} redeemTxWeightEstimator := &input.TxWeightEstimator{}
for _, vtxo := range vtxos { for _, vtxo := range vtxos {
if vtxo.Spent { if vtxo.Spent || vtxo.Redeemed || vtxo.Swept {
return nil, fmt.Errorf("all vtxos must be unspent") return nil, fmt.Errorf("all vtxos must be unspent")
} }
senderBytes, err := hex.DecodeString(vtxo.Pubkey) aspScript, err := common.P2TRScript(aspPubKey)
if err != nil {
return nil, err
}
sender, err := secp256k1.ParsePubKey(senderBytes)
if err != nil {
return nil, err
}
aspScript, err := p2trScript(aspPubKey)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -449,14 +438,28 @@ func (b *txBuilder) BuildAsyncPaymentTransactions(
Index: vtxo.VOut, Index: vtxo.VOut,
} }
vtxoScript, vtxoTree, err := b.getLeafScriptAndTree(sender, aspPubKey) vtxoScript, err := bitcointree.ParseVtxoScript(vtxo.Descriptor)
if err != nil { if err != nil {
return nil, err return nil, err
} }
vtxoTapKey, vtxoTree, err := vtxoScript.TapTree()
if err != nil {
return nil, err
}
vtxoOutputScript, err := common.P2TRScript(vtxoTapKey)
if err != nil {
return nil, err
}
var tapscript *waddrmgr.Tapscript
forfeitTxWeightEstimator := &input.TxWeightEstimator{}
if defaultVtxoScript, ok := vtxoScript.(*bitcointree.DefaultVtxoScript); ok {
forfeitClosure := &bitcointree.MultisigClosure{ forfeitClosure := &bitcointree.MultisigClosure{
Pubkey: sender, Pubkey: defaultVtxoScript.Owner,
AspPubkey: aspPubKey, AspPubkey: defaultVtxoScript.Asp,
} }
forfeitLeaf, err := forfeitClosure.Leaf() forfeitLeaf, err := forfeitClosure.Leaf()
@@ -464,20 +467,25 @@ func (b *txBuilder) BuildAsyncPaymentTransactions(
return nil, err return nil, err
} }
leafProof := vtxoTree.LeafMerkleProofs[vtxoTree.LeafProofIndex[forfeitLeaf.TapHash()]] forfeitProof, err := vtxoTree.GetTaprootMerkleProof(forfeitLeaf.TapHash())
ctrlBlock := leafProof.ToControlBlock(bitcointree.UnspendableKey())
ctrlBlockBytes, err := ctrlBlock.ToBytes()
if err != nil { if err != nil {
return nil, err return nil, err
} }
forfeitTxWeightEstimator := &input.TxWeightEstimator{} ctrlBlock, err := txscript.ParseControlBlock(forfeitProof.ControlBlock)
tapscript := &waddrmgr.Tapscript{ if err != nil {
RevealedScript: leafProof.Script, return nil, err
ControlBlock: &ctrlBlock, }
tapscript = &waddrmgr.Tapscript{
RevealedScript: forfeitProof.Script,
ControlBlock: ctrlBlock,
} }
forfeitTxWeightEstimator.AddTapscriptInput(64*2, tapscript) forfeitTxWeightEstimator.AddTapscriptInput(64*2, tapscript)
forfeitTxWeightEstimator.AddP2TROutput() // ASP output forfeitTxWeightEstimator.AddP2TROutput() // ASP output
} else {
return nil, fmt.Errorf("vtxo script is not a default vtxo script, cannot be async spent")
}
forfeitTxFee, err := b.wallet.MinRelayFee(context.Background(), uint64(forfeitTxWeightEstimator.VSize())) forfeitTxFee, err := b.wallet.MinRelayFee(context.Background(), uint64(forfeitTxWeightEstimator.VSize()))
if err != nil { if err != nil {
@@ -506,14 +514,18 @@ func (b *txBuilder) BuildAsyncPaymentTransactions(
unconditionnalForfeitPtx.Inputs[0].WitnessUtxo = &wire.TxOut{ unconditionnalForfeitPtx.Inputs[0].WitnessUtxo = &wire.TxOut{
Value: int64(vtxo.Amount), Value: int64(vtxo.Amount),
PkScript: vtxoScript, PkScript: vtxoOutputScript,
}
ctrlBlock, err := tapscript.ControlBlock.ToBytes()
if err != nil {
return nil, err
} }
unconditionnalForfeitPtx.Inputs[0].TaprootInternalKey = schnorr.SerializePubKey(bitcointree.UnspendableKey())
unconditionnalForfeitPtx.Inputs[0].TaprootLeafScript = []*psbt.TaprootTapLeafScript{ unconditionnalForfeitPtx.Inputs[0].TaprootLeafScript = []*psbt.TaprootTapLeafScript{
{ {
ControlBlock: ctrlBlockBytes, Script: tapscript.RevealedScript,
Script: forfeitLeaf.Script, ControlBlock: ctrlBlock,
LeafVersion: txscript.BaseLeafVersion, LeafVersion: txscript.BaseLeafVersion,
}, },
} }
@@ -542,16 +554,17 @@ func (b *txBuilder) BuildAsyncPaymentTransactions(
} }
for i, receiver := range receivers { for i, receiver := range receivers {
// TODO (@louisinger): Add revert policy (sender+ASP) offchainScript, err := bitcointree.ParseVtxoScript(receiver.Descriptor)
buf, err := hex.DecodeString(receiver.Pubkey)
if err != nil { if err != nil {
return nil, err return nil, err
} }
receiverPk, err := secp256k1.ParsePubKey(buf)
receiverVtxoTaprootKey, _, err := offchainScript.TapTree()
if err != nil { if err != nil {
return nil, err return nil, err
} }
newVtxoScript, _, err := b.getLeafScriptAndTree(receiverPk, aspPubKey)
newVtxoScript, err := common.P2TRScript(receiverVtxoTaprootKey)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -607,58 +620,12 @@ func (b *txBuilder) BuildAsyncPaymentTransactions(
}, nil }, nil
} }
func (b *txBuilder) GetBoardingScript(userPubkey, aspPubkey *secp256k1.PublicKey) (string, []byte, error) {
addr, script, _, err := b.craftBoardingTaproot(userPubkey, aspPubkey)
if err != nil {
return "", nil, err
}
return addr, script, nil
}
func (b *txBuilder) getLeafScriptAndTree(
userPubkey, aspPubkey *secp256k1.PublicKey,
) ([]byte, *txscript.IndexedTapScriptTree, error) {
redeemClosure := &bitcointree.CSVSigClosure{
Pubkey: userPubkey,
Seconds: uint(b.exitDelay),
}
redeemLeaf, err := redeemClosure.Leaf()
if err != nil {
return nil, nil, err
}
forfeitClosure := &bitcointree.MultisigClosure{
Pubkey: userPubkey,
AspPubkey: aspPubkey,
}
forfeitLeaf, err := forfeitClosure.Leaf()
if err != nil {
return nil, nil, err
}
taprootTree := txscript.AssembleTaprootScriptTree(
*redeemLeaf, *forfeitLeaf,
)
root := taprootTree.RootNode.TapHash()
unspendableKey := bitcointree.UnspendableKey()
taprootKey := txscript.ComputeTaprootOutputKey(unspendableKey, root[:])
outputScript, err := taprootOutputScript(taprootKey)
if err != nil {
return nil, nil, err
}
return outputScript, taprootTree, nil
}
func (b *txBuilder) createPoolTx( func (b *txBuilder) createPoolTx(
aspPubKey *secp256k1.PublicKey, sharedOutputAmount int64,
sharedOutputAmount int64, sharedOutputScript []byte, sharedOutputScript []byte,
payments []domain.Payment, boardingInputs []ports.BoardingInput, connectorAddress string, payments []domain.Payment,
boardingInputs []ports.BoardingInput,
connectorAddress string,
sweptRounds []domain.Round, sweptRounds []domain.Round,
) (*psbt.Packet, error) { ) (*psbt.Packet, error) {
connectorAddr, err := btcutil.DecodeAddress(connectorAddress, b.onchainNetwork()) connectorAddr, err := btcutil.DecodeAddress(connectorAddress, b.onchainNetwork())
@@ -729,7 +696,7 @@ func (b *txBuilder) createPoolTx(
} }
for _, input := range boardingInputs { for _, input := range boardingInputs {
targetAmount -= input.GetAmount() targetAmount -= input.Amount
} }
ctx := context.Background() ctx := context.Background()
@@ -769,7 +736,7 @@ func (b *txBuilder) createPoolTx(
ins := make([]*wire.OutPoint, 0) ins := make([]*wire.OutPoint, 0)
nSequences := make([]uint32, 0) nSequences := make([]uint32, 0)
witnessUtxos := make(map[int]*wire.TxOut) witnessUtxos := make(map[int]*wire.TxOut)
boardingTapLeaves := make(map[int]*psbt.TaprootTapLeafScript) tapLeaves := make(map[int]*psbt.TaprootTapLeafScript)
nextIndex := 0 nextIndex := 0
for _, utxo := range utxos { for _, utxo := range utxos {
@@ -796,23 +763,48 @@ func (b *txBuilder) createPoolTx(
nextIndex++ nextIndex++
} }
for _, input := range boardingInputs { for _, boardingInput := range boardingInputs {
ins = append(ins, &wire.OutPoint{ txHash, err := chainhash.NewHashFromStr(boardingInput.Txid)
Hash: input.GetHash(),
Index: input.GetIndex(),
})
nSequences = append(nSequences, wire.MaxTxInSequenceNum)
_, script, tapLeaf, err := b.craftBoardingTaproot(input.GetBoardingPubkey(), aspPubKey)
if err != nil { if err != nil {
return nil, err return nil, err
} }
boardingTapLeaves[nextIndex] = tapLeaf ins = append(ins, &wire.OutPoint{
witnessUtxos[nextIndex] = &wire.TxOut{ Hash: *txHash,
Value: int64(input.GetAmount()), Index: boardingInput.VtxoKey.VOut,
PkScript: script, })
nSequences = append(nSequences, wire.MaxTxInSequenceNum)
boardingVtxoScript, err := bitcointree.ParseVtxoScript(boardingInput.Descriptor)
if err != nil {
return nil, err
} }
boardingTapKey, boardingTapTree, err := boardingVtxoScript.TapTree()
if err != nil {
return nil, err
}
boardingOutputScript, err := common.P2TRScript(boardingTapKey)
if err != nil {
return nil, err
}
witnessUtxos[nextIndex] = &wire.TxOut{
Value: int64(boardingInput.Amount),
PkScript: boardingOutputScript,
}
biggestProof, err := common.BiggestLeafMerkleProof(boardingTapTree)
if err != nil {
return nil, err
}
tapLeaves[nextIndex] = &psbt.TaprootTapLeafScript{
Script: biggestProof.Script,
ControlBlock: biggestProof.ControlBlock,
}
nextIndex++ nextIndex++
} }
@@ -832,11 +824,8 @@ func (b *txBuilder) createPoolTx(
} }
} }
unspendableInternalKey := schnorr.SerializePubKey(bitcointree.UnspendableKey()) for inIndex, tapLeaf := range tapLeaves {
for inIndex, tapLeaf := range boardingTapLeaves {
updater.Upsbt.Inputs[inIndex].TaprootLeafScript = []*psbt.TaprootTapLeafScript{tapLeaf} updater.Upsbt.Inputs[inIndex].TaprootLeafScript = []*psbt.TaprootTapLeafScript{tapLeaf}
updater.Upsbt.Inputs[inIndex].TaprootInternalKey = unspendableInternalKey
} }
b64, err := ptx.B64Encode() b64, err := ptx.B64Encode()
@@ -991,6 +980,12 @@ func (b *txBuilder) createPoolTx(
} }
} }
// remove input taproot leaf script
// used only to compute an accurate fee estimation
for i := range ptx.Inputs {
ptx.Inputs[i].TaprootLeafScript = nil
}
return ptx, nil return ptx, nil
} }
@@ -1048,6 +1043,7 @@ func (b *txBuilder) VerifyAndCombinePartialTx(dest string, src string) (string,
} }
roundTx.Inputs[i].TaprootScriptSpendSig = sourceInput.TaprootScriptSpendSig roundTx.Inputs[i].TaprootScriptSpendSig = sourceInput.TaprootScriptSpendSig
roundTx.Inputs[i].TaprootLeafScript = sourceInput.TaprootLeafScript
} }
} }
@@ -1125,88 +1121,18 @@ 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) minRelayFeeForfeitTx() (uint64, error) {
// rebuild the forfeit leaf in order to estimate the input witness size
randomKey, err := secp256k1.GeneratePrivateKey()
if err != nil {
return 0, err
}
pubkey := randomKey.PubKey()
forfeitClosure := &bitcointree.MultisigClosure{
Pubkey: pubkey,
AspPubkey: pubkey,
}
leaf, err := forfeitClosure.Leaf()
if err != nil {
return 0, err
}
_, vtxoTaprootTree, err := b.getLeafScriptAndTree(pubkey, pubkey)
if err != nil {
return 0, err
}
merkleProofIndex := vtxoTaprootTree.LeafProofIndex[leaf.TapHash()]
merkleProof := vtxoTaprootTree.LeafMerkleProofs[merkleProofIndex]
controlBlock := merkleProof.ToControlBlock(bitcointree.UnspendableKey())
weightEstimator := &input.TxWeightEstimator{}
weightEstimator.AddP2WKHInput() // connector input
weightEstimator.AddTapscriptInput(64*2, &waddrmgr.Tapscript{
RevealedScript: merkleProof.Script,
ControlBlock: &controlBlock,
}) // forfeit input
weightEstimator.AddP2TROutput() // the asp output
return b.wallet.MinRelayFee(context.Background(), uint64(weightEstimator.VSize()))
}
func (b *txBuilder) createForfeitTxs( func (b *txBuilder) createForfeitTxs(
aspPubkey *secp256k1.PublicKey, payments []domain.Payment, connectors []*psbt.Packet, feeAmount uint64, aspPubkey *secp256k1.PublicKey, payments []domain.Payment, connectors []*psbt.Packet, minRelayFeeRate chainfee.SatPerKVByte,
) ([]string, error) { ) ([]string, error) {
aspScript, err := p2trScript(aspPubkey)
if err != nil {
return nil, err
}
forfeitTxs := make([]string, 0) forfeitTxs := make([]string, 0)
for _, payment := range payments { for _, payment := range payments {
for _, vtxo := range payment.Inputs { for _, vtxo := range payment.Inputs {
pubkeyBytes, err := hex.DecodeString(vtxo.Pubkey) offchainscript, err := bitcointree.ParseVtxoScript(vtxo.Descriptor)
if err != nil {
return nil, fmt.Errorf("failed to decode pubkey: %s", err)
}
vtxoPubkey, err := secp256k1.ParsePubKey(pubkeyBytes)
if err != nil { if err != nil {
return nil, err return nil, err
} }
vtxoScript, vtxoTaprootTree, err := b.getLeafScriptAndTree(vtxoPubkey, aspPubkey) vtxoTaprootKey, tapTree, err := offchainscript.TapTree()
if err != nil {
return nil, err
}
var forfeitProof *txscript.TapscriptProof
for _, proof := range vtxoTaprootTree.LeafMerkleProofs {
isForfeit, err := (&bitcointree.MultisigClosure{}).Decode(proof.Script)
if !isForfeit || err != nil {
continue
}
forfeitProof = &proof
break
}
if forfeitProof == nil {
return nil, fmt.Errorf("forfeit proof not found")
}
controlBlock := forfeitProof.ToControlBlock(bitcointree.UnspendableKey())
ctrlBlockBytes, err := controlBlock.ToBytes()
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -1216,21 +1142,46 @@ func (b *txBuilder) createForfeitTxs(
return nil, err return nil, err
} }
vtxoScript, err := common.P2TRScript(vtxoTaprootKey)
if err != nil {
return nil, err
}
feeAmount, err := common.ComputeForfeitMinRelayFee(minRelayFeeRate, tapTree)
if err != nil {
return nil, err
}
vtxoTxHash, err := chainhash.NewHashFromStr(vtxo.Txid)
if err != nil {
return nil, err
}
for _, connector := range connectors { for _, connector := range connectors {
txs, err := craftForfeitTxs( txs, err := bitcointree.BuildForfeitTxs(
connector, vtxo, connector,
&psbt.TaprootTapLeafScript{ &wire.OutPoint{
ControlBlock: ctrlBlockBytes, Hash: *vtxoTxHash,
Script: forfeitProof.Script, Index: vtxo.VOut,
LeafVersion: forfeitProof.LeafVersion,
}, },
vtxoScript, aspScript, feeAmount, int64(connectorAmount), vtxo.Amount,
connectorAmount,
feeAmount,
vtxoScript,
aspPubkey,
) )
if err != nil { if err != nil {
return nil, err return nil, err
} }
forfeitTxs = append(forfeitTxs, txs...) for _, tx := range txs {
b64, err := tx.B64Encode()
if err != nil {
return nil, err
}
forfeitTxs = append(forfeitTxs, b64)
}
} }
} }
} }
@@ -1337,61 +1288,6 @@ func (b *txBuilder) onchainNetwork() *chaincfg.Params {
} }
} }
// craftBoardingTaproot returns the addr, script and the leaf belonging to the ASP
func (b *txBuilder) craftBoardingTaproot(userPubkey, aspPubkey *secp256k1.PublicKey) (string, []byte, *psbt.TaprootTapLeafScript, error) {
multisigClosure := bitcointree.MultisigClosure{
Pubkey: userPubkey,
AspPubkey: aspPubkey,
}
csvClosure := bitcointree.CSVSigClosure{
Pubkey: userPubkey,
Seconds: uint(b.boardingExitDelay),
}
multisigLeaf, err := multisigClosure.Leaf()
if err != nil {
return "", nil, nil, err
}
csvLeaf, err := csvClosure.Leaf()
if err != nil {
return "", nil, nil, err
}
tree := txscript.AssembleTaprootScriptTree(*multisigLeaf, *csvLeaf)
root := tree.RootNode.TapHash()
taprootKey := txscript.ComputeTaprootOutputKey(bitcointree.UnspendableKey(), root[:])
script, err := txscript.PayToTaprootScript(taprootKey)
if err != nil {
return "", nil, nil, err
}
addr, err := btcutil.NewAddressTaproot(schnorr.SerializePubKey(taprootKey), b.onchainNetwork())
if err != nil {
return "", nil, nil, err
}
proofIndex := tree.LeafProofIndex[multisigLeaf.TapHash()]
proof := tree.LeafMerkleProofs[proofIndex]
ctrlBlock := proof.ToControlBlock(bitcointree.UnspendableKey())
ctrlBlockBytes, err := ctrlBlock.ToBytes()
if err != nil {
return "", nil, nil, err
}
tapLeaf := &psbt.TaprootTapLeafScript{
ControlBlock: ctrlBlockBytes,
Script: multisigLeaf.Script,
LeafVersion: txscript.BaseLeafVersion,
}
return addr.String(), script, tapLeaf, nil
}
func castToOutpoints(inputs []ports.TxInput) []ports.TxOutpoint { func castToOutpoints(inputs []ports.TxInput) []ports.TxOutpoint {
outpoints := make([]ports.TxOutpoint, 0, len(inputs)) outpoints := make([]ports.TxOutpoint, 0, len(inputs))
for _, input := range inputs { for _, input := range inputs {

View File

@@ -20,11 +20,11 @@ import (
) )
const ( const (
testingKey = "0218d5ca8b58797b7dbd65c075dd7ba7784b3f38ab71b1a5a8e3f94ba0257654a6" testingKey = "020000000000000000000000000000000000000000000000000000000000000001"
connectorAddress = "bc1py00yhcjpcj0k0sqra0etq0u3yy0purmspppsw0shyzyfe8c83tmq5h6kc2" connectorAddress = "bc1py00yhcjpcj0k0sqra0etq0u3yy0purmspppsw0shyzyfe8c83tmq5h6kc2"
roundLifetime = int64(1209344) roundLifetime = int64(1209344)
unilateralExitDelay = int64(512)
boardingExitDelay = int64(512) boardingExitDelay = int64(512)
minRelayFeeRate = 3
) )
var ( var (
@@ -53,7 +53,7 @@ func TestMain(m *testing.M) {
func TestBuildPoolTx(t *testing.T) { func TestBuildPoolTx(t *testing.T) {
builder := txbuilder.NewTxBuilder( builder := txbuilder.NewTxBuilder(
wallet, common.Bitcoin, roundLifetime, unilateralExitDelay, boardingExitDelay, wallet, common.Bitcoin, roundLifetime, boardingExitDelay,
) )
fixtures, err := parsePoolTxFixtures() fixtures, err := parsePoolTxFixtures()
@@ -64,15 +64,11 @@ func TestBuildPoolTx(t *testing.T) {
t.Run("valid", func(t *testing.T) { t.Run("valid", func(t *testing.T) {
for _, f := range fixtures.Valid { for _, f := range fixtures.Valid {
cosigners := make([]*secp256k1.PublicKey, 0) cosigners := make([]*secp256k1.PublicKey, 0)
for _, payment := range f.Payments { for range f.Payments {
for _, input := range payment.Inputs { randKey, err := secp256k1.GeneratePrivateKey()
pubkeyBytes, err := hex.DecodeString(input.Pubkey)
require.NoError(t, err)
pubkey, err := secp256k1.ParsePubKey(pubkeyBytes)
require.NoError(t, err) require.NoError(t, err)
cosigners = append(cosigners, pubkey) cosigners = append(cosigners, randKey.PubKey())
}
} }
poolTx, congestionTree, connAddr, err := builder.BuildPoolTx( poolTx, congestionTree, connAddr, err := builder.BuildPoolTx(
@@ -110,7 +106,7 @@ func TestBuildPoolTx(t *testing.T) {
func TestBuildForfeitTxs(t *testing.T) { func TestBuildForfeitTxs(t *testing.T) {
builder := txbuilder.NewTxBuilder( builder := txbuilder.NewTxBuilder(
wallet, common.Bitcoin, 1209344, unilateralExitDelay, boardingExitDelay, wallet, common.Bitcoin, 1209344, boardingExitDelay,
) )
fixtures, err := parseForfeitTxsFixtures() fixtures, err := parseForfeitTxsFixtures()
@@ -121,7 +117,7 @@ func TestBuildForfeitTxs(t *testing.T) {
t.Run("valid", func(t *testing.T) { t.Run("valid", func(t *testing.T) {
for _, f := range fixtures.Valid { for _, f := range fixtures.Valid {
connectors, forfeitTxs, err := builder.BuildForfeitTxs( connectors, forfeitTxs, err := builder.BuildForfeitTxs(
pubkey, f.PoolTx, f.Payments, pubkey, f.PoolTx, f.Payments, minRelayFeeRate,
) )
require.NoError(t, err) require.NoError(t, err)
require.Len(t, connectors, f.ExpectedNumOfConnectors) require.Len(t, connectors, f.ExpectedNumOfConnectors)
@@ -159,7 +155,7 @@ func TestBuildForfeitTxs(t *testing.T) {
t.Run("invalid", func(t *testing.T) { t.Run("invalid", func(t *testing.T) {
for _, f := range fixtures.Invalid { for _, f := range fixtures.Invalid {
connectors, forfeitTxs, err := builder.BuildForfeitTxs( connectors, forfeitTxs, err := builder.BuildForfeitTxs(
pubkey, f.PoolTx, f.Payments, pubkey, f.PoolTx, f.Payments, minRelayFeeRate,
) )
require.EqualError(t, err, f.ExpectedErr) require.EqualError(t, err, f.ExpectedErr)
require.Empty(t, connectors) require.Empty(t, connectors)

View File

@@ -38,20 +38,3 @@ func craftConnectorTx(
return ptx, nil return ptx, nil
} }
func getConnectorInputs(partialTx *psbt.Packet, connectorAmount int64) ([]*wire.OutPoint, []*wire.TxOut) {
inputs := make([]*wire.OutPoint, 0)
witnessUtxos := make([]*wire.TxOut, 0)
for i, output := range partialTx.UnsignedTx.TxOut {
if output.Value == connectorAmount {
inputs = append(inputs, &wire.OutPoint{
Hash: partialTx.UnsignedTx.TxHash(),
Index: uint32(i),
})
witnessUtxos = append(witnessUtxos, output)
}
}
return inputs, witnessUtxos
}

View File

@@ -1,78 +0,0 @@
package txbuilder
import (
"github.com/ark-network/ark/server/internal/core/domain"
"github.com/btcsuite/btcd/btcutil/psbt"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
)
func craftForfeitTxs(
connectorTx *psbt.Packet,
vtxo domain.Vtxo,
vtxoForfeitTapLeaf *psbt.TaprootTapLeafScript,
vtxoScript, aspScript []byte,
minRelayFee uint64,
connectorAmount int64,
) (forfeitTxs []string, err error) {
connectors, prevouts := getConnectorInputs(connectorTx, connectorAmount)
for i, connectorInput := range connectors {
connectorPrevout := prevouts[i]
vtxoHash, err := chainhash.NewHashFromStr(vtxo.Txid)
if err != nil {
return nil, err
}
vtxoInput := &wire.OutPoint{
Hash: *vtxoHash,
Index: vtxo.VOut,
}
partialTx, err := psbt.New(
[]*wire.OutPoint{connectorInput, vtxoInput},
[]*wire.TxOut{{
Value: int64(vtxo.Amount) + int64(connectorAmount) - int64(minRelayFee),
PkScript: aspScript,
}},
2,
0,
[]uint32{wire.MaxTxInSequenceNum, wire.MaxTxInSequenceNum},
)
if err != nil {
return nil, err
}
updater, err := psbt.NewUpdater(partialTx)
if err != nil {
return nil, err
}
if err := updater.AddInWitnessUtxo(connectorPrevout, 0); err != nil {
return nil, err
}
if err := updater.AddInWitnessUtxo(&wire.TxOut{
Value: int64(vtxo.Amount),
PkScript: vtxoScript,
}, 1); err != nil {
return nil, err
}
if err := updater.AddInSighashType(txscript.SigHashDefault, 1); err != nil {
return nil, err
}
updater.Upsbt.Inputs[1].TaprootLeafScript = []*psbt.TaprootTapLeafScript{vtxoForfeitTapLeaf}
tx, err := partialTx.B64Encode()
if err != nil {
return nil, err
}
forfeitTxs = append(forfeitTxs, tx)
}
return forfeitTxs, nil
}

View File

@@ -5,6 +5,7 @@ import (
"github.com/ark-network/ark/server/internal/core/ports" "github.com/ark-network/ark/server/internal/core/ports"
"github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
"github.com/stretchr/testify/mock" "github.com/stretchr/testify/mock"
) )
@@ -140,6 +141,17 @@ func (m *mockedWallet) MinRelayFee(ctx context.Context, vbytes uint64) (uint64,
return res, args.Error(1) return res, args.Error(1)
} }
func (m *mockedWallet) MinRelayFeeRate(ctx context.Context) chainfee.SatPerKVByte {
args := m.Called(ctx)
var res chainfee.SatPerKVByte
if a := args.Get(0); a != nil {
res = a.(chainfee.SatPerKVByte)
}
return res
}
func (m *mockedWallet) GetDustAmount(ctx context.Context) (uint64, error) { func (m *mockedWallet) GetDustAmount(ctx context.Context) (uint64, error) {
args := m.Called(ctx) args := m.Called(ctx)

View File

@@ -9,13 +9,13 @@
{ {
"txid": "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6", "txid": "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6",
"vout": 0, "vout": 0,
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002", "descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"amount": 1100 "amount": 1100
} }
], ],
"receivers": [ "receivers": [
{ {
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002", "descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"amount": 1100 "amount": 1100
} }
] ]
@@ -32,17 +32,17 @@
{ {
"txid": "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6", "txid": "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6",
"vout": 0, "vout": 0,
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002", "descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"amount": 1100 "amount": 1100
} }
], ],
"receivers": [ "receivers": [
{ {
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002", "descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"amount": 600 "amount": 600
}, },
{ {
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002", "descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"amount": 500 "amount": 500
} }
] ]
@@ -59,17 +59,17 @@
{ {
"txid": "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6", "txid": "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6",
"vout": 0, "vout": 0,
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002", "descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"amount": 1100 "amount": 1100
} }
], ],
"receivers": [ "receivers": [
{ {
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002", "descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"amount": 600 "amount": 600
}, },
{ {
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002", "descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"amount": 500 "amount": 500
} }
] ]
@@ -80,17 +80,17 @@
{ {
"txid": "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6", "txid": "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6",
"vout": 0, "vout": 0,
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002", "descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"amount": 1100 "amount": 1100
} }
], ],
"receivers": [ "receivers": [
{ {
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002", "descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"amount": 600 "amount": 600
}, },
{ {
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002", "descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"amount": 500 "amount": 500
} }
] ]
@@ -101,17 +101,17 @@
{ {
"txid": "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6", "txid": "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6",
"vout": 0, "vout": 0,
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002", "descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"amount": 1100 "amount": 1100
} }
], ],
"receivers": [ "receivers": [
{ {
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002", "descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"amount": 600 "amount": 600
}, },
{ {
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002", "descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"amount": 500 "amount": 500
} }
] ]
@@ -128,53 +128,54 @@
{ {
"txid": "755c820771284d85ea4bbcc246565b4eddadc44237a7e57a0f9cb78a840d1d41", "txid": "755c820771284d85ea4bbcc246565b4eddadc44237a7e57a0f9cb78a840d1d41",
"vout": 0, "vout": 0,
"pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5", "descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"amount": 1000 "amount": 1000
}, },
{ {
"txid": "66a0df86fcdeb84b8877adfe0b2c556dba30305d72ddbd4c49355f6930355357", "txid": "66a0df86fcdeb84b8877adfe0b2c556dba30305d72ddbd4c49355f6930355357",
"vout": 0, "vout": 0,
"pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5", "descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"amount": 1000 "amount": 1000
}, },
{ {
"txid": "9913159bc7aa493ca53cbb9cbc88f97ba01137c814009dc7ef520c3fafc67909", "txid": "9913159bc7aa493ca53cbb9cbc88f97ba01137c814009dc7ef520c3fafc67909",
"vout": 1, "vout": 1,
"pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5", "descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"amount": 500 "amount": 500
}, },
{ {
"txid": "5e10e77a7cdedc153be5193a4b6055a7802706ded4f2a9efefe86ed2f9a6ae60", "txid": "5e10e77a7cdedc153be5193a4b6055a7802706ded4f2a9efefe86ed2f9a6ae60",
"vout": 0, "vout": 0,
"pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5", "descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"amount": 1000 "amount": 1000
}, },
{ {
"txid": "5e10e77a7cdedc153be5193a4b6055a7802706ded4f2a9efefe86ed2f9a6ae60", "txid": "5e10e77a7cdedc153be5193a4b6055a7802706ded4f2a9efefe86ed2f9a6ae60",
"vout": 1, "vout": 1,
"pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5", "descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"amount": 1000 "amount": 1000
} }
], ],
"receivers": [ "receivers": [
{ {
"pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5", "descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"amount": 1000 "amount": 1000
}, },
{ {
"pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5", "pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5",
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"amount": 1000 "amount": 1000
}, },
{ {
"pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5", "descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"amount": 1000 "amount": 1000
}, },
{ {
"pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5", "descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"amount": 1000 "amount": 1000
}, },
{ {
"pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5", "descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"amount": 500 "amount": 500
} }
] ]
@@ -196,53 +197,53 @@
{ {
"txid": "755c820771284d85ea4bbcc246565b4eddadc44237a7e57a0f9cb78a840d1d41", "txid": "755c820771284d85ea4bbcc246565b4eddadc44237a7e57a0f9cb78a840d1d41",
"vout": 0, "vout": 0,
"pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5", "descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"amount": 1000 "amount": 1000
}, },
{ {
"txid": "66a0df86fcdeb84b8877adfe0b2c556dba30305d72ddbd4c49355f6930355357", "txid": "66a0df86fcdeb84b8877adfe0b2c556dba30305d72ddbd4c49355f6930355357",
"vout": 0, "vout": 0,
"pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5", "descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"amount": 1000 "amount": 1000
}, },
{ {
"txid": "9913159bc7aa493ca53cbb9cbc88f97ba01137c814009dc7ef520c3fafc67909", "txid": "9913159bc7aa493ca53cbb9cbc88f97ba01137c814009dc7ef520c3fafc67909",
"vout": 1, "vout": 1,
"pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5", "descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"amount": 500 "amount": 500
}, },
{ {
"txid": "5e10e77a7cdedc153be5193a4b6055a7802706ded4f2a9efefe86ed2f9a6ae60", "txid": "5e10e77a7cdedc153be5193a4b6055a7802706ded4f2a9efefe86ed2f9a6ae60",
"vout": 0, "vout": 0,
"pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5", "descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"amount": 1000 "amount": 1000
}, },
{ {
"txid": "5e10e77a7cdedc153be5193a4b6055a7802706ded4f2a9efefe86ed2f9a6ae60", "txid": "5e10e77a7cdedc153be5193a4b6055a7802706ded4f2a9efefe86ed2f9a6ae60",
"vout": 1, "vout": 1,
"pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5", "descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"amount": 1000 "amount": 1000
} }
], ],
"receivers": [ "receivers": [
{ {
"pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5", "descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"amount": 1000 "amount": 1000
}, },
{ {
"pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5", "descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"amount": 1000 "amount": 1000
}, },
{ {
"pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5", "descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"amount": 1000 "amount": 1000
}, },
{ {
"pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5", "descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"amount": 1000 "amount": 1000
}, },
{ {
"pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5", "descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
"amount": 500 "amount": 500
} }
] ]

View File

@@ -8,10 +8,6 @@ import (
"github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/decred/dcrd/dcrec/secp256k1/v4"
) )
func p2trScript(taprootKey *secp256k1.PublicKey) ([]byte, error) {
return txscript.NewScriptBuilder().AddOp(txscript.OP_1).AddData(schnorr.SerializePubKey(taprootKey)).Script()
}
func getOnchainReceivers( func getOnchainReceivers(
payments []domain.Payment, payments []domain.Payment,
) []domain.Receiver { ) []domain.Receiver {
@@ -28,19 +24,24 @@ func getOnchainReceivers(
func getOffchainReceivers( func getOffchainReceivers(
payments []domain.Payment, payments []domain.Payment,
) []bitcointree.Receiver { ) ([]bitcointree.Receiver, error) {
receivers := make([]bitcointree.Receiver, 0) receivers := make([]bitcointree.Receiver, 0)
for _, payment := range payments { for _, payment := range payments {
for _, receiver := range payment.Receivers { for _, receiver := range payment.Receivers {
if !receiver.IsOnchain() { if !receiver.IsOnchain() {
vtxoScript, err := bitcointree.ParseVtxoScript(receiver.Descriptor)
if err != nil {
return nil, err
}
receivers = append(receivers, bitcointree.Receiver{ receivers = append(receivers, bitcointree.Receiver{
Pubkey: receiver.Pubkey, Script: vtxoScript,
Amount: receiver.Amount, Amount: receiver.Amount,
}) })
} }
} }
} }
return receivers return receivers, nil
} }
func countSpentVtxos(payments []domain.Payment) uint64 { func countSpentVtxos(payments []domain.Payment) uint64 {

View File

@@ -727,6 +727,10 @@ func (s *service) WaitForSync(ctx context.Context, txid string) error {
} }
} }
func (s *service) MinRelayFeeRate(ctx context.Context) chainfee.SatPerKVByte {
return s.feeEstimator.RelayFeePerKW().FeePerKVByte()
}
func (s *service) MinRelayFee(ctx context.Context, vbytes uint64) (uint64, error) { func (s *service) MinRelayFee(ctx context.Context, vbytes uint64) (uint64, error) {
fee := s.feeEstimator.RelayFeePerKW().FeeForVByte(lntypes.VByte(vbytes)) fee := s.feeEstimator.RelayFeePerKW().FeeForVByte(lntypes.VByte(vbytes))
return uint64(fee.ToUnit(btcutil.AmountSatoshi)), nil return uint64(fee.ToUnit(btcutil.AmountSatoshi)), nil

View File

@@ -16,6 +16,7 @@ import (
"github.com/btcsuite/btcd/btcutil/psbt" "github.com/btcsuite/btcd/btcutil/psbt"
"github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/txscript"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
"github.com/vulpemventures/go-elements/elementsutil" "github.com/vulpemventures/go-elements/elementsutil"
"github.com/vulpemventures/go-elements/psetv2" "github.com/vulpemventures/go-elements/psetv2"
) )
@@ -58,7 +59,7 @@ func (s *service) SignTransaction(
} }
switch c := closure.(type) { switch c := closure.(type) {
case *tree.ForfeitClosure: case *tree.MultisigClosure:
asp := schnorr.SerializePubKey(c.AspPubkey) asp := schnorr.SerializePubKey(c.AspPubkey)
owner := schnorr.SerializePubKey(c.Pubkey) owner := schnorr.SerializePubKey(c.Pubkey)
@@ -274,6 +275,12 @@ func (s *service) LockConnectorUtxos(ctx context.Context, utxos []ports.TxOutpoi
return err return err
} }
var minRate = chainfee.SatPerKVByte(0.2 * 1000)
func (s *service) MinRelayFeeRate(ctx context.Context) chainfee.SatPerKVByte {
return minRate
}
func (s *service) MinRelayFee(ctx context.Context, vbytes uint64) (uint64, error) { func (s *service) MinRelayFee(ctx context.Context, vbytes uint64) (uint64, error) {
feeRate := 0.2 feeRate := 0.2
fee := uint64(float64(vbytes) * feeRate) fee := uint64(float64(vbytes) * feeRate)

View File

@@ -6,7 +6,6 @@ import (
"sync" "sync"
arkv1 "github.com/ark-network/ark/api-spec/protobuf/gen/ark/v1" arkv1 "github.com/ark-network/ark/api-spec/protobuf/gen/ark/v1"
"github.com/ark-network/ark/common"
"github.com/ark-network/ark/common/tree" "github.com/ark-network/ark/common/tree"
"github.com/ark-network/ark/server/internal/core/application" "github.com/ark-network/ark/server/internal/core/application"
"github.com/ark-network/ark/server/internal/core/domain" "github.com/ark-network/ark/server/internal/core/domain"
@@ -65,22 +64,27 @@ func (h *handler) CreatePayment(ctx context.Context, req *arkv1.CreatePaymentReq
return nil, status.Error(codes.InvalidArgument, err.Error()) return nil, status.Error(codes.InvalidArgument, err.Error())
} }
vtxosKeys := make([]domain.VtxoKey, 0, len(inputs))
for _, input := range inputs {
if !input.IsVtxo() {
return nil, status.Error(codes.InvalidArgument, "only vtxos input allowed")
}
vtxosKeys = append(vtxosKeys, input.VtxoKey())
}
receivers, err := parseReceivers(req.GetOutputs()) receivers, err := parseReceivers(req.GetOutputs())
if err != nil { if err != nil {
return nil, status.Error(codes.InvalidArgument, err.Error()) return nil, status.Error(codes.InvalidArgument, err.Error())
} }
for _, receiver := range receivers {
if receiver.Amount <= 0 {
return nil, status.Error(codes.InvalidArgument, "output amount must be greater than 0")
}
if len(receiver.OnchainAddress) > 0 {
return nil, status.Error(codes.InvalidArgument, "onchain address is not supported as async payment destination")
}
if len(receiver.Descriptor) <= 0 {
return nil, status.Error(codes.InvalidArgument, "missing output descriptor")
}
}
redeemTx, unconditionalForfeitTxs, err := h.svc.CreateAsyncPayment( redeemTx, unconditionalForfeitTxs, err := h.svc.CreateAsyncPayment(
ctx, vtxosKeys, receivers, ctx, inputs, receivers,
) )
if err != nil { if err != nil {
return nil, err return nil, err
@@ -112,8 +116,8 @@ func (h *handler) Ping(ctx context.Context, req *arkv1.PingRequest) (*arkv1.Ping
Id: e.Id, Id: e.Id,
PoolTx: e.PoolTx, PoolTx: e.PoolTx,
CongestionTree: castCongestionTree(e.CongestionTree), CongestionTree: castCongestionTree(e.CongestionTree),
ForfeitTxs: e.UnsignedForfeitTxs,
Connectors: e.Connectors, Connectors: e.Connectors,
MinRelayFeeRate: e.MinRelayFeeRate,
}, },
}, },
} }
@@ -321,7 +325,7 @@ func (h *handler) GetEventStream(_ *arkv1.GetEventStreamRequest, stream arkv1.Ar
} }
func (h *handler) ListVtxos(ctx context.Context, req *arkv1.ListVtxosRequest) (*arkv1.ListVtxosResponse, error) { func (h *handler) ListVtxos(ctx context.Context, req *arkv1.ListVtxosRequest) (*arkv1.ListVtxosResponse, error) {
hrp, userPubkey, aspPubkey, err := parseAddress(req.GetAddress()) _, userPubkey, _, err := parseAddress(req.GetAddress())
if err != nil { if err != nil {
return nil, status.Error(codes.InvalidArgument, err.Error()) return nil, status.Error(codes.InvalidArgument, err.Error())
} }
@@ -332,8 +336,8 @@ func (h *handler) ListVtxos(ctx context.Context, req *arkv1.ListVtxosRequest) (*
} }
return &arkv1.ListVtxosResponse{ return &arkv1.ListVtxosResponse{
SpendableVtxos: vtxoList(spendableVtxos).toProto(hrp, aspPubkey), SpendableVtxos: vtxoList(spendableVtxos).toProto(),
SpentVtxos: vtxoList(spentVtxos).toProto(hrp, aspPubkey), SpentVtxos: vtxoList(spentVtxos).toProto(),
}, nil }, nil
} }
@@ -370,13 +374,14 @@ func (h *handler) GetBoardingAddress(ctx context.Context, req *arkv1.GetBoarding
return nil, status.Error(codes.InvalidArgument, "invalid pubkey (parse error)") return nil, status.Error(codes.InvalidArgument, "invalid pubkey (parse error)")
} }
addr, err := h.svc.GetBoardingAddress(ctx, userPubkey) addr, descriptor, err := h.svc.GetBoardingAddress(ctx, userPubkey)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &arkv1.GetBoardingAddressResponse{ return &arkv1.GetBoardingAddressResponse{
Address: addr, Address: addr,
Descriptor_: descriptor,
}, nil }, nil
} }
@@ -481,8 +486,8 @@ func (h *handler) listenToEvents() {
Id: e.Id, Id: e.Id,
PoolTx: e.PoolTx, PoolTx: e.PoolTx,
CongestionTree: castCongestionTree(e.CongestionTree), CongestionTree: castCongestionTree(e.CongestionTree),
ForfeitTxs: e.UnsignedForfeitTxs,
Connectors: e.Connectors, Connectors: e.Connectors,
MinRelayFeeRate: e.MinRelayFeeRate,
}, },
}, },
} }
@@ -547,15 +552,9 @@ func (h *handler) listenToEvents() {
type vtxoList []domain.Vtxo type vtxoList []domain.Vtxo
func (v vtxoList) toProto(hrp string, aspKey *secp256k1.PublicKey) []*arkv1.Vtxo { func (v vtxoList) toProto() []*arkv1.Vtxo {
list := make([]*arkv1.Vtxo, 0, len(v)) list := make([]*arkv1.Vtxo, 0, len(v))
for _, vv := range v { for _, vv := range v {
addr := vv.OnchainAddress
if vv.Pubkey != "" {
buf, _ := hex.DecodeString(vv.Pubkey)
key, _ := secp256k1.ParsePubKey(buf)
addr, _ = common.EncodeAddress(hrp, key, aspKey)
}
var pendingData *arkv1.PendingPayment var pendingData *arkv1.PendingPayment
if vv.AsyncPayment != nil { if vv.AsyncPayment != nil {
pendingData = &arkv1.PendingPayment{ pendingData = &arkv1.PendingPayment{
@@ -564,18 +563,12 @@ func (v vtxoList) toProto(hrp string, aspKey *secp256k1.PublicKey) []*arkv1.Vtxo
} }
} }
list = append(list, &arkv1.Vtxo{ list = append(list, &arkv1.Vtxo{
Outpoint: &arkv1.Input{ Outpoint: &arkv1.Outpoint{
Input: &arkv1.Input_VtxoInput{
VtxoInput: &arkv1.VtxoInput{
Txid: vv.Txid, Txid: vv.Txid,
Vout: vv.VOut, Vout: vv.VOut,
}, },
}, Descriptor_: vv.Descriptor,
},
Receiver: &arkv1.Output{
Address: addr,
Amount: vv.Amount, Amount: vv.Amount,
},
PoolTxid: vv.PoolTx, PoolTxid: vv.PoolTx,
Spent: vv.Spent, Spent: vv.Spent,
ExpireAt: vv.ExpireAt, ExpireAt: vv.ExpireAt,

View File

@@ -1,13 +1,12 @@
package handlers package handlers
import ( import (
"encoding/hex"
"fmt" "fmt"
arkv1 "github.com/ark-network/ark/api-spec/protobuf/gen/ark/v1" arkv1 "github.com/ark-network/ark/api-spec/protobuf/gen/ark/v1"
"github.com/ark-network/ark/common" "github.com/ark-network/ark/common"
"github.com/ark-network/ark/server/internal/core/application"
"github.com/ark-network/ark/server/internal/core/domain" "github.com/ark-network/ark/server/internal/core/domain"
"github.com/ark-network/ark/server/internal/core/ports"
"github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/decred/dcrd/dcrec/secp256k1/v4"
) )
@@ -18,27 +17,19 @@ func parseAddress(addr string) (string, *secp256k1.PublicKey, *secp256k1.PublicK
return common.DecodeAddress(addr) return common.DecodeAddress(addr)
} }
func parseInputs(ins []*arkv1.Input) ([]application.Input, error) { func parseInputs(ins []*arkv1.Input) ([]ports.Input, error) {
if len(ins) <= 0 { if len(ins) <= 0 {
return nil, fmt.Errorf("missing inputs") return nil, fmt.Errorf("missing inputs")
} }
inputs := make([]application.Input, 0, len(ins)) inputs := make([]ports.Input, 0, len(ins))
for _, input := range ins { for _, input := range ins {
if input.GetBoardingInput() != nil { inputs = append(inputs, ports.Input{
desc := input.GetBoardingInput().GetDescriptor_() VtxoKey: domain.VtxoKey{
inputs = append(inputs, application.Input{ Txid: input.GetOutpoint().GetTxid(),
Txid: input.GetBoardingInput().GetTxid(), VOut: input.GetOutpoint().GetVout(),
Index: input.GetBoardingInput().GetVout(), },
Descriptor: desc, Descriptor: input.GetDescriptor_(),
})
continue
}
inputs = append(inputs, application.Input{
Txid: input.GetVtxoInput().GetTxid(),
Index: input.GetVtxoInput().GetVout(),
}) })
} }
@@ -51,21 +42,14 @@ func parseReceivers(outs []*arkv1.Output) ([]domain.Receiver, error) {
if out.GetAmount() == 0 { if out.GetAmount() == 0 {
return nil, fmt.Errorf("missing output amount") return nil, fmt.Errorf("missing output amount")
} }
if len(out.GetAddress()) <= 0 { if len(out.GetAddress()) <= 0 && len(out.GetDescriptor_()) <= 0 {
return nil, fmt.Errorf("missing output address") return nil, fmt.Errorf("missing output destination")
}
var pubkey, addr string
_, pk, _, err := common.DecodeAddress(out.GetAddress())
if err != nil {
addr = out.GetAddress()
}
if pk != nil {
pubkey = hex.EncodeToString(pk.SerializeCompressed())
} }
receivers = append(receivers, domain.Receiver{ receivers = append(receivers, domain.Receiver{
Pubkey: pubkey, Descriptor: out.GetDescriptor_(),
Amount: out.GetAmount(), Amount: out.GetAmount(),
OnchainAddress: addr, OnchainAddress: out.GetAddress(),
}) })
} }
return receivers, nil return receivers, nil