Add reversible policy to pending vtxos (#311)

* [server] descriptor-based vtxo script

* [server] fix unit tests

* [sdk] descriptor based vtxo

* empty config check & version flag support

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

* fix

* [sdk] several fixes

* [sdk][server] several fixes

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

* [common] improve parser

* [common] fix reversible vtxo parser

* [sdk] remove logs

* fix forfeit map

* remove debug log

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

* remove signing pubkey

* remove signer public key, craft forfeit txs client side

* go work sync

* fix linter errors

* rename MakeForfeitTxs to BuildForfeitTxs

* fix conflicts

* fix tests

* comment VtxoScript type

* revert ROUND_INTERVAL value

---------

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

View File

@@ -475,21 +475,6 @@
}
}
},
"v1BoardingInput": {
"type": "object",
"properties": {
"txid": {
"type": "string"
},
"vout": {
"type": "integer",
"format": "int64"
},
"descriptor": {
"type": "string"
}
}
},
"v1ClaimPaymentRequest": {
"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"
}
}
}

View File

@@ -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 {

View File

@@ -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

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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,9 +108,7 @@ type node interface {
}
type leaf struct {
aspKey *secp256k1.PublicKey
vtxoKey *secp256k1.PublicKey
exitDelay int64
vtxoScript VtxoScript
amount int64
}
@@ -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,20 +255,8 @@ 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,
vtxoScript: r.Script,
amount: int64(r.Amount),
}
nodes = append(nodes, leafNode)

View File

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

View File

@@ -2,6 +2,7 @@ package bitcointree_test
import (
"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"`
}

View File

@@ -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 {

View File

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

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

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

View File

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

View File

@@ -8,37 +8,149 @@ import (
"github.com/decred/dcrd/dcrec/secp256k1/v4"
)
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(second.Key.Hex)
keyBytes, err := hex.DecodeString(first.Key.Hex)
if err != nil {
return nil, 0, err
return nil, nil, nil, 0, err
}
if sender == nil {
sender, err = schnorr.ParsePubKey(keyBytes)
if err != nil {
return nil, nil, nil, 0, err
}
} else {
user, err = schnorr.ParsePubKey(keyBytes)
if err != nil {
return nil, 0, err
return nil, nil, nil, 0, err
}
}
if asp == nil {
keyBytes, err = hex.DecodeString(second.Key.Hex)
if err != nil {
return nil, nil, nil, 0, err
}
asp, err = schnorr.ParsePubKey(keyBytes)
if err != nil {
return nil, nil, nil, 0, err
}
}
}
}
if first, ok := andLeaf.First.(*Older); ok {
if second, ok := andLeaf.Second.(*PK); ok {
timeout = first.Timeout
keyBytes, err := hex.DecodeString(second.Key.Hex)
if err != nil {
return nil, nil, nil, 0, err
}
sender, err = schnorr.ParsePubKey(keyBytes)
if err != nil {
return nil, nil, nil, 0, err
}
}
}
}
}
if user == nil {
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

View File

@@ -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{})

View File

@@ -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

View File

@@ -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)
})
}
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -35,6 +35,7 @@ github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/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=

View File

@@ -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
@@ -49,7 +49,6 @@ type node struct {
asset string
feeSats uint64
roundLifetime int64
unilateralExitDelay 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")
@@ -427,7 +388,6 @@ func createPartialCongestionTree(
asset: asset,
feeSats: feeSatsPerNode,
roundLifetime: roundLifetime,
unilateralExitDelay: unilateralExitDelay,
}
nodes = append(nodes, leafNode)
}

View File

@@ -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
}

View File

@@ -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) {

View File

@@ -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
View File

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

11
common/utils.go Normal file
View File

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

62
common/vtxo.go Normal file
View File

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

View File

@@ -418,6 +418,7 @@ github.com/ark-network/ark/common v0.0.0-20240812222508-b097e943fb45/go.mod h1:8
github.com/ark-network/ark/common v0.0.0-20240812233307-18e343b31899/go.mod h1:8DYeb06Dl8onmrV09xfsdDMGv5HoVtWoKhLBLXOYHew=
github.com/ark-network/ark/common v0.0.0-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=

View File

@@ -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,7 +91,8 @@ type Vtxo struct {
}
type Output struct {
Address string
Address string // onchain output address
Descriptor string // offchain vtxo descriptor
Amount uint64
}
@@ -154,9 +135,9 @@ type Round struct {
type RoundFinalizationEvent struct {
ID string
Tx string
ForfeitTxs []string
Tree tree.CongestionTree
Connectors []string
MinRelayFeeRate chainfee.SatPerKVByte
}
func (e RoundFinalizationEvent) isRoundEvent() {}

View File

@@ -14,6 +14,7 @@ import (
"github.com/ark-network/ark/pkg/client-sdk/client"
"github.com/ark-network/ark/pkg/client-sdk/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,8 +319,19 @@ 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{
Destination: &arkv1.Output_Descriptor_{
Descriptor_: o.Descriptor,
},
Amount: o.Amount,
}
}
@@ -364,9 +371,9 @@ func (e event) toRoundEvent() (client.RoundEvent, error) {
return client.RoundFinalizationEvent{
ID: ee.GetId(),
Tx: ee.GetPoolTx(),
ForfeitTxs: ee.GetForfeitTxs(),
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,
}
}

View File

@@ -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,
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(),
ins = append(ins, &models.V1Input{
Outpoint: &models.V1Outpoint{
Txid: i.Txid,
Vout: int64(i.VOut),
},
}
} else {
input = &models.V1Input{
VtxoInput: &models.V1VtxoInput{
Txid: i.GetTxID(),
Vout: int64(i.GetVOut()),
},
}
}
ins = append(ins, input)
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,
MinRelayFeeRate: chainfee.SatPerKVByte(minRelayFeeRate),
}, nil
}
@@ -394,19 +390,16 @@ 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))
@@ -414,6 +407,7 @@ func (a *restClient) CreatePayment(
outs = append(outs, &models.V1Output{
Address: o.Address,
Amount: strconv.Itoa(int(o.Amount)),
Descriptor: o.Descriptor,
})
}
body := models.V1CreatePaymentRequest{

View File

@@ -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
}

View File

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

View File

@@ -17,11 +17,14 @@ import (
// swagger:model v1Output
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

View File

@@ -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"`
}

