mirror of
https://github.com/aljazceru/ark.git
synced 2025-12-17 04:04:21 +01:00
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:
@@ -475,21 +475,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1BoardingInput": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"txid": {
|
||||
"type": "string"
|
||||
},
|
||||
"vout": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"descriptor": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1ClaimPaymentRequest": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -668,11 +653,11 @@
|
||||
"v1Input": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"vtxoInput": {
|
||||
"$ref": "#/definitions/v1VtxoInput"
|
||||
"outpoint": {
|
||||
"$ref": "#/definitions/v1Outpoint"
|
||||
},
|
||||
"boardingInput": {
|
||||
"$ref": "#/definitions/v1BoardingInput"
|
||||
"descriptor": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -709,12 +694,28 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1Outpoint": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"txid": {
|
||||
"type": "string"
|
||||
},
|
||||
"vout": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1Output": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"address": {
|
||||
"type": "string",
|
||||
"description": "Either the offchain or onchain address."
|
||||
"title": "onchain"
|
||||
},
|
||||
"descriptor": {
|
||||
"type": "string",
|
||||
"title": "offchain"
|
||||
},
|
||||
"amount": {
|
||||
"type": "string",
|
||||
@@ -838,12 +839,6 @@
|
||||
"poolTx": {
|
||||
"type": "string"
|
||||
},
|
||||
"forfeitTxs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"congestionTree": {
|
||||
"$ref": "#/definitions/v1Tree"
|
||||
},
|
||||
@@ -852,6 +847,10 @@
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"minRelayFeeRate": {
|
||||
"type": "string",
|
||||
"format": "int64"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -970,10 +969,10 @@
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"outpoint": {
|
||||
"$ref": "#/definitions/v1Input"
|
||||
"$ref": "#/definitions/v1Outpoint"
|
||||
},
|
||||
"receiver": {
|
||||
"$ref": "#/definitions/v1Output"
|
||||
"descriptor": {
|
||||
"type": "string"
|
||||
},
|
||||
"spent": {
|
||||
"type": "boolean"
|
||||
@@ -996,18 +995,10 @@
|
||||
},
|
||||
"pendingData": {
|
||||
"$ref": "#/definitions/v1PendingPayment"
|
||||
}
|
||||
}
|
||||
},
|
||||
"v1VtxoInput": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"txid": {
|
||||
"type": "string"
|
||||
},
|
||||
"vout": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
"amount": {
|
||||
"type": "string",
|
||||
"format": "uint64"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,9 +196,9 @@ message GetInfoResponse {
|
||||
message RoundFinalizationEvent {
|
||||
string id = 1;
|
||||
string pool_tx = 2;
|
||||
repeated string forfeit_txs = 3;
|
||||
Tree congestion_tree = 4;
|
||||
repeated string connectors = 5;
|
||||
Tree congestion_tree = 3;
|
||||
repeated string connectors = 4;
|
||||
int64 min_relay_fee_rate = 5;
|
||||
}
|
||||
|
||||
message RoundFinalizedEvent {
|
||||
@@ -244,29 +244,23 @@ message Round {
|
||||
RoundStage stage = 8;
|
||||
}
|
||||
|
||||
message VtxoInput {
|
||||
message Outpoint {
|
||||
string txid = 1;
|
||||
uint32 vout = 2;
|
||||
}
|
||||
|
||||
message BoardingInput {
|
||||
string txid = 1;
|
||||
uint32 vout = 2;
|
||||
string descriptor = 3;
|
||||
}
|
||||
|
||||
message Input {
|
||||
oneof input {
|
||||
VtxoInput vtxo_input = 1;
|
||||
BoardingInput boarding_input = 2;
|
||||
}
|
||||
Outpoint outpoint = 1;
|
||||
string descriptor = 2;
|
||||
}
|
||||
|
||||
message Output {
|
||||
// Either the offchain or onchain address.
|
||||
string address = 1;
|
||||
oneof destination {
|
||||
string address = 1; // onchain
|
||||
string descriptor = 2; // offchain
|
||||
}
|
||||
// Amount to send in satoshis.
|
||||
uint64 amount = 2;
|
||||
uint64 amount = 3;
|
||||
}
|
||||
|
||||
message Tree {
|
||||
@@ -284,8 +278,8 @@ message Node {
|
||||
}
|
||||
|
||||
message Vtxo {
|
||||
Input outpoint = 1;
|
||||
Output receiver = 2;
|
||||
Outpoint outpoint = 1;
|
||||
string descriptor = 2;
|
||||
bool spent = 3;
|
||||
string pool_txid = 4;
|
||||
string spent_by = 5;
|
||||
@@ -293,6 +287,7 @@ message Vtxo {
|
||||
bool swept = 7;
|
||||
bool pending = 8;
|
||||
PendingPayment pending_data = 9;
|
||||
uint64 amount = 10;
|
||||
}
|
||||
|
||||
message PendingPayment {
|
||||
|
||||
@@ -131,6 +131,7 @@ func local_request_AdminService_GetRounds_0(ctx context.Context, marshaler runti
|
||||
// UnaryRPC :call AdminServiceServer directly.
|
||||
// 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.
|
||||
// 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 {
|
||||
|
||||
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".
|
||||
// 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
|
||||
// "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 {
|
||||
|
||||
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
@@ -486,6 +486,7 @@ func local_request_ArkService_CompletePayment_0(ctx context.Context, marshaler r
|
||||
// UnaryRPC :call ArkServiceServer directly.
|
||||
// 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.
|
||||
// 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 {
|
||||
|
||||
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".
|
||||
// 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
|
||||
// "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 {
|
||||
|
||||
mux.Handle("POST", pattern_ArkService_RegisterPayment_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
|
||||
@@ -211,6 +211,7 @@ func local_request_WalletService_GetBalance_0(ctx context.Context, marshaler run
|
||||
// UnaryRPC :call WalletInitializerServiceServer directly.
|
||||
// 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.
|
||||
// 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 {
|
||||
|
||||
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.
|
||||
// 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.
|
||||
// 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 {
|
||||
|
||||
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".
|
||||
// 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
|
||||
// "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 {
|
||||
|
||||
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".
|
||||
// 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
|
||||
// "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 {
|
||||
|
||||
mux.Handle("POST", pattern_WalletService_Lock_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package bitcointree
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
|
||||
"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
|
||||
func CraftSharedOutput(
|
||||
cosigners []*secp256k1.PublicKey, aspPubkey *secp256k1.PublicKey, receivers []Receiver,
|
||||
feeSatsPerNode uint64, roundLifetime, unilateralExitDelay int64,
|
||||
feeSatsPerNode uint64, roundLifetime int64,
|
||||
) ([]byte, int64, error) {
|
||||
aggregatedKey, _, err := createAggregatedKeyWithSweep(
|
||||
cosigners, aspPubkey, roundLifetime,
|
||||
@@ -26,7 +25,7 @@ func CraftSharedOutput(
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
root, err := createRootNode(aggregatedKey, cosigners, aspPubkey, receivers, feeSatsPerNode, unilateralExitDelay)
|
||||
root, err := createRootNode(aggregatedKey, cosigners, receivers, feeSatsPerNode)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
@@ -44,7 +43,7 @@ func CraftSharedOutput(
|
||||
// CraftCongestionTree creates all the tree's transactions
|
||||
func CraftCongestionTree(
|
||||
initialInput *wire.OutPoint, cosigners []*secp256k1.PublicKey, aspPubkey *secp256k1.PublicKey, receivers []Receiver,
|
||||
feeSatsPerNode uint64, roundLifetime, unilateralExitDelay int64,
|
||||
feeSatsPerNode uint64, roundLifetime int64,
|
||||
) (tree.CongestionTree, error) {
|
||||
aggregatedKey, sweepTapLeaf, err := createAggregatedKeyWithSweep(
|
||||
cosigners, aspPubkey, roundLifetime,
|
||||
@@ -53,7 +52,7 @@ func CraftCongestionTree(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
root, err := createRootNode(aggregatedKey, cosigners, aspPubkey, receivers, feeSatsPerNode, unilateralExitDelay)
|
||||
root, err := createRootNode(aggregatedKey, cosigners, receivers, feeSatsPerNode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -109,10 +108,8 @@ type node interface {
|
||||
}
|
||||
|
||||
type leaf struct {
|
||||
aspKey *secp256k1.PublicKey
|
||||
vtxoKey *secp256k1.PublicKey
|
||||
exitDelay int64
|
||||
amount int64
|
||||
vtxoScript VtxoScript
|
||||
amount int64
|
||||
}
|
||||
|
||||
type branch struct {
|
||||
@@ -145,36 +142,11 @@ func (l *leaf) getAmount() int64 {
|
||||
}
|
||||
|
||||
func (l *leaf) getOutputs() ([]*wire.TxOut, error) {
|
||||
redeemClosure := &CSVSigClosure{
|
||||
Pubkey: l.vtxoKey,
|
||||
Seconds: uint(l.exitDelay),
|
||||
}
|
||||
|
||||
redeemLeaf, err := redeemClosure.Leaf()
|
||||
taprootKey, _, err := l.vtxoScript.TapTree()
|
||||
if err != nil {
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -272,9 +244,10 @@ func getTx(
|
||||
}
|
||||
|
||||
func createRootNode(
|
||||
aggregatedKey *musig2.AggregateKey, cosigners []*secp256k1.PublicKey,
|
||||
aspPubkey *secp256k1.PublicKey, receivers []Receiver,
|
||||
feeSatsPerNode uint64, unilateralExitDelay int64,
|
||||
aggregatedKey *musig2.AggregateKey,
|
||||
cosigners []*secp256k1.PublicKey,
|
||||
receivers []Receiver,
|
||||
feeSatsPerNode uint64,
|
||||
) (root node, err error) {
|
||||
if len(receivers) == 0 {
|
||||
return nil, fmt.Errorf("no receivers provided")
|
||||
@@ -282,21 +255,9 @@ func createRootNode(
|
||||
|
||||
nodes := make([]node, 0, len(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{
|
||||
aspKey: aspPubkey,
|
||||
vtxoKey: receiverKey,
|
||||
exitDelay: unilateralExitDelay,
|
||||
amount: int64(r.Amount),
|
||||
vtxoScript: r.Script,
|
||||
amount: int64(r.Amount),
|
||||
}
|
||||
nodes = append(nodes, leafNode)
|
||||
}
|
||||
|
||||
85
common/bitcointree/forfeit.go
Normal file
85
common/bitcointree/forfeit.go
Normal 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
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package bitcointree_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
@@ -43,10 +44,9 @@ func TestRoundTripSignTree(t *testing.T) {
|
||||
_, sharedOutputAmount, err := bitcointree.CraftSharedOutput(
|
||||
cosigners,
|
||||
asp.PubKey(),
|
||||
f.Receivers,
|
||||
castReceivers(f.Receivers, asp.PubKey()),
|
||||
minRelayFee,
|
||||
lifetime,
|
||||
exitDelay,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -58,10 +58,9 @@ func TestRoundTripSignTree(t *testing.T) {
|
||||
},
|
||||
cosigners,
|
||||
asp.PubKey(),
|
||||
f.Receivers,
|
||||
castReceivers(f.Receivers, asp.PubKey()),
|
||||
minRelayFee,
|
||||
lifetime,
|
||||
exitDelay,
|
||||
)
|
||||
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 {
|
||||
Valid []struct {
|
||||
Receivers []bitcointree.Receiver `json:"receivers"`
|
||||
Receivers []receiverFixture `json:"receivers"`
|
||||
} `json:"valid"`
|
||||
}
|
||||
|
||||
|
||||
@@ -141,44 +141,6 @@ func (d *CSVSigClosure) Decode(script []byte) (bool, error) {
|
||||
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) {
|
||||
data32Index := bytes.Index(script, []byte{txscript.OP_DATA_32})
|
||||
if data32Index == -1 {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package bitcointree
|
||||
|
||||
type Receiver struct {
|
||||
Pubkey string
|
||||
Script VtxoScript
|
||||
Amount uint64
|
||||
}
|
||||
|
||||
236
common/bitcointree/vtxo.go
Normal file
236
common/bitcointree/vtxo.go
Normal 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
|
||||
}
|
||||
67
common/bitcointree/vtxo_test.go
Normal file
67
common/bitcointree/vtxo_test.go
Normal 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)))
|
||||
}
|
||||
@@ -8,37 +8,149 @@ import (
|
||||
"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,
|
||||
) (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 {
|
||||
if andLeaf, ok := leaf.(*And); ok {
|
||||
if first, ok := andLeaf.First.(*Older); ok {
|
||||
timeout = first.Timeout
|
||||
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, 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)
|
||||
if err != nil {
|
||||
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 second, ok := andLeaf.Second.(*PK); ok {
|
||||
keyBytes, err := hex.DecodeString(second.Key.Hex)
|
||||
if err != nil {
|
||||
return 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
|
||||
}
|
||||
|
||||
user, err = schnorr.ParsePubKey(keyBytes)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
sender, err = schnorr.ParsePubKey(keyBytes)
|
||||
if err != nil {
|
||||
return nil, nil, nil, 0, err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
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
|
||||
|
||||
@@ -204,6 +204,9 @@ func (e *And) Script(verify bool) (string, error) {
|
||||
|
||||
func parseExpression(policy string) (Expression, error) {
|
||||
policy = strings.TrimSpace(policy)
|
||||
if policy[0] == '{' {
|
||||
policy = policy[1:]
|
||||
}
|
||||
expressions := make([]Expression, 0)
|
||||
expressions = append(expressions, &PK{})
|
||||
expressions = append(expressions, &Older{})
|
||||
|
||||
@@ -41,6 +41,10 @@ func ParseTaprootDescriptor(desc string) (*TaprootDescriptor, error) {
|
||||
return nil, err
|
||||
}
|
||||
for _, scriptStr := range scriptParts {
|
||||
if scriptStr == "}" {
|
||||
continue
|
||||
}
|
||||
|
||||
leaf, err := parseExpression(scriptStr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -62,7 +62,7 @@ func TestParseTaprootDescriptor(t *testing.T) {
|
||||
},
|
||||
{
|
||||
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{
|
||||
InternalKey: descriptor.Key{Hex: "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0"},
|
||||
ScriptTree: []descriptor.Expression{
|
||||
@@ -70,7 +70,7 @@ func TestParseTaprootDescriptor(t *testing.T) {
|
||||
First: &descriptor.PK{
|
||||
Key: descriptor.XOnlyKey{
|
||||
descriptor.Key{
|
||||
Hex: "873079a0091c9b16abd1f8c508320b07f0d50144d09ccd792ce9c915dac60465",
|
||||
Hex: "973079a0091c9b16abd1f8c508320b07f0d50144d09ccd792ce9c915dac60465",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -125,16 +125,72 @@ func TestParseTaprootDescriptor(t *testing.T) {
|
||||
},
|
||||
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 {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := descriptor.ParseTaprootDescriptor(tt.desc)
|
||||
if (err != nil) != tt.wantErr {
|
||||
require.Equal(t, tt.wantErr, err != nil, err)
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.Equal(t, tt.expected, got)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, got)
|
||||
require.Equal(t, tt.expected, *got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/btcsuite/btcwallet/waddrmgr"
|
||||
"github.com/lightningnetwork/lnd/input"
|
||||
"github.com/lightningnetwork/lnd/lntypes"
|
||||
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||
)
|
||||
|
||||
var TreeTxSize = (&input.TxWeightEstimator{}).
|
||||
@@ -19,3 +23,29 @@ var ConnectorTxSize = (&input.TxWeightEstimator{}).
|
||||
AddP2WKHOutput().
|
||||
AddP2WKHOutput().
|
||||
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
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ require (
|
||||
github.com/btcsuite/btcd/btcutil v1.1.5
|
||||
github.com/btcsuite/btcd/btcutil/psbt v1.1.9
|
||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0
|
||||
github.com/btcsuite/btcwallet v0.16.10-0.20240718224643-db3a4a2543bd
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0
|
||||
github.com/lightningnetwork/lnd v0.18.2-beta
|
||||
github.com/stretchr/testify v1.9.0
|
||||
@@ -17,7 +18,6 @@ require (
|
||||
dario.cat/mergo 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/btcwallet v0.16.10-0.20240718224643-db3a4a2543bd // 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/txsizes v1.2.4 // indirect
|
||||
@@ -25,16 +25,19 @@ require (
|
||||
github.com/btcsuite/btcwallet/wtxmgr v1.5.3 // indirect
|
||||
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd // indirect
|
||||
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 // indirect
|
||||
github.com/btcsuite/winsvc v1.0.0 // indirect
|
||||
github.com/coreos/go-semver v0.3.1 // 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/lru v1.1.3 // indirect
|
||||
github.com/docker/cli v27.1.1+incompatible // 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-viper/mapstructure/v2 v2.1.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/snappy v0.0.4 // indirect
|
||||
github.com/google/btree v1.1.2 // indirect
|
||||
github.com/gorilla/websocket v1.5.3 // 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/jessevdk/go-flags v1.6.1 // 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/klauspost/cpuid/v2 v2.2.8 // 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/ltcsuite/ltcd v0.23.5 // 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/prometheus/client_golang v1.19.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/rogpeppe/go-internal v1.12.0 // 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/vulpemventures/fastsha256 v0.0.0-20160815193821-637e65642941 // indirect
|
||||
github.com/xiang90/probing v0.0.0-20221125231312-a49e3df8f510 // indirect
|
||||
|
||||
@@ -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/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
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-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
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/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
||||
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/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
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/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
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/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
|
||||
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=
|
||||
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/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
lukechampine.com/blake3 v1.3.0 h1:sJ3XhFINmHSrYCgl958hscfIa3bw8x4DqMP3u1YvoYE=
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package tree
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
|
||||
"github.com/ark-network/ark/common"
|
||||
"github.com/btcsuite/btcd/btcec/v2/schnorr"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
@@ -14,13 +14,13 @@ import (
|
||||
|
||||
func CraftCongestionTree(
|
||||
asset string, aspPubkey *secp256k1.PublicKey, receivers []Receiver,
|
||||
feeSatsPerNode uint64, roundLifetime, unilateralExitDelay int64,
|
||||
feeSatsPerNode uint64, roundLifetime int64,
|
||||
) (
|
||||
buildCongestionTree TreeFactory,
|
||||
sharedOutputScript []byte, sharedOutputAmount uint64, err error,
|
||||
) {
|
||||
root, err := createPartialCongestionTree(
|
||||
asset, aspPubkey, receivers, feeSatsPerNode, roundLifetime, unilateralExitDelay,
|
||||
asset, aspPubkey, receivers, feeSatsPerNode, roundLifetime,
|
||||
)
|
||||
if err != nil {
|
||||
return
|
||||
@@ -42,14 +42,13 @@ func CraftCongestionTree(
|
||||
}
|
||||
|
||||
type node struct {
|
||||
sweepKey *secp256k1.PublicKey
|
||||
receivers []Receiver
|
||||
left *node
|
||||
right *node
|
||||
asset string
|
||||
feeSats uint64
|
||||
roundLifetime int64
|
||||
unilateralExitDelay int64
|
||||
sweepKey *secp256k1.PublicKey
|
||||
receivers []Receiver
|
||||
left *node
|
||||
right *node
|
||||
asset string
|
||||
feeSats uint64
|
||||
roundLifetime int64
|
||||
|
||||
_inputTaprootKey *secp256k1.PublicKey
|
||||
_inputTaprootTree *taproot.IndexedElementsTapScriptTree
|
||||
@@ -242,53 +241,15 @@ func (n *node) getWitnessData() (
|
||||
}
|
||||
|
||||
func (n *node) getVtxoWitnessData() (
|
||||
*secp256k1.PublicKey, *taproot.IndexedElementsTapScriptTree, error,
|
||||
*secp256k1.PublicKey, common.TaprootTree, error,
|
||||
) {
|
||||
if !n.isLeaf() {
|
||||
return nil, nil, fmt.Errorf("cannot call vtxoWitness on a non-leaf node")
|
||||
}
|
||||
|
||||
key, err := hex.DecodeString(n.receivers[0].Pubkey)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
receiver := n.receivers[0]
|
||||
|
||||
pubkey, err := secp256k1.ParsePubKey(key)
|
||||
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
|
||||
return receiver.Script.TapTree()
|
||||
}
|
||||
|
||||
func (n *node) getTreeNode(
|
||||
@@ -413,7 +374,7 @@ func (n *node) createFinalCongestionTree() TreeFactory {
|
||||
|
||||
func createPartialCongestionTree(
|
||||
asset string, aspPubkey *secp256k1.PublicKey, receivers []Receiver,
|
||||
feeSatsPerNode uint64, roundLifetime, unilateralExitDelay int64,
|
||||
feeSatsPerNode uint64, roundLifetime int64,
|
||||
) (root *node, err error) {
|
||||
if len(receivers) == 0 {
|
||||
return nil, fmt.Errorf("no receivers provided")
|
||||
@@ -422,12 +383,11 @@ func createPartialCongestionTree(
|
||||
nodes := make([]*node, 0, len(receivers))
|
||||
for _, r := range receivers {
|
||||
leafNode := &node{
|
||||
sweepKey: aspPubkey,
|
||||
receivers: []Receiver{r},
|
||||
asset: asset,
|
||||
feeSats: feeSatsPerNode,
|
||||
roundLifetime: roundLifetime,
|
||||
unilateralExitDelay: unilateralExitDelay,
|
||||
sweepKey: aspPubkey,
|
||||
receivers: []Receiver{r},
|
||||
asset: asset,
|
||||
feeSats: feeSatsPerNode,
|
||||
roundLifetime: roundLifetime,
|
||||
}
|
||||
nodes = append(nodes, leafNode)
|
||||
}
|
||||
|
||||
@@ -1,31 +1,26 @@
|
||||
package txbuilder
|
||||
package tree
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/ark-network/ark/common/tree"
|
||||
"github.com/ark-network/ark/server/internal/core/domain"
|
||||
"github.com/ark-network/ark/common"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/btcsuite/btcwallet/waddrmgr"
|
||||
"github.com/lightningnetwork/lnd/input"
|
||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||
"github.com/vulpemventures/go-elements/elementsutil"
|
||||
"github.com/vulpemventures/go-elements/psetv2"
|
||||
"github.com/vulpemventures/go-elements/taproot"
|
||||
"github.com/vulpemventures/go-elements/transaction"
|
||||
)
|
||||
|
||||
func (b *txBuilder) craftForfeitTxs(
|
||||
func BuildForfeitTxs(
|
||||
connectorTx *psetv2.Pset,
|
||||
connectorAmount uint64,
|
||||
vtxo domain.Vtxo,
|
||||
vtxoForfeitTapleaf taproot.TapscriptElementsProof,
|
||||
vtxoScript, aspScript []byte,
|
||||
) (forfeitTxs []string, err error) {
|
||||
vtxoInput psetv2.InputArgs,
|
||||
vtxoAmount,
|
||||
connectorAmount,
|
||||
feeAmount uint64,
|
||||
vtxoScript []byte,
|
||||
aspPubKey *secp256k1.PublicKey,
|
||||
) (forfeitTxs []*psetv2.Pset, err error) {
|
||||
connectors, prevouts := getConnectorInputs(connectorTx, connectorAmount)
|
||||
|
||||
for i, connectorInput := range connectors {
|
||||
weightEstimator := &input.TxWeightEstimator{}
|
||||
|
||||
connectorPrevout := prevouts[i]
|
||||
asset := elementsutil.AssetHashFromBytes(connectorPrevout.Asset)
|
||||
|
||||
@@ -39,13 +34,6 @@ func (b *txBuilder) craftForfeitTxs(
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
@@ -58,9 +46,12 @@ func (b *txBuilder) craftForfeitTxs(
|
||||
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 {
|
||||
return nil, err
|
||||
@@ -70,27 +61,7 @@ func (b *txBuilder) craftForfeitTxs(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
unspendableKey := tree.UnspendableKey()
|
||||
|
||||
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()))
|
||||
aspScript, err := common.P2TRScript(aspPubKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -98,7 +69,7 @@ func (b *txBuilder) craftForfeitTxs(
|
||||
err = updater.AddOutputs([]psetv2.OutputArgs{
|
||||
{
|
||||
Asset: asset,
|
||||
Amount: vtxo.Amount + connectorAmount - feeAmount,
|
||||
Amount: vtxoAmount + connectorAmount - feeAmount,
|
||||
Script: aspScript,
|
||||
},
|
||||
{
|
||||
@@ -110,12 +81,28 @@ func (b *txBuilder) craftForfeitTxs(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tx, err := pset.ToBase64()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
forfeitTxs = append(forfeitTxs, tx)
|
||||
forfeitTxs = append(forfeitTxs, pset)
|
||||
}
|
||||
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
|
||||
}
|
||||
@@ -10,9 +10,6 @@ import (
|
||||
"github.com/btcsuite/btcd/btcec/v2/schnorr"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"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"
|
||||
)
|
||||
|
||||
@@ -40,7 +37,7 @@ type CSVSigClosure struct {
|
||||
Seconds uint
|
||||
}
|
||||
|
||||
type ForfeitClosure struct {
|
||||
type MultisigClosure struct {
|
||||
Pubkey *secp256k1.PublicKey
|
||||
AspPubkey *secp256k1.PublicKey
|
||||
}
|
||||
@@ -58,7 +55,7 @@ func DecodeClosure(script []byte) (Closure, error) {
|
||||
return closure, nil
|
||||
}
|
||||
|
||||
closure = &ForfeitClosure{}
|
||||
closure = &MultisigClosure{}
|
||||
if valid, err := closure.Decode(script); err == nil && valid {
|
||||
return closure, nil
|
||||
}
|
||||
@@ -66,7 +63,7 @@ func DecodeClosure(script []byte) (Closure, error) {
|
||||
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)
|
||||
userKeyBytes := schnorr.SerializePubKey(f.Pubkey)
|
||||
|
||||
@@ -81,7 +78,7 @@ func (f *ForfeitClosure) Leaf() (*taproot.TapElementsLeaf, error) {
|
||||
return &tapLeaf, nil
|
||||
}
|
||||
|
||||
func (f *ForfeitClosure) Decode(script []byte) (bool, error) {
|
||||
func (f *MultisigClosure) Decode(script []byte) (bool, error) {
|
||||
valid, aspPubKey, err := decodeChecksigScript(script)
|
||||
if err != nil {
|
||||
return false, err
|
||||
@@ -284,59 +281,6 @@ func (c *UnrollClosure) Decode(script []byte) (valid bool, err error) {
|
||||
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(
|
||||
script []byte, expectedIndex byte, isVerify bool,
|
||||
) (bool, *secp256k1.PublicKey, uint64, error) {
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
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 Receiver struct {
|
||||
Pubkey string
|
||||
Script VtxoScript
|
||||
Amount uint64
|
||||
}
|
||||
|
||||
131
common/tree/vtxo.go
Normal file
131
common/tree/vtxo.go
Normal 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
11
common/utils.go
Normal 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
62
common/vtxo.go
Normal 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
|
||||
}
|
||||
@@ -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-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-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/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=
|
||||
@@ -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-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-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/go.mod h1:wAG085dHOYqUpf+Ap+WOdrPTp5IYcDAs/x7PLa8Y5fM=
|
||||
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-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-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-20210616094352-59db8d763f22/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-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-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
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.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"github.com/ark-network/ark/common/bitcointree"
|
||||
"github.com/ark-network/ark/common/tree"
|
||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -37,7 +38,7 @@ type ASPClient interface {
|
||||
ctx context.Context, signedForfeitTxs []string, signedRoundTx string,
|
||||
) error
|
||||
CreatePayment(
|
||||
ctx context.Context, inputs []VtxoKey, outputs []Output,
|
||||
ctx context.Context, inputs []Input, outputs []Output,
|
||||
) (string, []string, error)
|
||||
CompletePayment(
|
||||
ctx context.Context, signedRedeemTx string, signedUnconditionalForfeitTxs []string,
|
||||
@@ -67,40 +68,19 @@ type RoundEventChannel struct {
|
||||
Err error
|
||||
}
|
||||
|
||||
type Input interface {
|
||||
GetTxID() string
|
||||
GetVOut() uint32
|
||||
GetDescriptor() string
|
||||
}
|
||||
|
||||
type VtxoKey struct {
|
||||
type Outpoint struct {
|
||||
Txid string
|
||||
VOut uint32
|
||||
}
|
||||
|
||||
func (k VtxoKey) GetTxID() string {
|
||||
return k.Txid
|
||||
}
|
||||
|
||||
func (k VtxoKey) GetVOut() uint32 {
|
||||
return k.VOut
|
||||
}
|
||||
|
||||
func (k VtxoKey) GetDescriptor() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
type BoardingInput struct {
|
||||
VtxoKey
|
||||
type Input struct {
|
||||
Outpoint
|
||||
Descriptor string
|
||||
}
|
||||
|
||||
func (k BoardingInput) GetDescriptor() string {
|
||||
return k.Descriptor
|
||||
}
|
||||
|
||||
type Vtxo struct {
|
||||
VtxoKey
|
||||
Outpoint
|
||||
Descriptor string
|
||||
Amount uint64
|
||||
RoundTxid string
|
||||
ExpiresAt *time.Time
|
||||
@@ -111,8 +91,9 @@ type Vtxo struct {
|
||||
}
|
||||
|
||||
type Output struct {
|
||||
Address string
|
||||
Amount uint64
|
||||
Address string // onchain output address
|
||||
Descriptor string // offchain vtxo descriptor
|
||||
Amount uint64
|
||||
}
|
||||
|
||||
type RoundStage int
|
||||
@@ -152,11 +133,11 @@ type Round struct {
|
||||
}
|
||||
|
||||
type RoundFinalizationEvent struct {
|
||||
ID string
|
||||
Tx string
|
||||
ForfeitTxs []string
|
||||
Tree tree.CongestionTree
|
||||
Connectors []string
|
||||
ID string
|
||||
Tx string
|
||||
Tree tree.CongestionTree
|
||||
Connectors []string
|
||||
MinRelayFeeRate chainfee.SatPerKVByte
|
||||
}
|
||||
|
||||
func (e RoundFinalizationEvent) isRoundEvent() {}
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/ark-network/ark/pkg/client-sdk/client"
|
||||
"github.com/ark-network/ark/pkg/client-sdk/internal/utils"
|
||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
@@ -202,15 +203,10 @@ func (a *grpcClient) FinalizePayment(
|
||||
}
|
||||
|
||||
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) {
|
||||
insCast := make([]client.Input, 0, len(inputs))
|
||||
for _, in := range inputs {
|
||||
insCast = append(insCast, in)
|
||||
}
|
||||
|
||||
req := &arkv1.CreatePaymentRequest{
|
||||
Inputs: ins(insCast).toProto(),
|
||||
Inputs: ins(inputs).toProto(),
|
||||
Outputs: outs(outputs).toProto(),
|
||||
}
|
||||
resp, err := a.svc.CreatePayment(ctx, req)
|
||||
@@ -323,9 +319,20 @@ func (a *grpcClient) SendTreeSignatures(
|
||||
type out client.Output
|
||||
|
||||
func (o out) toProto() *arkv1.Output {
|
||||
if len(o.Address) > 0 {
|
||||
return &arkv1.Output{
|
||||
Destination: &arkv1.Output_Address{
|
||||
Address: o.Address,
|
||||
},
|
||||
Amount: o.Amount,
|
||||
}
|
||||
}
|
||||
|
||||
return &arkv1.Output{
|
||||
Address: o.Address,
|
||||
Amount: o.Amount,
|
||||
Destination: &arkv1.Output_Descriptor_{
|
||||
Descriptor_: o.Descriptor,
|
||||
},
|
||||
Amount: o.Amount,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -362,11 +369,11 @@ func (e event) toRoundEvent() (client.RoundEvent, error) {
|
||||
if ee := e.GetRoundFinalization(); ee != nil {
|
||||
tree := treeFromProto{ee.GetCongestionTree()}.parse()
|
||||
return client.RoundFinalizationEvent{
|
||||
ID: ee.GetId(),
|
||||
Tx: ee.GetPoolTx(),
|
||||
ForfeitTxs: ee.GetForfeitTxs(),
|
||||
Tree: tree,
|
||||
Connectors: ee.GetConnectors(),
|
||||
ID: ee.GetId(),
|
||||
Tx: ee.GetPoolTx(),
|
||||
Tree: tree,
|
||||
Connectors: ee.GetConnectors(),
|
||||
MinRelayFeeRate: chainfee.SatPerKVByte(ee.MinRelayFeeRate),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -430,17 +437,18 @@ func (v vtxo) toVtxo() client.Vtxo {
|
||||
uncondForfeitTxs = v.GetPendingData().GetUnconditionalForfeitTxs()
|
||||
}
|
||||
return client.Vtxo{
|
||||
VtxoKey: client.VtxoKey{
|
||||
Txid: v.GetOutpoint().GetVtxoInput().GetTxid(),
|
||||
VOut: v.GetOutpoint().GetVtxoInput().GetVout(),
|
||||
Outpoint: client.Outpoint{
|
||||
Txid: v.GetOutpoint().GetTxid(),
|
||||
VOut: v.GetOutpoint().GetVout(),
|
||||
},
|
||||
Amount: v.GetReceiver().GetAmount(),
|
||||
Amount: v.GetAmount(),
|
||||
RoundTxid: v.GetPoolTxid(),
|
||||
ExpiresAt: expiresAt,
|
||||
Pending: v.GetPending(),
|
||||
RedeemTx: redeemTx,
|
||||
UnconditionalForfeitTxs: uncondForfeitTxs,
|
||||
SpentBy: v.GetSpentBy(),
|
||||
Descriptor: v.GetDescriptor_(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -455,25 +463,12 @@ func (v vtxos) toVtxos() []client.Vtxo {
|
||||
}
|
||||
|
||||
func toProtoInput(i client.Input) *arkv1.Input {
|
||||
if len(i.GetDescriptor()) > 0 {
|
||||
return &arkv1.Input{
|
||||
Input: &arkv1.Input_BoardingInput{
|
||||
BoardingInput: &arkv1.BoardingInput{
|
||||
Txid: i.GetTxID(),
|
||||
Vout: i.GetVOut(),
|
||||
Descriptor_: i.GetDescriptor(),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return &arkv1.Input{
|
||||
Input: &arkv1.Input_VtxoInput{
|
||||
VtxoInput: &arkv1.VtxoInput{
|
||||
Txid: i.GetTxID(),
|
||||
Vout: i.GetVOut(),
|
||||
},
|
||||
Outpoint: &arkv1.Outpoint{
|
||||
Txid: i.Txid,
|
||||
Vout: i.VOut,
|
||||
},
|
||||
Descriptor_: i.Descriptor,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||
httptransport "github.com/go-openapi/runtime/client"
|
||||
"github.com/go-openapi/strfmt"
|
||||
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||
)
|
||||
|
||||
type restClient struct {
|
||||
@@ -146,7 +147,7 @@ func (a *restClient) ListVtxos(
|
||||
expiresAt = &t
|
||||
}
|
||||
|
||||
amount, err := strconv.Atoi(v.Receiver.Amount)
|
||||
amount, err := strconv.Atoi(v.Amount)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
@@ -159,9 +160,9 @@ func (a *restClient) ListVtxos(
|
||||
}
|
||||
|
||||
spendableVtxos = append(spendableVtxos, client.Vtxo{
|
||||
VtxoKey: client.VtxoKey{
|
||||
Txid: v.Outpoint.VtxoInput.Txid,
|
||||
VOut: uint32(v.Outpoint.VtxoInput.Vout),
|
||||
Outpoint: client.Outpoint{
|
||||
Txid: v.Outpoint.Txid,
|
||||
VOut: uint32(v.Outpoint.Vout),
|
||||
},
|
||||
Amount: uint64(amount),
|
||||
RoundTxid: v.PoolTxid,
|
||||
@@ -170,6 +171,7 @@ func (a *restClient) ListVtxos(
|
||||
RedeemTx: redeemTx,
|
||||
UnconditionalForfeitTxs: uncondForfeitTxs,
|
||||
SpentBy: v.SpentBy,
|
||||
Descriptor: v.Descriptor,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -185,20 +187,21 @@ func (a *restClient) ListVtxos(
|
||||
expiresAt = &t
|
||||
}
|
||||
|
||||
amount, err := strconv.Atoi(v.Receiver.Amount)
|
||||
amount, err := strconv.Atoi(v.Amount)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
spentVtxos = append(spentVtxos, client.Vtxo{
|
||||
VtxoKey: client.VtxoKey{
|
||||
Txid: v.Outpoint.VtxoInput.Txid,
|
||||
VOut: uint32(v.Outpoint.VtxoInput.Vout),
|
||||
Outpoint: client.Outpoint{
|
||||
Txid: v.Outpoint.Txid,
|
||||
VOut: uint32(v.Outpoint.Vout),
|
||||
},
|
||||
Amount: uint64(amount),
|
||||
RoundTxid: v.PoolTxid,
|
||||
ExpiresAt: expiresAt,
|
||||
SpentBy: v.SpentBy,
|
||||
Amount: uint64(amount),
|
||||
RoundTxid: v.PoolTxid,
|
||||
ExpiresAt: expiresAt,
|
||||
SpentBy: v.SpentBy,
|
||||
Descriptor: v.Descriptor,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -249,26 +252,13 @@ func (a *restClient) RegisterPayment(
|
||||
) (string, error) {
|
||||
ins := make([]*models.V1Input, 0, len(inputs))
|
||||
for _, i := range inputs {
|
||||
var input *models.V1Input
|
||||
|
||||
if len(i.GetDescriptor()) > 0 {
|
||||
input = &models.V1Input{
|
||||
BoardingInput: &models.V1BoardingInput{
|
||||
Txid: i.GetTxID(),
|
||||
Vout: int64(i.GetVOut()),
|
||||
Descriptor: i.GetDescriptor(),
|
||||
},
|
||||
}
|
||||
} else {
|
||||
input = &models.V1Input{
|
||||
VtxoInput: &models.V1VtxoInput{
|
||||
Txid: i.GetTxID(),
|
||||
Vout: int64(i.GetVOut()),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
ins = append(ins, input)
|
||||
ins = append(ins, &models.V1Input{
|
||||
Outpoint: &models.V1Outpoint{
|
||||
Txid: i.Txid,
|
||||
Vout: int64(i.VOut),
|
||||
},
|
||||
Descriptor: i.Descriptor,
|
||||
})
|
||||
}
|
||||
body := &models.V1RegisterPaymentRequest{
|
||||
Inputs: ins,
|
||||
@@ -328,12 +318,18 @@ func (a *restClient) Ping(
|
||||
}
|
||||
if e := payload.RoundFinalization; e != nil {
|
||||
tree := treeFromProto{e.CongestionTree}.parse()
|
||||
|
||||
minRelayFeeRate, err := strconv.Atoi(e.MinRelayFeeRate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return client.RoundFinalizationEvent{
|
||||
ID: e.ID,
|
||||
Tx: e.PoolTx,
|
||||
ForfeitTxs: e.ForfeitTxs,
|
||||
Tree: tree,
|
||||
Connectors: e.Connectors,
|
||||
ID: e.ID,
|
||||
Tx: e.PoolTx,
|
||||
Tree: tree,
|
||||
Connectors: e.Connectors,
|
||||
MinRelayFeeRate: chainfee.SatPerKVByte(minRelayFeeRate),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -394,26 +390,24 @@ func (a *restClient) FinalizePayment(
|
||||
}
|
||||
|
||||
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) {
|
||||
ins := make([]*models.V1Input, 0, len(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{
|
||||
VtxoInput: &models.V1VtxoInput{
|
||||
Outpoint: &models.V1Outpoint{
|
||||
Txid: i.Txid,
|
||||
Vout: int64(i.VOut),
|
||||
},
|
||||
Descriptor: i.Descriptor,
|
||||
})
|
||||
}
|
||||
outs := make([]*models.V1Output, 0, len(outputs))
|
||||
for _, o := range outputs {
|
||||
outs = append(outs, &models.V1Output{
|
||||
Address: o.Address,
|
||||
Amount: strconv.Itoa(int(o.Amount)),
|
||||
Address: o.Address,
|
||||
Amount: strconv.Itoa(int(o.Amount)),
|
||||
Descriptor: o.Descriptor,
|
||||
})
|
||||
}
|
||||
body := models.V1CreatePaymentRequest{
|
||||
|
||||
@@ -18,22 +18,18 @@ import (
|
||||
// swagger:model v1Input
|
||||
type V1Input struct {
|
||||
|
||||
// boarding input
|
||||
BoardingInput *V1BoardingInput `json:"boardingInput,omitempty"`
|
||||
// descriptor
|
||||
Descriptor string `json:"descriptor,omitempty"`
|
||||
|
||||
// vtxo input
|
||||
VtxoInput *V1VtxoInput `json:"vtxoInput,omitempty"`
|
||||
// outpoint
|
||||
Outpoint *V1Outpoint `json:"outpoint,omitempty"`
|
||||
}
|
||||
|
||||
// Validate validates this v1 input
|
||||
func (m *V1Input) Validate(formats strfmt.Registry) error {
|
||||
var res []error
|
||||
|
||||
if err := m.validateBoardingInput(formats); err != nil {
|
||||
res = append(res, err)
|
||||
}
|
||||
|
||||
if err := m.validateVtxoInput(formats); err != nil {
|
||||
if err := m.validateOutpoint(formats); err != nil {
|
||||
res = append(res, err)
|
||||
}
|
||||
|
||||
@@ -43,36 +39,17 @@ func (m *V1Input) Validate(formats strfmt.Registry) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *V1Input) validateBoardingInput(formats strfmt.Registry) error {
|
||||
if swag.IsZero(m.BoardingInput) { // not required
|
||||
func (m *V1Input) validateOutpoint(formats strfmt.Registry) error {
|
||||
if swag.IsZero(m.Outpoint) { // not required
|
||||
return nil
|
||||
}
|
||||
|
||||
if m.BoardingInput != nil {
|
||||
if err := m.BoardingInput.Validate(formats); err != nil {
|
||||
if m.Outpoint != nil {
|
||||
if err := m.Outpoint.Validate(formats); err != nil {
|
||||
if ve, ok := err.(*errors.Validation); ok {
|
||||
return ve.ValidateName("boardingInput")
|
||||
return ve.ValidateName("outpoint")
|
||||
} else if ce, ok := err.(*errors.CompositeError); ok {
|
||||
return ce.ValidateName("boardingInput")
|
||||
}
|
||||
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 ce.ValidateName("outpoint")
|
||||
}
|
||||
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 {
|
||||
var res []error
|
||||
|
||||
if err := m.contextValidateBoardingInput(ctx, formats); err != nil {
|
||||
res = append(res, err)
|
||||
}
|
||||
|
||||
if err := m.contextValidateVtxoInput(ctx, formats); err != nil {
|
||||
if err := m.contextValidateOutpoint(ctx, formats); err != nil {
|
||||
res = append(res, err)
|
||||
}
|
||||
|
||||
@@ -99,40 +72,19 @@ func (m *V1Input) ContextValidate(ctx context.Context, formats strfmt.Registry)
|
||||
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
|
||||
}
|
||||
|
||||
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 {
|
||||
return ve.ValidateName("boardingInput")
|
||||
return ve.ValidateName("outpoint")
|
||||
} else if ce, ok := err.(*errors.CompositeError); ok {
|
||||
return ce.ValidateName("boardingInput")
|
||||
}
|
||||
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 ce.ValidateName("outpoint")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
53
pkg/client-sdk/client/rest/service/models/v1_outpoint.go
Normal file
53
pkg/client-sdk/client/rest/service/models/v1_outpoint.go
Normal 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
|
||||
}
|
||||
@@ -17,11 +17,14 @@ import (
|
||||
// swagger:model v1Output
|
||||
type V1Output struct {
|
||||
|
||||
// Either the offchain or onchain address.
|
||||
// onchain
|
||||
Address string `json:"address,omitempty"`
|
||||
|
||||
// Amount to send in satoshis.
|
||||
Amount string `json:"amount,omitempty"`
|
||||
|
||||
// offchain
|
||||
Descriptor string `json:"descriptor,omitempty"`
|
||||
}
|
||||
|
||||
// Validate validates this v1 output
|
||||
|
||||
@@ -24,12 +24,12 @@ type V1RoundFinalizationEvent struct {
|
||||
// connectors
|
||||
Connectors []string `json:"connectors"`
|
||||
|
||||
// forfeit txs
|
||||
ForfeitTxs []string `json:"forfeitTxs"`
|
||||
|
||||
// id
|
||||
ID string `json:"id,omitempty"`
|
||||
|
||||
// min relay fee rate
|
||||
MinRelayFeeRate string `json:"minRelayFeeRate,omitempty"`
|
||||
|
||||
// pool tx
|
||||
PoolTx string `json:"poolTx,omitempty"`
|
||||
}
|
||||
|
||||
@@ -18,11 +18,17 @@ import (
|
||||
// swagger:model v1Vtxo
|
||||
type V1Vtxo struct {
|
||||
|
||||
// amount
|
||||
Amount string `json:"amount,omitempty"`
|
||||
|
||||
// descriptor
|
||||
Descriptor string `json:"descriptor,omitempty"`
|
||||
|
||||
// expire at
|
||||
ExpireAt string `json:"expireAt,omitempty"`
|
||||
|
||||
// outpoint
|
||||
Outpoint *V1Input `json:"outpoint,omitempty"`
|
||||
Outpoint *V1Outpoint `json:"outpoint,omitempty"`
|
||||
|
||||
// pending
|
||||
Pending bool `json:"pending,omitempty"`
|
||||
@@ -33,9 +39,6 @@ type V1Vtxo struct {
|
||||
// pool txid
|
||||
PoolTxid string `json:"poolTxid,omitempty"`
|
||||
|
||||
// receiver
|
||||
Receiver *V1Output `json:"receiver,omitempty"`
|
||||
|
||||
// spent
|
||||
Spent bool `json:"spent,omitempty"`
|
||||
|
||||
@@ -58,10 +61,6 @@ func (m *V1Vtxo) Validate(formats strfmt.Registry) error {
|
||||
res = append(res, err)
|
||||
}
|
||||
|
||||
if err := m.validateReceiver(formats); err != nil {
|
||||
res = append(res, err)
|
||||
}
|
||||
|
||||
if len(res) > 0 {
|
||||
return errors.CompositeValidationError(res...)
|
||||
}
|
||||
@@ -106,25 +105,6 @@ func (m *V1Vtxo) validatePendingData(formats strfmt.Registry) error {
|
||||
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
|
||||
func (m *V1Vtxo) ContextValidate(ctx context.Context, formats strfmt.Registry) error {
|
||||
var res []error
|
||||
@@ -137,10 +117,6 @@ func (m *V1Vtxo) ContextValidate(ctx context.Context, formats strfmt.Registry) e
|
||||
res = append(res, err)
|
||||
}
|
||||
|
||||
if err := m.contextValidateReceiver(ctx, formats); err != nil {
|
||||
res = append(res, err)
|
||||
}
|
||||
|
||||
if len(res) > 0 {
|
||||
return errors.CompositeValidationError(res...)
|
||||
}
|
||||
@@ -189,27 +165,6 @@ func (m *V1Vtxo) contextValidatePendingData(ctx context.Context, formats strfmt.
|
||||
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
|
||||
func (m *V1Vtxo) MarshalBinary() ([]byte, error) {
|
||||
if m == nil {
|
||||
|
||||
@@ -226,7 +226,7 @@ func loadFixtures(jsonStr string) (vtxos, []Transaction, error) {
|
||||
return vtxos{}, nil, err
|
||||
}
|
||||
spendable[i] = client.Vtxo{
|
||||
VtxoKey: client.VtxoKey{
|
||||
Outpoint: client.Outpoint{
|
||||
Txid: vtxo.Outpoint.Txid,
|
||||
VOut: vtxo.Outpoint.Vout,
|
||||
},
|
||||
@@ -251,7 +251,7 @@ func loadFixtures(jsonStr string) (vtxos, []Transaction, error) {
|
||||
return vtxos{}, nil, err
|
||||
}
|
||||
spent[i] = client.Vtxo{
|
||||
VtxoKey: client.VtxoKey{
|
||||
Outpoint: client.Outpoint{
|
||||
Txid: vtxo.Outpoint.Txid,
|
||||
VOut: vtxo.Outpoint.Vout,
|
||||
},
|
||||
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
"time"
|
||||
|
||||
"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/pkg/client-sdk/client"
|
||||
"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/chaincfg/chainhash"
|
||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/vulpemventures/go-elements/address"
|
||||
"github.com/vulpemventures/go-elements/psetv2"
|
||||
"github.com/vulpemventures/go-elements/taproot"
|
||||
)
|
||||
|
||||
type liquidReceiver struct {
|
||||
@@ -435,18 +436,27 @@ func (a *covenantArkClient) CollaborativeRedeem(
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
desc, err := a.offchainAddressToDefaultVtxoDescriptor(offchainAddr)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
receivers = append(receivers, client.Output{
|
||||
Address: offchainAddr,
|
||||
Amount: changeAmount,
|
||||
Descriptor: desc,
|
||||
Amount: changeAmount,
|
||||
})
|
||||
}
|
||||
|
||||
inputs := make([]client.Input, 0, len(selectedCoins))
|
||||
|
||||
for _, coin := range selectedCoins {
|
||||
inputs = append(inputs, client.VtxoKey{
|
||||
Txid: coin.Txid,
|
||||
VOut: coin.VOut,
|
||||
inputs = append(inputs, client.Input{
|
||||
Outpoint: client.Outpoint{
|
||||
Txid: coin.Txid,
|
||||
VOut: coin.VOut,
|
||||
},
|
||||
Descriptor: coin.Descriptor,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -460,7 +470,7 @@ func (a *covenantArkClient) CollaborativeRedeem(
|
||||
}
|
||||
|
||||
poolTxID, err := a.handleRoundStream(
|
||||
ctx, paymentID, selectedCoins, false, receivers,
|
||||
ctx, paymentID, selectedCoins, nil, "", receivers,
|
||||
)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -500,14 +510,22 @@ func (a *covenantArkClient) Claim(ctx context.Context) (string, error) {
|
||||
return "", fmt.Errorf("no funds to claim")
|
||||
}
|
||||
|
||||
receiver := client.Output{
|
||||
Address: myselfOffchain,
|
||||
Amount: pendingBalance,
|
||||
desc, err := a.offchainAddressToDefaultVtxoDescriptor(myselfOffchain)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
desc := strings.ReplaceAll(a.BoardingDescriptorTemplate, "USER", hex.EncodeToString(schnorr.SerializePubKey(mypubkey)))
|
||||
receiver := client.Output{
|
||||
Descriptor: desc,
|
||||
Amount: pendingBalance,
|
||||
}
|
||||
|
||||
return a.selfTransferAllPendingPayments(ctx, boardingUtxos, receiver, desc)
|
||||
return a.selfTransferAllPendingPayments(
|
||||
ctx,
|
||||
boardingUtxos,
|
||||
receiver,
|
||||
hex.EncodeToString(mypubkey.SerializeCompressed()),
|
||||
)
|
||||
}
|
||||
|
||||
func (a *covenantArkClient) GetTransactionHistory(ctx context.Context) ([]Transaction, error) {
|
||||
@@ -577,14 +595,18 @@ func (a *covenantArkClient) getClaimableBoardingUtxos(ctx context.Context) ([]ex
|
||||
descriptorStr := strings.ReplaceAll(
|
||||
a.BoardingDescriptorTemplate, "USER", myPubkeyStr,
|
||||
)
|
||||
desc, err := descriptor.ParseTaprootDescriptor(descriptorStr)
|
||||
|
||||
boardingScript, err := tree.ParseVtxoScript(descriptorStr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, boardingTimeout, err := descriptor.ParseBoardingDescriptor(*desc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
var boardingTimeout uint
|
||||
|
||||
if defaultVtxo, ok := boardingScript.(*tree.DefaultVtxoScript); ok {
|
||||
boardingTimeout = defaultVtxo.ExitDelay
|
||||
} else {
|
||||
return nil, fmt.Errorf("unsupported boarding descriptor: %s", descriptorStr)
|
||||
}
|
||||
|
||||
for _, addr := range boardingAddrs {
|
||||
@@ -800,9 +822,14 @@ func (a *covenantArkClient) sendOffchain(
|
||||
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{
|
||||
Address: receiver.To(),
|
||||
Amount: receiver.Amount(),
|
||||
Descriptor: desc,
|
||||
Amount: receiver.Amount(),
|
||||
})
|
||||
sumOfReceivers += receiver.Amount()
|
||||
}
|
||||
@@ -828,18 +855,27 @@ func (a *covenantArkClient) sendOffchain(
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
desc, err := a.offchainAddressToDefaultVtxoDescriptor(offchainAddr)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
changeReceiver := client.Output{
|
||||
Address: offchainAddr,
|
||||
Amount: changeAmount,
|
||||
Descriptor: desc,
|
||||
Amount: changeAmount,
|
||||
}
|
||||
receiversOutput = append(receiversOutput, changeReceiver)
|
||||
}
|
||||
|
||||
inputs := make([]client.Input, 0, len(selectedCoins))
|
||||
for _, coin := range selectedCoins {
|
||||
inputs = append(inputs, client.VtxoKey{
|
||||
Txid: coin.Txid,
|
||||
VOut: coin.VOut,
|
||||
inputs = append(inputs, client.Input{
|
||||
Outpoint: client.Outpoint{
|
||||
Txid: coin.Txid,
|
||||
VOut: coin.VOut,
|
||||
},
|
||||
Descriptor: coin.Descriptor,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -859,7 +895,7 @@ func (a *covenantArkClient) sendOffchain(
|
||||
log.Infof("payment registered with id: %s", paymentID)
|
||||
|
||||
poolTxID, err := a.handleRoundStream(
|
||||
ctx, paymentID, selectedCoins, false, receiversOutput,
|
||||
ctx, paymentID, selectedCoins, nil, "", receiversOutput,
|
||||
)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -900,16 +936,46 @@ func (a *covenantArkClient) addInputs(
|
||||
return err
|
||||
}
|
||||
|
||||
_, leafProof, _, _, err := tree.ComputeVtxoTaprootScript(
|
||||
userPubkey, aspPubkey, utxo.Delay, utils.ToElementsNetwork(a.Network),
|
||||
)
|
||||
vtxoScript := &tree.DefaultVtxoScript{
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -921,7 +987,8 @@ func (a *covenantArkClient) handleRoundStream(
|
||||
ctx context.Context,
|
||||
paymentID string,
|
||||
vtxosToSign []client.Vtxo,
|
||||
mustSignRoundTx bool,
|
||||
boardingUtxos []explorer.Utxo,
|
||||
boardingDescriptor string,
|
||||
receivers []client.Output,
|
||||
) (string, error) {
|
||||
eventsCh, err := a.client.GetEventStream(ctx, paymentID)
|
||||
@@ -955,7 +1022,7 @@ func (a *covenantArkClient) handleRoundStream(
|
||||
log.Info("a round finalization started")
|
||||
|
||||
signedForfeitTxs, signedRoundTx, err := a.handleRoundFinalization(
|
||||
ctx, event.(client.RoundFinalizationEvent), vtxosToSign, mustSignRoundTx, receivers,
|
||||
ctx, event.(client.RoundFinalizationEvent), vtxosToSign, boardingUtxos, boardingDescriptor, receivers,
|
||||
)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -979,30 +1046,104 @@ func (a *covenantArkClient) handleRoundStream(
|
||||
}
|
||||
|
||||
func (a *covenantArkClient) handleRoundFinalization(
|
||||
ctx context.Context, event client.RoundFinalizationEvent,
|
||||
vtxos []client.Vtxo, mustSignRoundTx bool, receivers []client.Output,
|
||||
ctx context.Context,
|
||||
event client.RoundFinalizationEvent,
|
||||
vtxos []client.Vtxo,
|
||||
boardingUtxos []explorer.Utxo,
|
||||
boardingDescriptor string,
|
||||
receivers []client.Output,
|
||||
) (signedForfeits []string, signedRoundTx string, err error) {
|
||||
if err = a.validateCongestionTree(event, receivers); err != nil {
|
||||
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 {
|
||||
signedForfeits, err = a.loopAndSign(
|
||||
ctx, event.ForfeitTxs, vtxos, event.Connectors,
|
||||
)
|
||||
signedForfeits, err = a.createAndSignForfeits(ctx, vtxos, event.Connectors, event.MinRelayFeeRate, myPubkey)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if mustSignRoundTx {
|
||||
signedRoundTx, err = a.wallet.SignTransaction(ctx, a.explorer, event.Tx)
|
||||
if len(boardingUtxos) > 0 {
|
||||
boardingVtxoScript, err := tree.ParseVtxoScript(boardingDescriptor)
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
return signedForfeits, signedRoundTx, nil
|
||||
}
|
||||
|
||||
func (a *covenantArkClient) validateCongestionTree(
|
||||
@@ -1016,7 +1157,7 @@ func (a *covenantArkClient) validateCongestionTree(
|
||||
|
||||
connectors := event.Connectors
|
||||
|
||||
if !utils.IsLiquidOnchainOnly(receivers) {
|
||||
if !utils.IsOnchainOnly(receivers) {
|
||||
if err := tree.ValidateCongestionTree(
|
||||
event.Tree, poolTx, a.StoreData.AspPubkey, a.RoundLifetime,
|
||||
); err != nil {
|
||||
@@ -1029,7 +1170,7 @@ func (a *covenantArkClient) validateCongestionTree(
|
||||
}
|
||||
|
||||
if err := a.validateReceivers(
|
||||
ptx, receivers, event.Tree, a.StoreData.AspPubkey,
|
||||
ptx, receivers, event.Tree,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1043,14 +1184,13 @@ func (a *covenantArkClient) validateReceivers(
|
||||
ptx *psetv2.Pset,
|
||||
receivers []client.Output,
|
||||
congestionTree tree.CongestionTree,
|
||||
aspPubkey *secp256k1.PublicKey,
|
||||
) error {
|
||||
for _, receiver := range receivers {
|
||||
isOnChain, onchainScript, userPubkey, err := utils.ParseLiquidAddress(
|
||||
isOnChain, onchainScript, err := utils.ParseLiquidAddress(
|
||||
receiver.Address,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("invalid receiver address: %s err = %s", receiver.Address, err)
|
||||
}
|
||||
|
||||
if isOnChain {
|
||||
@@ -1059,7 +1199,7 @@ func (a *covenantArkClient) validateReceivers(
|
||||
}
|
||||
} else {
|
||||
if err := a.validateOffChainReceiver(
|
||||
congestionTree, receiver, userPubkey, aspPubkey,
|
||||
congestionTree, receiver,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1095,13 +1235,15 @@ func (a *covenantArkClient) validateOnChainReceiver(
|
||||
func (a *covenantArkClient) validateOffChainReceiver(
|
||||
congestionTree tree.CongestionTree,
|
||||
receiver client.Output,
|
||||
userPubkey, aspPubkey *secp256k1.PublicKey,
|
||||
) error {
|
||||
found := false
|
||||
net := utils.ToElementsNetwork(a.Network)
|
||||
outputTapKey, _, _, _, err := tree.ComputeVtxoTaprootScript(
|
||||
userPubkey, aspPubkey, uint(a.UnilateralExitDelay), net,
|
||||
)
|
||||
|
||||
receiverVtxoScript, err := tree.ParseVtxoScript(receiver.Descriptor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
outputTapKey, _, err := receiverVtxoScript.TapTree()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1136,36 +1278,105 @@ func (a *covenantArkClient) validateOffChainReceiver(
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *covenantArkClient) loopAndSign(
|
||||
func (a *covenantArkClient) createAndSignForfeits(
|
||||
ctx context.Context,
|
||||
forfeitTxs []string, vtxosToSign []client.Vtxo, connectors []string,
|
||||
vtxosToSign []client.Vtxo,
|
||||
connectors []string,
|
||||
feeRate chainfee.SatPerKVByte,
|
||||
myPubKey *secp256k1.PublicKey,
|
||||
) ([]string, error) {
|
||||
signedForfeits := make([]string, 0)
|
||||
connectorsPsets := make([]*psetv2.Pset, 0, len(connectors))
|
||||
|
||||
connectorsTxids := make([]string, 0, len(connectors))
|
||||
for _, connector := range connectors {
|
||||
p, _ := psetv2.NewPsetFromBase64(connector)
|
||||
utx, _ := p.UnsignedTx()
|
||||
txid := utx.TxHash().String()
|
||||
connectorsTxids = append(connectorsTxids, txid)
|
||||
}
|
||||
|
||||
for _, forfeitTx := range forfeitTxs {
|
||||
pset, err := psetv2.NewPsetFromBase64(forfeitTx)
|
||||
p, err := psetv2.NewPsetFromBase64(connector)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, input := range pset.Inputs {
|
||||
inputTxid := chainhash.Hash(input.PreviousTxid).String()
|
||||
for _, coin := range vtxosToSign {
|
||||
if inputTxid == coin.Txid {
|
||||
signedPset, err := a.signForfeitTx(ctx, forfeitTx, pset, connectorsTxids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
signedForfeits = append(signedForfeits, signedPset)
|
||||
connectorsPsets = append(connectorsPsets, p)
|
||||
}
|
||||
|
||||
for _, vtxo := range vtxosToSign {
|
||||
vtxoScript, err := tree.ParseVtxoScript(vtxo.Descriptor)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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(
|
||||
ctx context.Context, targetAmount uint64, exclude []explorer.Utxo,
|
||||
) ([]explorer.Utxo, uint64, error) {
|
||||
@@ -1208,14 +1401,17 @@ func (a *covenantArkClient) coinSelectOnchain(
|
||||
descriptorStr := strings.ReplaceAll(
|
||||
a.BoardingDescriptorTemplate, "USER", myPubkeyStr,
|
||||
)
|
||||
desc, err := descriptor.ParseTaprootDescriptor(descriptorStr)
|
||||
boardingScript, err := tree.ParseVtxoScript(descriptorStr)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
_, boardingTimeout, err := descriptor.ParseBoardingDescriptor(*desc)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
var boardingTimeout uint
|
||||
|
||||
if defaultVtxo, ok := boardingScript.(*tree.DefaultVtxoScript); ok {
|
||||
boardingTimeout = defaultVtxo.ExitDelay
|
||||
} else {
|
||||
return nil, 0, fmt.Errorf("unsupported boarding descriptor: %s", descriptorStr)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
@@ -1387,13 +1583,17 @@ func (a *covenantArkClient) getVtxos(
|
||||
}
|
||||
|
||||
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) {
|
||||
inputs := make([]client.Input, 0, len(boardingUtxo))
|
||||
inputs := make([]client.Input, 0, len(boardingUtxos))
|
||||
|
||||
for _, utxo := range boardingUtxo {
|
||||
inputs = append(inputs, client.BoardingInput{
|
||||
VtxoKey: client.VtxoKey{
|
||||
boardingDescriptor := strings.ReplaceAll(
|
||||
a.BoardingDescriptorTemplate, "USER", mypubkey[2:],
|
||||
)
|
||||
|
||||
for _, utxo := range boardingUtxos {
|
||||
inputs = append(inputs, client.Input{
|
||||
Outpoint: client.Outpoint{
|
||||
Txid: utxo.Txid,
|
||||
VOut: utxo.Vout,
|
||||
},
|
||||
@@ -1413,7 +1613,7 @@ func (a *covenantArkClient) selfTransferAllPendingPayments(
|
||||
}
|
||||
|
||||
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 {
|
||||
return "", err
|
||||
@@ -1422,6 +1622,21 @@ func (a *covenantArkClient) selfTransferAllPendingPayments(
|
||||
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) {
|
||||
utxos, err := a.getClaimableBoardingUtxos(ctx)
|
||||
if err != nil {
|
||||
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
|
||||
"github.com/ark-network/ark/common"
|
||||
"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/pkg/client-sdk/client"
|
||||
"github.com/ark-network/ark/pkg/client-sdk/explorer"
|
||||
@@ -28,6 +27,7 @@ import (
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
@@ -425,18 +425,27 @@ func (a *covenantlessArkClient) CollaborativeRedeem(
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
desc, err := a.offchainAddressToDefaultVtxoDescriptor(offchainAddr)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
receivers = append(receivers, client.Output{
|
||||
Address: offchainAddr,
|
||||
Amount: changeAmount,
|
||||
Descriptor: desc,
|
||||
Amount: changeAmount,
|
||||
})
|
||||
}
|
||||
|
||||
inputs := make([]client.Input, 0, len(selectedCoins))
|
||||
|
||||
for _, coin := range selectedCoins {
|
||||
inputs = append(inputs, client.VtxoKey{
|
||||
Txid: coin.Txid,
|
||||
VOut: coin.VOut,
|
||||
inputs = append(inputs, client.Input{
|
||||
Outpoint: client.Outpoint{
|
||||
Txid: coin.Txid,
|
||||
VOut: coin.VOut,
|
||||
},
|
||||
Descriptor: coin.Descriptor,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -459,7 +468,7 @@ func (a *covenantlessArkClient) CollaborativeRedeem(
|
||||
}
|
||||
|
||||
poolTxID, err := a.handleRoundStream(
|
||||
ctx, paymentID, selectedCoins, false, receivers, roundEphemeralKey,
|
||||
ctx, paymentID, selectedCoins, nil, "", receivers, roundEphemeralKey,
|
||||
)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -478,7 +487,7 @@ func (a *covenantlessArkClient) SendAsync(
|
||||
|
||||
netParams := utils.ToBitcoinNetwork(a.Network)
|
||||
for _, receiver := range receivers {
|
||||
isOnchain, _, _, err := utils.ParseBitcoinAddress(receiver.To(), netParams)
|
||||
isOnchain, _, err := utils.ParseBitcoinAddress(receiver.To(), netParams)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -516,9 +525,27 @@ func (a *covenantlessArkClient) SendAsync(
|
||||
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{
|
||||
Address: receiver.To(),
|
||||
Amount: receiver.Amount(),
|
||||
Descriptor: desc,
|
||||
Amount: receiver.Amount(),
|
||||
})
|
||||
sumOfReceivers += receiver.Amount()
|
||||
}
|
||||
@@ -535,17 +562,28 @@ func (a *covenantlessArkClient) SendAsync(
|
||||
}
|
||||
|
||||
if changeAmount > 0 {
|
||||
changeDesc, err := a.offchainAddressToDefaultVtxoDescriptor(offchainAddrs[0])
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
changeReceiver := client.Output{
|
||||
Address: offchainAddrs[0],
|
||||
Amount: changeAmount,
|
||||
Descriptor: changeDesc,
|
||||
Amount: changeAmount,
|
||||
}
|
||||
receiversOutput = append(receiversOutput, changeReceiver)
|
||||
}
|
||||
|
||||
inputs := make([]client.VtxoKey, 0, len(selectedCoins))
|
||||
inputs := make([]client.Input, 0, len(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(
|
||||
@@ -556,23 +594,13 @@ func (a *covenantlessArkClient) SendAsync(
|
||||
|
||||
// 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)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err = a.client.CompletePayment(
|
||||
ctx, signedRedeemTx, signedUnconditionalForfeitTxs,
|
||||
ctx, signedRedeemTx, unconditionalForfeitTxs,
|
||||
); err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -612,13 +640,23 @@ func (a *covenantlessArkClient) Claim(ctx context.Context) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
receiver := client.Output{
|
||||
Address: myselfOffchain,
|
||||
Amount: pendingBalance,
|
||||
desc, err := a.offchainAddressToDefaultVtxoDescriptor(myselfOffchain)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
desc := strings.ReplaceAll(a.BoardingDescriptorTemplate, "USER", hex.EncodeToString(schnorr.SerializePubKey(mypubkey)))
|
||||
return a.selfTransferAllPendingPayments(ctx, pendingVtxos, boardingUtxos, receiver, desc)
|
||||
receiver := client.Output{
|
||||
Descriptor: desc,
|
||||
Amount: pendingBalance,
|
||||
}
|
||||
|
||||
return a.selfTransferAllPendingPayments(
|
||||
ctx,
|
||||
pendingVtxos,
|
||||
boardingUtxos,
|
||||
receiver,
|
||||
hex.EncodeToString(mypubkey.SerializeCompressed()),
|
||||
)
|
||||
}
|
||||
|
||||
func (a *covenantlessArkClient) GetTransactionHistory(ctx context.Context) ([]Transaction, error) {
|
||||
@@ -818,9 +856,14 @@ func (a *covenantlessArkClient) sendOffchain(
|
||||
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{
|
||||
Address: receiver.To(),
|
||||
Amount: receiver.Amount(),
|
||||
Descriptor: desc,
|
||||
Amount: receiver.Amount(),
|
||||
})
|
||||
sumOfReceivers += receiver.Amount()
|
||||
}
|
||||
@@ -846,18 +889,27 @@ func (a *covenantlessArkClient) sendOffchain(
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
desc, err := a.offchainAddressToDefaultVtxoDescriptor(offchainAddr)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
changeReceiver := client.Output{
|
||||
Address: offchainAddr,
|
||||
Amount: changeAmount,
|
||||
Descriptor: desc,
|
||||
Amount: changeAmount,
|
||||
}
|
||||
receiversOutput = append(receiversOutput, changeReceiver)
|
||||
}
|
||||
|
||||
inputs := make([]client.Input, 0, len(selectedCoins))
|
||||
for _, coin := range selectedCoins {
|
||||
inputs = append(inputs, client.VtxoKey{
|
||||
Txid: coin.Txid,
|
||||
VOut: coin.VOut,
|
||||
inputs = append(inputs, client.Input{
|
||||
Outpoint: client.Outpoint{
|
||||
Txid: coin.Txid,
|
||||
VOut: coin.VOut,
|
||||
},
|
||||
Descriptor: coin.Descriptor,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -882,7 +934,7 @@ func (a *covenantlessArkClient) sendOffchain(
|
||||
log.Infof("payment registered with id: %s", paymentID)
|
||||
|
||||
poolTxID, err := a.handleRoundStream(
|
||||
ctx, paymentID, selectedCoins, false, receiversOutput, roundEphemeralKey,
|
||||
ctx, paymentID, selectedCoins, nil, "", receiversOutput, roundEphemeralKey,
|
||||
)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -926,25 +978,38 @@ func (a *covenantlessArkClient) addInputs(
|
||||
Sequence: sequence,
|
||||
})
|
||||
|
||||
_, leafProof, err := bitcointree.ComputeVtxoTaprootScript(
|
||||
userPubkey, aspPubkey, utxo.Delay,
|
||||
)
|
||||
vtxoScript := &bitcointree.DefaultVtxoScript{
|
||||
Owner: userPubkey,
|
||||
Asp: aspPubkey,
|
||||
ExitDelay: utxo.Delay,
|
||||
}
|
||||
|
||||
exitClosure := &bitcointree.CSVSigClosure{
|
||||
Pubkey: userPubkey,
|
||||
Seconds: uint(utxo.Delay),
|
||||
}
|
||||
|
||||
exitLeaf, err := exitClosure.Leaf()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
controlBlock := leafProof.ToControlBlock(bitcointree.UnspendableKey())
|
||||
controlBlockBytes, err := controlBlock.ToBytes()
|
||||
_, taprootTree, err := vtxoScript.TapTree()
|
||||
if err != nil {
|
||||
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{
|
||||
TaprootLeafScript: []*psbt.TaprootTapLeafScript{
|
||||
{
|
||||
ControlBlock: controlBlockBytes,
|
||||
ControlBlock: leafProof.ControlBlock,
|
||||
Script: leafProof.Script,
|
||||
LeafVersion: leafProof.LeafVersion,
|
||||
LeafVersion: txscript.BaseLeafVersion,
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -957,7 +1022,8 @@ func (a *covenantlessArkClient) handleRoundStream(
|
||||
ctx context.Context,
|
||||
paymentID string,
|
||||
vtxosToSign []client.Vtxo,
|
||||
mustSignRoundTx bool,
|
||||
boardingUtxos []explorer.Utxo,
|
||||
boardingDescriptor string,
|
||||
receivers []client.Output,
|
||||
roundEphemeralKey *secp256k1.PrivateKey,
|
||||
) (string, error) {
|
||||
@@ -1033,7 +1099,7 @@ func (a *covenantlessArkClient) handleRoundStream(
|
||||
log.Info("a round finalization started")
|
||||
|
||||
signedForfeitTxs, signedRoundTx, err := a.handleRoundFinalization(
|
||||
ctx, event.(client.RoundFinalizationEvent), vtxosToSign, mustSignRoundTx, receivers,
|
||||
ctx, event.(client.RoundFinalizationEvent), vtxosToSign, boardingUtxos, boardingDescriptor, receivers,
|
||||
)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -1134,30 +1200,103 @@ func (a *covenantlessArkClient) handleRoundSigningNoncesGenerated(
|
||||
}
|
||||
|
||||
func (a *covenantlessArkClient) handleRoundFinalization(
|
||||
ctx context.Context, event client.RoundFinalizationEvent,
|
||||
vtxos []client.Vtxo, mustSignRoundTx bool, receivers []client.Output,
|
||||
) (signedForfeits []string, signedRoundTx string, err error) {
|
||||
ctx context.Context,
|
||||
event client.RoundFinalizationEvent,
|
||||
vtxos []client.Vtxo,
|
||||
boardingUtxos []explorer.Utxo,
|
||||
boardingDescriptor string,
|
||||
receivers []client.Output,
|
||||
) ([]string, string, error) {
|
||||
if err := a.validateCongestionTree(event, receivers); err != nil {
|
||||
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 {
|
||||
signedForfeits, err = a.loopAndSign(
|
||||
ctx, event.ForfeitTxs, vtxos, event.Connectors,
|
||||
signedForfeits, err := a.createAndSignForfeits(
|
||||
ctx, vtxos, event.Connectors, event.MinRelayFeeRate, myPubkey,
|
||||
)
|
||||
if err != nil {
|
||||
return
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
forfeits = signedForfeits
|
||||
}
|
||||
|
||||
if mustSignRoundTx {
|
||||
signedRoundTx, err = a.wallet.SignTransaction(ctx, a.explorer, event.Tx)
|
||||
if len(boardingUtxos) > 0 {
|
||||
boardingVtxoScript, err := bitcointree.ParseVtxoScript(boardingDescriptor)
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
return forfeits, "", nil
|
||||
}
|
||||
|
||||
func (a *covenantlessArkClient) validateCongestionTree(
|
||||
@@ -1169,8 +1308,7 @@ func (a *covenantlessArkClient) validateCongestionTree(
|
||||
return err
|
||||
}
|
||||
|
||||
netParams := utils.ToBitcoinNetwork(a.Network)
|
||||
if !utils.IsBitcoinOnchainOnly(receivers, netParams) {
|
||||
if !utils.IsOnchainOnly(receivers) {
|
||||
if err := bitcointree.ValidateCongestionTree(
|
||||
event.Tree, poolTx, a.StoreData.AspPubkey, a.RoundLifetime,
|
||||
); err != nil {
|
||||
@@ -1183,7 +1321,7 @@ func (a *covenantlessArkClient) validateCongestionTree(
|
||||
// }
|
||||
|
||||
if err := a.validateReceivers(
|
||||
ptx, receivers, event.Tree, a.StoreData.AspPubkey,
|
||||
ptx, receivers, event.Tree,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1197,15 +1335,14 @@ func (a *covenantlessArkClient) validateReceivers(
|
||||
ptx *psbt.Packet,
|
||||
receivers []client.Output,
|
||||
congestionTree tree.CongestionTree,
|
||||
aspPubkey *secp256k1.PublicKey,
|
||||
) error {
|
||||
netParams := utils.ToBitcoinNetwork(a.Network)
|
||||
for _, receiver := range receivers {
|
||||
isOnChain, onchainScript, userPubkey, err := utils.ParseBitcoinAddress(
|
||||
isOnChain, onchainScript, err := utils.ParseBitcoinAddress(
|
||||
receiver.Address, netParams,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("invalid receiver address: %s err = %s", receiver.Address, err)
|
||||
}
|
||||
|
||||
if isOnChain {
|
||||
@@ -1214,7 +1351,7 @@ func (a *covenantlessArkClient) validateReceivers(
|
||||
}
|
||||
} else {
|
||||
if err := a.validateOffChainReceiver(
|
||||
congestionTree, receiver, userPubkey, aspPubkey,
|
||||
congestionTree, receiver,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1250,12 +1387,15 @@ func (a *covenantlessArkClient) validateOnChainReceiver(
|
||||
func (a *covenantlessArkClient) validateOffChainReceiver(
|
||||
congestionTree tree.CongestionTree,
|
||||
receiver client.Output,
|
||||
userPubkey, aspPubkey *secp256k1.PublicKey,
|
||||
) error {
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
@@ -1298,57 +1438,107 @@ func (a *covenantlessArkClient) validateOffChainReceiver(
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *covenantlessArkClient) loopAndSign(
|
||||
func (a *covenantlessArkClient) createAndSignForfeits(
|
||||
ctx context.Context,
|
||||
forfeitTxs []string, vtxosToSign []client.Vtxo, connectors []string,
|
||||
vtxosToSign []client.Vtxo,
|
||||
connectors []string,
|
||||
feeRate chainfee.SatPerKVByte,
|
||||
myPubkey *secp256k1.PublicKey,
|
||||
) ([]string, error) {
|
||||
signedForfeits := make([]string, 0)
|
||||
|
||||
connectorsTxids := make([]string, 0, len(connectors))
|
||||
signedForfeits := make([]string, 0)
|
||||
connectorsPsets := make([]*psbt.Packet, 0, len(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)
|
||||
|
||||
connectorsPsets = append(connectorsPsets, p)
|
||||
}
|
||||
|
||||
for _, forfeitTx := range forfeitTxs {
|
||||
ptx, err := psbt.NewFromRawBytes(strings.NewReader(forfeitTx), true)
|
||||
for _, vtxo := range vtxosToSign {
|
||||
vtxoScript, err := bitcointree.ParseVtxoScript(vtxo.Descriptor)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, input := range ptx.UnsignedTx.TxIn {
|
||||
inputTxid := input.PreviousOutPoint.Hash.String()
|
||||
vtxoTapKey, vtxoTapTree, err := vtxoScript.TapTree()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
feeAmount, err := common.ComputeForfeitMinRelayFee(feeRate, vtxoTapTree)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !connectorFound {
|
||||
return nil, fmt.Errorf("connector txid %s not found in the connectors list", connectorTxid)
|
||||
}
|
||||
vtxoOutputScript, err := common.P2TRScript(vtxoTapKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
signedForfeitTx, err := a.wallet.SignTransaction(ctx, a.explorer, forfeitTx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
vtxoTxHash, err := chainhash.NewHashFromStr(vtxo.Txid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
signedForfeits = append(signedForfeits, signedForfeitTx)
|
||||
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
|
||||
@@ -1371,14 +1561,18 @@ func (a *covenantlessArkClient) coinSelectOnchain(
|
||||
descriptorStr := strings.ReplaceAll(
|
||||
a.BoardingDescriptorTemplate, "USER", myPubkeyStr,
|
||||
)
|
||||
desc, err := descriptor.ParseTaprootDescriptor(descriptorStr)
|
||||
|
||||
boardingScript, err := bitcointree.ParseVtxoScript(descriptorStr)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
_, boardingTimeout, err := descriptor.ParseBoardingDescriptor(*desc)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
var boardingTimeout uint
|
||||
|
||||
if defaultVtxo, ok := boardingScript.(*bitcointree.DefaultVtxoScript); ok {
|
||||
boardingTimeout = defaultVtxo.ExitDelay
|
||||
} else {
|
||||
return nil, 0, fmt.Errorf("unsupported boarding descriptor: %s", descriptorStr)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
@@ -1567,14 +1761,18 @@ func (a *covenantlessArkClient) getClaimableBoardingUtxos(ctx context.Context) (
|
||||
descriptorStr := strings.ReplaceAll(
|
||||
a.BoardingDescriptorTemplate, "USER", myPubkeyStr,
|
||||
)
|
||||
desc, err := descriptor.ParseTaprootDescriptor(descriptorStr)
|
||||
|
||||
boardingScript, err := bitcointree.ParseVtxoScript(descriptorStr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, boardingTimeout, err := descriptor.ParseBoardingDescriptor(*desc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
var boardingTimeout uint
|
||||
|
||||
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)
|
||||
@@ -1644,17 +1842,27 @@ func (a *covenantlessArkClient) getVtxos(
|
||||
}
|
||||
|
||||
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) {
|
||||
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 {
|
||||
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 {
|
||||
inputs = append(inputs, client.BoardingInput{
|
||||
VtxoKey: client.VtxoKey{
|
||||
for _, utxo := range boardingUtxos {
|
||||
inputs = append(inputs, client.Input{
|
||||
Outpoint: client.Outpoint{
|
||||
Txid: utxo.Txid,
|
||||
VOut: utxo.Vout,
|
||||
},
|
||||
@@ -1682,7 +1890,7 @@ func (a *covenantlessArkClient) selfTransferAllPendingPayments(
|
||||
}
|
||||
|
||||
roundTxid, err := a.handleRoundStream(
|
||||
ctx, paymentID, pendingVtxos, len(boardingUtxo) > 0, outputs, roundEphemeralKey,
|
||||
ctx, paymentID, pendingVtxos, boardingUtxos, boardingDescriptor, outputs, roundEphemeralKey,
|
||||
)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -1691,6 +1899,42 @@ func (a *covenantlessArkClient) selfTransferAllPendingPayments(
|
||||
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) {
|
||||
utxos, err := a.getClaimableBoardingUtxos(ctx)
|
||||
if err != nil {
|
||||
|
||||
@@ -18,6 +18,7 @@ require (
|
||||
github.com/go-openapi/strfmt v0.23.0
|
||||
github.com/go-openapi/swag v0.23.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/stretchr/testify v1.9.0
|
||||
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/go-socks v0.0.0-20170105172521-4720035b7bfd // indirect
|
||||
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 // indirect
|
||||
github.com/btcsuite/winsvc v1.0.0 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/containerd/continuity v0.4.3 // 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/docker/docker v27.1.1+incompatible // 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/stdr v1.2.2 // 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/loads v0.22.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/grpc-ecosystem/grpc-gateway/v2 v2.21.0 // 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/jrick/logrotate v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/kkdai/bstream v1.0.0 // 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/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/fn v1.2.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/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // 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/runc v1.1.13 // indirect
|
||||
github.com/opentracing/opentracing-go v1.2.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // 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/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
|
||||
go.etcd.io/etcd/client/v2 v2.305.15 // indirect
|
||||
|
||||
@@ -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/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.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
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-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/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
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/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||
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.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.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc=
|
||||
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.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.26.0 h1:03cDLK28U6hWvCAns6NeydX3zIm4SF3ci69ulidS32Q=
|
||||
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/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/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/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/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
|
||||
@@ -67,60 +67,34 @@ func CoinSelect(
|
||||
}
|
||||
|
||||
func ParseLiquidAddress(addr string) (
|
||||
bool, []byte, *secp256k1.PublicKey, error,
|
||||
bool, []byte, error,
|
||||
) {
|
||||
outputScript, err := address.ToOutputScript(addr)
|
||||
if err != nil {
|
||||
_, userPubkey, _, err := common.DecodeAddress(addr)
|
||||
if err != nil {
|
||||
return false, nil, nil, err
|
||||
}
|
||||
return false, nil, userPubkey, nil
|
||||
return false, nil, nil
|
||||
}
|
||||
|
||||
return true, outputScript, nil, nil
|
||||
return true, outputScript, nil
|
||||
}
|
||||
|
||||
func ParseBitcoinAddress(addr string, net chaincfg.Params) (
|
||||
bool, []byte, *secp256k1.PublicKey, error,
|
||||
bool, []byte, error,
|
||||
) {
|
||||
btcAddr, err := btcutil.DecodeAddress(addr, &net)
|
||||
if err != nil {
|
||||
_, userPubkey, _, err := common.DecodeAddress(addr)
|
||||
if err != nil {
|
||||
return false, nil, nil, err
|
||||
}
|
||||
return false, nil, userPubkey, nil
|
||||
return false, nil, nil
|
||||
}
|
||||
|
||||
onchainScript, err := txscript.PayToAddrScript(btcAddr)
|
||||
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 {
|
||||
isOnChain, _, _, err := ParseBitcoinAddress(receiver.Address, net)
|
||||
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
|
||||
}
|
||||
isOnChain := len(receiver.Address) > 0
|
||||
|
||||
if !isOnChain {
|
||||
return false
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
|
||||
"github.com/ark-network/ark/common"
|
||||
"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/internal/utils"
|
||||
"github.com/ark-network/ark/pkg/client-sdk/store"
|
||||
@@ -219,9 +218,13 @@ func (w *bitcoinWallet) getAddress(
|
||||
|
||||
netParams := utils.ToBitcoinNetwork(data.Network)
|
||||
|
||||
vtxoTapKey, _, err := bitcointree.ComputeVtxoTaprootScript(
|
||||
w.walletData.Pubkey, data.AspPubkey, uint(data.UnilateralExitDelay),
|
||||
)
|
||||
defaultVtxoScript := &bitcointree.DefaultVtxoScript{
|
||||
Asp: data.AspPubkey,
|
||||
Owner: w.walletData.Pubkey,
|
||||
ExitDelay: uint(data.UnilateralExitDelay),
|
||||
}
|
||||
|
||||
vtxoTapKey, _, err := defaultVtxoScript.TapTree()
|
||||
if err != nil {
|
||||
return "", "", "", err
|
||||
}
|
||||
@@ -239,19 +242,12 @@ func (w *bitcoinWallet) getAddress(
|
||||
data.BoardingDescriptorTemplate, "USER", myPubkeyStr,
|
||||
)
|
||||
|
||||
desc, err := descriptor.ParseTaprootDescriptor(descriptorStr)
|
||||
boardingVtxoScript, err := bitcointree.ParseVtxoScript(descriptorStr)
|
||||
if err != nil {
|
||||
return "", "", "", err
|
||||
}
|
||||
|
||||
_, boardingTimeout, err := descriptor.ParseBoardingDescriptor(*desc)
|
||||
if err != nil {
|
||||
return "", "", "", err
|
||||
}
|
||||
|
||||
boardingTapKey, _, err := bitcointree.ComputeVtxoTaprootScript(
|
||||
w.walletData.Pubkey, data.AspPubkey, boardingTimeout,
|
||||
)
|
||||
boardingTapKey, _, err := boardingVtxoScript.TapTree()
|
||||
if err != nil {
|
||||
return "", "", "", err
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"strings"
|
||||
|
||||
"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/pkg/client-sdk/explorer"
|
||||
"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/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/vulpemventures/go-elements/payment"
|
||||
"github.com/vulpemventures/go-elements/psetv2"
|
||||
"github.com/vulpemventures/go-elements/transaction"
|
||||
)
|
||||
@@ -165,7 +165,7 @@ func (s *liquidWallet) SignTransaction(
|
||||
switch c := closure.(type) {
|
||||
case *tree.CSVSigClosure:
|
||||
sign = bytes.Equal(c.Pubkey.SerializeCompressed()[1:], serializedPubKey[1:])
|
||||
case *tree.ForfeitClosure:
|
||||
case *tree.MultisigClosure:
|
||||
sign = bytes.Equal(c.Pubkey.SerializeCompressed()[1:], serializedPubKey[1:])
|
||||
}
|
||||
|
||||
@@ -242,9 +242,23 @@ func (w *liquidWallet) getAddress(
|
||||
|
||||
liquidNet := utils.ToElementsNetwork(data.Network)
|
||||
|
||||
_, _, _, redemptionAddr, err := tree.ComputeVtxoTaprootScript(
|
||||
w.walletData.Pubkey, data.AspPubkey, uint(data.UnilateralExitDelay), liquidNet,
|
||||
)
|
||||
vtxoScript := &tree.DefaultVtxoScript{
|
||||
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 {
|
||||
return "", "", "", err
|
||||
}
|
||||
@@ -254,19 +268,22 @@ func (w *liquidWallet) getAddress(
|
||||
data.BoardingDescriptorTemplate, "USER", myPubkeyStr,
|
||||
)
|
||||
|
||||
desc, err := descriptor.ParseTaprootDescriptor(descriptorStr)
|
||||
onboardingScript, err := tree.ParseVtxoScript(descriptorStr)
|
||||
if err != nil {
|
||||
return "", "", "", err
|
||||
}
|
||||
|
||||
_, boardingTimeout, err := descriptor.ParseBoardingDescriptor(*desc)
|
||||
tapKey, _, err := onboardingScript.TapTree()
|
||||
if err != nil {
|
||||
return "", "", "", err
|
||||
}
|
||||
|
||||
_, _, _, boardingAddr, err := tree.ComputeVtxoTaprootScript(
|
||||
w.walletData.Pubkey, data.AspPubkey, boardingTimeout, liquidNet,
|
||||
)
|
||||
p2tr, err := payment.FromTweakedKey(tapKey, &liquidNet, nil)
|
||||
if err != nil {
|
||||
return "", "", "", err
|
||||
}
|
||||
|
||||
boardingAddr, err := p2tr.TaprootAddress()
|
||||
if err != nil {
|
||||
return "", "", "", err
|
||||
}
|
||||
|
||||
@@ -277,11 +277,11 @@ func (c *Config) txBuilderService() error {
|
||||
switch c.TxBuilderType {
|
||||
case "covenant":
|
||||
svc = txbuilder.NewTxBuilder(
|
||||
c.wallet, c.Network, c.RoundLifetime, c.UnilateralExitDelay, c.BoardingExitDelay,
|
||||
c.wallet, c.Network, c.RoundLifetime, c.BoardingExitDelay,
|
||||
)
|
||||
case "covenantless":
|
||||
svc = cltxbuilder.NewTxBuilder(
|
||||
c.wallet, c.Network, c.RoundLifetime, c.UnilateralExitDelay, c.BoardingExitDelay,
|
||||
c.wallet, c.Network, c.RoundLifetime, c.BoardingExitDelay,
|
||||
)
|
||||
default:
|
||||
err = fmt.Errorf("unknown tx builder type")
|
||||
|
||||
@@ -18,6 +18,8 @@ import (
|
||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"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/transaction"
|
||||
)
|
||||
@@ -118,159 +120,158 @@ func (s *covenantService) Stop() {
|
||||
close(s.eventsCh)
|
||||
}
|
||||
|
||||
func (s *covenantService) GetBoardingAddress(ctx context.Context, userPubkey *secp256k1.PublicKey) (string, error) {
|
||||
addr, _, err := s.builder.GetBoardingScript(userPubkey, s.pubkey)
|
||||
if err != nil {
|
||||
return "", err
|
||||
func (s *covenantService) GetBoardingAddress(ctx context.Context, userPubkey *secp256k1.PublicKey) (string, string, error) {
|
||||
vtxoScript := &tree.DefaultVtxoScript{
|
||||
Asp: s.pubkey,
|
||||
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) {
|
||||
vtxosInputs := make([]domain.VtxoKey, 0)
|
||||
boardingInputs := make([]Input, 0)
|
||||
func (s *covenantService) SpendVtxos(ctx context.Context, inputs []ports.Input) (string, error) {
|
||||
vtxosInputs := make([]domain.Vtxo, 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()
|
||||
|
||||
for _, in := range boardingInputs {
|
||||
if _, ok := boardingTxs[in.Txid]; !ok {
|
||||
txhex, err := s.wallet.GetTransaction(ctx, in.Txid)
|
||||
boardingTxs := make(map[string]*transaction.Transaction, 0) // txid -> txhex
|
||||
|
||||
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 {
|
||||
return "", fmt.Errorf("failed to get tx %s: %s", input.Txid, err)
|
||||
}
|
||||
|
||||
tx, err := transaction.NewTxFromHex(txhex)
|
||||
if err != nil {
|
||||
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 {
|
||||
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 {
|
||||
return "", fmt.Errorf("tx %s expired", input.Txid)
|
||||
}
|
||||
|
||||
boardingTxs[input.Txid] = tx
|
||||
}
|
||||
|
||||
tx := boardingTxs[input.Txid]
|
||||
boardingInput, err := s.newBoardingInput(tx, input)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get tx %s: %s", in.Txid, err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
confirmed, blocktime, err := s.wallet.IsTransactionConfirmed(ctx, in.Txid)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to check tx %s: %s", in.Txid, err)
|
||||
}
|
||||
|
||||
if !confirmed {
|
||||
return "", fmt.Errorf("tx %s not confirmed", in.Txid)
|
||||
}
|
||||
|
||||
if blocktime+int64(s.boardingExitDelay) < now {
|
||||
return "", fmt.Errorf("tx %s expired", in.Txid)
|
||||
}
|
||||
|
||||
boardingTxs[in.Txid] = txhex
|
||||
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)
|
||||
}
|
||||
|
||||
utxos := make([]ports.BoardingInput, 0, len(boardingInputs))
|
||||
|
||||
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)
|
||||
payment, err := domain.NewPayment(vtxosInputs)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := s.paymentRequests.push(*payment, utxos); err != nil {
|
||||
if err := s.paymentRequests.push(*payment, boardingInputs); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return payment.Id, nil
|
||||
}
|
||||
|
||||
func (s *covenantService) newBoardingInput(
|
||||
txhex string, vout uint32, desc descriptor.TaprootDescriptor,
|
||||
) (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) {
|
||||
func (s *covenantService) newBoardingInput(tx *transaction.Transaction, input ports.Input) (*ports.BoardingInput, error) {
|
||||
if len(tx.Outputs) <= int(input.VtxoKey.VOut) {
|
||||
return nil, fmt.Errorf("output not found")
|
||||
}
|
||||
|
||||
out := tx.Outputs[vout]
|
||||
script := out.Script
|
||||
output := tx.Outputs[input.VtxoKey.VOut]
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
scriptFromDescriptor, err := tree.ComputeOutputScript(desc)
|
||||
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)
|
||||
amount, err := elementsutil.ValueFromBytes(output.Value)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse value: %s", err)
|
||||
}
|
||||
|
||||
return &boardingInput{
|
||||
txId: tx.TxHash(),
|
||||
vout: vout,
|
||||
boardingPubKey: pubkey,
|
||||
amount: value,
|
||||
boardingScript, err := tree.ParseVtxoScript(input.Descriptor)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse boarding descriptor: %s", err)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -315,7 +316,7 @@ func (s *covenantService) CompleteAsyncPayment(ctx context.Context, redeemTx str
|
||||
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")
|
||||
}
|
||||
|
||||
@@ -373,10 +374,10 @@ func (s *covenantService) GetInfo(ctx context.Context) (*ServiceInfo, error) {
|
||||
Network: s.network.Name,
|
||||
Dust: dust,
|
||||
BoardingDescriptorTemplate: fmt.Sprintf(
|
||||
descriptor.BoardingDescriptorTemplate,
|
||||
descriptor.DefaultVtxoDescriptorTemplate,
|
||||
hex.EncodeToString(tree.UnspendableKey().SerializeCompressed()),
|
||||
hex.EncodeToString(schnorr.SerializePubKey(s.pubkey)),
|
||||
"USER",
|
||||
hex.EncodeToString(schnorr.SerializePubKey(s.pubkey)),
|
||||
s.boardingExitDelay,
|
||||
"USER",
|
||||
),
|
||||
@@ -496,8 +497,10 @@ func (s *covenantService) startFinalization() {
|
||||
|
||||
var forfeitTxs, connectors []string
|
||||
|
||||
minRelayFeeRate := s.wallet.MinRelayFeeRate(ctx)
|
||||
|
||||
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 {
|
||||
round.Fail(fmt.Errorf("failed to create connectors and forfeit txs: %s", err))
|
||||
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)
|
||||
|
||||
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(
|
||||
@@ -515,8 +524,6 @@ func (s *covenantService) startFinalization() {
|
||||
return
|
||||
}
|
||||
|
||||
s.forfeitTxs.push(forfeitTxs)
|
||||
|
||||
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]
|
||||
switch e := lastEvent.(type) {
|
||||
case domain.RoundFinalizationStarted:
|
||||
forfeitTxs := s.forfeitTxs.view()
|
||||
ev := domain.RoundFinalizationStarted{
|
||||
Id: e.Id,
|
||||
CongestionTree: e.CongestionTree,
|
||||
Connectors: e.Connectors,
|
||||
PoolTx: e.PoolTx,
|
||||
UnsignedForfeitTxs: forfeitTxs,
|
||||
Id: e.Id,
|
||||
CongestionTree: e.CongestionTree,
|
||||
Connectors: e.Connectors,
|
||||
PoolTx: e.PoolTx,
|
||||
MinRelayFeeRate: int64(s.wallet.MinRelayFeeRate(context.Background())),
|
||||
}
|
||||
s.lastEvent = ev
|
||||
s.eventsCh <- ev
|
||||
@@ -888,31 +894,56 @@ func (s *covenantService) getNewVtxos(round *domain.Round) []domain.Vtxo {
|
||||
for _, node := range leaves {
|
||||
tx, _ := psetv2.NewPsetFromBase64(node.Tx)
|
||||
for i, out := range tx.Outputs {
|
||||
if len(out.Script) <= 0 {
|
||||
continue // skip fee outputs
|
||||
}
|
||||
|
||||
desc := ""
|
||||
found := false
|
||||
|
||||
for _, p := range round.Payments {
|
||||
var pubkey string
|
||||
found := false
|
||||
if found {
|
||||
break
|
||||
}
|
||||
|
||||
for _, r := range p.Receivers {
|
||||
if r.IsOnchain() {
|
||||
continue
|
||||
}
|
||||
|
||||
buf, _ := hex.DecodeString(r.Pubkey)
|
||||
pk, _ := secp256k1.ParsePubKey(buf)
|
||||
script, _ := s.builder.GetVtxoScript(pk, s.pubkey)
|
||||
vtxoScript, err := tree.ParseVtxoScript(r.Descriptor)
|
||||
if err != nil {
|
||||
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) {
|
||||
found = true
|
||||
pubkey = r.Pubkey
|
||||
desc = r.Descriptor
|
||||
break
|
||||
}
|
||||
}
|
||||
if found {
|
||||
vtxos = append(vtxos, domain.Vtxo{
|
||||
VtxoKey: domain.VtxoKey{Txid: node.Txid, VOut: uint32(i)},
|
||||
Receiver: domain.Receiver{Pubkey: pubkey, Amount: out.Value},
|
||||
PoolTx: round.Txid,
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if found {
|
||||
vtxos = append(vtxos, domain.Vtxo{
|
||||
VtxoKey: domain.VtxoKey{Txid: node.Txid, VOut: uint32(i)},
|
||||
Receiver: domain.Receiver{Descriptor: desc, Amount: uint64(out.Value)},
|
||||
PoolTx: round.Txid,
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -984,15 +1015,17 @@ func (s *covenantService) restoreWatchingVtxos() error {
|
||||
func (s *covenantService) extractVtxosScripts(vtxos []domain.Vtxo) ([]string, error) {
|
||||
indexedScripts := make(map[string]struct{})
|
||||
for _, vtxo := range vtxos {
|
||||
buf, err := hex.DecodeString(vtxo.Pubkey)
|
||||
vtxoScript, err := tree.ParseVtxoScript(vtxo.Receiver.Descriptor)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
userPubkey, err := secp256k1.ParsePubKey(buf)
|
||||
|
||||
tapKey, _, err := vtxoScript.TapTree()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
script, err := s.builder.GetVtxoScript(userPubkey, s.pubkey)
|
||||
|
||||
script, err := common.P2TRScript(tapKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1019,6 +1052,19 @@ func (s *covenantService) saveEvents(
|
||||
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(
|
||||
forfeits []string, connectorTxid string, connectorVout uint32, vtxoTxid string,
|
||||
) (string, error) {
|
||||
|
||||
@@ -16,7 +16,9 @@ import (
|
||||
"github.com/ark-network/ark/server/internal/core/domain"
|
||||
"github.com/ark-network/ark/server/internal/core/ports"
|
||||
"github.com/btcsuite/btcd/btcec/v2/schnorr"
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/btcsuite/btcd/btcutil/psbt"
|
||||
"github.com/btcsuite/btcd/chaincfg"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||
@@ -153,10 +155,9 @@ func (s *covenantlessService) CompleteAsyncPayment(
|
||||
return fmt.Errorf("async payment not found")
|
||||
}
|
||||
|
||||
txs := append([]string{redeemTx}, unconditionalForfeitTxs...)
|
||||
vtxoRepo := s.repoManager.Vtxos()
|
||||
|
||||
for _, tx := range txs {
|
||||
for _, tx := range []string{redeemTx} {
|
||||
ptx, err := psbt.NewFromRawBytes(strings.NewReader(tx), true)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse tx: %s", err)
|
||||
@@ -200,39 +201,40 @@ func (s *covenantlessService) CompleteAsyncPayment(
|
||||
return fmt.Errorf("vtxo already swept")
|
||||
}
|
||||
|
||||
// verify that the user signs the tx using the right public key
|
||||
|
||||
vtxoPublicKey, err := hex.DecodeString(vtxo[0].Pubkey)
|
||||
vtxoScript, err := bitcointree.ParseVtxoScript(vtxo[0].Descriptor)
|
||||
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 parse pubkey: %s", err)
|
||||
return fmt.Errorf("failed to get taproot key: %s", err)
|
||||
}
|
||||
|
||||
xonlyPubkey := schnorr.SerializePubKey(pubkey)
|
||||
// verify that the user signs a forfeit closure
|
||||
var userPubKey *secp256k1.PublicKey
|
||||
|
||||
// find signature belonging to the pubkey
|
||||
found := false
|
||||
aspXOnlyPubKey := schnorr.SerializePubKey(s.pubkey)
|
||||
|
||||
for _, sig := range input.TaprootScriptSpendSig {
|
||||
if bytes.Equal(sig.XOnlyPubKey, xonlyPubkey) {
|
||||
found = true
|
||||
if !bytes.Equal(sig.XOnlyPubKey, aspXOnlyPubKey) {
|
||||
parsed, err := schnorr.ParsePubKey(sig.XOnlyPubKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse pubkey: %s", err)
|
||||
}
|
||||
userPubKey = parsed
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
return fmt.Errorf("signature not found for pubkey")
|
||||
if userPubKey == nil {
|
||||
return fmt.Errorf("redeem transaction is not signed")
|
||||
}
|
||||
|
||||
// verify witness utxo
|
||||
|
||||
pkscript, err := s.builder.GetVtxoScript(pubkey, s.pubkey)
|
||||
pkscript, err := common.P2TRScript(vtxoTapKey)
|
||||
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) {
|
||||
@@ -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 {
|
||||
spentVtxos = append(spentVtxos, domain.VtxoKey{
|
||||
Txid: in.PreviousOutPoint.Hash.String(),
|
||||
@@ -267,8 +269,8 @@ func (s *covenantlessService) CompleteAsyncPayment(
|
||||
VOut: uint32(outIndex),
|
||||
},
|
||||
Receiver: domain.Receiver{
|
||||
Pubkey: asyncPayData.receivers[outIndex].Pubkey,
|
||||
Amount: uint64(out.Value),
|
||||
Descriptor: asyncPayData.receivers[outIndex].Descriptor,
|
||||
Amount: uint64(out.Value),
|
||||
},
|
||||
ExpireAt: asyncPayData.expireAt,
|
||||
AsyncPayment: &domain.AsyncPaymentTxs{
|
||||
@@ -300,9 +302,14 @@ func (s *covenantlessService) CompleteAsyncPayment(
|
||||
}
|
||||
|
||||
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) {
|
||||
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 {
|
||||
return "", nil, err
|
||||
}
|
||||
@@ -310,6 +317,8 @@ func (s *covenantlessService) CreateAsyncPayment(
|
||||
return "", nil, fmt.Errorf("vtxos not found")
|
||||
}
|
||||
|
||||
vtxosInputs := make([]domain.Vtxo, 0, len(inputs))
|
||||
|
||||
expiration := vtxos[0].ExpireAt
|
||||
for _, vtxo := range vtxos {
|
||||
if vtxo.Spent {
|
||||
@@ -327,10 +336,12 @@ func (s *covenantlessService) CreateAsyncPayment(
|
||||
if vtxo.ExpireAt < expiration {
|
||||
expiration = vtxo.ExpireAt
|
||||
}
|
||||
|
||||
vtxosInputs = append(vtxosInputs, vtxo)
|
||||
}
|
||||
|
||||
res, err := s.builder.BuildAsyncPaymentTransactions(
|
||||
vtxos, s.pubkey, receivers,
|
||||
vtxosInputs, s.pubkey, receivers,
|
||||
)
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
func (s *covenantlessService) SpendVtxos(ctx context.Context, inputs []Input) (string, error) {
|
||||
vtxosInputs := make([]domain.VtxoKey, 0)
|
||||
boardingInputs := make([]Input, 0)
|
||||
func (s *covenantlessService) GetBoardingAddress(
|
||||
ctx context.Context, userPubkey *secp256k1.PublicKey,
|
||||
) (address string, descriptor string, err error) {
|
||||
vtxoScript := &bitcointree.DefaultVtxoScript{
|
||||
Asp: s.pubkey,
|
||||
Owner: userPubkey,
|
||||
ExitDelay: uint(s.boardingExitDelay),
|
||||
}
|
||||
|
||||
tapKey, _, err := vtxoScript.TapTree()
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to get taproot key: %s", err)
|
||||
}
|
||||
|
||||
addr, err := btcutil.NewAddressTaproot(
|
||||
schnorr.SerializePubKey(tapKey), s.chainParams(),
|
||||
)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to get address: %s", err)
|
||||
}
|
||||
|
||||
return addr.EncodeAddress(), vtxoScript.ToDescriptor(), nil
|
||||
}
|
||||
|
||||
func (s *covenantlessService) SpendVtxos(ctx context.Context, inputs []ports.Input) (string, error) {
|
||||
vtxosInputs := make([]domain.Vtxo, 0)
|
||||
boardingInputs := make([]ports.BoardingInput, 0)
|
||||
|
||||
now := time.Now().Unix()
|
||||
|
||||
boardingTxs := make(map[string]wire.MsgTx, 0) // txid -> txhex
|
||||
|
||||
for _, input := range inputs {
|
||||
if input.IsVtxo() {
|
||||
vtxosInputs = append(vtxosInputs, input.VtxoKey())
|
||||
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 {
|
||||
return "", fmt.Errorf("failed to get tx %s: %s", input.Txid, err)
|
||||
}
|
||||
|
||||
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 {
|
||||
return "", fmt.Errorf("failed to check tx %s: %s", input.Txid, err)
|
||||
}
|
||||
|
||||
if !confirmed {
|
||||
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 {
|
||||
return "", fmt.Errorf("tx %s expired", input.Txid)
|
||||
}
|
||||
|
||||
boardingTxs[input.Txid] = tx
|
||||
}
|
||||
|
||||
tx := boardingTxs[input.Txid]
|
||||
boardingInput, err := s.newBoardingInput(tx, input)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
boardingInputs = append(boardingInputs, *boardingInput)
|
||||
continue
|
||||
}
|
||||
|
||||
boardingInputs = append(boardingInputs, input)
|
||||
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)
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
for _, in := range boardingInputs {
|
||||
if _, ok := boardingTxs[in.Txid]; !ok {
|
||||
// check if the tx exists and is confirmed
|
||||
txhex, err := s.wallet.GetTransaction(ctx, in.Txid)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get tx %s: %s", in.Txid, err)
|
||||
}
|
||||
|
||||
confirmed, blocktime, err := s.wallet.IsTransactionConfirmed(ctx, in.Txid)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to check tx %s: %s", in.Txid, err)
|
||||
}
|
||||
|
||||
if !confirmed {
|
||||
return "", fmt.Errorf("tx %s not confirmed", in.Txid)
|
||||
}
|
||||
|
||||
// if the exit path is available, forbid registering the boarding utxo
|
||||
if blocktime+int64(s.boardingExitDelay) < now {
|
||||
return "", fmt.Errorf("tx %s expired", in.Txid)
|
||||
}
|
||||
|
||||
boardingTxs[in.Txid] = txhex
|
||||
}
|
||||
}
|
||||
|
||||
utxos := make([]ports.BoardingInput, 0, len(boardingInputs))
|
||||
|
||||
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)
|
||||
payment, err := domain.NewPayment(vtxosInputs)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := s.paymentRequests.push(*payment, utxos); err != nil {
|
||||
if err := s.paymentRequests.push(*payment, boardingInputs); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return payment.Id, nil
|
||||
}
|
||||
|
||||
func (s *covenantlessService) newBoardingInput(txhex string, vout uint32, desc descriptor.TaprootDescriptor) (ports.BoardingInput, error) {
|
||||
var tx wire.MsgTx
|
||||
|
||||
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) {
|
||||
func (s *covenantlessService) newBoardingInput(tx wire.MsgTx, input ports.Input) (*ports.BoardingInput, error) {
|
||||
if len(tx.TxOut) <= int(input.VtxoKey.VOut) {
|
||||
return nil, fmt.Errorf("output not found")
|
||||
}
|
||||
|
||||
out := tx.TxOut[vout]
|
||||
script := out.PkScript
|
||||
output := tx.TxOut[input.VtxoKey.VOut]
|
||||
|
||||
scriptFromDescriptor, err := bitcointree.ComputeOutputScript(desc)
|
||||
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)
|
||||
boardingScript, err := bitcointree.ParseVtxoScript(input.Descriptor)
|
||||
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)
|
||||
tapKey, _, err := boardingScript.TapTree()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get boarding script: %s", err)
|
||||
return nil, fmt.Errorf("failed to get taproot key: %s", err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(script, expectedScript) {
|
||||
return nil, fmt.Errorf("invalid boarding input output script")
|
||||
expectedScriptPubKey, err := common.P2TRScript(tapKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get script pubkey: %s", err)
|
||||
}
|
||||
|
||||
return &boardingInput{
|
||||
txId: tx.TxHash(),
|
||||
vout: vout,
|
||||
boardingPubKey: pubkey,
|
||||
amount: uint64(out.Value),
|
||||
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")
|
||||
}
|
||||
} else {
|
||||
return nil, fmt.Errorf("only default vtxo script is supported for boarding")
|
||||
}
|
||||
|
||||
return &ports.BoardingInput{
|
||||
Amount: uint64(output.Value),
|
||||
Input: input,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -584,27 +599,16 @@ func (s *covenantlessService) GetInfo(ctx context.Context) (*ServiceInfo, error)
|
||||
Network: s.network.Name,
|
||||
Dust: dust,
|
||||
BoardingDescriptorTemplate: fmt.Sprintf(
|
||||
descriptor.BoardingDescriptorTemplate,
|
||||
descriptor.DefaultVtxoDescriptorTemplate,
|
||||
hex.EncodeToString(bitcointree.UnspendableKey().SerializeCompressed()),
|
||||
hex.EncodeToString(schnorr.SerializePubKey(s.pubkey)),
|
||||
"USER",
|
||||
hex.EncodeToString(schnorr.SerializePubKey(s.pubkey)),
|
||||
s.boardingExitDelay,
|
||||
"USER",
|
||||
),
|
||||
}, 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 {
|
||||
pubkeyBytes, err := hex.DecodeString(pubkey)
|
||||
if err != nil {
|
||||
@@ -944,14 +948,22 @@ func (s *covenantlessService) startFinalization() {
|
||||
|
||||
var forfeitTxs, connectors []string
|
||||
|
||||
minRelayFeeRate := s.wallet.MinRelayFeeRate(ctx)
|
||||
|
||||
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 {
|
||||
round.Fail(fmt.Errorf("failed to create connectors and forfeit txs: %s", err))
|
||||
log.WithError(err).Warn("failed to create connectors and forfeit txs")
|
||||
return
|
||||
}
|
||||
log.Debugf("forfeit transactions created for round %s", round.Id)
|
||||
|
||||
if err := s.forfeitTxs.push(forfeitTxs); err != nil {
|
||||
round.Fail(fmt.Errorf("failed to store forfeit txs: %s", err))
|
||||
log.WithError(err).Warn("failed to store forfeit txs")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := round.StartFinalization(
|
||||
@@ -962,8 +974,6 @@ func (s *covenantlessService) startFinalization() {
|
||||
return
|
||||
}
|
||||
|
||||
s.forfeitTxs.push(forfeitTxs)
|
||||
|
||||
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]
|
||||
switch e := lastEvent.(type) {
|
||||
case domain.RoundFinalizationStarted:
|
||||
forfeitTxs := s.forfeitTxs.view()
|
||||
ev := domain.RoundFinalizationStarted{
|
||||
Id: e.Id,
|
||||
CongestionTree: e.CongestionTree,
|
||||
Connectors: e.Connectors,
|
||||
PoolTx: e.PoolTx,
|
||||
UnsignedForfeitTxs: forfeitTxs,
|
||||
Id: e.Id,
|
||||
CongestionTree: e.CongestionTree,
|
||||
Connectors: e.Connectors,
|
||||
PoolTx: e.PoolTx,
|
||||
MinRelayFeeRate: int64(s.wallet.MinRelayFeeRate(context.Background())),
|
||||
}
|
||||
s.lastEvent = ev
|
||||
s.eventsCh <- ev
|
||||
@@ -1291,36 +1300,52 @@ func (s *covenantlessService) getNewVtxos(round *domain.Round) []domain.Vtxo {
|
||||
continue
|
||||
}
|
||||
for i, out := range tx.UnsignedTx.TxOut {
|
||||
desc := ""
|
||||
found := false
|
||||
|
||||
for _, p := range round.Payments {
|
||||
var pubkey string
|
||||
found := false
|
||||
if found {
|
||||
break
|
||||
}
|
||||
|
||||
for _, r := range p.Receivers {
|
||||
if r.IsOnchain() {
|
||||
continue
|
||||
}
|
||||
|
||||
buf, _ := hex.DecodeString(r.Pubkey)
|
||||
pk, _ := secp256k1.ParsePubKey(buf)
|
||||
script, err := s.builder.GetVtxoScript(pk, s.pubkey)
|
||||
vtxoScript, err := bitcointree.ParseVtxoScript(r.Descriptor)
|
||||
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
|
||||
}
|
||||
|
||||
if bytes.Equal(script, out.PkScript) {
|
||||
found = true
|
||||
pubkey = r.Pubkey
|
||||
desc = r.Descriptor
|
||||
break
|
||||
}
|
||||
}
|
||||
if found {
|
||||
vtxos = append(vtxos, domain.Vtxo{
|
||||
VtxoKey: domain.VtxoKey{Txid: node.Txid, VOut: uint32(i)},
|
||||
Receiver: domain.Receiver{Pubkey: pubkey, Amount: uint64(out.Value)},
|
||||
PoolTx: round.Txid,
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if found {
|
||||
vtxos = append(vtxos, domain.Vtxo{
|
||||
VtxoKey: domain.VtxoKey{Txid: node.Txid, VOut: uint32(i)},
|
||||
Receiver: domain.Receiver{Descriptor: desc, Amount: uint64(out.Value)},
|
||||
PoolTx: round.Txid,
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1391,16 +1416,19 @@ func (s *covenantlessService) restoreWatchingVtxos() error {
|
||||
|
||||
func (s *covenantlessService) extractVtxosScripts(vtxos []domain.Vtxo) ([]string, error) {
|
||||
indexedScripts := make(map[string]struct{})
|
||||
|
||||
for _, vtxo := range vtxos {
|
||||
buf, err := hex.DecodeString(vtxo.Pubkey)
|
||||
vtxoScript, err := bitcointree.ParseVtxoScript(vtxo.Receiver.Descriptor)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
userPubkey, err := secp256k1.ParsePubKey(buf)
|
||||
|
||||
tapKey, _, err := vtxoScript.TapTree()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
script, err := s.builder.GetVtxoScript(userPubkey, s.pubkey)
|
||||
|
||||
script, err := common.P2TRScript(tapKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1427,6 +1455,19 @@ func (s *covenantlessService) saveEvents(
|
||||
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 {
|
||||
mutx.Lock()
|
||||
defer mutx.Unlock()
|
||||
|
||||
@@ -2,10 +2,9 @@ package application
|
||||
|
||||
import (
|
||||
"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/ports"
|
||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||
)
|
||||
|
||||
@@ -16,7 +15,7 @@ var (
|
||||
type Service interface {
|
||||
Start() error
|
||||
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
|
||||
SignVtxos(ctx context.Context, forfeitTxs []string) error
|
||||
SignRoundTx(ctx context.Context, roundTx string) error
|
||||
@@ -33,12 +32,14 @@ type Service interface {
|
||||
GetInfo(ctx context.Context) (*ServiceInfo, error)
|
||||
// Async payments
|
||||
CreateAsyncPayment(
|
||||
ctx context.Context, inputs []domain.VtxoKey, receivers []domain.Receiver,
|
||||
ctx context.Context, inputs []ports.Input, receivers []domain.Receiver,
|
||||
) (string, []string, error)
|
||||
CompleteAsyncPayment(
|
||||
ctx context.Context, redeemTx string, unconditionalForfeitTxs []string,
|
||||
) 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
|
||||
RegisterCosignerPubkey(ctx context.Context, paymentId string, ephemeralPublicKey string) error
|
||||
RegisterCosignerNonces(
|
||||
@@ -67,30 +68,6 @@ type WalletStatus struct {
|
||||
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 {
|
||||
txid string
|
||||
vout uint32
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
"github.com/ark-network/ark/common/tree"
|
||||
"github.com/ark-network/ark/server/internal/core/domain"
|
||||
"github.com/ark-network/ark/server/internal/core/ports"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
@@ -180,14 +179,19 @@ func newForfeitTxsMap(txBuilder ports.TxBuilder) *forfeitTxsMap {
|
||||
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()
|
||||
defer m.lock.Unlock()
|
||||
|
||||
for _, tx := range txs {
|
||||
signed, txid, _ := m.builder.VerifyTapscriptPartialSigs(tx)
|
||||
m.forfeitTxs[txid] = &signedTx{tx, signed}
|
||||
txid, err := m.builder.GetTxID(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.forfeitTxs[txid] = &signedTx{tx, false}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *forfeitTxsMap) sign(txs []string) error {
|
||||
@@ -229,17 +233,6 @@ func (m *forfeitTxsMap) pop() (signed, unsigned []string) {
|
||||
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
|
||||
// returns the sweepable outputs as ports.SweepInput mapped by their expiration time
|
||||
func findSweepableOutputs(
|
||||
@@ -313,26 +306,3 @@ func getSpentVtxos(payments map[string]domain.Payment) []domain.VtxoKey {
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
@@ -18,12 +18,12 @@ type RoundStarted struct {
|
||||
}
|
||||
|
||||
type RoundFinalizationStarted struct {
|
||||
Id string
|
||||
CongestionTree tree.CongestionTree // BTC: signed
|
||||
Connectors []string
|
||||
ConnectorAddress string
|
||||
UnsignedForfeitTxs []string
|
||||
PoolTx string
|
||||
Id string
|
||||
CongestionTree tree.CongestionTree // BTC: signed
|
||||
Connectors []string
|
||||
ConnectorAddress string
|
||||
PoolTx string
|
||||
MinRelayFeeRate int64
|
||||
}
|
||||
|
||||
type RoundFinalized struct {
|
||||
|
||||
@@ -68,7 +68,7 @@ func (p Payment) validate(ignoreOuts bool) error {
|
||||
return fmt.Errorf("missing outputs")
|
||||
}
|
||||
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")
|
||||
}
|
||||
}
|
||||
@@ -96,7 +96,7 @@ func (k VtxoKey) Hash() string {
|
||||
}
|
||||
|
||||
type Receiver struct {
|
||||
Pubkey string
|
||||
Descriptor string
|
||||
Amount uint64
|
||||
OnchainAddress string
|
||||
}
|
||||
|
||||
@@ -1,12 +1,23 @@
|
||||
package domain_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/ark-network/ark/common/descriptor"
|
||||
"github.com/ark-network/ark/server/internal/core/domain"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var desc = fmt.Sprintf(
|
||||
descriptor.DefaultVtxoDescriptorTemplate,
|
||||
"030000000000000000000000000000000000000000000000000000000000000001",
|
||||
"0000000000000000000000000000000000000000000000000000000000000001",
|
||||
"0000000000000000000000000000000000000000000000000000000000000001",
|
||||
512,
|
||||
"0000000000000000000000000000000000000000000000000000000000000001",
|
||||
)
|
||||
|
||||
var inputs = []domain.Vtxo{
|
||||
{
|
||||
VtxoKey: domain.VtxoKey{
|
||||
@@ -14,8 +25,8 @@ var inputs = []domain.Vtxo{
|
||||
VOut: 0,
|
||||
},
|
||||
Receiver: domain.Receiver{
|
||||
Pubkey: "030000000000000000000000000000000000000000000000000000000000000001",
|
||||
Amount: 1000,
|
||||
Descriptor: desc,
|
||||
Amount: 1000,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -40,12 +51,12 @@ func TestPayment(t *testing.T) {
|
||||
|
||||
err = payment.AddReceivers([]domain.Receiver{
|
||||
{
|
||||
Pubkey: "030000000000000000000000000000000000000000000000000000000000000001",
|
||||
Amount: 450,
|
||||
Descriptor: desc,
|
||||
Amount: 450,
|
||||
},
|
||||
{
|
||||
Pubkey: "020000000000000000000000000000000000000000000000000000000000000002",
|
||||
Amount: 550,
|
||||
Descriptor: desc,
|
||||
Amount: 550,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -14,28 +14,30 @@ var (
|
||||
payments = []domain.Payment{
|
||||
{
|
||||
Id: "0",
|
||||
Inputs: []domain.Vtxo{{
|
||||
VtxoKey: domain.VtxoKey{
|
||||
Txid: txid,
|
||||
VOut: 0,
|
||||
Inputs: []domain.Vtxo{
|
||||
{
|
||||
VtxoKey: domain.VtxoKey{
|
||||
Txid: txid,
|
||||
VOut: 0,
|
||||
},
|
||||
Receiver: domain.Receiver{
|
||||
Descriptor: desc,
|
||||
Amount: 2000,
|
||||
},
|
||||
},
|
||||
Receiver: domain.Receiver{
|
||||
Pubkey: pubkey,
|
||||
Amount: 2000,
|
||||
},
|
||||
}},
|
||||
},
|
||||
Receivers: []domain.Receiver{
|
||||
{
|
||||
Pubkey: pubkey,
|
||||
Amount: 700,
|
||||
Descriptor: desc,
|
||||
Amount: 700,
|
||||
},
|
||||
{
|
||||
Pubkey: pubkey,
|
||||
Amount: 700,
|
||||
Descriptor: desc,
|
||||
Amount: 700,
|
||||
},
|
||||
{
|
||||
Pubkey: pubkey,
|
||||
Amount: 600,
|
||||
Descriptor: desc,
|
||||
Amount: 600,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -48,8 +50,8 @@ var (
|
||||
VOut: 0,
|
||||
},
|
||||
Receiver: domain.Receiver{
|
||||
Pubkey: pubkey,
|
||||
Amount: 1000,
|
||||
Descriptor: desc,
|
||||
Amount: 1000,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -58,21 +60,20 @@ var (
|
||||
VOut: 0,
|
||||
},
|
||||
Receiver: domain.Receiver{
|
||||
Pubkey: pubkey,
|
||||
Amount: 1000,
|
||||
Descriptor: desc,
|
||||
Amount: 1000,
|
||||
},
|
||||
},
|
||||
},
|
||||
Receivers: []domain.Receiver{{
|
||||
Pubkey: pubkey,
|
||||
Amount: 2000,
|
||||
Descriptor: desc,
|
||||
Amount: 2000,
|
||||
}},
|
||||
},
|
||||
}
|
||||
emptyPtx = "cHNldP8BAgQCAAAAAQQBAAEFAQABBgEDAfsEAgAAAAA="
|
||||
emptyTx = "0200000000000000000000"
|
||||
txid = "0000000000000000000000000000000000000000000000000000000000000000"
|
||||
pubkey = "030000000000000000000000000000000000000000000000000000000000000001"
|
||||
congestionTree = tree.CongestionTree{
|
||||
{
|
||||
{
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"github.com/ark-network/ark/server/internal/core/domain"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||
)
|
||||
|
||||
type SweepInput interface {
|
||||
@@ -16,11 +17,14 @@ type SweepInput interface {
|
||||
GetInternalKey() *secp256k1.PublicKey
|
||||
}
|
||||
|
||||
type BoardingInput interface {
|
||||
GetAmount() uint64
|
||||
GetIndex() uint32
|
||||
GetHash() chainhash.Hash
|
||||
GetBoardingPubkey() *secp256k1.PublicKey
|
||||
type Input struct {
|
||||
domain.VtxoKey
|
||||
Descriptor string
|
||||
}
|
||||
|
||||
type BoardingInput struct {
|
||||
Input
|
||||
Amount uint64
|
||||
}
|
||||
|
||||
type TxBuilder interface {
|
||||
@@ -28,9 +32,8 @@ type TxBuilder interface {
|
||||
aspPubkey *secp256k1.PublicKey, payments []domain.Payment, boardingInputs []BoardingInput, sweptRounds []domain.Round,
|
||||
cosigners ...*secp256k1.PublicKey,
|
||||
) (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)
|
||||
GetVtxoScript(userPubkey, aspPubkey *secp256k1.PublicKey) ([]byte, error)
|
||||
GetSweepInput(parentblocktime int64, node tree.Node) (expirationtime int64, sweepInput SweepInput, err error)
|
||||
FinalizeAndExtract(tx string) (txhex string, err error)
|
||||
VerifyTapscriptPartialSigs(tx string) (valid bool, txid string, err error)
|
||||
@@ -40,6 +43,6 @@ type TxBuilder interface {
|
||||
vtxosToSpend []domain.Vtxo,
|
||||
aspPubKey *secp256k1.PublicKey, receivers []domain.Receiver,
|
||||
) (*domain.AsyncPaymentTxs, error)
|
||||
GetBoardingScript(userPubkey, aspPubkey *secp256k1.PublicKey) (addr string, script []byte, err error)
|
||||
VerifyAndCombinePartialTx(dest string, src string) (string, error)
|
||||
GetTxID(tx string) (string, error)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
|
||||
"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.
|
||||
@@ -30,6 +31,7 @@ type WalletService interface {
|
||||
WaitForSync(ctx context.Context, txid string) error
|
||||
EstimateFees(ctx context.Context, psbt string) (uint64, error)
|
||||
MinRelayFee(ctx context.Context, vbytes uint64) (uint64, error)
|
||||
MinRelayFeeRate(ctx context.Context) chainfee.SatPerKVByte
|
||||
ListConnectorUtxos(ctx context.Context, connectorAddress string) ([]TxInput, error)
|
||||
MainAccountBalance(ctx context.Context) (uint64, uint64, error)
|
||||
ConnectorsAccountBalance(ctx context.Context) (uint64, uint64, error)
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/ark-network/ark/server/internal/core/domain"
|
||||
@@ -100,7 +101,13 @@ func (r *vtxoRepository) GetAllVtxos(
|
||||
) ([]domain.Vtxo, []domain.Vtxo, error) {
|
||||
query := badgerhold.Where("Redeemed").Eq(false)
|
||||
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)
|
||||
if err != nil {
|
||||
|
||||
@@ -4,12 +4,14 @@ import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
"sort"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ark-network/ark/common/descriptor"
|
||||
"github.com/ark-network/ark/common/tree"
|
||||
"github.com/ark-network/ark/server/internal/core/domain"
|
||||
"github.com/ark-network/ark/server/internal/core/ports"
|
||||
@@ -22,8 +24,26 @@ import (
|
||||
const (
|
||||
emptyPtx = "cHNldP8BAgQCAAAAAQQBAAEFAQABBgEDAfsEAgAAAAA="
|
||||
emptyTx = "0200000000000000000000"
|
||||
pubkey1 = "0300000000000000000000000000000000000000000000000000000000000000001"
|
||||
pubkey2 = "0200000000000000000000000000000000000000000000000000000000000000002"
|
||||
pubkey1 = "00000000000000000000000000000000000000000000000000000000000000001"
|
||||
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{
|
||||
@@ -251,19 +271,20 @@ func testRoundRepository(t *testing.T, svc ports.RepoManager) {
|
||||
PoolTx: randomString(32),
|
||||
ExpireAt: 7980322,
|
||||
Receiver: domain.Receiver{
|
||||
Pubkey: randomString(36),
|
||||
Amount: 300,
|
||||
Descriptor: randomString(120),
|
||||
Amount: 300,
|
||||
},
|
||||
},
|
||||
},
|
||||
Receivers: []domain.Receiver{{
|
||||
Pubkey: randomString(36),
|
||||
Amount: 300,
|
||||
Descriptor: randomString(120),
|
||||
Amount: 300,
|
||||
}},
|
||||
},
|
||||
{
|
||||
Id: uuid.New().String(),
|
||||
Inputs: []domain.Vtxo{
|
||||
|
||||
{
|
||||
VtxoKey: domain.VtxoKey{
|
||||
Txid: randomString(32),
|
||||
@@ -272,19 +293,19 @@ func testRoundRepository(t *testing.T, svc ports.RepoManager) {
|
||||
PoolTx: randomString(32),
|
||||
ExpireAt: 7980322,
|
||||
Receiver: domain.Receiver{
|
||||
Pubkey: randomString(36),
|
||||
Amount: 600,
|
||||
Descriptor: randomString(120),
|
||||
Amount: 600,
|
||||
},
|
||||
},
|
||||
},
|
||||
Receivers: []domain.Receiver{
|
||||
{
|
||||
Pubkey: randomString(36),
|
||||
Amount: 400,
|
||||
Descriptor: randomString(120),
|
||||
Amount: 400,
|
||||
},
|
||||
{
|
||||
Pubkey: randomString(34),
|
||||
Amount: 200,
|
||||
Descriptor: randomString(120),
|
||||
Amount: 200,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -350,8 +371,8 @@ func testVtxoRepository(t *testing.T, svc ports.RepoManager) {
|
||||
VOut: 0,
|
||||
},
|
||||
Receiver: domain.Receiver{
|
||||
Pubkey: pubkey1,
|
||||
Amount: 1000,
|
||||
Descriptor: desc1,
|
||||
Amount: 1000,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -360,8 +381,8 @@ func testVtxoRepository(t *testing.T, svc ports.RepoManager) {
|
||||
VOut: 1,
|
||||
},
|
||||
Receiver: domain.Receiver{
|
||||
Pubkey: pubkey1,
|
||||
Amount: 2000,
|
||||
Descriptor: desc1,
|
||||
Amount: 2000,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -371,8 +392,8 @@ func testVtxoRepository(t *testing.T, svc ports.RepoManager) {
|
||||
VOut: 1,
|
||||
},
|
||||
Receiver: domain.Receiver{
|
||||
Pubkey: pubkey2,
|
||||
Amount: 2000,
|
||||
Descriptor: desc2,
|
||||
Amount: 2000,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -531,7 +552,7 @@ type sortReceivers []domain.Receiver
|
||||
|
||||
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) 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
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -165,7 +165,7 @@ func (r *roundRepository) AddOrUpdateRound(ctx context.Context, round domain.Rou
|
||||
ctx,
|
||||
queries.UpsertReceiverParams{
|
||||
PaymentID: payment.Id,
|
||||
Pubkey: receiver.Pubkey,
|
||||
Descriptor: receiver.Descriptor,
|
||||
Amount: int64(receiver.Amount),
|
||||
OnchainAddress: receiver.OnchainAddress,
|
||||
},
|
||||
@@ -320,7 +320,7 @@ func (r *roundRepository) GetSweptRounds(ctx context.Context) ([]domain.Round, e
|
||||
|
||||
func rowToReceiver(row queries.PaymentReceiverVw) domain.Receiver {
|
||||
return domain.Receiver{
|
||||
Pubkey: row.Pubkey.String,
|
||||
Descriptor: row.Descriptor.String,
|
||||
Amount: uint64(row.Amount.Int64),
|
||||
OnchainAddress: row.OnchainAddress.String,
|
||||
}
|
||||
@@ -413,8 +413,8 @@ func readRoundRows(rows []roundPaymentTxReceiverVtxoRow) ([]*domain.Round, error
|
||||
|
||||
found := false
|
||||
for _, rcv := range payment.Receivers {
|
||||
if v.receiver.Pubkey.Valid && v.receiver.Amount.Valid {
|
||||
if rcv.Pubkey == v.receiver.Pubkey.String && int64(rcv.Amount) == v.receiver.Amount.Int64 {
|
||||
if v.receiver.Descriptor.Valid && v.receiver.Amount.Valid {
|
||||
if rcv.Descriptor == v.receiver.Descriptor.String && int64(rcv.Amount) == v.receiver.Amount.Int64 {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
@@ -470,8 +470,8 @@ func rowToPaymentVtxoVw(row queries.PaymentVtxoVw) domain.Vtxo {
|
||||
VOut: uint32(row.Vout.Int64),
|
||||
},
|
||||
Receiver: domain.Receiver{
|
||||
Pubkey: row.Pubkey.String,
|
||||
Amount: uint64(row.Amount.Int64),
|
||||
Descriptor: row.Descriptor.String,
|
||||
Amount: uint64(row.Amount.Int64),
|
||||
},
|
||||
PoolTx: row.PoolTx.String,
|
||||
SpentBy: row.SpentBy.String,
|
||||
|
||||
@@ -15,29 +15,29 @@ type Payment struct {
|
||||
|
||||
type PaymentReceiverVw struct {
|
||||
PaymentID sql.NullString
|
||||
Pubkey sql.NullString
|
||||
Descriptor sql.NullString
|
||||
Amount sql.NullInt64
|
||||
OnchainAddress sql.NullString
|
||||
}
|
||||
|
||||
type PaymentVtxoVw struct {
|
||||
Txid sql.NullString
|
||||
Vout sql.NullInt64
|
||||
Pubkey sql.NullString
|
||||
Amount sql.NullInt64
|
||||
PoolTx sql.NullString
|
||||
SpentBy sql.NullString
|
||||
Spent sql.NullBool
|
||||
Redeemed sql.NullBool
|
||||
Swept sql.NullBool
|
||||
ExpireAt sql.NullInt64
|
||||
PaymentID sql.NullString
|
||||
RedeemTx sql.NullString
|
||||
Txid sql.NullString
|
||||
Vout sql.NullInt64
|
||||
Amount sql.NullInt64
|
||||
PoolTx sql.NullString
|
||||
SpentBy sql.NullString
|
||||
Spent sql.NullBool
|
||||
Redeemed sql.NullBool
|
||||
Swept sql.NullBool
|
||||
ExpireAt sql.NullInt64
|
||||
PaymentID sql.NullString
|
||||
RedeemTx sql.NullString
|
||||
Descriptor sql.NullString
|
||||
}
|
||||
|
||||
type Receiver struct {
|
||||
PaymentID string
|
||||
Pubkey string
|
||||
Descriptor string
|
||||
Amount int64
|
||||
OnchainAddress string
|
||||
}
|
||||
@@ -103,16 +103,16 @@ type UncondForfeitTxVw struct {
|
||||
}
|
||||
|
||||
type Vtxo struct {
|
||||
Txid string
|
||||
Vout int64
|
||||
Pubkey string
|
||||
Amount int64
|
||||
PoolTx string
|
||||
SpentBy string
|
||||
Spent bool
|
||||
Redeemed bool
|
||||
Swept bool
|
||||
ExpireAt int64
|
||||
PaymentID sql.NullString
|
||||
RedeemTx sql.NullString
|
||||
Txid string
|
||||
Vout int64
|
||||
Amount int64
|
||||
PoolTx string
|
||||
SpentBy string
|
||||
Spent bool
|
||||
Redeemed bool
|
||||
Swept bool
|
||||
ExpireAt int64
|
||||
PaymentID sql.NullString
|
||||
RedeemTx sql.NullString
|
||||
Descriptor sql.NullString
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ func (q *Queries) MarkVtxoAsSwept(ctx context.Context, arg MarkVtxoAsSweptParams
|
||||
}
|
||||
|
||||
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
|
||||
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
|
||||
@@ -78,7 +78,6 @@ func (q *Queries) SelectNotRedeemedVtxos(ctx context.Context) ([]SelectNotRedeem
|
||||
if err := rows.Scan(
|
||||
&i.Vtxo.Txid,
|
||||
&i.Vtxo.Vout,
|
||||
&i.Vtxo.Pubkey,
|
||||
&i.Vtxo.Amount,
|
||||
&i.Vtxo.PoolTx,
|
||||
&i.Vtxo.SpentBy,
|
||||
@@ -88,6 +87,7 @@ func (q *Queries) SelectNotRedeemedVtxos(ctx context.Context) ([]SelectNotRedeem
|
||||
&i.Vtxo.ExpireAt,
|
||||
&i.Vtxo.PaymentID,
|
||||
&i.Vtxo.RedeemTx,
|
||||
&i.Vtxo.Descriptor,
|
||||
&i.UncondForfeitTxVw.ID,
|
||||
&i.UncondForfeitTxVw.Tx,
|
||||
&i.UncondForfeitTxVw.VtxoTxid,
|
||||
@@ -108,11 +108,11 @@ func (q *Queries) SelectNotRedeemedVtxos(ctx context.Context) ([]SelectNotRedeem
|
||||
}
|
||||
|
||||
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
|
||||
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
|
||||
WHERE redeemed = false AND pubkey = ?
|
||||
WHERE redeemed = false AND INSTR(descriptor, ?) > 0
|
||||
`
|
||||
|
||||
type SelectNotRedeemedVtxosWithPubkeyRow struct {
|
||||
@@ -120,8 +120,8 @@ type SelectNotRedeemedVtxosWithPubkeyRow struct {
|
||||
UncondForfeitTxVw UncondForfeitTxVw
|
||||
}
|
||||
|
||||
func (q *Queries) SelectNotRedeemedVtxosWithPubkey(ctx context.Context, pubkey string) ([]SelectNotRedeemedVtxosWithPubkeyRow, error) {
|
||||
rows, err := q.db.QueryContext(ctx, selectNotRedeemedVtxosWithPubkey, pubkey)
|
||||
func (q *Queries) SelectNotRedeemedVtxosWithPubkey(ctx context.Context, instr string) ([]SelectNotRedeemedVtxosWithPubkeyRow, error) {
|
||||
rows, err := q.db.QueryContext(ctx, selectNotRedeemedVtxosWithPubkey, instr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -132,7 +132,6 @@ func (q *Queries) SelectNotRedeemedVtxosWithPubkey(ctx context.Context, pubkey s
|
||||
if err := rows.Scan(
|
||||
&i.Vtxo.Txid,
|
||||
&i.Vtxo.Vout,
|
||||
&i.Vtxo.Pubkey,
|
||||
&i.Vtxo.Amount,
|
||||
&i.Vtxo.PoolTx,
|
||||
&i.Vtxo.SpentBy,
|
||||
@@ -142,6 +141,7 @@ func (q *Queries) SelectNotRedeemedVtxosWithPubkey(ctx context.Context, pubkey s
|
||||
&i.Vtxo.ExpireAt,
|
||||
&i.Vtxo.PaymentID,
|
||||
&i.Vtxo.RedeemTx,
|
||||
&i.Vtxo.Descriptor,
|
||||
&i.UncondForfeitTxVw.ID,
|
||||
&i.UncondForfeitTxVw.Tx,
|
||||
&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,
|
||||
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,
|
||||
payment_receiver_vw.payment_id, payment_receiver_vw.pubkey, 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_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.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
|
||||
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
|
||||
@@ -276,12 +276,11 @@ func (q *Queries) SelectRoundWithRoundId(ctx context.Context, id string) ([]Sele
|
||||
&i.RoundTxVw.ParentTxid,
|
||||
&i.RoundTxVw.IsLeaf,
|
||||
&i.PaymentReceiverVw.PaymentID,
|
||||
&i.PaymentReceiverVw.Pubkey,
|
||||
&i.PaymentReceiverVw.Descriptor,
|
||||
&i.PaymentReceiverVw.Amount,
|
||||
&i.PaymentReceiverVw.OnchainAddress,
|
||||
&i.PaymentVtxoVw.Txid,
|
||||
&i.PaymentVtxoVw.Vout,
|
||||
&i.PaymentVtxoVw.Pubkey,
|
||||
&i.PaymentVtxoVw.Amount,
|
||||
&i.PaymentVtxoVw.PoolTx,
|
||||
&i.PaymentVtxoVw.SpentBy,
|
||||
@@ -291,6 +290,7 @@ func (q *Queries) SelectRoundWithRoundId(ctx context.Context, id string) ([]Sele
|
||||
&i.PaymentVtxoVw.ExpireAt,
|
||||
&i.PaymentVtxoVw.PaymentID,
|
||||
&i.PaymentVtxoVw.RedeemTx,
|
||||
&i.PaymentVtxoVw.Descriptor,
|
||||
); err != nil {
|
||||
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,
|
||||
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,
|
||||
payment_receiver_vw.payment_id, payment_receiver_vw.pubkey, 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_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.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
|
||||
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
|
||||
@@ -361,12 +361,11 @@ func (q *Queries) SelectRoundWithRoundTxId(ctx context.Context, txid string) ([]
|
||||
&i.RoundTxVw.ParentTxid,
|
||||
&i.RoundTxVw.IsLeaf,
|
||||
&i.PaymentReceiverVw.PaymentID,
|
||||
&i.PaymentReceiverVw.Pubkey,
|
||||
&i.PaymentReceiverVw.Descriptor,
|
||||
&i.PaymentReceiverVw.Amount,
|
||||
&i.PaymentReceiverVw.OnchainAddress,
|
||||
&i.PaymentVtxoVw.Txid,
|
||||
&i.PaymentVtxoVw.Vout,
|
||||
&i.PaymentVtxoVw.Pubkey,
|
||||
&i.PaymentVtxoVw.Amount,
|
||||
&i.PaymentVtxoVw.PoolTx,
|
||||
&i.PaymentVtxoVw.SpentBy,
|
||||
@@ -376,6 +375,7 @@ func (q *Queries) SelectRoundWithRoundTxId(ctx context.Context, txid string) ([]
|
||||
&i.PaymentVtxoVw.ExpireAt,
|
||||
&i.PaymentVtxoVw.PaymentID,
|
||||
&i.PaymentVtxoVw.RedeemTx,
|
||||
&i.PaymentVtxoVw.Descriptor,
|
||||
); err != nil {
|
||||
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,
|
||||
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,
|
||||
payment_receiver_vw.payment_id, payment_receiver_vw.pubkey, 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_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.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
|
||||
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
|
||||
@@ -446,12 +446,11 @@ func (q *Queries) SelectSweepableRounds(ctx context.Context) ([]SelectSweepableR
|
||||
&i.RoundTxVw.ParentTxid,
|
||||
&i.RoundTxVw.IsLeaf,
|
||||
&i.PaymentReceiverVw.PaymentID,
|
||||
&i.PaymentReceiverVw.Pubkey,
|
||||
&i.PaymentReceiverVw.Descriptor,
|
||||
&i.PaymentReceiverVw.Amount,
|
||||
&i.PaymentReceiverVw.OnchainAddress,
|
||||
&i.PaymentVtxoVw.Txid,
|
||||
&i.PaymentVtxoVw.Vout,
|
||||
&i.PaymentVtxoVw.Pubkey,
|
||||
&i.PaymentVtxoVw.Amount,
|
||||
&i.PaymentVtxoVw.PoolTx,
|
||||
&i.PaymentVtxoVw.SpentBy,
|
||||
@@ -461,6 +460,7 @@ func (q *Queries) SelectSweepableRounds(ctx context.Context) ([]SelectSweepableR
|
||||
&i.PaymentVtxoVw.ExpireAt,
|
||||
&i.PaymentVtxoVw.PaymentID,
|
||||
&i.PaymentVtxoVw.RedeemTx,
|
||||
&i.PaymentVtxoVw.Descriptor,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -476,7 +476,7 @@ func (q *Queries) SelectSweepableRounds(ctx context.Context) ([]SelectSweepableR
|
||||
}
|
||||
|
||||
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
|
||||
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
|
||||
@@ -500,7 +500,6 @@ func (q *Queries) SelectSweepableVtxos(ctx context.Context) ([]SelectSweepableVt
|
||||
if err := rows.Scan(
|
||||
&i.Vtxo.Txid,
|
||||
&i.Vtxo.Vout,
|
||||
&i.Vtxo.Pubkey,
|
||||
&i.Vtxo.Amount,
|
||||
&i.Vtxo.PoolTx,
|
||||
&i.Vtxo.SpentBy,
|
||||
@@ -510,6 +509,7 @@ func (q *Queries) SelectSweepableVtxos(ctx context.Context) ([]SelectSweepableVt
|
||||
&i.Vtxo.ExpireAt,
|
||||
&i.Vtxo.PaymentID,
|
||||
&i.Vtxo.RedeemTx,
|
||||
&i.Vtxo.Descriptor,
|
||||
&i.UncondForfeitTxVw.ID,
|
||||
&i.UncondForfeitTxVw.Tx,
|
||||
&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,
|
||||
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,
|
||||
payment_receiver_vw.payment_id, payment_receiver_vw.pubkey, 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_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.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
|
||||
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
|
||||
@@ -585,12 +585,11 @@ func (q *Queries) SelectSweptRounds(ctx context.Context) ([]SelectSweptRoundsRow
|
||||
&i.RoundTxVw.ParentTxid,
|
||||
&i.RoundTxVw.IsLeaf,
|
||||
&i.PaymentReceiverVw.PaymentID,
|
||||
&i.PaymentReceiverVw.Pubkey,
|
||||
&i.PaymentReceiverVw.Descriptor,
|
||||
&i.PaymentReceiverVw.Amount,
|
||||
&i.PaymentReceiverVw.OnchainAddress,
|
||||
&i.PaymentVtxoVw.Txid,
|
||||
&i.PaymentVtxoVw.Vout,
|
||||
&i.PaymentVtxoVw.Pubkey,
|
||||
&i.PaymentVtxoVw.Amount,
|
||||
&i.PaymentVtxoVw.PoolTx,
|
||||
&i.PaymentVtxoVw.SpentBy,
|
||||
@@ -600,6 +599,7 @@ func (q *Queries) SelectSweptRounds(ctx context.Context) ([]SelectSweptRoundsRow
|
||||
&i.PaymentVtxoVw.ExpireAt,
|
||||
&i.PaymentVtxoVw.PaymentID,
|
||||
&i.PaymentVtxoVw.RedeemTx,
|
||||
&i.PaymentVtxoVw.Descriptor,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -615,7 +615,7 @@ func (q *Queries) SelectSweptRounds(ctx context.Context) ([]SelectSweptRoundsRow
|
||||
}
|
||||
|
||||
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
|
||||
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
|
||||
@@ -638,7 +638,6 @@ func (q *Queries) SelectVtxoByOutpoint(ctx context.Context, arg SelectVtxoByOutp
|
||||
err := row.Scan(
|
||||
&i.Vtxo.Txid,
|
||||
&i.Vtxo.Vout,
|
||||
&i.Vtxo.Pubkey,
|
||||
&i.Vtxo.Amount,
|
||||
&i.Vtxo.PoolTx,
|
||||
&i.Vtxo.SpentBy,
|
||||
@@ -648,6 +647,7 @@ func (q *Queries) SelectVtxoByOutpoint(ctx context.Context, arg SelectVtxoByOutp
|
||||
&i.Vtxo.ExpireAt,
|
||||
&i.Vtxo.PaymentID,
|
||||
&i.Vtxo.RedeemTx,
|
||||
&i.Vtxo.Descriptor,
|
||||
&i.UncondForfeitTxVw.ID,
|
||||
&i.UncondForfeitTxVw.Tx,
|
||||
&i.UncondForfeitTxVw.VtxoTxid,
|
||||
@@ -658,7 +658,7 @@ func (q *Queries) SelectVtxoByOutpoint(ctx context.Context, arg SelectVtxoByOutp
|
||||
}
|
||||
|
||||
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
|
||||
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
|
||||
@@ -682,7 +682,6 @@ func (q *Queries) SelectVtxosByPoolTxid(ctx context.Context, poolTx string) ([]S
|
||||
if err := rows.Scan(
|
||||
&i.Vtxo.Txid,
|
||||
&i.Vtxo.Vout,
|
||||
&i.Vtxo.Pubkey,
|
||||
&i.Vtxo.Amount,
|
||||
&i.Vtxo.PoolTx,
|
||||
&i.Vtxo.SpentBy,
|
||||
@@ -692,6 +691,7 @@ func (q *Queries) SelectVtxosByPoolTxid(ctx context.Context, poolTx string) ([]S
|
||||
&i.Vtxo.ExpireAt,
|
||||
&i.Vtxo.PaymentID,
|
||||
&i.Vtxo.RedeemTx,
|
||||
&i.Vtxo.Descriptor,
|
||||
&i.UncondForfeitTxVw.ID,
|
||||
&i.UncondForfeitTxVw.Tx,
|
||||
&i.UncondForfeitTxVw.VtxoTxid,
|
||||
@@ -757,16 +757,16 @@ func (q *Queries) UpsertPayment(ctx context.Context, arg UpsertPaymentParams) er
|
||||
}
|
||||
|
||||
const upsertReceiver = `-- name: UpsertReceiver :exec
|
||||
INSERT INTO receiver (payment_id, pubkey, amount, onchain_address) VALUES (?, ?, ?, ?)
|
||||
ON CONFLICT(payment_id, pubkey) DO UPDATE SET
|
||||
INSERT INTO receiver (payment_id, descriptor, amount, onchain_address) VALUES (?, ?, ?, ?)
|
||||
ON CONFLICT(payment_id, descriptor) DO UPDATE SET
|
||||
amount = EXCLUDED.amount,
|
||||
onchain_address = EXCLUDED.onchain_address,
|
||||
pubkey = EXCLUDED.pubkey
|
||||
descriptor = EXCLUDED.descriptor
|
||||
`
|
||||
|
||||
type UpsertReceiverParams struct {
|
||||
PaymentID string
|
||||
Pubkey string
|
||||
Descriptor string
|
||||
Amount int64
|
||||
OnchainAddress string
|
||||
}
|
||||
@@ -774,7 +774,7 @@ type UpsertReceiverParams struct {
|
||||
func (q *Queries) UpsertReceiver(ctx context.Context, arg UpsertReceiverParams) error {
|
||||
_, err := q.db.ExecContext(ctx, upsertReceiver,
|
||||
arg.PaymentID,
|
||||
arg.Pubkey,
|
||||
arg.Descriptor,
|
||||
arg.Amount,
|
||||
arg.OnchainAddress,
|
||||
)
|
||||
@@ -909,9 +909,9 @@ func (q *Queries) UpsertUnconditionalForfeitTx(ctx context.Context, arg UpsertUn
|
||||
}
|
||||
|
||||
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
|
||||
pubkey = EXCLUDED.pubkey,
|
||||
descriptor = EXCLUDED.descriptor,
|
||||
amount = EXCLUDED.amount,
|
||||
pool_tx = EXCLUDED.pool_tx,
|
||||
spent_by = EXCLUDED.spent_by,
|
||||
@@ -923,24 +923,24 @@ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(txid, vout) DO UPDATE SET
|
||||
`
|
||||
|
||||
type UpsertVtxoParams struct {
|
||||
Txid string
|
||||
Vout int64
|
||||
Pubkey string
|
||||
Amount int64
|
||||
PoolTx string
|
||||
SpentBy string
|
||||
Spent bool
|
||||
Redeemed bool
|
||||
Swept bool
|
||||
ExpireAt int64
|
||||
RedeemTx sql.NullString
|
||||
Txid string
|
||||
Vout int64
|
||||
Descriptor sql.NullString
|
||||
Amount int64
|
||||
PoolTx string
|
||||
SpentBy string
|
||||
Spent bool
|
||||
Redeemed bool
|
||||
Swept bool
|
||||
ExpireAt int64
|
||||
RedeemTx sql.NullString
|
||||
}
|
||||
|
||||
func (q *Queries) UpsertVtxo(ctx context.Context, arg UpsertVtxoParams) error {
|
||||
_, err := q.db.ExecContext(ctx, upsertVtxo,
|
||||
arg.Txid,
|
||||
arg.Vout,
|
||||
arg.Pubkey,
|
||||
arg.Descriptor,
|
||||
arg.Amount,
|
||||
arg.PoolTx,
|
||||
arg.SpentBy,
|
||||
|
||||
@@ -44,11 +44,11 @@ INSERT INTO payment (id, round_id) VALUES (?, ?)
|
||||
ON CONFLICT(id) DO UPDATE SET round_id = EXCLUDED.round_id;
|
||||
|
||||
-- name: UpsertReceiver :exec
|
||||
INSERT INTO receiver (payment_id, pubkey, amount, onchain_address) VALUES (?, ?, ?, ?)
|
||||
ON CONFLICT(payment_id, pubkey) DO UPDATE SET
|
||||
INSERT INTO receiver (payment_id, descriptor, amount, onchain_address) VALUES (?, ?, ?, ?)
|
||||
ON CONFLICT(payment_id, descriptor) DO UPDATE SET
|
||||
amount = EXCLUDED.amount,
|
||||
onchain_address = EXCLUDED.onchain_address,
|
||||
pubkey = EXCLUDED.pubkey;
|
||||
descriptor = EXCLUDED.descriptor;
|
||||
|
||||
-- name: UpdateVtxoPaymentId :exec
|
||||
UPDATE vtxo SET payment_id = ? WHERE txid = ? AND vout = ?;
|
||||
@@ -120,9 +120,9 @@ VALUES (?, ?, ?, ?) ON CONFLICT(id) DO UPDATE SET
|
||||
position = EXCLUDED.position;
|
||||
|
||||
-- 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
|
||||
pubkey = EXCLUDED.pubkey,
|
||||
descriptor = EXCLUDED.descriptor,
|
||||
amount = EXCLUDED.amount,
|
||||
pool_tx = EXCLUDED.pool_tx,
|
||||
spent_by = EXCLUDED.spent_by,
|
||||
@@ -151,7 +151,7 @@ SELECT sqlc.embed(vtxo),
|
||||
sqlc.embed(uncond_forfeit_tx_vw)
|
||||
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
|
||||
WHERE redeemed = false AND pubkey = ?;
|
||||
WHERE redeemed = false AND INSTR(descriptor, ?) > 0;
|
||||
|
||||
-- name: SelectVtxoByOutpoint :one
|
||||
SELECT sqlc.embed(vtxo),
|
||||
|
||||
@@ -43,17 +43,17 @@ func (v *vxtoRepository) AddVtxos(ctx context.Context, vtxos []domain.Vtxo) erro
|
||||
}
|
||||
if err := querierWithTx.UpsertVtxo(
|
||||
ctx, queries.UpsertVtxoParams{
|
||||
Txid: vtxo.Txid,
|
||||
Vout: int64(vtxo.VOut),
|
||||
Pubkey: vtxo.Pubkey,
|
||||
Amount: int64(vtxo.Amount),
|
||||
PoolTx: vtxo.PoolTx,
|
||||
SpentBy: vtxo.SpentBy,
|
||||
Spent: vtxo.Spent,
|
||||
Redeemed: vtxo.Redeemed,
|
||||
Swept: vtxo.Swept,
|
||||
ExpireAt: vtxo.ExpireAt,
|
||||
RedeemTx: sql.NullString{String: redeemTx, Valid: true},
|
||||
Txid: vtxo.Txid,
|
||||
Vout: int64(vtxo.VOut),
|
||||
Descriptor: sql.NullString{String: vtxo.Descriptor, Valid: true},
|
||||
Amount: int64(vtxo.Amount),
|
||||
PoolTx: vtxo.PoolTx,
|
||||
SpentBy: vtxo.SpentBy,
|
||||
Spent: vtxo.Spent,
|
||||
Redeemed: vtxo.Redeemed,
|
||||
Swept: vtxo.Swept,
|
||||
ExpireAt: vtxo.ExpireAt,
|
||||
RedeemTx: sql.NullString{String: redeemTx, Valid: true},
|
||||
},
|
||||
); err != nil {
|
||||
return err
|
||||
@@ -100,6 +100,10 @@ func (v *vxtoRepository) GetAllVtxos(ctx context.Context, pubkey string) ([]doma
|
||||
|
||||
var rows []vtxoWithUnconditionalForfeitTxs
|
||||
if withPubkey {
|
||||
if len(pubkey) == 66 {
|
||||
pubkey = pubkey[2:]
|
||||
}
|
||||
|
||||
res, err := v.querier.SelectNotRedeemedVtxosWithPubkey(ctx, pubkey)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
@@ -294,8 +298,8 @@ func rowToVtxo(row queries.Vtxo, uncondForfeitTxs []queries.UncondForfeitTxVw) d
|
||||
VOut: uint32(row.Vout),
|
||||
},
|
||||
Receiver: domain.Receiver{
|
||||
Pubkey: row.Pubkey,
|
||||
Amount: uint64(row.Amount),
|
||||
Descriptor: row.Descriptor.String,
|
||||
Amount: uint64(row.Amount),
|
||||
},
|
||||
PoolTx: row.PoolTx,
|
||||
SpentBy: row.SpentBy,
|
||||
|
||||
@@ -3,7 +3,6 @@ package txbuilder
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
|
||||
"github.com/ark-network/ark/common"
|
||||
@@ -14,6 +13,7 @@ import (
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||
"github.com/vulpemventures/go-elements/address"
|
||||
"github.com/vulpemventures/go-elements/elementsutil"
|
||||
"github.com/vulpemventures/go-elements/network"
|
||||
@@ -27,7 +27,6 @@ type txBuilder struct {
|
||||
wallet ports.WalletService
|
||||
net common.Network
|
||||
roundLifetime int64 // in seconds
|
||||
exitDelay int64 // in seconds
|
||||
boardingExitDelay int64 // in seconds
|
||||
}
|
||||
|
||||
@@ -35,27 +34,13 @@ func NewTxBuilder(
|
||||
wallet ports.WalletService,
|
||||
net common.Network,
|
||||
roundLifetime int64,
|
||||
exitDelay int64,
|
||||
boardingExitDelay int64,
|
||||
) 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) {
|
||||
addr, script, _, err := b.getBoardingTaproot(owner, asp)
|
||||
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) GetTxID(tx string) (string, error) {
|
||||
return getTxid(tx)
|
||||
}
|
||||
|
||||
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(
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
@@ -118,7 +107,7 @@ func (b *txBuilder) BuildForfeitTxs(
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
forfeitTxs, err = b.createForfeitTxs(aspPubkey, payments, connectorTxs, connectorAmount)
|
||||
forfeitTxs, err = b.createForfeitTxs(aspPubkey, payments, connectorTxs, connectorAmount, minRelayFeeRate)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
@@ -159,8 +148,13 @@ func (b *txBuilder) BuildPoolTx(
|
||||
return "", nil, "", err
|
||||
}
|
||||
|
||||
receivers, err := getOffchainReceivers(payments)
|
||||
if err != nil {
|
||||
return "", nil, "", err
|
||||
}
|
||||
|
||||
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 {
|
||||
return "", nil, "", err
|
||||
@@ -270,7 +264,7 @@ func (b *txBuilder) VerifyTapscriptPartialSigs(tx string) (bool, string, error)
|
||||
rootHash := tapLeaf.ControlBlock.RootHash(tapLeaf.Script)
|
||||
tapKeyFromControlBlock := taproot.ComputeTaprootOutputKey(tree.UnspendableKey(), rootHash[:])
|
||||
|
||||
pkscript, err := p2trScript(tapKeyFromControlBlock)
|
||||
pkscript, err := common.P2TRScript(tapKeyFromControlBlock)
|
||||
if err != nil {
|
||||
return false, txid, err
|
||||
}
|
||||
@@ -376,51 +370,13 @@ func (b *txBuilder) BuildAsyncPaymentTransactions(
|
||||
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(
|
||||
sharedOutputAmount uint64,
|
||||
sharedOutputScript []byte,
|
||||
payments []domain.Payment,
|
||||
boardingInputs []ports.BoardingInput,
|
||||
aspPubKey *secp256k1.PublicKey, connectorAddress string,
|
||||
aspPubKey *secp256k1.PublicKey,
|
||||
connectorAddress string,
|
||||
sweptRounds []domain.Round,
|
||||
) (*psetv2.Pset, error) {
|
||||
aspScript, err := p2wpkhScript(aspPubKey, b.onchainNetwork())
|
||||
@@ -487,7 +443,7 @@ func (b *txBuilder) createPoolTx(
|
||||
}
|
||||
|
||||
for _, in := range boardingInputs {
|
||||
targetAmount -= in.GetAmount()
|
||||
targetAmount -= in.Amount
|
||||
}
|
||||
ctx := context.Background()
|
||||
|
||||
@@ -529,8 +485,8 @@ func (b *txBuilder) createPoolTx(
|
||||
if err := updater.AddInputs(
|
||||
[]psetv2.InputArgs{
|
||||
{
|
||||
Txid: in.GetHash().String(),
|
||||
TxIndex: in.GetIndex(),
|
||||
Txid: in.Txid,
|
||||
TxIndex: in.VtxoKey.VOut,
|
||||
},
|
||||
},
|
||||
); err != nil {
|
||||
@@ -544,21 +500,27 @@ func (b *txBuilder) createPoolTx(
|
||||
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 {
|
||||
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 {
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -740,10 +702,13 @@ func (b *txBuilder) VerifyAndCombinePartialTx(dest string, src string) (string,
|
||||
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
|
||||
}
|
||||
|
||||
if err := roundSigner.SignTaprootInputTapscriptSig(i, partialSig); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
return roundSigner.Pset.ToBase64()
|
||||
@@ -823,56 +788,59 @@ func (b *txBuilder) createConnectors(
|
||||
}
|
||||
|
||||
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) {
|
||||
aspScript, err := p2wpkhScript(aspPubkey, b.onchainNetwork())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
forfeitTxs := make([]string, 0)
|
||||
for _, payment := range payments {
|
||||
for _, vtxo := range payment.Inputs {
|
||||
pubkeyBytes, err := hex.DecodeString(vtxo.Pubkey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode pubkey: %s", err)
|
||||
}
|
||||
|
||||
vtxoPubkey, err := secp256k1.ParsePubKey(pubkeyBytes)
|
||||
offchainScript, err := tree.ParseVtxoScript(vtxo.Descriptor)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
vtxoScript, vtxoTaprootTree, err := b.getLeafScriptAndTree(vtxoPubkey, aspPubkey)
|
||||
vtxoTapKey, vtxoTree, err := offchainScript.TapTree()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var forfeitProof *taproot.TapscriptElementsProof
|
||||
|
||||
for _, proof := range vtxoTaprootTree.LeafMerkleProofs {
|
||||
isForfeit, err := (&tree.ForfeitClosure{}).Decode(proof.Script)
|
||||
if !isForfeit || err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
forfeitProof = &proof
|
||||
break
|
||||
vtxoScript, err := common.P2TRScript(vtxoTapKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if forfeitProof == nil {
|
||||
return nil, fmt.Errorf("forfeit proof not found")
|
||||
feeAmount, err := common.ComputeForfeitMinRelayFee(minRelayFeeRate, vtxoTree)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, connector := range connectors {
|
||||
txs, err := b.craftForfeitTxs(
|
||||
connector, connectorAmount, vtxo, *forfeitProof, vtxoScript, aspScript,
|
||||
txs, err := tree.BuildForfeitTxs(
|
||||
connector,
|
||||
psetv2.InputArgs{
|
||||
Txid: vtxo.Txid,
|
||||
TxIndex: vtxo.VOut,
|
||||
},
|
||||
vtxo.Amount,
|
||||
connectorAmount,
|
||||
feeAmount,
|
||||
vtxoScript,
|
||||
aspPubkey,
|
||||
)
|
||||
if err != nil {
|
||||
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) {
|
||||
for _, leaf := range input.TapLeafScript {
|
||||
closure := &tree.CSVSigClosure{}
|
||||
|
||||
@@ -20,12 +20,12 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
testingKey = "0218d5ca8b58797b7dbd65c075dd7ba7784b3f38ab71b1a5a8e3f94ba0257654a6"
|
||||
connectorAddress = "tex1qekd5u0qj8jl07vy60830xy7n9qtmcx9u3s0cqc"
|
||||
minRelayFee = uint64(30)
|
||||
roundLifetime = int64(1209344)
|
||||
unilateralExitDelay = int64(512)
|
||||
boardingExitDelay = int64(512)
|
||||
testingKey = "020000000000000000000000000000000000000000000000000000000000000001"
|
||||
connectorAddress = "tex1qekd5u0qj8jl07vy60830xy7n9qtmcx9u3s0cqc"
|
||||
minRelayFee = uint64(30)
|
||||
roundLifetime = int64(1209344)
|
||||
boardingExitDelay = int64(512)
|
||||
minRelayFeeRate = 3
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -54,7 +54,7 @@ func TestMain(m *testing.M) {
|
||||
|
||||
func TestBuildPoolTx(t *testing.T) {
|
||||
builder := txbuilder.NewTxBuilder(
|
||||
wallet, common.Liquid, roundLifetime, unilateralExitDelay, boardingExitDelay,
|
||||
wallet, common.Liquid, roundLifetime, boardingExitDelay,
|
||||
)
|
||||
|
||||
fixtures, err := parsePoolTxFixtures()
|
||||
@@ -99,7 +99,7 @@ func TestBuildPoolTx(t *testing.T) {
|
||||
|
||||
func TestBuildForfeitTxs(t *testing.T) {
|
||||
builder := txbuilder.NewTxBuilder(
|
||||
wallet, common.Liquid, 1209344, unilateralExitDelay, boardingExitDelay,
|
||||
wallet, common.Liquid, 1209344, boardingExitDelay,
|
||||
)
|
||||
|
||||
fixtures, err := parseForfeitTxsFixtures()
|
||||
@@ -110,7 +110,7 @@ func TestBuildForfeitTxs(t *testing.T) {
|
||||
t.Run("valid", func(t *testing.T) {
|
||||
for _, f := range fixtures.Valid {
|
||||
connectors, forfeitTxs, err := builder.BuildForfeitTxs(
|
||||
pubkey, f.PoolTx, f.Payments,
|
||||
pubkey, f.PoolTx, f.Payments, minRelayFeeRate,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, connectors, f.ExpectedNumOfConnectors)
|
||||
@@ -148,7 +148,7 @@ func TestBuildForfeitTxs(t *testing.T) {
|
||||
t.Run("invalid", func(t *testing.T) {
|
||||
for _, f := range fixtures.Invalid {
|
||||
connectors, forfeitTxs, err := builder.BuildForfeitTxs(
|
||||
pubkey, f.PoolTx, f.Payments,
|
||||
pubkey, f.PoolTx, f.Payments, minRelayFeeRate,
|
||||
)
|
||||
require.EqualError(t, err, f.ExpectedErr)
|
||||
require.Empty(t, connectors)
|
||||
|
||||
@@ -51,24 +51,3 @@ func craftConnectorTx(
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
|
||||
"github.com/ark-network/ark/server/internal/core/ports"
|
||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
@@ -259,6 +260,16 @@ func (m *mockedWallet) GetTransaction(ctx context.Context, txid string) (string,
|
||||
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 {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ func sweepTransaction(
|
||||
|
||||
root := leaf.ControlBlock.RootHash(leaf.Script)
|
||||
taprootKey := taproot.ComputeTaprootOutputKey(leaf.ControlBlock.InternalKey, root)
|
||||
script, err := p2trScript(taprootKey)
|
||||
script, err := common.P2TRScript(taprootKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -9,13 +9,14 @@
|
||||
{
|
||||
"txid": "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6",
|
||||
"vout": 0,
|
||||
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002",
|
||||
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
|
||||
"signerPubkey": "020000000000000000000000000000000000000000000000000000000000000001",
|
||||
"amount": 1100
|
||||
}
|
||||
],
|
||||
"receivers": [
|
||||
{
|
||||
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002",
|
||||
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
|
||||
"amount": 1100
|
||||
}
|
||||
]
|
||||
@@ -32,17 +33,18 @@
|
||||
{
|
||||
"txid": "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6",
|
||||
"vout": 0,
|
||||
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002",
|
||||
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
|
||||
"signerPubkey": "020000000000000000000000000000000000000000000000000000000000000001",
|
||||
"amount": 1100
|
||||
}
|
||||
],
|
||||
"receivers": [
|
||||
{
|
||||
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002",
|
||||
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
|
||||
"amount": 600
|
||||
},
|
||||
{
|
||||
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002",
|
||||
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
|
||||
"amount": 500
|
||||
}
|
||||
]
|
||||
@@ -59,17 +61,18 @@
|
||||
{
|
||||
"txid": "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6",
|
||||
"vout": 0,
|
||||
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002",
|
||||
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
|
||||
"signerPubkey": "020000000000000000000000000000000000000000000000000000000000000001",
|
||||
"amount": 1100
|
||||
}
|
||||
],
|
||||
"receivers": [
|
||||
{
|
||||
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002",
|
||||
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
|
||||
"amount": 600
|
||||
},
|
||||
{
|
||||
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002",
|
||||
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
|
||||
"amount": 500
|
||||
}
|
||||
]
|
||||
@@ -80,17 +83,18 @@
|
||||
{
|
||||
"txid": "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6",
|
||||
"vout": 0,
|
||||
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002",
|
||||
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
|
||||
"signerPubkey": "020000000000000000000000000000000000000000000000000000000000000001",
|
||||
"amount": 1100
|
||||
}
|
||||
],
|
||||
"receivers": [
|
||||
{
|
||||
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002",
|
||||
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
|
||||
"amount": 600
|
||||
},
|
||||
{
|
||||
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002",
|
||||
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
|
||||
"amount": 500
|
||||
}
|
||||
]
|
||||
@@ -101,17 +105,18 @@
|
||||
{
|
||||
"txid": "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6",
|
||||
"vout": 0,
|
||||
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002",
|
||||
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
|
||||
"signerPubkey": "020000000000000000000000000000000000000000000000000000000000000001",
|
||||
"amount": 1100
|
||||
}
|
||||
],
|
||||
"receivers": [
|
||||
{
|
||||
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002",
|
||||
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
|
||||
"amount": 600
|
||||
},
|
||||
{
|
||||
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002",
|
||||
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
|
||||
"amount": 500
|
||||
}
|
||||
]
|
||||
@@ -128,53 +133,58 @@
|
||||
{
|
||||
"txid": "755c820771284d85ea4bbcc246565b4eddadc44237a7e57a0f9cb78a840d1d41",
|
||||
"vout": 0,
|
||||
"pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5",
|
||||
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
|
||||
"signerPubkey": "020000000000000000000000000000000000000000000000000000000000000001",
|
||||
"amount": 1000
|
||||
},
|
||||
{
|
||||
"txid": "66a0df86fcdeb84b8877adfe0b2c556dba30305d72ddbd4c49355f6930355357",
|
||||
"vout": 0,
|
||||
"pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5",
|
||||
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
|
||||
"signerPubkey": "020000000000000000000000000000000000000000000000000000000000000001",
|
||||
"amount": 1000
|
||||
},
|
||||
{
|
||||
"txid": "9913159bc7aa493ca53cbb9cbc88f97ba01137c814009dc7ef520c3fafc67909",
|
||||
"vout": 1,
|
||||
"pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5",
|
||||
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
|
||||
"signerPubkey": "020000000000000000000000000000000000000000000000000000000000000001",
|
||||
"amount": 500
|
||||
},
|
||||
{
|
||||
"txid": "5e10e77a7cdedc153be5193a4b6055a7802706ded4f2a9efefe86ed2f9a6ae60",
|
||||
"vout": 0,
|
||||
"pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5",
|
||||
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
|
||||
"signerPubkey": "020000000000000000000000000000000000000000000000000000000000000001",
|
||||
"amount": 1000
|
||||
},
|
||||
{
|
||||
"txid": "5e10e77a7cdedc153be5193a4b6055a7802706ded4f2a9efefe86ed2f9a6ae60",
|
||||
"vout": 1,
|
||||
"pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5",
|
||||
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
|
||||
"signerPubkey": "020000000000000000000000000000000000000000000000000000000000000001",
|
||||
"amount": 1000
|
||||
}
|
||||
],
|
||||
"receivers": [
|
||||
{
|
||||
"pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5",
|
||||
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
|
||||
"amount": 1000
|
||||
},
|
||||
{
|
||||
"pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5",
|
||||
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
|
||||
"amount": 1000
|
||||
},
|
||||
{
|
||||
"pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5",
|
||||
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
|
||||
"amount": 1000
|
||||
},
|
||||
{
|
||||
"pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5",
|
||||
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
|
||||
"amount": 1000
|
||||
},
|
||||
{
|
||||
"pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5",
|
||||
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
|
||||
"amount": 500
|
||||
}
|
||||
]
|
||||
@@ -196,23 +206,25 @@
|
||||
{
|
||||
"txid": "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6",
|
||||
"vout": 0,
|
||||
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002",
|
||||
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
|
||||
"signerPubkey": "020000000000000000000000000000000000000000000000000000000000000001",
|
||||
"amount": 600
|
||||
},
|
||||
{
|
||||
"txid": "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6",
|
||||
"vout": 1,
|
||||
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002",
|
||||
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
|
||||
"signerPubkey": "020000000000000000000000000000000000000000000000000000000000000001",
|
||||
"amount": 500
|
||||
}
|
||||
],
|
||||
"receivers": [
|
||||
{
|
||||
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002",
|
||||
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
|
||||
"amount": 600
|
||||
},
|
||||
{
|
||||
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002",
|
||||
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
|
||||
"amount": 500
|
||||
}
|
||||
]
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"github.com/ark-network/ark/common/tree"
|
||||
"github.com/ark-network/ark/server/internal/core/domain"
|
||||
"github.com/ark-network/ark/server/internal/core/ports"
|
||||
"github.com/btcsuite/btcd/btcec/v2/schnorr"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||
"github.com/vulpemventures/go-elements/address"
|
||||
@@ -62,19 +61,24 @@ func getOnchainReceivers(
|
||||
|
||||
func getOffchainReceivers(
|
||||
payments []domain.Payment,
|
||||
) []tree.Receiver {
|
||||
) ([]tree.Receiver, error) {
|
||||
receivers := make([]tree.Receiver, 0)
|
||||
for _, payment := range payments {
|
||||
for _, receiver := range payment.Receivers {
|
||||
if !receiver.IsOnchain() {
|
||||
vtxoScript, err := tree.ParseVtxoScript(receiver.Descriptor)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
receivers = append(receivers, tree.Receiver{
|
||||
Pubkey: receiver.Pubkey,
|
||||
Script: vtxoScript,
|
||||
Amount: receiver.Amount,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return receivers
|
||||
return receivers, nil
|
||||
}
|
||||
|
||||
func toWitnessUtxo(in ports.TxInput) (*transaction.TxOutput, error) {
|
||||
@@ -136,10 +140,6 @@ func addInputs(
|
||||
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 {
|
||||
for _, p := range payments {
|
||||
for _, r := range p.Receivers {
|
||||
|
||||
@@ -22,20 +22,29 @@ import (
|
||||
"github.com/btcsuite/btcwallet/waddrmgr"
|
||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||
"github.com/lightningnetwork/lnd/input"
|
||||
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||
)
|
||||
|
||||
type txBuilder struct {
|
||||
wallet ports.WalletService
|
||||
net common.Network
|
||||
roundLifetime int64 // in seconds
|
||||
exitDelay int64 // in seconds
|
||||
boardingExitDelay int64 // in seconds
|
||||
}
|
||||
|
||||
func NewTxBuilder(
|
||||
wallet ports.WalletService, net common.Network, roundLifetime, exitDelay, boardingExitDelay int64,
|
||||
wallet ports.WalletService, net common.Network, roundLifetime, boardingExitDelay int64,
|
||||
) 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) {
|
||||
@@ -64,7 +73,7 @@ func (b *txBuilder) VerifyTapscriptPartialSigs(tx string) (bool, string, error)
|
||||
|
||||
rootHash := controlBlock.RootHash(tapLeaf.Script)
|
||||
tapKeyFromControlBlock := txscript.ComputeTaprootOutputKey(bitcointree.UnspendableKey(), rootHash[:])
|
||||
pkscript, err := p2trScript(tapKeyFromControlBlock)
|
||||
pkscript, err := common.P2TRScript(tapKeyFromControlBlock)
|
||||
if err != nil {
|
||||
return false, txid, err
|
||||
}
|
||||
@@ -173,14 +182,6 @@ func (b *txBuilder) FinalizeAndExtract(tx string) (string, error) {
|
||||
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) {
|
||||
sweepPsbt, err := sweepTransaction(
|
||||
b.wallet,
|
||||
@@ -227,7 +228,7 @@ func (b *txBuilder) BuildSweepTx(inputs []ports.SweepInput) (signedSweepTx strin
|
||||
}
|
||||
|
||||
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) {
|
||||
connectorPkScript, err := b.getConnectorPkScript(poolTx)
|
||||
if err != nil {
|
||||
@@ -244,12 +245,7 @@ func (b *txBuilder) BuildForfeitTxs(
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
minRelayFeeForfeitTx, err := b.minRelayFeeForfeitTx()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
forfeitTxs, err = b.createForfeitTxs(aspPubkey, payments, connectorTxs, minRelayFeeForfeitTx)
|
||||
forfeitTxs, err = b.createForfeitTxs(aspPubkey, payments, connectorTxs, minRelayFeeRate)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
@@ -275,7 +271,10 @@ func (b *txBuilder) BuildPoolTx(
|
||||
return "", nil, "", fmt.Errorf("missing cosigners")
|
||||
}
|
||||
|
||||
receivers := getOffchainReceivers(payments)
|
||||
receivers, err := getOffchainReceivers(payments)
|
||||
if err != nil {
|
||||
return "", nil, "", err
|
||||
}
|
||||
|
||||
feeAmount, err := b.minRelayFeeTreeTx()
|
||||
if err != nil {
|
||||
@@ -284,7 +283,7 @@ func (b *txBuilder) BuildPoolTx(
|
||||
|
||||
if !isOnchainOnly(payments) {
|
||||
sharedOutputScript, sharedOutputAmount, err = bitcointree.CraftSharedOutput(
|
||||
cosigners, aspPubkey, receivers, feeAmount, b.roundLifetime, b.exitDelay,
|
||||
cosigners, aspPubkey, receivers, feeAmount, b.roundLifetime,
|
||||
)
|
||||
if err != nil {
|
||||
return
|
||||
@@ -297,7 +296,7 @@ func (b *txBuilder) BuildPoolTx(
|
||||
}
|
||||
|
||||
ptx, err := b.createPoolTx(
|
||||
aspPubkey, sharedOutputAmount, sharedOutputScript, payments, boardingInputs, connectorAddress, sweptRounds,
|
||||
sharedOutputAmount, sharedOutputScript, payments, boardingInputs, connectorAddress, sweptRounds,
|
||||
)
|
||||
if err != nil {
|
||||
return
|
||||
@@ -315,7 +314,7 @@ func (b *txBuilder) BuildPoolTx(
|
||||
}
|
||||
|
||||
congestionTree, err = bitcointree.CraftCongestionTree(
|
||||
initialOutpoint, cosigners, aspPubkey, receivers, feeAmount, b.roundLifetime, b.exitDelay,
|
||||
initialOutpoint, cosigners, aspPubkey, receivers, feeAmount, b.roundLifetime,
|
||||
)
|
||||
if err != nil {
|
||||
return
|
||||
@@ -401,7 +400,6 @@ func (b *txBuilder) FindLeaves(congestionTree tree.CongestionTree, fromtxid stri
|
||||
return foundLeaves, nil
|
||||
}
|
||||
|
||||
// TODO add locktimes to txs
|
||||
func (b *txBuilder) BuildAsyncPaymentTransactions(
|
||||
vtxos []domain.Vtxo, aspPubKey *secp256k1.PublicKey, receivers []domain.Receiver,
|
||||
) (*domain.AsyncPaymentTxs, error) {
|
||||
@@ -410,6 +408,7 @@ func (b *txBuilder) BuildAsyncPaymentTransactions(
|
||||
}
|
||||
|
||||
for _, vtxo := range vtxos {
|
||||
// TODO allow to chain async payment ?
|
||||
if vtxo.AsyncPayment != nil {
|
||||
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))
|
||||
redeemTxWeightEstimator := &input.TxWeightEstimator{}
|
||||
for _, vtxo := range vtxos {
|
||||
if vtxo.Spent {
|
||||
if vtxo.Spent || vtxo.Redeemed || vtxo.Swept {
|
||||
return nil, fmt.Errorf("all vtxos must be unspent")
|
||||
}
|
||||
|
||||
senderBytes, err := hex.DecodeString(vtxo.Pubkey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sender, err := secp256k1.ParsePubKey(senderBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
aspScript, err := p2trScript(aspPubKey)
|
||||
aspScript, err := common.P2TRScript(aspPubKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -449,35 +438,54 @@ func (b *txBuilder) BuildAsyncPaymentTransactions(
|
||||
Index: vtxo.VOut,
|
||||
}
|
||||
|
||||
vtxoScript, vtxoTree, err := b.getLeafScriptAndTree(sender, aspPubKey)
|
||||
vtxoScript, err := bitcointree.ParseVtxoScript(vtxo.Descriptor)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
forfeitClosure := &bitcointree.MultisigClosure{
|
||||
Pubkey: sender,
|
||||
AspPubkey: aspPubKey,
|
||||
}
|
||||
|
||||
forfeitLeaf, err := forfeitClosure.Leaf()
|
||||
vtxoTapKey, vtxoTree, err := vtxoScript.TapTree()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
leafProof := vtxoTree.LeafMerkleProofs[vtxoTree.LeafProofIndex[forfeitLeaf.TapHash()]]
|
||||
ctrlBlock := leafProof.ToControlBlock(bitcointree.UnspendableKey())
|
||||
ctrlBlockBytes, err := ctrlBlock.ToBytes()
|
||||
vtxoOutputScript, err := common.P2TRScript(vtxoTapKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var tapscript *waddrmgr.Tapscript
|
||||
forfeitTxWeightEstimator := &input.TxWeightEstimator{}
|
||||
tapscript := &waddrmgr.Tapscript{
|
||||
RevealedScript: leafProof.Script,
|
||||
ControlBlock: &ctrlBlock,
|
||||
|
||||
if defaultVtxoScript, ok := vtxoScript.(*bitcointree.DefaultVtxoScript); ok {
|
||||
forfeitClosure := &bitcointree.MultisigClosure{
|
||||
Pubkey: defaultVtxoScript.Owner,
|
||||
AspPubkey: defaultVtxoScript.Asp,
|
||||
}
|
||||
|
||||
forfeitLeaf, err := forfeitClosure.Leaf()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
forfeitProof, err := vtxoTree.GetTaprootMerkleProof(forfeitLeaf.TapHash())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ctrlBlock, err := txscript.ParseControlBlock(forfeitProof.ControlBlock)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tapscript = &waddrmgr.Tapscript{
|
||||
RevealedScript: forfeitProof.Script,
|
||||
ControlBlock: ctrlBlock,
|
||||
}
|
||||
forfeitTxWeightEstimator.AddTapscriptInput(64*2, tapscript)
|
||||
forfeitTxWeightEstimator.AddP2TROutput() // ASP output
|
||||
} else {
|
||||
return nil, fmt.Errorf("vtxo script is not a default vtxo script, cannot be async spent")
|
||||
}
|
||||
forfeitTxWeightEstimator.AddTapscriptInput(64*2, tapscript)
|
||||
forfeitTxWeightEstimator.AddP2TROutput() // ASP output
|
||||
|
||||
forfeitTxFee, err := b.wallet.MinRelayFee(context.Background(), uint64(forfeitTxWeightEstimator.VSize()))
|
||||
if err != nil {
|
||||
@@ -506,14 +514,18 @@ func (b *txBuilder) BuildAsyncPaymentTransactions(
|
||||
|
||||
unconditionnalForfeitPtx.Inputs[0].WitnessUtxo = &wire.TxOut{
|
||||
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{
|
||||
{
|
||||
ControlBlock: ctrlBlockBytes,
|
||||
Script: forfeitLeaf.Script,
|
||||
Script: tapscript.RevealedScript,
|
||||
ControlBlock: ctrlBlock,
|
||||
LeafVersion: txscript.BaseLeafVersion,
|
||||
},
|
||||
}
|
||||
@@ -542,16 +554,17 @@ func (b *txBuilder) BuildAsyncPaymentTransactions(
|
||||
}
|
||||
|
||||
for i, receiver := range receivers {
|
||||
// TODO (@louisinger): Add revert policy (sender+ASP)
|
||||
buf, err := hex.DecodeString(receiver.Pubkey)
|
||||
offchainScript, err := bitcointree.ParseVtxoScript(receiver.Descriptor)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
receiverPk, err := secp256k1.ParsePubKey(buf)
|
||||
|
||||
receiverVtxoTaprootKey, _, err := offchainScript.TapTree()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
newVtxoScript, _, err := b.getLeafScriptAndTree(receiverPk, aspPubKey)
|
||||
|
||||
newVtxoScript, err := common.P2TRScript(receiverVtxoTaprootKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -607,58 +620,12 @@ func (b *txBuilder) BuildAsyncPaymentTransactions(
|
||||
}, 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(
|
||||
aspPubKey *secp256k1.PublicKey,
|
||||
sharedOutputAmount int64, sharedOutputScript []byte,
|
||||
payments []domain.Payment, boardingInputs []ports.BoardingInput, connectorAddress string,
|
||||
sharedOutputAmount int64,
|
||||
sharedOutputScript []byte,
|
||||
payments []domain.Payment,
|
||||
boardingInputs []ports.BoardingInput,
|
||||
connectorAddress string,
|
||||
sweptRounds []domain.Round,
|
||||
) (*psbt.Packet, error) {
|
||||
connectorAddr, err := btcutil.DecodeAddress(connectorAddress, b.onchainNetwork())
|
||||
@@ -729,7 +696,7 @@ func (b *txBuilder) createPoolTx(
|
||||
}
|
||||
|
||||
for _, input := range boardingInputs {
|
||||
targetAmount -= input.GetAmount()
|
||||
targetAmount -= input.Amount
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
@@ -769,7 +736,7 @@ func (b *txBuilder) createPoolTx(
|
||||
ins := make([]*wire.OutPoint, 0)
|
||||
nSequences := make([]uint32, 0)
|
||||
witnessUtxos := make(map[int]*wire.TxOut)
|
||||
boardingTapLeaves := make(map[int]*psbt.TaprootTapLeafScript)
|
||||
tapLeaves := make(map[int]*psbt.TaprootTapLeafScript)
|
||||
nextIndex := 0
|
||||
|
||||
for _, utxo := range utxos {
|
||||
@@ -796,23 +763,48 @@ func (b *txBuilder) createPoolTx(
|
||||
nextIndex++
|
||||
}
|
||||
|
||||
for _, input := range boardingInputs {
|
||||
ins = append(ins, &wire.OutPoint{
|
||||
Hash: input.GetHash(),
|
||||
Index: input.GetIndex(),
|
||||
})
|
||||
nSequences = append(nSequences, wire.MaxTxInSequenceNum)
|
||||
|
||||
_, script, tapLeaf, err := b.craftBoardingTaproot(input.GetBoardingPubkey(), aspPubKey)
|
||||
for _, boardingInput := range boardingInputs {
|
||||
txHash, err := chainhash.NewHashFromStr(boardingInput.Txid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
boardingTapLeaves[nextIndex] = tapLeaf
|
||||
witnessUtxos[nextIndex] = &wire.TxOut{
|
||||
Value: int64(input.GetAmount()),
|
||||
PkScript: script,
|
||||
ins = append(ins, &wire.OutPoint{
|
||||
Hash: *txHash,
|
||||
Index: boardingInput.VtxoKey.VOut,
|
||||
})
|
||||
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++
|
||||
}
|
||||
|
||||
@@ -832,11 +824,8 @@ func (b *txBuilder) createPoolTx(
|
||||
}
|
||||
}
|
||||
|
||||
unspendableInternalKey := schnorr.SerializePubKey(bitcointree.UnspendableKey())
|
||||
|
||||
for inIndex, tapLeaf := range boardingTapLeaves {
|
||||
for inIndex, tapLeaf := range tapLeaves {
|
||||
updater.Upsbt.Inputs[inIndex].TaprootLeafScript = []*psbt.TaprootTapLeafScript{tapLeaf}
|
||||
updater.Upsbt.Inputs[inIndex].TaprootInternalKey = unspendableInternalKey
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -1048,6 +1043,7 @@ func (b *txBuilder) VerifyAndCombinePartialTx(dest string, src string) (string,
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
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(
|
||||
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) {
|
||||
aspScript, err := p2trScript(aspPubkey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
forfeitTxs := make([]string, 0)
|
||||
for _, payment := range payments {
|
||||
for _, vtxo := range payment.Inputs {
|
||||
pubkeyBytes, err := hex.DecodeString(vtxo.Pubkey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode pubkey: %s", err)
|
||||
}
|
||||
|
||||
vtxoPubkey, err := secp256k1.ParsePubKey(pubkeyBytes)
|
||||
offchainscript, err := bitcointree.ParseVtxoScript(vtxo.Descriptor)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
vtxoScript, vtxoTaprootTree, err := b.getLeafScriptAndTree(vtxoPubkey, aspPubkey)
|
||||
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()
|
||||
vtxoTaprootKey, tapTree, err := offchainscript.TapTree()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1216,21 +1142,46 @@ func (b *txBuilder) createForfeitTxs(
|
||||
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 {
|
||||
txs, err := craftForfeitTxs(
|
||||
connector, vtxo,
|
||||
&psbt.TaprootTapLeafScript{
|
||||
ControlBlock: ctrlBlockBytes,
|
||||
Script: forfeitProof.Script,
|
||||
LeafVersion: forfeitProof.LeafVersion,
|
||||
txs, err := bitcointree.BuildForfeitTxs(
|
||||
connector,
|
||||
&wire.OutPoint{
|
||||
Hash: *vtxoTxHash,
|
||||
Index: vtxo.VOut,
|
||||
},
|
||||
vtxoScript, aspScript, feeAmount, int64(connectorAmount),
|
||||
vtxo.Amount,
|
||||
connectorAmount,
|
||||
feeAmount,
|
||||
vtxoScript,
|
||||
aspPubkey,
|
||||
)
|
||||
if err != nil {
|
||||
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 {
|
||||
outpoints := make([]ports.TxOutpoint, 0, len(inputs))
|
||||
for _, input := range inputs {
|
||||
|
||||
@@ -20,11 +20,11 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
testingKey = "0218d5ca8b58797b7dbd65c075dd7ba7784b3f38ab71b1a5a8e3f94ba0257654a6"
|
||||
connectorAddress = "bc1py00yhcjpcj0k0sqra0etq0u3yy0purmspppsw0shyzyfe8c83tmq5h6kc2"
|
||||
roundLifetime = int64(1209344)
|
||||
unilateralExitDelay = int64(512)
|
||||
boardingExitDelay = int64(512)
|
||||
testingKey = "020000000000000000000000000000000000000000000000000000000000000001"
|
||||
connectorAddress = "bc1py00yhcjpcj0k0sqra0etq0u3yy0purmspppsw0shyzyfe8c83tmq5h6kc2"
|
||||
roundLifetime = int64(1209344)
|
||||
boardingExitDelay = int64(512)
|
||||
minRelayFeeRate = 3
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -53,7 +53,7 @@ func TestMain(m *testing.M) {
|
||||
|
||||
func TestBuildPoolTx(t *testing.T) {
|
||||
builder := txbuilder.NewTxBuilder(
|
||||
wallet, common.Bitcoin, roundLifetime, unilateralExitDelay, boardingExitDelay,
|
||||
wallet, common.Bitcoin, roundLifetime, boardingExitDelay,
|
||||
)
|
||||
|
||||
fixtures, err := parsePoolTxFixtures()
|
||||
@@ -64,15 +64,11 @@ func TestBuildPoolTx(t *testing.T) {
|
||||
t.Run("valid", func(t *testing.T) {
|
||||
for _, f := range fixtures.Valid {
|
||||
cosigners := make([]*secp256k1.PublicKey, 0)
|
||||
for _, payment := range f.Payments {
|
||||
for _, input := range payment.Inputs {
|
||||
pubkeyBytes, err := hex.DecodeString(input.Pubkey)
|
||||
require.NoError(t, err)
|
||||
pubkey, err := secp256k1.ParsePubKey(pubkeyBytes)
|
||||
require.NoError(t, err)
|
||||
for range f.Payments {
|
||||
randKey, err := secp256k1.GeneratePrivateKey()
|
||||
require.NoError(t, err)
|
||||
|
||||
cosigners = append(cosigners, pubkey)
|
||||
}
|
||||
cosigners = append(cosigners, randKey.PubKey())
|
||||
}
|
||||
|
||||
poolTx, congestionTree, connAddr, err := builder.BuildPoolTx(
|
||||
@@ -110,7 +106,7 @@ func TestBuildPoolTx(t *testing.T) {
|
||||
|
||||
func TestBuildForfeitTxs(t *testing.T) {
|
||||
builder := txbuilder.NewTxBuilder(
|
||||
wallet, common.Bitcoin, 1209344, unilateralExitDelay, boardingExitDelay,
|
||||
wallet, common.Bitcoin, 1209344, boardingExitDelay,
|
||||
)
|
||||
|
||||
fixtures, err := parseForfeitTxsFixtures()
|
||||
@@ -121,7 +117,7 @@ func TestBuildForfeitTxs(t *testing.T) {
|
||||
t.Run("valid", func(t *testing.T) {
|
||||
for _, f := range fixtures.Valid {
|
||||
connectors, forfeitTxs, err := builder.BuildForfeitTxs(
|
||||
pubkey, f.PoolTx, f.Payments,
|
||||
pubkey, f.PoolTx, f.Payments, minRelayFeeRate,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, connectors, f.ExpectedNumOfConnectors)
|
||||
@@ -159,7 +155,7 @@ func TestBuildForfeitTxs(t *testing.T) {
|
||||
t.Run("invalid", func(t *testing.T) {
|
||||
for _, f := range fixtures.Invalid {
|
||||
connectors, forfeitTxs, err := builder.BuildForfeitTxs(
|
||||
pubkey, f.PoolTx, f.Payments,
|
||||
pubkey, f.PoolTx, f.Payments, minRelayFeeRate,
|
||||
)
|
||||
require.EqualError(t, err, f.ExpectedErr)
|
||||
require.Empty(t, connectors)
|
||||
|
||||
@@ -38,20 +38,3 @@ func craftConnectorTx(
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
|
||||
"github.com/ark-network/ark/server/internal/core/ports"
|
||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
@@ -140,6 +141,17 @@ func (m *mockedWallet) MinRelayFee(ctx context.Context, vbytes uint64) (uint64,
|
||||
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) {
|
||||
args := m.Called(ctx)
|
||||
|
||||
|
||||
@@ -9,13 +9,13 @@
|
||||
{
|
||||
"txid": "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6",
|
||||
"vout": 0,
|
||||
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002",
|
||||
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
|
||||
"amount": 1100
|
||||
}
|
||||
],
|
||||
"receivers": [
|
||||
{
|
||||
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002",
|
||||
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
|
||||
"amount": 1100
|
||||
}
|
||||
]
|
||||
@@ -32,17 +32,17 @@
|
||||
{
|
||||
"txid": "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6",
|
||||
"vout": 0,
|
||||
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002",
|
||||
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
|
||||
"amount": 1100
|
||||
}
|
||||
],
|
||||
"receivers": [
|
||||
{
|
||||
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002",
|
||||
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
|
||||
"amount": 600
|
||||
},
|
||||
{
|
||||
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002",
|
||||
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
|
||||
"amount": 500
|
||||
}
|
||||
]
|
||||
@@ -59,17 +59,17 @@
|
||||
{
|
||||
"txid": "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6",
|
||||
"vout": 0,
|
||||
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002",
|
||||
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
|
||||
"amount": 1100
|
||||
}
|
||||
],
|
||||
"receivers": [
|
||||
{
|
||||
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002",
|
||||
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
|
||||
"amount": 600
|
||||
},
|
||||
{
|
||||
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002",
|
||||
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
|
||||
"amount": 500
|
||||
}
|
||||
]
|
||||
@@ -80,17 +80,17 @@
|
||||
{
|
||||
"txid": "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6",
|
||||
"vout": 0,
|
||||
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002",
|
||||
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
|
||||
"amount": 1100
|
||||
}
|
||||
],
|
||||
"receivers": [
|
||||
{
|
||||
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002",
|
||||
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
|
||||
"amount": 600
|
||||
},
|
||||
{
|
||||
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002",
|
||||
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
|
||||
"amount": 500
|
||||
}
|
||||
]
|
||||
@@ -101,17 +101,17 @@
|
||||
{
|
||||
"txid": "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6",
|
||||
"vout": 0,
|
||||
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002",
|
||||
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
|
||||
"amount": 1100
|
||||
}
|
||||
],
|
||||
"receivers": [
|
||||
{
|
||||
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002",
|
||||
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
|
||||
"amount": 600
|
||||
},
|
||||
{
|
||||
"pubkey": "020000000000000000000000000000000000000000000000000000000000000002",
|
||||
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
|
||||
"amount": 500
|
||||
}
|
||||
]
|
||||
@@ -128,53 +128,54 @@
|
||||
{
|
||||
"txid": "755c820771284d85ea4bbcc246565b4eddadc44237a7e57a0f9cb78a840d1d41",
|
||||
"vout": 0,
|
||||
"pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5",
|
||||
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
|
||||
"amount": 1000
|
||||
},
|
||||
{
|
||||
"txid": "66a0df86fcdeb84b8877adfe0b2c556dba30305d72ddbd4c49355f6930355357",
|
||||
"vout": 0,
|
||||
"pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5",
|
||||
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
|
||||
"amount": 1000
|
||||
},
|
||||
{
|
||||
"txid": "9913159bc7aa493ca53cbb9cbc88f97ba01137c814009dc7ef520c3fafc67909",
|
||||
"vout": 1,
|
||||
"pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5",
|
||||
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
|
||||
"amount": 500
|
||||
},
|
||||
{
|
||||
"txid": "5e10e77a7cdedc153be5193a4b6055a7802706ded4f2a9efefe86ed2f9a6ae60",
|
||||
"vout": 0,
|
||||
"pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5",
|
||||
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
|
||||
"amount": 1000
|
||||
},
|
||||
{
|
||||
"txid": "5e10e77a7cdedc153be5193a4b6055a7802706ded4f2a9efefe86ed2f9a6ae60",
|
||||
"vout": 1,
|
||||
"pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5",
|
||||
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
|
||||
"amount": 1000
|
||||
}
|
||||
],
|
||||
"receivers": [
|
||||
{
|
||||
"pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5",
|
||||
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
|
||||
"amount": 1000
|
||||
},
|
||||
{
|
||||
"pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5",
|
||||
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
|
||||
"amount": 1000
|
||||
},
|
||||
{
|
||||
"pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5",
|
||||
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
|
||||
"amount": 1000
|
||||
},
|
||||
{
|
||||
"pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5",
|
||||
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
|
||||
"amount": 1000
|
||||
},
|
||||
{
|
||||
"pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5",
|
||||
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
|
||||
"amount": 500
|
||||
}
|
||||
]
|
||||
@@ -196,53 +197,53 @@
|
||||
{
|
||||
"txid": "755c820771284d85ea4bbcc246565b4eddadc44237a7e57a0f9cb78a840d1d41",
|
||||
"vout": 0,
|
||||
"pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5",
|
||||
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
|
||||
"amount": 1000
|
||||
},
|
||||
{
|
||||
"txid": "66a0df86fcdeb84b8877adfe0b2c556dba30305d72ddbd4c49355f6930355357",
|
||||
"vout": 0,
|
||||
"pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5",
|
||||
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
|
||||
"amount": 1000
|
||||
},
|
||||
{
|
||||
"txid": "9913159bc7aa493ca53cbb9cbc88f97ba01137c814009dc7ef520c3fafc67909",
|
||||
"vout": 1,
|
||||
"pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5",
|
||||
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
|
||||
"amount": 500
|
||||
},
|
||||
{
|
||||
"txid": "5e10e77a7cdedc153be5193a4b6055a7802706ded4f2a9efefe86ed2f9a6ae60",
|
||||
"vout": 0,
|
||||
"pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5",
|
||||
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
|
||||
"amount": 1000
|
||||
},
|
||||
{
|
||||
"txid": "5e10e77a7cdedc153be5193a4b6055a7802706ded4f2a9efefe86ed2f9a6ae60",
|
||||
"vout": 1,
|
||||
"pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5",
|
||||
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
|
||||
"amount": 1000
|
||||
}
|
||||
],
|
||||
"receivers": [
|
||||
{
|
||||
"pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5",
|
||||
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
|
||||
"amount": 1000
|
||||
},
|
||||
{
|
||||
"pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5",
|
||||
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
|
||||
"amount": 1000
|
||||
},
|
||||
{
|
||||
"pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5",
|
||||
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
|
||||
"amount": 1000
|
||||
},
|
||||
{
|
||||
"pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5",
|
||||
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
|
||||
"amount": 1000
|
||||
},
|
||||
{
|
||||
"pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5",
|
||||
"descriptor": "tr(020000000000000000000000000000000000000000000000000000000000000001,{ and(pk(0000000000000000000000000000000000000000000000000000000000000001), pk(0000000000000000000000000000000000000000000000000000000000000001)), and(older(512), pk(0000000000000000000000000000000000000000000000000000000000000001)) })",
|
||||
"amount": 500
|
||||
}
|
||||
]
|
||||
|
||||
@@ -8,10 +8,6 @@ import (
|
||||
"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(
|
||||
payments []domain.Payment,
|
||||
) []domain.Receiver {
|
||||
@@ -28,19 +24,24 @@ func getOnchainReceivers(
|
||||
|
||||
func getOffchainReceivers(
|
||||
payments []domain.Payment,
|
||||
) []bitcointree.Receiver {
|
||||
) ([]bitcointree.Receiver, error) {
|
||||
receivers := make([]bitcointree.Receiver, 0)
|
||||
for _, payment := range payments {
|
||||
for _, receiver := range payment.Receivers {
|
||||
if !receiver.IsOnchain() {
|
||||
vtxoScript, err := bitcointree.ParseVtxoScript(receiver.Descriptor)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
receivers = append(receivers, bitcointree.Receiver{
|
||||
Pubkey: receiver.Pubkey,
|
||||
Script: vtxoScript,
|
||||
Amount: receiver.Amount,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return receivers
|
||||
return receivers, nil
|
||||
}
|
||||
|
||||
func countSpentVtxos(payments []domain.Payment) uint64 {
|
||||
|
||||
@@ -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) {
|
||||
fee := s.feeEstimator.RelayFeePerKW().FeeForVByte(lntypes.VByte(vbytes))
|
||||
return uint64(fee.ToUnit(btcutil.AmountSatoshi)), nil
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"github.com/btcsuite/btcd/btcutil/psbt"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||
"github.com/vulpemventures/go-elements/elementsutil"
|
||||
"github.com/vulpemventures/go-elements/psetv2"
|
||||
)
|
||||
@@ -58,7 +59,7 @@ func (s *service) SignTransaction(
|
||||
}
|
||||
|
||||
switch c := closure.(type) {
|
||||
case *tree.ForfeitClosure:
|
||||
case *tree.MultisigClosure:
|
||||
asp := schnorr.SerializePubKey(c.AspPubkey)
|
||||
owner := schnorr.SerializePubKey(c.Pubkey)
|
||||
|
||||
@@ -274,6 +275,12 @@ func (s *service) LockConnectorUtxos(ctx context.Context, utxos []ports.TxOutpoi
|
||||
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) {
|
||||
feeRate := 0.2
|
||||
fee := uint64(float64(vbytes) * feeRate)
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"sync"
|
||||
|
||||
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/server/internal/core/application"
|
||||
"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())
|
||||
}
|
||||
|
||||
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())
|
||||
if err != nil {
|
||||
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(
|
||||
ctx, vtxosKeys, receivers,
|
||||
ctx, inputs, receivers,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -109,11 +113,11 @@ func (h *handler) Ping(ctx context.Context, req *arkv1.PingRequest) (*arkv1.Ping
|
||||
resp = &arkv1.PingResponse{
|
||||
Event: &arkv1.PingResponse_RoundFinalization{
|
||||
RoundFinalization: &arkv1.RoundFinalizationEvent{
|
||||
Id: e.Id,
|
||||
PoolTx: e.PoolTx,
|
||||
CongestionTree: castCongestionTree(e.CongestionTree),
|
||||
ForfeitTxs: e.UnsignedForfeitTxs,
|
||||
Connectors: e.Connectors,
|
||||
Id: e.Id,
|
||||
PoolTx: e.PoolTx,
|
||||
CongestionTree: castCongestionTree(e.CongestionTree),
|
||||
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) {
|
||||
hrp, userPubkey, aspPubkey, err := parseAddress(req.GetAddress())
|
||||
_, userPubkey, _, err := parseAddress(req.GetAddress())
|
||||
if err != nil {
|
||||
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{
|
||||
SpendableVtxos: vtxoList(spendableVtxos).toProto(hrp, aspPubkey),
|
||||
SpentVtxos: vtxoList(spentVtxos).toProto(hrp, aspPubkey),
|
||||
SpendableVtxos: vtxoList(spendableVtxos).toProto(),
|
||||
SpentVtxos: vtxoList(spentVtxos).toProto(),
|
||||
}, 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)")
|
||||
}
|
||||
|
||||
addr, err := h.svc.GetBoardingAddress(ctx, userPubkey)
|
||||
addr, descriptor, err := h.svc.GetBoardingAddress(ctx, userPubkey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &arkv1.GetBoardingAddressResponse{
|
||||
Address: addr,
|
||||
Address: addr,
|
||||
Descriptor_: descriptor,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -478,11 +483,11 @@ func (h *handler) listenToEvents() {
|
||||
ev = &arkv1.GetEventStreamResponse{
|
||||
Event: &arkv1.GetEventStreamResponse_RoundFinalization{
|
||||
RoundFinalization: &arkv1.RoundFinalizationEvent{
|
||||
Id: e.Id,
|
||||
PoolTx: e.PoolTx,
|
||||
CongestionTree: castCongestionTree(e.CongestionTree),
|
||||
ForfeitTxs: e.UnsignedForfeitTxs,
|
||||
Connectors: e.Connectors,
|
||||
Id: e.Id,
|
||||
PoolTx: e.PoolTx,
|
||||
CongestionTree: castCongestionTree(e.CongestionTree),
|
||||
Connectors: e.Connectors,
|
||||
MinRelayFeeRate: e.MinRelayFeeRate,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -547,15 +552,9 @@ func (h *handler) listenToEvents() {
|
||||
|
||||
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))
|
||||
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
|
||||
if vv.AsyncPayment != nil {
|
||||
pendingData = &arkv1.PendingPayment{
|
||||
@@ -564,18 +563,12 @@ func (v vtxoList) toProto(hrp string, aspKey *secp256k1.PublicKey) []*arkv1.Vtxo
|
||||
}
|
||||
}
|
||||
list = append(list, &arkv1.Vtxo{
|
||||
Outpoint: &arkv1.Input{
|
||||
Input: &arkv1.Input_VtxoInput{
|
||||
VtxoInput: &arkv1.VtxoInput{
|
||||
Txid: vv.Txid,
|
||||
Vout: vv.VOut,
|
||||
},
|
||||
},
|
||||
},
|
||||
Receiver: &arkv1.Output{
|
||||
Address: addr,
|
||||
Amount: vv.Amount,
|
||||
Outpoint: &arkv1.Outpoint{
|
||||
Txid: vv.Txid,
|
||||
Vout: vv.VOut,
|
||||
},
|
||||
Descriptor_: vv.Descriptor,
|
||||
Amount: vv.Amount,
|
||||
PoolTxid: vv.PoolTx,
|
||||
Spent: vv.Spent,
|
||||
ExpireAt: vv.ExpireAt,
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
|
||||
arkv1 "github.com/ark-network/ark/api-spec/protobuf/gen/ark/v1"
|
||||
"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/ports"
|
||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||
)
|
||||
|
||||
@@ -18,27 +17,19 @@ func parseAddress(addr string) (string, *secp256k1.PublicKey, *secp256k1.PublicK
|
||||
return common.DecodeAddress(addr)
|
||||
}
|
||||
|
||||
func parseInputs(ins []*arkv1.Input) ([]application.Input, error) {
|
||||
func parseInputs(ins []*arkv1.Input) ([]ports.Input, error) {
|
||||
if len(ins) <= 0 {
|
||||
return nil, fmt.Errorf("missing inputs")
|
||||
}
|
||||
|
||||
inputs := make([]application.Input, 0, len(ins))
|
||||
inputs := make([]ports.Input, 0, len(ins))
|
||||
for _, input := range ins {
|
||||
if input.GetBoardingInput() != nil {
|
||||
desc := input.GetBoardingInput().GetDescriptor_()
|
||||
inputs = append(inputs, application.Input{
|
||||
Txid: input.GetBoardingInput().GetTxid(),
|
||||
Index: input.GetBoardingInput().GetVout(),
|
||||
Descriptor: desc,
|
||||
})
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
inputs = append(inputs, application.Input{
|
||||
Txid: input.GetVtxoInput().GetTxid(),
|
||||
Index: input.GetVtxoInput().GetVout(),
|
||||
inputs = append(inputs, ports.Input{
|
||||
VtxoKey: domain.VtxoKey{
|
||||
Txid: input.GetOutpoint().GetTxid(),
|
||||
VOut: input.GetOutpoint().GetVout(),
|
||||
},
|
||||
Descriptor: input.GetDescriptor_(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -51,21 +42,14 @@ func parseReceivers(outs []*arkv1.Output) ([]domain.Receiver, error) {
|
||||
if out.GetAmount() == 0 {
|
||||
return nil, fmt.Errorf("missing output amount")
|
||||
}
|
||||
if len(out.GetAddress()) <= 0 {
|
||||
return nil, fmt.Errorf("missing output address")
|
||||
}
|
||||
var pubkey, addr string
|
||||
_, pk, _, err := common.DecodeAddress(out.GetAddress())
|
||||
if err != nil {
|
||||
addr = out.GetAddress()
|
||||
}
|
||||
if pk != nil {
|
||||
pubkey = hex.EncodeToString(pk.SerializeCompressed())
|
||||
if len(out.GetAddress()) <= 0 && len(out.GetDescriptor_()) <= 0 {
|
||||
return nil, fmt.Errorf("missing output destination")
|
||||
}
|
||||
|
||||
receivers = append(receivers, domain.Receiver{
|
||||
Pubkey: pubkey,
|
||||
Descriptor: out.GetDescriptor_(),
|
||||
Amount: out.GetAmount(),
|
||||
OnchainAddress: addr,
|
||||
OnchainAddress: out.GetAddress(),
|
||||
})
|
||||
}
|
||||
return receivers, nil
|
||||
|
||||
Reference in New Issue
Block a user