View File

@@ -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 {

View File

@@ -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,
},

View File

@@ -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,8 +436,14 @@ 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,
Descriptor: desc,
Amount: changeAmount,
})
}
@@ -444,9 +451,12 @@ func (a *covenantArkClient) CollaborativeRedeem(
inputs := make([]client.Input, 0, len(selectedCoins))
for _, coin := range selectedCoins {
inputs = append(inputs, client.VtxoKey{
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")
}
desc, err := a.offchainAddressToDefaultVtxoDescriptor(myselfOffchain)
if err != nil {
return "", err
}
receiver := client.Output{
Address: myselfOffchain,
Descriptor: desc,
Amount: pendingBalance,
}
desc := strings.ReplaceAll(a.BoardingDescriptorTemplate, "USER", hex.EncodeToString(schnorr.SerializePubKey(mypubkey)))
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,8 +822,13 @@ 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(),
Descriptor: desc,
Amount: receiver.Amount(),
})
sumOfReceivers += receiver.Amount()
@@ -828,8 +855,14 @@ func (a *covenantArkClient) sendOffchain(
if err != nil {
return "", err
}
desc, err := a.offchainAddressToDefaultVtxoDescriptor(offchainAddr)
if err != nil {
return "", err
}
changeReceiver := client.Output{
Address: offchainAddr,
Descriptor: desc,
Amount: changeAmount,
}
receiversOutput = append(receiversOutput, changeReceiver)
@@ -837,9 +870,12 @@ func (a *covenantArkClient) sendOffchain(
inputs := make([]client.Input, 0, len(selectedCoins))
for _, coin := range selectedCoins {
inputs = append(inputs, client.VtxoKey{
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
}
}
}
return
b64, err := updater.Pset.ToBase64()
if err != nil {
return nil, "", err
}
signedRoundTx, err = a.wallet.SignTransaction(ctx, a.explorer, b64)
if err != nil {
return nil, "", err
}
}
return signedForfeits, signedRoundTx, nil
}
func (a *covenantArkClient) validateCongestionTree(
@@ -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)
connectorsPsets = append(connectorsPsets, p)
}
for _, vtxo := range vtxosToSign {
vtxoScript, err := tree.ParseVtxoScript(vtxo.Descriptor)
if err != nil {
return nil, err
}
signedForfeits = append(signedForfeits, signedPset)
vtxoTapKey, vtxoTapTree, err := vtxoScript.TapTree()
if err != nil {
return nil, err
}
feeAmount, err := common.ComputeForfeitMinRelayFee(feeRate, vtxoTapTree)
if err != nil {
return nil, err
}
vtxoOutputScript, err := common.P2TRScript(vtxoTapKey)
if err != nil {
return nil, err
}
vtxoInput := psetv2.InputArgs{
Txid: vtxo.Txid,
TxIndex: vtxo.VOut,
}
forfeitClosure := &tree.MultisigClosure{
Pubkey: myPubKey,
AspPubkey: a.AspPubkey,
}
forfeitLeaf, err := forfeitClosure.Leaf()
if err != nil {
return nil, err
}
leafProof, err := vtxoTapTree.GetTaprootMerkleProof(forfeitLeaf.TapHash())
if err != nil {
return nil, err
}
ctrlBlock, err := taproot.ParseControlBlock(leafProof.ControlBlock)
if err != nil {
return nil, err
}
tapscript := psetv2.TapLeafScript{
TapElementsLeaf: taproot.NewBaseTapElementsLeaf(leafProof.Script),
ControlBlock: *ctrlBlock,
}
for _, connectorPset := range connectorsPsets {
forfeits, err := tree.BuildForfeitTxs(
connectorPset, vtxoInput, vtxo.Amount, a.Dust, feeAmount, vtxoOutputScript, a.AspPubkey,
)
if err != nil {
return nil, err
}
for _, forfeit := range forfeits {
updater, err := psetv2.NewUpdater(forfeit)
if err != nil {
return nil, err
}
if err := updater.AddInTapLeafScript(1, tapscript); err != nil {
return nil, err
}
b64, err := updater.Pset.ToBase64()
if err != nil {
return nil, err
}
signedForfeit, err := a.wallet.SignTransaction(ctx, a.explorer, b64)
if err != nil {
return nil, err
}
signedForfeits = append(signedForfeits, signedForfeit)
}
}
}
@@ -1173,24 +1384,6 @@ func (a *covenantArkClient) loopAndSign(
return signedForfeits, nil
}
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 {

View File

@@ -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,8 +425,14 @@ 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,
Descriptor: desc,
Amount: changeAmount,
})
}
@@ -434,9 +440,12 @@ func (a *covenantlessArkClient) CollaborativeRedeem(
inputs := make([]client.Input, 0, len(selectedCoins))
for _, coin := range selectedCoins {
inputs = append(inputs, client.VtxoKey{
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,8 +525,26 @@ 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(),
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],
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
}
desc, err := a.offchainAddressToDefaultVtxoDescriptor(myselfOffchain)
if err != nil {
return "", err
}
receiver := client.Output{
Address: myselfOffchain,
Descriptor: desc,
Amount: pendingBalance,
}
desc := strings.ReplaceAll(a.BoardingDescriptorTemplate, "USER", hex.EncodeToString(schnorr.SerializePubKey(mypubkey)))
return a.selfTransferAllPendingPayments(ctx, pendingVtxos, boardingUtxos, receiver, desc)
return a.selfTransferAllPendingPayments(
ctx,
pendingVtxos,
boardingUtxos,
receiver,
hex.EncodeToString(mypubkey.SerializeCompressed()),
)
}
func (a *covenantlessArkClient) GetTransactionHistory(ctx context.Context) ([]Transaction, error) {
@@ -818,8 +856,13 @@ func (a *covenantlessArkClient) sendOffchain(
return "", fmt.Errorf("invalid amount (%d), must be greater than dust %d", receiver.Amount(), a.Dust)
}
desc, err := a.offchainAddressToDefaultVtxoDescriptor(receiver.To())
if err != nil {
return "", err
}
receiversOutput = append(receiversOutput, client.Output{
Address: receiver.To(),
Descriptor: desc,
Amount: receiver.Amount(),
})
sumOfReceivers += receiver.Amount()
@@ -846,8 +889,14 @@ func (a *covenantlessArkClient) sendOffchain(
if err != nil {
return "", err
}
desc, err := a.offchainAddressToDefaultVtxoDescriptor(offchainAddr)
if err != nil {
return "", err
}
changeReceiver := client.Output{
Address: offchainAddr,
Descriptor: desc,
Amount: changeAmount,
}
receiversOutput = append(receiversOutput, changeReceiver)
@@ -855,9 +904,12 @@ func (a *covenantlessArkClient) sendOffchain(
inputs := make([]client.Input, 0, len(selectedCoins))
for _, coin := range selectedCoins {
inputs = append(inputs, client.VtxoKey{
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
}
if mustSignRoundTx {
signedRoundTx, err = a.wallet.SignTransaction(ctx, a.explorer, event.Tx)
forfeits = signedForfeits
}
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
}
}
}
return
b64, err := roundPtx.B64Encode()
if err != nil {
return nil, "", err
}
signedRoundTx, err := a.wallet.SignTransaction(ctx, a.explorer, b64)
if err != nil {
return nil, "", err
}
return forfeits, signedRoundTx, nil
}
return forfeits, "", nil
}
func (a *covenantlessArkClient) validateCongestionTree(
@@ -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)
connectorsPsets := make([]*psbt.Packet, 0, len(connectors))
connectorsTxids := make([]string, 0, len(connectors))
for _, connector := range connectors {
ptx, err := psbt.NewFromRawBytes(strings.NewReader(connector), true)
if err != nil {
return nil, err
}
txid := ptx.UnsignedTx.TxHash().String()
connectorsTxids = append(connectorsTxids, txid)
}
for _, forfeitTx := range forfeitTxs {
ptx, err := psbt.NewFromRawBytes(strings.NewReader(forfeitTx), true)
p, err := psbt.NewFromRawBytes(strings.NewReader(connector), true)
if err != nil {
return nil, err
}
for _, input := range ptx.UnsignedTx.TxIn {
inputTxid := input.PreviousOutPoint.Hash.String()
for _, coin := range vtxosToSign {
// check if it contains one of the input to sign
if inputTxid == coin.Txid {
// verify that the connector is in the connectors list
connectorTxid := ptx.UnsignedTx.TxIn[0].PreviousOutPoint.Hash.String()
connectorFound := false
for _, txid := range connectorsTxids {
if txid == connectorTxid {
connectorFound = true
break
}
connectorsPsets = append(connectorsPsets, p)
}
if !connectorFound {
return nil, fmt.Errorf("connector txid %s not found in the connectors list", connectorTxid)
}
signedForfeitTx, err := a.wallet.SignTransaction(ctx, a.explorer, forfeitTx)
for _, vtxo := range vtxosToSign {
vtxoScript, err := bitcointree.ParseVtxoScript(vtxo.Descriptor)
if err != nil {
return nil, err
}
signedForfeits = append(signedForfeits, signedForfeitTx)
}
vtxoTapKey, vtxoTapTree, err := vtxoScript.TapTree()
if err != nil {
return nil, err
}
feeAmount, err := common.ComputeForfeitMinRelayFee(feeRate, vtxoTapTree)
if err != nil {
return nil, err
}
vtxoOutputScript, err := common.P2TRScript(vtxoTapKey)
if err != nil {
return nil, err
}
vtxoTxHash, err := chainhash.NewHashFromStr(vtxo.Txid)
if err != nil {
return nil, err
}
vtxoInput := &wire.OutPoint{
Hash: *vtxoTxHash,
Index: vtxo.VOut,
}
forfeitClosure := &bitcointree.MultisigClosure{
Pubkey: myPubkey,
AspPubkey: a.AspPubkey,
}
forfeitLeaf, err := forfeitClosure.Leaf()
if err != nil {
return nil, err
}
leafProof, err := vtxoTapTree.GetTaprootMerkleProof(forfeitLeaf.TapHash())
if err != nil {
return nil, err
}
tapscript := psbt.TaprootTapLeafScript{
ControlBlock: leafProof.ControlBlock,
Script: leafProof.Script,
LeafVersion: txscript.BaseLeafVersion,
}
for _, connectorPset := range connectorsPsets {
forfeits, err := bitcointree.BuildForfeitTxs(
connectorPset, vtxoInput, vtxo.Amount, a.Dust, feeAmount, vtxoOutputScript, a.AspPubkey,
)
if err != nil {
return nil, err
}
if len(forfeits) <= 0 {
return nil, fmt.Errorf("no forfeit txs created dust = %d", a.Dust)
}
for _, forfeit := range forfeits {
forfeit.Inputs[1].TaprootLeafScript = []*psbt.TaprootTapLeafScript{&tapscript}
b64, err := forfeit.B64Encode()
if err != nil {
return nil, err
}
signedForfeit, err := a.wallet.SignTransaction(ctx, a.explorer, b64)
if err != nil {
return nil, err
}
signedForfeits = append(signedForfeits, signedForfeit)
}
}
}
return signedForfeits, nil
@@ -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 {

View File

@@ -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

View File

@@ -93,6 +93,7 @@ github.com/fergusstrange/embedded-postgres v1.28.0 h1:Atixd24HCuBHBavnG4eiZAjRiz
github.com/fergusstrange/embedded-postgres v1.28.0/go.mod h1:t/MLs0h9ukYM6FSt99R7InCHs1nW0ordoVCcnzmpTYw=
github.com/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=

View File

@@ -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

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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")

View File

@@ -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", in.Txid, err)
return "", fmt.Errorf("failed to get tx %s: %s", input.Txid, err)
}
confirmed, blocktime, err := s.wallet.IsTransactionConfirmed(ctx, in.Txid)
tx, err := transaction.NewTxFromHex(txhex)
if err != nil {
return "", fmt.Errorf("failed to check tx %s: %s", in.Txid, err)
return "", fmt.Errorf("failed to parse tx %s: %s", input.Txid, err)
}
confirmed, blocktime, err := s.wallet.IsTransactionConfirmed(ctx, input.Txid)
if err != nil {
return "", fmt.Errorf("failed to check tx %s: %s", input.Txid, err)
}
if !confirmed {
return "", fmt.Errorf("tx %s not confirmed", in.Txid)
return "", fmt.Errorf("tx %s not confirmed", input.Txid)
}
// if the exit path is available, forbid registering the boarding utxo
if blocktime+int64(s.boardingExitDelay) < now {
return "", fmt.Errorf("tx %s expired", in.Txid)
return "", fmt.Errorf("tx %s expired", input.Txid)
}
boardingTxs[in.Txid] = txhex
}
boardingTxs[input.Txid] = tx
}
utxos := make([]ports.BoardingInput, 0, len(boardingInputs))
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)
tx := boardingTxs[input.Txid]
boardingInput, err := s.newBoardingInput(tx, input)
if err != nil {
return "", err
}
if err := s.paymentRequests.push(*payment, utxos); err != nil {
boardingInputs = append(boardingInputs, *boardingInput)
continue
}
vtxo := vtxosResult[0]
if vtxo.Spent {
return "", fmt.Errorf("input %s:%d already spent", vtxo.Txid, vtxo.VOut)
}
if vtxo.Redeemed {
return "", fmt.Errorf("input %s:%d already redeemed", vtxo.Txid, vtxo.VOut)
}
if vtxo.Swept {
return "", fmt.Errorf("input %s:%d already swept", vtxo.Txid, vtxo.VOut)
}
vtxosInputs = append(vtxosInputs, vtxo)
}
payment, err := domain.NewPayment(vtxosInputs)
if err != nil {
return "", err
}
if err := s.paymentRequests.push(*payment, boardingInputs); err != nil {
return "", err
}
return 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,
MinRelayFeeRate: int64(s.wallet.MinRelayFeeRate(context.Background())),
}
s.lastEvent = ev
s.eventsCh <- ev
@@ -888,34 +894,59 @@ func (s *covenantService) getNewVtxos(round *domain.Round) []domain.Vtxo {
for _, node := range leaves {
tx, _ := psetv2.NewPsetFromBase64(node.Tx)
for i, out := range tx.Outputs {
for _, p := range round.Payments {
var pubkey string
if len(out.Script) <= 0 {
continue // skip fee outputs
}
desc := ""
found := false
for _, p := range round.Payments {
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},
Receiver: domain.Receiver{Descriptor: desc, Amount: uint64(out.Value)},
PoolTx: round.Txid,
})
break
}
}
}
}
return vtxos
}
@@ -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) {

View File

@@ -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 get taproot key: %s", err)
}
// verify that the user signs a forfeit closure
var userPubKey *secp256k1.PublicKey
aspXOnlyPubKey := schnorr.SerializePubKey(s.pubkey)
for _, sig := range input.TaprootScriptSpendSig {
if !bytes.Equal(sig.XOnlyPubKey, aspXOnlyPubKey) {
parsed, err := schnorr.ParsePubKey(sig.XOnlyPubKey)
if err != nil {
return fmt.Errorf("failed to parse pubkey: %s", err)
}
xonlyPubkey := schnorr.SerializePubKey(pubkey)
// find signature belonging to the pubkey
found := false
for _, sig := range input.TaprootScriptSpendSig {
if bytes.Equal(sig.XOnlyPubKey, xonlyPubkey) {
found = true
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,7 +269,7 @@ func (s *covenantlessService) CompleteAsyncPayment(
VOut: uint32(outIndex),
},
Receiver: domain.Receiver{
Pubkey: asyncPayData.receivers[outIndex].Pubkey,
Descriptor: asyncPayData.receivers[outIndex].Descriptor,
Amount: uint64(out.Value),
},
ExpireAt: asyncPayData.expireAt,
@@ -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)
for _, input := range inputs {
if input.IsVtxo() {
vtxosInputs = append(vtxosInputs, input.VtxoKey())
continue
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),
}
boardingInputs = append(boardingInputs, input)
}
vtxos := make([]domain.Vtxo, 0)
if len(vtxosInputs) > 0 {
var err error
vtxos, err = s.repoManager.Vtxos().GetVtxos(ctx, vtxosInputs)
tapKey, _, err := vtxoScript.TapTree()
if err != nil {
return "", err
}
for _, v := range vtxos {
if v.Spent {
return "", fmt.Errorf("input %s:%d already spent", v.Txid, v.VOut)
return "", "", fmt.Errorf("failed to get taproot key: %s", err)
}
if v.Redeemed {
return "", fmt.Errorf("input %s:%d already redeemed", v.Txid, v.VOut)
addr, err := btcutil.NewAddressTaproot(
schnorr.SerializePubKey(tapKey), s.chainParams(),
)
if err != nil {
return "", "", fmt.Errorf("failed to get address: %s", err)
}
if v.Spent {
return "", fmt.Errorf("input %s:%d already spent", v.Txid, v.VOut)
}
}
}
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)
boardingTxs := make(map[string]string, 0) // txid -> txhex
now := time.Now().Unix()
for _, in := range boardingInputs {
if _, ok := boardingTxs[in.Txid]; !ok {
boardingTxs := make(map[string]wire.MsgTx, 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, in.Txid)
txhex, err := s.wallet.GetTransaction(ctx, input.Txid)
if err != nil {
return "", fmt.Errorf("failed to get tx %s: %s", in.Txid, err)
return "", fmt.Errorf("failed to get tx %s: %s", input.Txid, err)
}
confirmed, blocktime, err := s.wallet.IsTransactionConfirmed(ctx, in.Txid)
var tx wire.MsgTx
if err := tx.Deserialize(hex.NewDecoder(strings.NewReader(txhex))); err != nil {
return "", fmt.Errorf("failed to deserialize tx %s: %s", input.Txid, err)
}
confirmed, blocktime, err := s.wallet.IsTransactionConfirmed(ctx, input.Txid)
if err != nil {
return "", fmt.Errorf("failed to check tx %s: %s", in.Txid, err)
return "", fmt.Errorf("failed to check tx %s: %s", input.Txid, err)
}
if !confirmed {
return "", fmt.Errorf("tx %s not confirmed", in.Txid)
return "", fmt.Errorf("tx %s not confirmed", input.Txid)
}
// if the exit path is available, forbid registering the boarding utxo
if blocktime+int64(s.boardingExitDelay) < now {
return "", fmt.Errorf("tx %s expired", in.Txid)
return "", fmt.Errorf("tx %s expired", input.Txid)
}
boardingTxs[in.Txid] = txhex
}
boardingTxs[input.Txid] = tx
}
utxos := make([]ports.BoardingInput, 0, len(boardingInputs))
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)
tx := boardingTxs[input.Txid]
boardingInput, err := s.newBoardingInput(tx, input)
if err != nil {
return "", err
}
if err := s.paymentRequests.push(*payment, utxos); err != nil {
boardingInputs = append(boardingInputs, *boardingInput)
continue
}
vtxo := vtxosResult[0]
if vtxo.Spent {
return "", fmt.Errorf("input %s:%d already spent", vtxo.Txid, vtxo.VOut)
}
if vtxo.Redeemed {
return "", fmt.Errorf("input %s:%d already redeemed", vtxo.Txid, vtxo.VOut)
}
if vtxo.Swept {
return "", fmt.Errorf("input %s:%d already swept", vtxo.Txid, vtxo.VOut)
}
vtxosInputs = append(vtxosInputs, vtxo)
}
payment, err := domain.NewPayment(vtxosInputs)
if err != nil {
return "", err
}
if err := s.paymentRequests.push(*payment, boardingInputs); err != nil {
return "", err
}
return 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) {
tapKey, _, err := boardingScript.TapTree()
if err != nil {
return nil, fmt.Errorf("failed to get taproot key: %s", err)
}
expectedScriptPubKey, err := common.P2TRScript(tapKey)
if err != nil {
return nil, fmt.Errorf("failed to get script pubkey: %s", err)
}
if !bytes.Equal(output.PkScript, expectedScriptPubKey) {
return nil, fmt.Errorf("descriptor does not match script in transaction output")
}
if defaultVtxoScript, ok := boardingScript.(*bitcointree.DefaultVtxoScript); ok {
if !bytes.Equal(schnorr.SerializePubKey(defaultVtxoScript.Asp), schnorr.SerializePubKey(s.pubkey)) {
return nil, fmt.Errorf("invalid boarding descriptor, ASP mismatch")
}
if defaultVtxoScript.ExitDelay != uint(s.boardingExitDelay) {
return nil, fmt.Errorf("invalid boarding descriptor, timeout mismatch")
}
_, expectedScript, err := s.builder.GetBoardingScript(pubkey, s.pubkey)
if err != nil {
return nil, fmt.Errorf("failed to get boarding script: %s", err)
} else {
return nil, fmt.Errorf("only default vtxo script is supported for boarding")
}
if !bytes.Equal(script, expectedScript) {
return nil, fmt.Errorf("invalid boarding input output script")
}
return &boardingInput{
txId: tx.TxHash(),
vout: vout,
boardingPubKey: pubkey,
amount: uint64(out.Value),
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,
MinRelayFeeRate: int64(s.wallet.MinRelayFeeRate(context.Background())),
}
s.lastEvent = ev
s.eventsCh <- ev
@@ -1291,39 +1300,55 @@ func (s *covenantlessService) getNewVtxos(round *domain.Round) []domain.Vtxo {
continue
}
for i, out := range tx.UnsignedTx.TxOut {
for _, p := range round.Payments {
var pubkey string
desc := ""
found := false
for _, p := range round.Payments {
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)},
Receiver: domain.Receiver{Descriptor: desc, Amount: uint64(out.Value)},
PoolTx: round.Txid,
})
break
}
}
}
}
return vtxos
}
@@ -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()

View File

@@ -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

View File

@@ -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
}

View File

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

View File

@@ -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
}

View File

@@ -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,7 +25,7 @@ var inputs = []domain.Vtxo{
VOut: 0,
},
Receiver: domain.Receiver{
Pubkey: "030000000000000000000000000000000000000000000000000000000000000001",
Descriptor: desc,
Amount: 1000,
},
},
@@ -40,11 +51,11 @@ func TestPayment(t *testing.T) {
err = payment.AddReceivers([]domain.Receiver{
{
Pubkey: "030000000000000000000000000000000000000000000000000000000000000001",
Descriptor: desc,
Amount: 450,
},
{
Pubkey: "020000000000000000000000000000000000000000000000000000000000000002",
Descriptor: desc,
Amount: 550,
},
})

View File

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

View File

@@ -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)
}

View File

@@ -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)

View File

@@ -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 {

View File

@@ -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),
Descriptor: randomString(120),
Amount: 300,
},
},
},
Receivers: []domain.Receiver{{
Pubkey: randomString(36),
Descriptor: randomString(120),
Amount: 300,
}},
},
{
Id: uuid.New().String(),
Inputs: []domain.Vtxo{
{
VtxoKey: domain.VtxoKey{
Txid: randomString(32),
@@ -272,18 +293,18 @@ func testRoundRepository(t *testing.T, svc ports.RepoManager) {
PoolTx: randomString(32),
ExpireAt: 7980322,
Receiver: domain.Receiver{
Pubkey: randomString(36),
Descriptor: randomString(120),
Amount: 600,
},
},
},
Receivers: []domain.Receiver{
{
Pubkey: randomString(36),
Descriptor: randomString(120),
Amount: 400,
},
{
Pubkey: randomString(34),
Descriptor: randomString(120),
Amount: 200,
},
},
@@ -350,7 +371,7 @@ func testVtxoRepository(t *testing.T, svc ports.RepoManager) {
VOut: 0,
},
Receiver: domain.Receiver{
Pubkey: pubkey1,
Descriptor: desc1,
Amount: 1000,
},
},
@@ -360,7 +381,7 @@ func testVtxoRepository(t *testing.T, svc ports.RepoManager) {
VOut: 1,
},
Receiver: domain.Receiver{
Pubkey: pubkey1,
Descriptor: desc1,
Amount: 2000,
},
},
@@ -371,7 +392,7 @@ func testVtxoRepository(t *testing.T, svc ports.RepoManager) {
VOut: 1,
},
Receiver: domain.Receiver{
Pubkey: pubkey2,
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

View File

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

View File

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

View File

@@ -165,7 +165,7 @@ func (r *roundRepository) AddOrUpdateRound(ctx context.Context, round domain.Rou
ctx,
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,7 +470,7 @@ func rowToPaymentVtxoVw(row queries.PaymentVtxoVw) domain.Vtxo {
VOut: uint32(row.Vout.Int64),
},
Receiver: domain.Receiver{
Pubkey: row.Pubkey.String,
Descriptor: row.Descriptor.String,
Amount: uint64(row.Amount.Int64),
},
PoolTx: row.PoolTx.String,

View File

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

View File

@@ -54,7 +54,7 @@ func (q *Queries) MarkVtxoAsSwept(ctx context.Context, arg MarkVtxoAsSweptParams
}
const selectNotRedeemedVtxos = `-- name: SelectNotRedeemedVtxos :many
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,
@@ -925,7 +925,7 @@ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(txid, vout) DO UPDATE SET
type UpsertVtxoParams struct {
Txid string
Vout int64
Pubkey string
Descriptor sql.NullString
Amount int64
PoolTx string
SpentBy string
@@ -940,7 +940,7 @@ 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,

View File

@@ -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),

View File

@@ -45,7 +45,7 @@ func (v *vxtoRepository) AddVtxos(ctx context.Context, vtxos []domain.Vtxo) erro
ctx, queries.UpsertVtxoParams{
Txid: vtxo.Txid,
Vout: int64(vtxo.VOut),
Pubkey: vtxo.Pubkey,
Descriptor: sql.NullString{String: vtxo.Descriptor, Valid: true},
Amount: int64(vtxo.Amount),
PoolTx: vtxo.PoolTx,
SpentBy: vtxo.SpentBy,
@@ -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,7 +298,7 @@ func rowToVtxo(row queries.Vtxo, uncondForfeitTxs []queries.UncondForfeitTxVw) d
VOut: uint32(row.Vout),
},
Receiver: domain.Receiver{
Pubkey: row.Pubkey,
Descriptor: row.Descriptor.String,
Amount: uint64(row.Amount),
},
PoolTx: row.PoolTx,

View File

@@ -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
vtxoScript, err := common.P2TRScript(vtxoTapKey)
if err != nil {
return nil, err
}
forfeitProof = &proof
break
}
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{}

View File

@@ -20,12 +20,12 @@ import (
)
const (
testingKey = "0218d5ca8b58797b7dbd65c075dd7ba7784b3f38ab71b1a5a8e3f94ba0257654a6"
testingKey = "020000000000000000000000000000000000000000000000000000000000000001"
connectorAddress = "tex1qekd5u0qj8jl07vy60830xy7n9qtmcx9u3s0cqc"
minRelayFee = uint64(30)
roundLifetime = int64(1209344)
unilateralExitDelay = int64(512)
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)

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}
]

View File

@@ -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 {

View File

@@ -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,14 +438,28 @@ 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
}
vtxoTapKey, vtxoTree, err := vtxoScript.TapTree()
if err != nil {
return nil, err
}
vtxoOutputScript, err := common.P2TRScript(vtxoTapKey)
if err != nil {
return nil, err
}
var tapscript *waddrmgr.Tapscript
forfeitTxWeightEstimator := &input.TxWeightEstimator{}
if defaultVtxoScript, ok := vtxoScript.(*bitcointree.DefaultVtxoScript); ok {
forfeitClosure := &bitcointree.MultisigClosure{
Pubkey: sender,
AspPubkey: aspPubKey,
Pubkey: defaultVtxoScript.Owner,
AspPubkey: defaultVtxoScript.Asp,
}
forfeitLeaf, err := forfeitClosure.Leaf()
@@ -464,20 +467,25 @@ func (b *txBuilder) BuildAsyncPaymentTransactions(
return nil, err
}
leafProof := vtxoTree.LeafMerkleProofs[vtxoTree.LeafProofIndex[forfeitLeaf.TapHash()]]
ctrlBlock := leafProof.ToControlBlock(bitcointree.UnspendableKey())
ctrlBlockBytes, err := ctrlBlock.ToBytes()
forfeitProof, err := vtxoTree.GetTaprootMerkleProof(forfeitLeaf.TapHash())
if err != nil {
return nil, err
}
forfeitTxWeightEstimator := &input.TxWeightEstimator{}
tapscript := &waddrmgr.Tapscript{
RevealedScript: leafProof.Script,
ControlBlock: &ctrlBlock,
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")
}
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 {

View File

@@ -20,11 +20,11 @@ import (
)
const (
testingKey = "0218d5ca8b58797b7dbd65c075dd7ba7784b3f38ab71b1a5a8e3f94ba0257654a6"
testingKey = "020000000000000000000000000000000000000000000000000000000000000001"
connectorAddress = "bc1py00yhcjpcj0k0sqra0etq0u3yy0purmspppsw0shyzyfe8c83tmq5h6kc2"
roundLifetime = int64(1209344)
unilateralExitDelay = int64(512)
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)
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)

View File

@@ -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
}

View File

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

View File

@@ -5,6 +5,7 @@ import (
"github.com/ark-network/ark/server/internal/core/ports"
"github.com/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)

View File

@@ -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
}
]

View File

@@ -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 {

View File

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

View File

@@ -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)

View File

@@ -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
@@ -112,8 +116,8 @@ func (h *handler) Ping(ctx context.Context, req *arkv1.PingRequest) (*arkv1.Ping
Id: e.Id,
PoolTx: e.PoolTx,
CongestionTree: castCongestionTree(e.CongestionTree),
ForfeitTxs: e.UnsignedForfeitTxs,
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,
Descriptor_: descriptor,
}, nil
}
@@ -481,8 +486,8 @@ func (h *handler) listenToEvents() {
Id: e.Id,
PoolTx: e.PoolTx,
CongestionTree: castCongestionTree(e.CongestionTree),
ForfeitTxs: e.UnsignedForfeitTxs,
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{
Outpoint: &arkv1.Outpoint{
Txid: vv.Txid,
Vout: vv.VOut,
},
},
},
Receiver: &arkv1.Output{
Address: addr,
Descriptor_: vv.Descriptor,
Amount: vv.Amount,
},
PoolTxid: vv.PoolTx,
Spent: vv.Spent,
ExpireAt: vv.ExpireAt,

View File

@@ -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