From 8eea472269a1a3aad667540afe9e1c262295e23a Mon Sep 17 00:00:00 2001 From: Louis Date: Mon, 22 Jan 2024 13:44:35 +0100 Subject: [PATCH 1/7] replace Transfer by SelectUtxos --- asp/internal/core/application/service.go | 16 ++- asp/internal/core/domain/round.go | 4 +- asp/internal/core/ports/wallet.go | 5 +- .../infrastructure/db/service_test.go | 2 +- .../ocean-wallet/transaction.go | 36 +++---- .../tx-builder/covenant/builder.go | 98 ++++++++++++------- .../tx-builder/covenant/builder_test.go | 31 +++++- .../tx-builder/dummy/builder.go | 98 ++++++++++++------- .../tx-builder/dummy/builder_test.go | 23 ++++- 9 files changed, 196 insertions(+), 117 deletions(-) diff --git a/asp/internal/core/application/service.go b/asp/internal/core/application/service.go index 353464576..eac1e5356 100644 --- a/asp/internal/core/application/service.go +++ b/asp/internal/core/application/service.go @@ -267,21 +267,21 @@ func (s *service) startFinalization() { return } - signedPoolTx, tree, err := s.builder.BuildPoolTx(s.pubkey, s.wallet, payments, s.minRelayFee) + unsignedPoolTx, tree, err := s.builder.BuildPoolTx(s.pubkey, s.wallet, payments, s.minRelayFee) if err != nil { changes = round.Fail(fmt.Errorf("failed to create pool tx: %s", err)) log.WithError(err).Warn("failed to create pool tx") return } - connectors, forfeitTxs, err := s.builder.BuildForfeitTxs(s.pubkey, signedPoolTx, payments) + connectors, forfeitTxs, err := s.builder.BuildForfeitTxs(s.pubkey, unsignedPoolTx, payments) if err != nil { changes = 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 } - events, _ := round.StartFinalization(connectors, tree, signedPoolTx) + events, _ := round.StartFinalization(connectors, tree, unsignedPoolTx) changes = append(changes, events...) s.forfeitTxs.push(forfeitTxs) @@ -318,7 +318,15 @@ func (s *service) finalizeRound() { return } - txid, err := s.wallet.BroadcastTransaction(ctx, round.TxHex) + log.Debugf("signing round transaction %s\n", round.Id) + signedPoolTx, err := s.wallet.SignPset(ctx, round.UnsignedTx, true) + if err != nil { + changes = round.Fail(fmt.Errorf("failed to sign round tx: %s", err)) + log.WithError(err).Warn("failed to sign round tx") + return + } + + txid, err := s.wallet.BroadcastTransaction(ctx, signedPoolTx) if err != nil { changes = round.Fail(fmt.Errorf("failed to broadcast pool tx: %s", err)) log.WithError(err).Warn("failed to broadcast pool tx") diff --git a/asp/internal/core/domain/round.go b/asp/internal/core/domain/round.go index 2ccd35b58..de1ff8403 100644 --- a/asp/internal/core/domain/round.go +++ b/asp/internal/core/domain/round.go @@ -39,7 +39,7 @@ type Round struct { Stage Stage Payments map[string]Payment Txid string - TxHex string + UnsignedTx string ForfeitTxs []string CongestionTree CongestionTree Connectors []string @@ -83,7 +83,7 @@ func (r *Round) On(event RoundEvent, replayed bool) { r.Stage.Code = FinalizationStage r.CongestionTree = e.CongestionTree r.Connectors = append([]string{}, e.Connectors...) - r.TxHex = e.PoolTx + r.UnsignedTx = e.PoolTx case RoundFinalized: r.Stage.Ended = true r.Txid = e.Txid diff --git a/asp/internal/core/ports/wallet.go b/asp/internal/core/ports/wallet.go index 9ea10217a..294ca9201 100644 --- a/asp/internal/core/ports/wallet.go +++ b/asp/internal/core/ports/wallet.go @@ -13,7 +13,7 @@ type WalletService interface { SignPset( ctx context.Context, pset string, extractRawTx bool, ) (string, error) - Transfer(ctx context.Context, outs []TxOutput) (string, error) + SelectUtxos(ctx context.Context, asset string, amount uint64) ([]TxInput, uint64, error) BroadcastTransaction(ctx context.Context, txHex string) (string, error) Close() } @@ -27,9 +27,6 @@ type WalletStatus interface { type TxInput interface { GetTxid() string GetIndex() uint32 - GetScript() string - GetScriptSigSize() int - GetWitnessSize() int } type TxOutput interface { diff --git a/asp/internal/infrastructure/db/service_test.go b/asp/internal/infrastructure/db/service_test.go index 3e4162535..f942a3c21 100644 --- a/asp/internal/infrastructure/db/service_test.go +++ b/asp/internal/infrastructure/db/service_test.go @@ -376,7 +376,7 @@ func roundsMatch(expected, got domain.Round) assert.Comparison { if expected.Txid != got.Txid { return false } - if expected.TxHex != got.TxHex { + if expected.UnsignedTx != got.UnsignedTx { return false } if !reflect.DeepEqual(expected.ForfeitTxs, got.ForfeitTxs) { diff --git a/asp/internal/infrastructure/ocean-wallet/transaction.go b/asp/internal/infrastructure/ocean-wallet/transaction.go index 82535325f..e7c665783 100644 --- a/asp/internal/infrastructure/ocean-wallet/transaction.go +++ b/asp/internal/infrastructure/ocean-wallet/transaction.go @@ -32,18 +32,22 @@ func (s *service) SignPset( return ptx.ToBase64() } -func (s *service) Transfer( - ctx context.Context, outs []ports.TxOutput, -) (string, error) { - res, err := s.txClient.Transfer(ctx, &pb.TransferRequest{ - AccountName: accountLabel, - Receivers: outputList(outs).toProto(), - MillisatsPerByte: msatsPerByte, +func (s *service) SelectUtxos(ctx context.Context, asset string, amount uint64) ([]ports.TxInput, uint64, error) { + res, err := s.txClient.SelectUtxos(ctx, &pb.SelectUtxosRequest{ + AccountName: accountLabel, + TargetAsset: asset, + TargetAmount: amount, }) if err != nil { - return "", err + return nil, 0, err } - return res.GetTxHex(), nil + + inputs := make([]ports.TxInput, 0, len(res.GetUtxos())) + for _, utxo := range res.GetUtxos() { + inputs = append(inputs, utxo) + } + + return inputs, res.GetChange(), nil } func (s *service) BroadcastTransaction( @@ -59,17 +63,3 @@ func (s *service) BroadcastTransaction( } return res.GetTxid(), nil } - -type outputList []ports.TxOutput - -func (l outputList) toProto() []*pb.Output { - list := make([]*pb.Output, 0, len(l)) - for _, out := range l { - list = append(list, &pb.Output{ - Amount: out.GetAmount(), - Script: out.GetScript(), - Asset: out.GetAsset(), - }) - } - return list -} diff --git a/asp/internal/infrastructure/tx-builder/covenant/builder.go b/asp/internal/infrastructure/tx-builder/covenant/builder.go index eb05ad348..6cf3153e6 100644 --- a/asp/internal/infrastructure/tx-builder/covenant/builder.go +++ b/asp/internal/infrastructure/tx-builder/covenant/builder.go @@ -19,6 +19,7 @@ import ( const ( connectorAmount = 450 + poolTxFeeAmount = 300 ) type txBuilder struct { @@ -171,8 +172,6 @@ func (b *txBuilder) BuildPoolTx( return } - aspScript := hex.EncodeToString(aspScriptBytes) - offchainReceivers, onchainReceivers := receiversFromPayments(payments) numberOfConnectors := numberOfVTXOs(payments) connectorOutputAmount := connectorAmount * numberOfConnectors @@ -189,38 +188,76 @@ func (b *txBuilder) BuildPoolTx( return } - sharedOutputScriptHex := hex.EncodeToString(sharedOutputScript) - - poolTxOuts := []ports.TxOutput{ - newOutput(sharedOutputScriptHex, sharedOutputAmount, b.net.AssetID), - newOutput(aspScript, connectorOutputAmount, b.net.AssetID), + outputs := []psetv2.OutputArgs{ + { + Asset: b.net.AssetID, + Amount: sharedOutputAmount, + Script: sharedOutputScript, + }, + { + Asset: b.net.AssetID, + Amount: connectorOutputAmount, + Script: aspScriptBytes, + }, + { + Asset: b.net.AssetID, + Amount: poolTxFeeAmount, + }, } + amountToSelect := sharedOutputAmount + connectorOutputAmount + poolTxFeeAmount + for _, receiver := range onchainReceivers { - buf, _ := address.ToOutputScript(receiver.OnchainAddress) - script := hex.EncodeToString(buf) - poolTxOuts = append(poolTxOuts, newOutput(script, receiver.Amount, b.net.AssetID)) + amountToSelect += receiver.Amount + + receiverScript, err := address.ToOutputScript(receiver.OnchainAddress) + if err != nil { + return "", nil, err + } + + outputs = append(outputs, psetv2.OutputArgs{ + Asset: b.net.AssetID, + Amount: receiver.Amount, + Script: receiverScript, + }) + } + + utxos, change, err := wallet.SelectUtxos(ctx, b.net.AssetID, amountToSelect) + if err != nil { + return + } + + if change > 0 { + outputs = append(outputs, psetv2.OutputArgs{ + Asset: b.net.AssetID, + Amount: change, + Script: aspScriptBytes, + }) } - txHex, err := wallet.Transfer(ctx, poolTxOuts) + poolPartialTx, err := psetv2.New(toInputArgs(utxos), outputs, nil) if err != nil { return } - tx, err := transaction.NewTxFromHex(txHex) + utx, err := poolPartialTx.UnsignedTx() if err != nil { return } tree, err := makeTree(psetv2.InputArgs{ - Txid: tx.TxHash().String(), + Txid: utx.TxHash().String(), TxIndex: 0, }) if err != nil { return } - poolTx = txHex + poolTx, err = poolPartialTx.ToBase64() + if err != nil { + return + } + congestionTree = tree return } @@ -321,28 +358,15 @@ func receiversFromPayments( return } -type output struct { - script string - amount uint64 - asset string -} - -func newOutput(script string, amount uint64, asset string) ports.TxOutput { - return &output{ - script: script, - amount: amount, - asset: asset, +func toInputArgs( + ins []ports.TxInput, +) []psetv2.InputArgs { + inputs := make([]psetv2.InputArgs, 0, len(ins)) + for _, in := range ins { + inputs = append(inputs, psetv2.InputArgs{ + Txid: in.GetTxid(), + TxIndex: in.GetIndex(), + }) } -} - -func (o *output) GetAsset() string { - return o.asset -} - -func (o *output) GetAmount() uint64 { - return o.amount -} - -func (o *output) GetScript() string { - return o.script + return inputs } diff --git a/asp/internal/infrastructure/tx-builder/covenant/builder_test.go b/asp/internal/infrastructure/tx-builder/covenant/builder_test.go index 2c81de472..069ccf4d8 100644 --- a/asp/internal/infrastructure/tx-builder/covenant/builder_test.go +++ b/asp/internal/infrastructure/tx-builder/covenant/builder_test.go @@ -84,6 +84,19 @@ func createTestPoolTx(sharedOutputAmount, numberOfInputs uint64) (string, error) type mockedWalletService struct{} +type input struct { + txid string + vout uint32 +} + +func (i *input) GetTxid() string { + return i.txid +} + +func (i *input) GetIndex() uint32 { + return i.vout +} + // BroadcastTransaction implements ports.WalletService. func (*mockedWalletService) BroadcastTransaction(ctx context.Context, txHex string) (string, error) { panic("unimplemented") @@ -114,9 +127,13 @@ func (*mockedWalletService) Status(ctx context.Context) (ports.WalletStatus, err panic("unimplemented") } -// Transfer implements ports.WalletService. -func (*mockedWalletService) Transfer(ctx context.Context, outs []ports.TxOutput) (string, error) { - return createTestPoolTx(outs[0].GetAmount(), 1) +func (*mockedWalletService) SelectUtxos(ctx context.Context, asset string, amount uint64) ([]ports.TxInput, uint64, error) { + fakeInput := input{ + txid: "2f8f5733734fd44d581976bd3c1aee098bd606402df2ce02ce908287f1d5ede4", + vout: 0, + } + + return []ports.TxInput{&fakeInput}, 0, nil } func TestBuildCongestionTree(t *testing.T) { @@ -252,10 +269,14 @@ func TestBuildCongestionTree(t *testing.T) { require.Equal(t, f.expectedNodesNum, tree.NumberOfNodes()) require.Len(t, tree.Leaves(), f.expectedLeavesNum) - poolTransaction, err := transaction.NewTxFromHex(poolTx) + poolTransaction, err := psetv2.NewPsetFromBase64(poolTx) require.NoError(t, err) - poolTxID := poolTransaction.TxHash().String() + utx, err := poolTransaction.UnsignedTx() + require.NoError(t, err) + + poolTxID := utx.TxHash().String() + require.NoError(t, err) // check the root require.Len(t, tree[0], 1) diff --git a/asp/internal/infrastructure/tx-builder/dummy/builder.go b/asp/internal/infrastructure/tx-builder/dummy/builder.go index b84e5f802..5b7c73b2e 100644 --- a/asp/internal/infrastructure/tx-builder/dummy/builder.go +++ b/asp/internal/infrastructure/tx-builder/dummy/builder.go @@ -2,7 +2,6 @@ package txbuilder import ( "context" - "encoding/hex" "github.com/ark-network/ark/internal/core/domain" "github.com/ark-network/ark/internal/core/ports" @@ -98,43 +97,79 @@ func (b *txBuilder) BuildPoolTx( return "", nil, err } - aspScript := hex.EncodeToString(aspScriptBytes) - offchainReceivers, onchainReceivers := receiversFromPayments(payments) sharedOutputAmount := sumReceivers(offchainReceivers) numberOfConnectors := numberOfVTXOs(payments) connectorOutputAmount := connectorAmount * numberOfConnectors - poolTxOuts := []ports.TxOutput{ - newOutput(aspScript, sharedOutputAmount, b.net.AssetID), - newOutput(aspScript, connectorOutputAmount, b.net.AssetID), + ctx := context.Background() + + outputs := []psetv2.OutputArgs{ + { + Asset: b.net.AssetID, + Amount: sharedOutputAmount, + Script: aspScriptBytes, + }, + { + Asset: b.net.AssetID, + Amount: connectorOutputAmount, + Script: aspScriptBytes, + }, } + + amountToSelect := sharedOutputAmount + connectorOutputAmount + for _, receiver := range onchainReceivers { - buf, _ := address.ToOutputScript(receiver.OnchainAddress) - script := hex.EncodeToString(buf) - poolTxOuts = append(poolTxOuts, newOutput(script, receiver.Amount, b.net.AssetID)) + amountToSelect += receiver.Amount + + receiverScript, err := address.ToOutputScript(receiver.OnchainAddress) + if err != nil { + return "", nil, err + } + + outputs = append(outputs, psetv2.OutputArgs{ + Asset: b.net.AssetID, + Amount: receiver.Amount, + Script: receiverScript, + }) } - ctx := context.Background() + utxos, change, err := wallet.SelectUtxos(ctx, b.net.AssetID, amountToSelect) + if err != nil { + return + } - poolTx, err = wallet.Transfer(ctx, poolTxOuts) + if change > 0 { + outputs = append(outputs, psetv2.OutputArgs{ + Asset: b.net.AssetID, + Amount: change, + Script: aspScriptBytes, + }) + } + + poolPartialTx, err := psetv2.New(toInputArgs(utxos), outputs, nil) if err != nil { - return "", nil, err + return } - poolTxID, err := getTxid(poolTx) + utx, err := poolPartialTx.UnsignedTx() if err != nil { - return "", nil, err + return } congestionTree, err = buildCongestionTree( newOutputScriptFactory(aspPubkey, b.net), b.net, - poolTxID, + utx.TxHash().String(), offchainReceivers, ) + poolTx, err = poolPartialTx.ToBase64() + if err != nil { + return + } + return poolTx, congestionTree, err } @@ -218,28 +253,15 @@ func sumReceivers(receivers []domain.Receiver) uint64 { return sum } -type output struct { - script string - amount uint64 - asset string -} - -func newOutput(script string, amount uint64, asset string) ports.TxOutput { - return &output{ - script: script, - amount: amount, - asset: asset, +func toInputArgs( + ins []ports.TxInput, +) []psetv2.InputArgs { + inputs := make([]psetv2.InputArgs, 0, len(ins)) + for _, in := range ins { + inputs = append(inputs, psetv2.InputArgs{ + Txid: in.GetTxid(), + TxIndex: in.GetIndex(), + }) } -} - -func (o *output) GetAmount() uint64 { - return o.amount -} - -func (o *output) GetAsset() string { - return o.asset -} - -func (o *output) GetScript() string { - return o.script + return inputs } diff --git a/asp/internal/infrastructure/tx-builder/dummy/builder_test.go b/asp/internal/infrastructure/tx-builder/dummy/builder_test.go index 7c62f47a2..9408512ad 100644 --- a/asp/internal/infrastructure/tx-builder/dummy/builder_test.go +++ b/asp/internal/infrastructure/tx-builder/dummy/builder_test.go @@ -74,6 +74,19 @@ func createTestPoolTx(sharedOutputAmount, numberOfInputs uint64) (string, error) return pset.ToBase64() } +type input struct { + txid string + vout uint32 +} + +func (i *input) GetTxid() string { + return i.txid +} + +func (i *input) GetIndex() uint32 { + return i.vout +} + type mockedWalletService struct{} // BroadcastTransaction implements ports.WalletService. @@ -106,9 +119,13 @@ func (*mockedWalletService) Status(ctx context.Context) (ports.WalletStatus, err panic("unimplemented") } -// Transfer implements ports.WalletService. -func (*mockedWalletService) Transfer(ctx context.Context, outs []ports.TxOutput) (string, error) { - return createTestPoolTx(1000, (450+500)*1) +func (*mockedWalletService) SelectUtxos(ctx context.Context, asset string, amount uint64) ([]ports.TxInput, uint64, error) { + fakeInput := input{ + txid: "2f8f5733734fd44d581976bd3c1aee098bd606402df2ce02ce908287f1d5ede4", + vout: 0, + } + + return []ports.TxInput{&fakeInput}, 0, nil } func TestBuildCongestionTree(t *testing.T) { From edc229441828a3b6fed82b1a8c229579dd0ebef0 Mon Sep 17 00:00:00 2001 From: Louis Date: Mon, 22 Jan 2024 18:27:26 +0100 Subject: [PATCH 2/7] Wallet.SignPset: handle unset WitnessUtxo --- asp/internal/core/ports/wallet.go | 6 -- .../ocean-wallet/transaction.go | 64 ++++++++++++++++++- 2 files changed, 61 insertions(+), 9 deletions(-) diff --git a/asp/internal/core/ports/wallet.go b/asp/internal/core/ports/wallet.go index 294ca9201..9245f14f4 100644 --- a/asp/internal/core/ports/wallet.go +++ b/asp/internal/core/ports/wallet.go @@ -28,9 +28,3 @@ type TxInput interface { GetTxid() string GetIndex() uint32 } - -type TxOutput interface { - GetAmount() uint64 - GetAsset() string - GetScript() string -} diff --git a/asp/internal/infrastructure/ocean-wallet/transaction.go b/asp/internal/infrastructure/ocean-wallet/transaction.go index e7c665783..31ae1414a 100644 --- a/asp/internal/infrastructure/ocean-wallet/transaction.go +++ b/asp/internal/infrastructure/ocean-wallet/transaction.go @@ -6,7 +6,9 @@ import ( pb "github.com/ark-network/ark/api-spec/protobuf/gen/ocean/v1" "github.com/ark-network/ark/internal/core/ports" + "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/vulpemventures/go-elements/psetv2" + "github.com/vulpemventures/go-elements/transaction" ) const msatsPerByte = 110 @@ -14,22 +16,78 @@ const msatsPerByte = 110 func (s *service) SignPset( ctx context.Context, pset string, extractRawTx bool, ) (string, error) { + ptx, err := psetv2.NewPsetFromBase64(pset) + if err != nil { + return "", err + } + + updater, err := psetv2.NewUpdater(ptx) + if err != nil { + return "", err + } + + for inputIndex, in := range ptx.Inputs { + if in.WitnessUtxo == nil { + resp, err := s.txClient.GetTransaction(ctx, &pb.GetTransactionRequest{ + Txid: chainhash.Hash(in.PreviousTxid).String(), + }) + if err != nil { + return "", err + } + + txHex := resp.GetTxHex() + tx, err := transaction.NewTxFromHex(txHex) + if err != nil { + return "", err + } + + if len(tx.Outputs) <= int(in.PreviousTxIndex) { + return "", fmt.Errorf("invalid previous tx index, cannot set witness utxo") + } + + if err := updater.AddInWitnessUtxo(inputIndex, tx.Outputs[in.PreviousTxIndex]); err != nil { + return "", err + } + } + } + + updatedPset, err := updater.Pset.ToBase64() + if err != nil { + return "", err + } + res, err := s.txClient.SignPset(ctx, &pb.SignPsetRequest{ - Pset: pset, + Pset: updatedPset, }) if err != nil { return "", err } signedPset := res.GetPset() + if !extractRawTx { return signedPset, nil } - ptx, _ := psetv2.NewPsetFromBase64(signedPset) + ptx, err = psetv2.NewPsetFromBase64(signedPset) + if err != nil { + return "", err + } + if err := psetv2.MaybeFinalizeAll(ptx); err != nil { return "", fmt.Errorf("failed to finalize signed pset: %s", err) } - return ptx.ToBase64() + + extractedTx, err := psetv2.Extract(ptx) + if err != nil { + return "", fmt.Errorf("failed to extract signed pset: %s", err) + } + + txHex, err := extractedTx.ToHex() + if err != nil { + return "", fmt.Errorf("failed to convert extracted tx to hex: %s", err) + } + + return txHex, nil } func (s *service) SelectUtxos(ctx context.Context, asset string, amount uint64) ([]ports.TxInput, uint64, error) { From 68ff7bac8bfe895edccc0ba7e9344ef4f7221cac Mon Sep 17 00:00:00 2001 From: Louis Date: Mon, 22 Jan 2024 18:35:56 +0100 Subject: [PATCH 3/7] fix linter --- asp/internal/infrastructure/ocean-wallet/transaction.go | 2 -- asp/internal/infrastructure/tx-builder/dummy/builder.go | 3 +++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/asp/internal/infrastructure/ocean-wallet/transaction.go b/asp/internal/infrastructure/ocean-wallet/transaction.go index 31ae1414a..9d2b6eaff 100644 --- a/asp/internal/infrastructure/ocean-wallet/transaction.go +++ b/asp/internal/infrastructure/ocean-wallet/transaction.go @@ -11,8 +11,6 @@ import ( "github.com/vulpemventures/go-elements/transaction" ) -const msatsPerByte = 110 - func (s *service) SignPset( ctx context.Context, pset string, extractRawTx bool, ) (string, error) { diff --git a/asp/internal/infrastructure/tx-builder/dummy/builder.go b/asp/internal/infrastructure/tx-builder/dummy/builder.go index 5b7c73b2e..673c8d029 100644 --- a/asp/internal/infrastructure/tx-builder/dummy/builder.go +++ b/asp/internal/infrastructure/tx-builder/dummy/builder.go @@ -164,6 +164,9 @@ func (b *txBuilder) BuildPoolTx( utx.TxHash().String(), offchainReceivers, ) + if err != nil { + return + } poolTx, err = poolPartialTx.ToBase64() if err != nil { From 908016c37d179800bf1cd4b90537ab2b6de44cd4 Mon Sep 17 00:00:00 2001 From: Louis Singer Date: Tue, 23 Jan 2024 14:29:32 +0100 Subject: [PATCH 4/7] renaming variables --- .../infrastructure/tx-builder/covenant/builder.go | 12 ++++++------ .../infrastructure/tx-builder/dummy/builder.go | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/asp/internal/infrastructure/tx-builder/covenant/builder.go b/asp/internal/infrastructure/tx-builder/covenant/builder.go index 6cf3153e6..172886892 100644 --- a/asp/internal/infrastructure/tx-builder/covenant/builder.go +++ b/asp/internal/infrastructure/tx-builder/covenant/builder.go @@ -205,10 +205,10 @@ func (b *txBuilder) BuildPoolTx( }, } - amountToSelect := sharedOutputAmount + connectorOutputAmount + poolTxFeeAmount + targetAmount := sharedOutputAmount + connectorOutputAmount + poolTxFeeAmount for _, receiver := range onchainReceivers { - amountToSelect += receiver.Amount + targetAmount += receiver.Amount receiverScript, err := address.ToOutputScript(receiver.OnchainAddress) if err != nil { @@ -222,7 +222,7 @@ func (b *txBuilder) BuildPoolTx( }) } - utxos, change, err := wallet.SelectUtxos(ctx, b.net.AssetID, amountToSelect) + utxos, change, err := wallet.SelectUtxos(ctx, b.net.AssetID, targetAmount) if err != nil { return } @@ -235,12 +235,12 @@ func (b *txBuilder) BuildPoolTx( }) } - poolPartialTx, err := psetv2.New(toInputArgs(utxos), outputs, nil) + ptx, err := psetv2.New(toInputArgs(utxos), outputs, nil) if err != nil { return } - utx, err := poolPartialTx.UnsignedTx() + utx, err := ptx.UnsignedTx() if err != nil { return } @@ -253,7 +253,7 @@ func (b *txBuilder) BuildPoolTx( return } - poolTx, err = poolPartialTx.ToBase64() + poolTx, err = ptx.ToBase64() if err != nil { return } diff --git a/asp/internal/infrastructure/tx-builder/dummy/builder.go b/asp/internal/infrastructure/tx-builder/dummy/builder.go index 673c8d029..d01dd1901 100644 --- a/asp/internal/infrastructure/tx-builder/dummy/builder.go +++ b/asp/internal/infrastructure/tx-builder/dummy/builder.go @@ -148,12 +148,12 @@ func (b *txBuilder) BuildPoolTx( }) } - poolPartialTx, err := psetv2.New(toInputArgs(utxos), outputs, nil) + ptx, err := psetv2.New(toInputArgs(utxos), outputs, nil) if err != nil { return } - utx, err := poolPartialTx.UnsignedTx() + utx, err := ptx.UnsignedTx() if err != nil { return } @@ -168,7 +168,7 @@ func (b *txBuilder) BuildPoolTx( return } - poolTx, err = poolPartialTx.ToBase64() + poolTx, err = ptx.ToBase64() if err != nil { return } From 413bea4a3ae35afc8bdaa521385d1f400b4ca591 Mon Sep 17 00:00:00 2001 From: Louis Singer Date: Wed, 24 Jan 2024 09:20:54 +0100 Subject: [PATCH 5/7] add witnessUtxo while creating pool transaction --- asp/internal/core/ports/wallet.go | 3 + .../ocean-wallet/transaction.go | 55 ++++-------------- .../tx-builder/covenant/builder.go | 58 +++++++++++++++++-- .../tx-builder/covenant/builder_test.go | 31 ++++++---- .../tx-builder/dummy/builder_test.go | 12 ++++ common/tree/validation.go | 14 +++-- 6 files changed, 107 insertions(+), 66 deletions(-) diff --git a/asp/internal/core/ports/wallet.go b/asp/internal/core/ports/wallet.go index 9245f14f4..a655c59fe 100644 --- a/asp/internal/core/ports/wallet.go +++ b/asp/internal/core/ports/wallet.go @@ -27,4 +27,7 @@ type WalletStatus interface { type TxInput interface { GetTxid() string GetIndex() uint32 + GetScript() string + GetAsset() string + GetValue() uint64 } diff --git a/asp/internal/infrastructure/ocean-wallet/transaction.go b/asp/internal/infrastructure/ocean-wallet/transaction.go index 9d2b6eaff..d29c443e4 100644 --- a/asp/internal/infrastructure/ocean-wallet/transaction.go +++ b/asp/internal/infrastructure/ocean-wallet/transaction.go @@ -6,56 +6,18 @@ import ( pb "github.com/ark-network/ark/api-spec/protobuf/gen/ocean/v1" "github.com/ark-network/ark/internal/core/ports" - "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/vulpemventures/go-elements/psetv2" - "github.com/vulpemventures/go-elements/transaction" +) + +const ( + zero32 = "0000000000000000000000000000000000000000000000000000000000000000" ) func (s *service) SignPset( ctx context.Context, pset string, extractRawTx bool, ) (string, error) { - ptx, err := psetv2.NewPsetFromBase64(pset) - if err != nil { - return "", err - } - - updater, err := psetv2.NewUpdater(ptx) - if err != nil { - return "", err - } - - for inputIndex, in := range ptx.Inputs { - if in.WitnessUtxo == nil { - resp, err := s.txClient.GetTransaction(ctx, &pb.GetTransactionRequest{ - Txid: chainhash.Hash(in.PreviousTxid).String(), - }) - if err != nil { - return "", err - } - - txHex := resp.GetTxHex() - tx, err := transaction.NewTxFromHex(txHex) - if err != nil { - return "", err - } - - if len(tx.Outputs) <= int(in.PreviousTxIndex) { - return "", fmt.Errorf("invalid previous tx index, cannot set witness utxo") - } - - if err := updater.AddInWitnessUtxo(inputIndex, tx.Outputs[in.PreviousTxIndex]); err != nil { - return "", err - } - } - } - - updatedPset, err := updater.Pset.ToBase64() - if err != nil { - return "", err - } - res, err := s.txClient.SignPset(ctx, &pb.SignPsetRequest{ - Pset: updatedPset, + Pset: pset, }) if err != nil { return "", err @@ -66,7 +28,7 @@ func (s *service) SignPset( return signedPset, nil } - ptx, err = psetv2.NewPsetFromBase64(signedPset) + ptx, err := psetv2.NewPsetFromBase64(signedPset) if err != nil { return "", err } @@ -100,6 +62,11 @@ func (s *service) SelectUtxos(ctx context.Context, asset string, amount uint64) inputs := make([]ports.TxInput, 0, len(res.GetUtxos())) for _, utxo := range res.GetUtxos() { + // check that the utxos are not confidential + if utxo.GetAssetBlinder() != zero32 || utxo.GetValueBlinder() != zero32 { + return nil, 0, fmt.Errorf("utxo is confidential") + } + inputs = append(inputs, utxo) } diff --git a/asp/internal/infrastructure/tx-builder/covenant/builder.go b/asp/internal/infrastructure/tx-builder/covenant/builder.go index e82f34415..bbe03a732 100644 --- a/asp/internal/infrastructure/tx-builder/covenant/builder.go +++ b/asp/internal/infrastructure/tx-builder/covenant/builder.go @@ -3,10 +3,12 @@ package txbuilder import ( "context" "encoding/hex" + "fmt" "github.com/ark-network/ark/common/tree" "github.com/ark-network/ark/internal/core/domain" "github.com/ark-network/ark/internal/core/ports" + "github.com/btcsuite/btcd/txscript" "github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/vulpemventures/go-elements/address" "github.com/vulpemventures/go-elements/elementsutil" @@ -22,6 +24,8 @@ const ( poolTxFeeAmount = 300 ) +var emptyNonce = []byte{0x00} + type txBuilder struct { net *network.Network } @@ -45,11 +49,7 @@ func p2wpkhScript(publicKey *secp256k1.PublicKey, net *network.Network) ([]byte, func getTxid(txStr string) (string, error) { pset, err := psetv2.NewPsetFromBase64(txStr) if err != nil { - tx, err := transaction.NewTxFromHex(txStr) - if err != nil { - return "", err - } - return tx.TxHash().String(), nil + return "", err } utx, err := pset.UnsignedTx() @@ -117,7 +117,7 @@ func (b *txBuilder) BuildForfeitTxs( pubkeyBytes, err := hex.DecodeString(vtxo.Pubkey) if err != nil { - return nil, nil, err + return nil, nil, fmt.Errorf("failed to decode pubkey: %s", err) } vtxoPubkey, err := secp256k1.ParsePubKey(pubkeyBytes) @@ -240,6 +240,26 @@ func (b *txBuilder) BuildPoolTx( return } + updater, err := psetv2.NewUpdater(ptx) + if err != nil { + return + } + + for i, utxo := range utxos { + witnessUtxo, err := toWitnessUtxo(utxo) + if err != nil { + return "", nil, err + } + + if err := updater.AddInWitnessUtxo(i, witnessUtxo); err != nil { + return "", nil, err + } + + if err := updater.AddInSighashType(i, txscript.SigHashAll); err != nil { + return "", nil, err + } + } + utx, err := ptx.UnsignedTx() if err != nil { return @@ -370,3 +390,29 @@ func toInputArgs( } return inputs } + +func toWitnessUtxo(in ports.TxInput) (*transaction.TxOutput, error) { + valueBytes, err := elementsutil.ValueToBytes(in.GetValue()) + if err != nil { + return nil, fmt.Errorf("failed to convert value to bytes: %s", err) + } + + assetBytes, err := elementsutil.AssetHashToBytes(in.GetAsset()) + if err != nil { + return nil, fmt.Errorf("failed to convert asset to bytes: %s", err) + } + + scriptBytes, err := hex.DecodeString(in.GetScript()) + if err != nil { + return nil, fmt.Errorf("failed to decode script: %s", err) + } + + return &transaction.TxOutput{ + Asset: assetBytes, + Value: valueBytes, + Script: scriptBytes, + Nonce: emptyNonce, + RangeProof: nil, + SurjectionProof: nil, + }, err +} diff --git a/asp/internal/infrastructure/tx-builder/covenant/builder_test.go b/asp/internal/infrastructure/tx-builder/covenant/builder_test.go index fd4b709f0..f730c8578 100644 --- a/asp/internal/infrastructure/tx-builder/covenant/builder_test.go +++ b/asp/internal/infrastructure/tx-builder/covenant/builder_test.go @@ -15,7 +15,6 @@ import ( "github.com/vulpemventures/go-elements/network" "github.com/vulpemventures/go-elements/payment" "github.com/vulpemventures/go-elements/psetv2" - "github.com/vulpemventures/go-elements/transaction" ) const ( @@ -73,12 +72,7 @@ func createTestPoolTx(sharedOutputAmount, numberOfInputs uint64) (string, error) return "", err } - utx, err := pset.UnsignedTx() - if err != nil { - return "", err - } - - return utx.ToHex() + return pset.ToBase64() } type mockedWalletService struct{} @@ -96,6 +90,18 @@ func (i *input) GetIndex() uint32 { return i.vout } +func (i *input) GetScript() string { + return "a914ea9f486e82efb3dd83a69fd96e3f0113757da03c87" +} + +func (i *input) GetAsset() string { + return "5ac9f65c0efcc4775e0baec4ec03abdde22473cd3cf33c0419ca290e0751b225" +} + +func (i *input) GetValue() uint64 { + return 1000 +} + // BroadcastTransaction implements ports.WalletService. func (*mockedWalletService) BroadcastTransaction(ctx context.Context, txHex string) (string, error) { panic("unimplemented") @@ -391,13 +397,16 @@ func TestBuildForfeitTxs(t *testing.T) { builder := txbuilder.NewTxBuilder(network.Liquid) // TODO: replace with fixture. - poolTxHex, err := createTestPoolTx(1000, 2) + poolTxBase64, err := createTestPoolTx(1000, 2) + require.NoError(t, err) + + poolTx, err := psetv2.NewPsetFromBase64(poolTxBase64) require.NoError(t, err) - poolTx, err := transaction.NewTxFromHex(poolTxHex) + utx, err := poolTx.UnsignedTx() require.NoError(t, err) - poolTxid := poolTx.TxHash().String() + poolTxid := utx.TxHash().String() fixtures := []struct { payments []domain.Payment @@ -453,7 +462,7 @@ func TestBuildForfeitTxs(t *testing.T) { for _, f := range fixtures { connectors, forfeitTxs, err := builder.BuildForfeitTxs( - key, poolTxHex, f.payments, + key, poolTxBase64, f.payments, ) require.NoError(t, err) require.Len(t, connectors, f.expectedNumOfConnectors) diff --git a/asp/internal/infrastructure/tx-builder/dummy/builder_test.go b/asp/internal/infrastructure/tx-builder/dummy/builder_test.go index 9408512ad..9b91df970 100644 --- a/asp/internal/infrastructure/tx-builder/dummy/builder_test.go +++ b/asp/internal/infrastructure/tx-builder/dummy/builder_test.go @@ -87,6 +87,18 @@ func (i *input) GetIndex() uint32 { return i.vout } +func (i *input) GetScript() string { + return "a914ea9f486e82efb3dd83a69fd96e3f0113757da03c87" +} + +func (i *input) GetAsset() string { + return "5ac9f65c0efcc4775e0baec4ec03abdde22473cd3cf33c0419ca290e0751b225" +} + +func (i *input) GetValue() uint64 { + return 1000 +} + type mockedWalletService struct{} // BroadcastTransaction implements ports.WalletService. diff --git a/common/tree/validation.go b/common/tree/validation.go index a9677eaa2..b35579855 100644 --- a/common/tree/validation.go +++ b/common/tree/validation.go @@ -9,10 +9,8 @@ import ( "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/elementsutil" "github.com/vulpemventures/go-elements/psetv2" "github.com/vulpemventures/go-elements/taproot" - "github.com/vulpemventures/go-elements/transaction" ) var ( @@ -70,17 +68,23 @@ func ValidateCongestionTree( unspendableKeyBytes, _ := hex.DecodeString(UnspendablePoint) unspendableKey, _ := secp256k1.ParsePubKey(unspendableKeyBytes) - poolTransaction, err := transaction.NewTxFromHex(poolTxHex) + poolTransaction, err := psetv2.NewPsetFromBase64(poolTxHex) if err != nil { return ErrInvalidPoolTransaction } - poolTxAmount, err := elementsutil.ValueFromBytes(poolTransaction.Outputs[sharedOutputIndex].Value) + if len(poolTransaction.Outputs) < sharedOutputIndex+1 { + return ErrInvalidPoolTransaction + } + + poolTxAmount := poolTransaction.Outputs[sharedOutputIndex].Value + + utx, err := poolTransaction.UnsignedTx() if err != nil { return ErrInvalidPoolTransaction } - poolTxID := poolTransaction.TxHash().String() + poolTxID := utx.TxHash().String() nbNodes := tree.NumberOfNodes() if nbNodes == 0 { From faef57f4c51c8ab95bdb7f81cd9861a925f3fb04 Mon Sep 17 00:00:00 2001 From: Louis Singer Date: Wed, 24 Jan 2024 12:21:06 +0100 Subject: [PATCH 6/7] add EstimateFees in ports.Wallet --- asp/internal/core/ports/wallet.go | 1 + .../ocean-wallet/transaction.go | 51 ++++++++++++ .../tx-builder/covenant/builder.go | 80 +++++++++++++++++-- .../tx-builder/covenant/builder_test.go | 13 ++- .../tx-builder/dummy/builder_test.go | 13 ++- common/tree/validation.go | 65 +++++++-------- noah/common.go | 16 ++-- 7 files changed, 188 insertions(+), 51 deletions(-) diff --git a/asp/internal/core/ports/wallet.go b/asp/internal/core/ports/wallet.go index a655c59fe..c0379397c 100644 --- a/asp/internal/core/ports/wallet.go +++ b/asp/internal/core/ports/wallet.go @@ -15,6 +15,7 @@ type WalletService interface { ) (string, error) SelectUtxos(ctx context.Context, asset string, amount uint64) ([]TxInput, uint64, error) BroadcastTransaction(ctx context.Context, txHex string) (string, error) + EstimateFees(ctx context.Context, pset string) (uint64, error) Close() } diff --git a/asp/internal/infrastructure/ocean-wallet/transaction.go b/asp/internal/infrastructure/ocean-wallet/transaction.go index d29c443e4..a0a8f08d7 100644 --- a/asp/internal/infrastructure/ocean-wallet/transaction.go +++ b/asp/internal/infrastructure/ocean-wallet/transaction.go @@ -2,10 +2,13 @@ package oceanwallet import ( "context" + "encoding/hex" "fmt" pb "github.com/ark-network/ark/api-spec/protobuf/gen/ocean/v1" "github.com/ark-network/ark/internal/core/ports" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/vulpemventures/go-elements/elementsutil" "github.com/vulpemventures/go-elements/psetv2" ) @@ -86,3 +89,51 @@ func (s *service) BroadcastTransaction( } return res.GetTxid(), nil } + +func (s *service) EstimateFees( + ctx context.Context, pset string, +) (uint64, error) { + tx, err := psetv2.NewPsetFromBase64(pset) + if err != nil { + return 0, err + } + + inputs := make([]*pb.Input, 0, len(tx.Inputs)) + outputs := make([]*pb.Output, 0, len(tx.Outputs)) + + for _, in := range tx.Inputs { + if in.WitnessUtxo == nil { + return 0, fmt.Errorf("missing witness utxo, cannot estimate fees") + } + + inputs = append(inputs, &pb.Input{ + Txid: chainhash.Hash(in.PreviousTxid).String(), + Index: in.PreviousTxIndex, + Script: hex.EncodeToString(in.WitnessUtxo.Script), + }) + } + + for _, out := range tx.Outputs { + outputs = append(outputs, &pb.Output{ + Asset: elementsutil.AssetHashFromBytes( + append([]byte{0x01}, out.Asset...), + ), + Amount: out.Value, + Script: hex.EncodeToString(out.Script), + }) + } + + fee, err := s.txClient.EstimateFees( + ctx, + &pb.EstimateFeesRequest{ + Inputs: inputs, + Outputs: outputs, + }, + ) + if err != nil { + return 0, fmt.Errorf("failed to estimate fees: %s", err) + } + + // we add 5 sats in order to avoid min-relay-fee not met errors + return fee.GetFeeAmount() + 5, nil +} diff --git a/asp/internal/infrastructure/tx-builder/covenant/builder.go b/asp/internal/infrastructure/tx-builder/covenant/builder.go index bbe03a732..d01673e85 100644 --- a/asp/internal/infrastructure/tx-builder/covenant/builder.go +++ b/asp/internal/infrastructure/tx-builder/covenant/builder.go @@ -21,7 +21,6 @@ import ( const ( connectorAmount = 450 - poolTxFeeAmount = 300 ) var emptyNonce = []byte{0x00} @@ -199,13 +198,9 @@ func (b *txBuilder) BuildPoolTx( Amount: connectorOutputAmount, Script: aspScriptBytes, }, - { - Asset: b.net.AssetID, - Amount: poolTxFeeAmount, - }, } - targetAmount := sharedOutputAmount + connectorOutputAmount + poolTxFeeAmount + targetAmount := sharedOutputAmount + connectorOutputAmount for _, receiver := range onchainReceivers { targetAmount += receiver.Amount @@ -260,6 +255,79 @@ func (b *txBuilder) BuildPoolTx( } } + b64, err := ptx.ToBase64() + if err != nil { + return + } + + feesAmount, err := wallet.EstimateFees(ctx, b64) + if err != nil { + return + } + + if feesAmount == change { + // fees = change, remove change output + updater.Pset.Outputs = ptx.Outputs[:len(ptx.Outputs)-1] + } else if feesAmount < change { + // change covers the fees, reduce change amount + updater.Pset.Outputs[len(ptx.Outputs)-1].Value = change - feesAmount + } else { + // change is not enough to cover fees, re-select utxos + if change > 0 { + // remove change output if present + updater.Pset.Outputs = ptx.Outputs[:len(ptx.Outputs)-1] + } + newUtxos, newChange, err := wallet.SelectUtxos(ctx, b.net.AssetID, feesAmount-change) + if err != nil { + return "", nil, err + } + + if err := updater.AddInputs(toInputArgs(newUtxos)); err != nil { + return "", nil, err + } + + if newChange > 0 { + if err := updater.AddOutputs([]psetv2.OutputArgs{ + { + Asset: b.net.AssetID, + Amount: newChange, + Script: aspScriptBytes, + }, + }); err != nil { + return "", nil, err + } + } + + nbInputs := len(utxos) + + for i, utxo := range newUtxos { + + witnessUtxo, err := toWitnessUtxo(utxo) + if err != nil { + return "", nil, err + } + + if err := updater.AddInWitnessUtxo(i+nbInputs, witnessUtxo); err != nil { + return "", nil, err + } + + if err := updater.AddInSighashType(i+nbInputs, txscript.SigHashAll); err != nil { + return "", nil, err + } + } + + } + + // add fee output + if err := updater.AddOutputs([]psetv2.OutputArgs{ + { + Asset: b.net.AssetID, + Amount: feesAmount, + }, + }); err != nil { + return "", nil, err + } + utx, err := ptx.UnsignedTx() if err != nil { return diff --git a/asp/internal/infrastructure/tx-builder/covenant/builder_test.go b/asp/internal/infrastructure/tx-builder/covenant/builder_test.go index f730c8578..0c9900311 100644 --- a/asp/internal/infrastructure/tx-builder/covenant/builder_test.go +++ b/asp/internal/infrastructure/tx-builder/covenant/builder_test.go @@ -2,6 +2,8 @@ package txbuilder_test import ( "context" + "crypto/rand" + "encoding/hex" "testing" "github.com/ark-network/ark/common" @@ -133,14 +135,23 @@ func (*mockedWalletService) Status(ctx context.Context) (ports.WalletStatus, err } func (*mockedWalletService) SelectUtxos(ctx context.Context, asset string, amount uint64) ([]ports.TxInput, uint64, error) { + // random txid + bytes := make([]byte, 32) + if _, err := rand.Read(bytes); err != nil { + return nil, 0, err + } fakeInput := input{ - txid: "2f8f5733734fd44d581976bd3c1aee098bd606402df2ce02ce908287f1d5ede4", + txid: hex.EncodeToString(bytes), vout: 0, } return []ports.TxInput{&fakeInput}, 0, nil } +func (*mockedWalletService) EstimateFees(ctx context.Context, pset string) (uint64, error) { + return 100, nil +} + func TestBuildCongestionTree(t *testing.T) { builder := txbuilder.NewTxBuilder(network.Liquid) diff --git a/asp/internal/infrastructure/tx-builder/dummy/builder_test.go b/asp/internal/infrastructure/tx-builder/dummy/builder_test.go index 9b91df970..54ac7c049 100644 --- a/asp/internal/infrastructure/tx-builder/dummy/builder_test.go +++ b/asp/internal/infrastructure/tx-builder/dummy/builder_test.go @@ -2,6 +2,8 @@ package txbuilder_test import ( "context" + "crypto/rand" + "encoding/hex" "testing" "github.com/ark-network/ark/common" @@ -132,14 +134,23 @@ func (*mockedWalletService) Status(ctx context.Context) (ports.WalletStatus, err } func (*mockedWalletService) SelectUtxos(ctx context.Context, asset string, amount uint64) ([]ports.TxInput, uint64, error) { + // random txid + bytes := make([]byte, 32) + if _, err := rand.Read(bytes); err != nil { + return nil, 0, err + } fakeInput := input{ - txid: "2f8f5733734fd44d581976bd3c1aee098bd606402df2ce02ce908287f1d5ede4", + txid: hex.EncodeToString(bytes), vout: 0, } return []ports.TxInput{&fakeInput}, 0, nil } +func (*mockedWalletService) EstimateFees(ctx context.Context, pset string) (uint64, error) { + return 100, nil +} + func TestBuildCongestionTree(t *testing.T) { builder := txbuilder.NewTxBuilder(network.Liquid) diff --git a/common/tree/validation.go b/common/tree/validation.go index b35579855..8376ec0a1 100644 --- a/common/tree/validation.go +++ b/common/tree/validation.go @@ -14,35 +14,36 @@ import ( ) var ( - ErrInvalidPoolTransaction = errors.New("invalid pool transaction") - ErrEmptyTree = errors.New("empty congestion tree") - ErrInvalidRootLevel = errors.New("root level must have only one node") - ErrNoLeaves = errors.New("no leaves in the tree") - ErrNodeTransactionEmpty = errors.New("node transaction is empty") - ErrNodeTxidEmpty = errors.New("node txid is empty") - ErrNodeParentTxidEmpty = errors.New("node parent txid is empty") - ErrNodeTxidDifferent = errors.New("node txid differs from node transaction") - ErrNumberOfInputs = errors.New("node transaction should have only one input") - ErrNumberOfOutputs = errors.New("node transaction should have only three or two outputs") - ErrParentTxidInput = errors.New("parent txid should be the input of the node transaction") - ErrNumberOfChildren = errors.New("node branch transaction should have two children") - ErrLeafChildren = errors.New("leaf node should have max 1 child") - ErrInvalidChildTxid = errors.New("invalid child txid") - ErrNumberOfTapscripts = errors.New("input should have two tapscripts leaves") - ErrInternalKey = errors.New("taproot internal key is not unspendable") - ErrInvalidTaprootScript = errors.New("invalid taproot script") - ErrInvalidLeafTaprootScript = errors.New("invalid leaf taproot script") - ErrInvalidAmount = errors.New("children amount is different from parent amount") - ErrInvalidAsset = errors.New("invalid output asset") - ErrInvalidSweepSequence = errors.New("invalid sweep sequence") - ErrInvalidASP = errors.New("invalid ASP") - ErrMissingFeeOutput = errors.New("missing fee output") - ErrInvalidLeftOutput = errors.New("invalid left output") - ErrInvalidRightOutput = errors.New("invalid right output") - ErrMissingSweepTapscript = errors.New("missing sweep tapscript") - ErrMissingBranchTapscript = errors.New("missing branch tapscript") - ErrInvalidLeaf = errors.New("leaf node shouldn't have children") - ErrWrongPoolTxID = errors.New("root input should be the pool tx outpoint") + ErrInvalidPoolTransaction = errors.New("invalid pool transaction") + ErrInvalidPoolTransactionOutputs = errors.New("invalid number of outputs in pool transaction") + ErrEmptyTree = errors.New("empty congestion tree") + ErrInvalidRootLevel = errors.New("root level must have only one node") + ErrNoLeaves = errors.New("no leaves in the tree") + ErrNodeTransactionEmpty = errors.New("node transaction is empty") + ErrNodeTxidEmpty = errors.New("node txid is empty") + ErrNodeParentTxidEmpty = errors.New("node parent txid is empty") + ErrNodeTxidDifferent = errors.New("node txid differs from node transaction") + ErrNumberOfInputs = errors.New("node transaction should have only one input") + ErrNumberOfOutputs = errors.New("node transaction should have only three or two outputs") + ErrParentTxidInput = errors.New("parent txid should be the input of the node transaction") + ErrNumberOfChildren = errors.New("node branch transaction should have two children") + ErrLeafChildren = errors.New("leaf node should have max 1 child") + ErrInvalidChildTxid = errors.New("invalid child txid") + ErrNumberOfTapscripts = errors.New("input should have two tapscripts leaves") + ErrInternalKey = errors.New("taproot internal key is not unspendable") + ErrInvalidTaprootScript = errors.New("invalid taproot script") + ErrInvalidLeafTaprootScript = errors.New("invalid leaf taproot script") + ErrInvalidAmount = errors.New("children amount is different from parent amount") + ErrInvalidAsset = errors.New("invalid output asset") + ErrInvalidSweepSequence = errors.New("invalid sweep sequence") + ErrInvalidASP = errors.New("invalid ASP") + ErrMissingFeeOutput = errors.New("missing fee output") + ErrInvalidLeftOutput = errors.New("invalid left output") + ErrInvalidRightOutput = errors.New("invalid right output") + ErrMissingSweepTapscript = errors.New("missing sweep tapscript") + ErrMissingBranchTapscript = errors.New("missing branch tapscript") + ErrInvalidLeaf = errors.New("leaf node shouldn't have children") + ErrWrongPoolTxID = errors.New("root input should be the pool tx outpoint") ) const ( @@ -61,20 +62,20 @@ const ( // - input and output amounts func ValidateCongestionTree( tree CongestionTree, - poolTxHex string, + poolTx string, aspPublicKey *secp256k1.PublicKey, roundLifetimeSeconds uint, ) error { unspendableKeyBytes, _ := hex.DecodeString(UnspendablePoint) unspendableKey, _ := secp256k1.ParsePubKey(unspendableKeyBytes) - poolTransaction, err := psetv2.NewPsetFromBase64(poolTxHex) + poolTransaction, err := psetv2.NewPsetFromBase64(poolTx) if err != nil { return ErrInvalidPoolTransaction } if len(poolTransaction.Outputs) < sharedOutputIndex+1 { - return ErrInvalidPoolTransaction + return ErrInvalidPoolTransactionOutputs } poolTxAmount := poolTransaction.Outputs[sharedOutputIndex].Value diff --git a/noah/common.go b/noah/common.go index 3dcf108d0..1268b108a 100644 --- a/noah/common.go +++ b/noah/common.go @@ -19,12 +19,10 @@ import ( "github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/urfave/cli/v2" "github.com/vulpemventures/go-elements/address" - "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/taproot" - "github.com/vulpemventures/go-elements/transaction" "golang.org/x/term" ) @@ -87,7 +85,7 @@ func privateKeyFromPassword() (*secp256k1.PrivateKey, error) { encryptedPrivateKey, err := hex.DecodeString(encryptedPrivateKeyString) if err != nil { - return nil, err + return nil, fmt.Errorf("invalid encrypted private key: %s", err) } password, err := readPassword() @@ -384,9 +382,10 @@ func handleRoundStream( if event.GetRoundFinalization() != nil { // stop pinging as soon as we receive some forfeit txs pingStop() + fmt.Println("round finalization started") poolPartialTx := event.GetRoundFinalization().GetPoolPartialTx() - poolTransaction, err := transaction.NewTxFromHex(poolPartialTx) + poolTransaction, err := psetv2.NewPsetFromBase64(poolPartialTx) if err != nil { return "", err } @@ -429,13 +428,8 @@ func handleRoundStream( found := false for _, output := range poolTransaction.Outputs { if bytes.Equal(output.Script, onchainScript) { - outputValue, err := elementsutil.ValueFromBytes(output.Value) - if err != nil { - return "", err - } - - if outputValue != receiver.Amount { - return "", fmt.Errorf("invalid collaborative exit output amount: got %d, want %d", outputValue, receiver.Amount) + if output.Value != receiver.Amount { + return "", fmt.Errorf("invalid collaborative exit output amount: got %d, want %d", output.Value, receiver.Amount) } found = true From af580b2f14391aa7deffa23f8a83c78dfb2fc806 Mon Sep 17 00:00:00 2001 From: Louis Singer Date: Wed, 24 Jan 2024 12:28:54 +0100 Subject: [PATCH 7/7] replace createTestPoolTx by a constant pset --- .../tx-builder/covenant/builder_test.go | 64 +------------------ .../tx-builder/dummy/builder_test.go | 63 +----------------- 2 files changed, 6 insertions(+), 121 deletions(-) diff --git a/asp/internal/infrastructure/tx-builder/covenant/builder_test.go b/asp/internal/infrastructure/tx-builder/covenant/builder_test.go index 0c9900311..c7cfeda7e 100644 --- a/asp/internal/infrastructure/tx-builder/covenant/builder_test.go +++ b/asp/internal/infrastructure/tx-builder/covenant/builder_test.go @@ -15,68 +15,14 @@ import ( secp256k1 "github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/stretchr/testify/require" "github.com/vulpemventures/go-elements/network" - "github.com/vulpemventures/go-elements/payment" "github.com/vulpemventures/go-elements/psetv2" ) const ( testingKey = "apub1qgvdtj5ttpuhkldavhq8thtm5auyk0ec4dcmrfdgu0u5hgp9we22v3hrs4x" + fakePoolTx = "cHNldP8BAgQCAAAAAQQBAQEFAQMBBgEDAfsEAgAAAAABDiDk7dXxh4KQzgLO8i1ABtaLCe4aPL12GVhN1E9zM1ePLwEPBAAAAAABEAT/////AAEDCOgDAAAAAAAAAQQWABSNnpy01UJqd99eTg2M1IpdKId11gf8BHBzZXQCICWyUQcOKcoZBDzzPM1zJOLdqwPsxK4LXnfE/A5c9slaB/wEcHNldAgEAAAAAAABAwh4BQAAAAAAAAEEFgAUjZ6ctNVCanffXk4NjNSKXSiHddYH/ARwc2V0AiAlslEHDinKGQQ88zzNcyTi3asD7MSuC153xPwOXPbJWgf8BHBzZXQIBAAAAAAAAQMI9AEAAAAAAAABBAAH/ARwc2V0AiAlslEHDinKGQQ88zzNcyTi3asD7MSuC153xPwOXPbJWgf8BHBzZXQIBAAAAAAA" ) -func createTestPoolTx(sharedOutputAmount, numberOfInputs uint64) (string, error) { - _, key, err := common.DecodePubKey(testingKey) - if err != nil { - return "", err - } - - payment := payment.FromPublicKey(key, &network.Testnet, nil) - script := payment.WitnessScript - - pset, err := psetv2.New(nil, nil, nil) - if err != nil { - return "", err - } - - updater, err := psetv2.NewUpdater(pset) - if err != nil { - return "", err - } - - err = updater.AddInputs([]psetv2.InputArgs{ - { - Txid: "2f8f5733734fd44d581976bd3c1aee098bd606402df2ce02ce908287f1d5ede4", - TxIndex: 0, - }, - }) - if err != nil { - return "", err - } - - connectorsAmount := numberOfInputs*450 + 500 - - err = updater.AddOutputs([]psetv2.OutputArgs{ - { - Asset: network.Regtest.AssetID, - Amount: sharedOutputAmount, - Script: script, - }, - { - Asset: network.Regtest.AssetID, - Amount: connectorsAmount, - Script: script, - }, - { - Asset: network.Regtest.AssetID, - Amount: 500, - }, - }) - if err != nil { - return "", err - } - - return pset.ToBase64() -} - type mockedWalletService struct{} type input struct { @@ -407,11 +353,7 @@ func TestBuildCongestionTree(t *testing.T) { func TestBuildForfeitTxs(t *testing.T) { builder := txbuilder.NewTxBuilder(network.Liquid) - // TODO: replace with fixture. - poolTxBase64, err := createTestPoolTx(1000, 2) - require.NoError(t, err) - - poolTx, err := psetv2.NewPsetFromBase64(poolTxBase64) + poolTx, err := psetv2.NewPsetFromBase64(fakePoolTx) require.NoError(t, err) utx, err := poolTx.UnsignedTx() @@ -473,7 +415,7 @@ func TestBuildForfeitTxs(t *testing.T) { for _, f := range fixtures { connectors, forfeitTxs, err := builder.BuildForfeitTxs( - key, poolTxBase64, f.payments, + key, fakePoolTx, f.payments, ) require.NoError(t, err) require.Len(t, connectors, f.expectedNumOfConnectors) diff --git a/asp/internal/infrastructure/tx-builder/dummy/builder_test.go b/asp/internal/infrastructure/tx-builder/dummy/builder_test.go index 54ac7c049..41ce9d175 100644 --- a/asp/internal/infrastructure/tx-builder/dummy/builder_test.go +++ b/asp/internal/infrastructure/tx-builder/dummy/builder_test.go @@ -14,68 +14,14 @@ import ( secp256k1 "github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/stretchr/testify/require" "github.com/vulpemventures/go-elements/network" - "github.com/vulpemventures/go-elements/payment" "github.com/vulpemventures/go-elements/psetv2" ) const ( testingKey = "apub1qgvdtj5ttpuhkldavhq8thtm5auyk0ec4dcmrfdgu0u5hgp9we22v3hrs4x" + fakePoolTx = "cHNldP8BAgQCAAAAAQQBAQEFAQMBBgEDAfsEAgAAAAABDiDk7dXxh4KQzgLO8i1ABtaLCe4aPL12GVhN1E9zM1ePLwEPBAAAAAABEAT/////AAEDCOgDAAAAAAAAAQQWABSNnpy01UJqd99eTg2M1IpdKId11gf8BHBzZXQCICWyUQcOKcoZBDzzPM1zJOLdqwPsxK4LXnfE/A5c9slaB/wEcHNldAgEAAAAAAABAwh4BQAAAAAAAAEEFgAUjZ6ctNVCanffXk4NjNSKXSiHddYH/ARwc2V0AiAlslEHDinKGQQ88zzNcyTi3asD7MSuC153xPwOXPbJWgf8BHBzZXQIBAAAAAAAAQMI9AEAAAAAAAABBAAH/ARwc2V0AiAlslEHDinKGQQ88zzNcyTi3asD7MSuC153xPwOXPbJWgf8BHBzZXQIBAAAAAAA" ) -func createTestPoolTx(sharedOutputAmount, numberOfInputs uint64) (string, error) { - _, key, err := common.DecodePubKey(testingKey) - if err != nil { - return "", err - } - - payment := payment.FromPublicKey(key, &network.Testnet, nil) - script := payment.WitnessScript - - pset, err := psetv2.New(nil, nil, nil) - if err != nil { - return "", err - } - - updater, err := psetv2.NewUpdater(pset) - if err != nil { - return "", err - } - - err = updater.AddInputs([]psetv2.InputArgs{ - { - Txid: "2f8f5733734fd44d581976bd3c1aee098bd606402df2ce02ce908287f1d5ede4", - TxIndex: 0, - }, - }) - if err != nil { - return "", err - } - - connectorsAmount := numberOfInputs * (450 + 500) - - err = updater.AddOutputs([]psetv2.OutputArgs{ - { - Asset: network.Regtest.AssetID, - Amount: sharedOutputAmount, - Script: script, - }, - { - Asset: network.Regtest.AssetID, - Amount: connectorsAmount, - Script: script, - }, - { - Asset: network.Regtest.AssetID, - Amount: 500, - }, - }) - if err != nil { - return "", err - } - - return pset.ToBase64() -} - type input struct { txid string vout uint32 @@ -330,10 +276,7 @@ func TestBuildCongestionTree(t *testing.T) { func TestBuildForfeitTxs(t *testing.T) { builder := txbuilder.NewTxBuilder(network.Liquid) - poolTx, err := createTestPoolTx(1000, 450*2) - require.NoError(t, err) - - poolPset, err := psetv2.NewPsetFromBase64(poolTx) + poolPset, err := psetv2.NewPsetFromBase64(fakePoolTx) require.NoError(t, err) poolTxUnsigned, err := poolPset.UnsignedTx() @@ -395,7 +338,7 @@ func TestBuildForfeitTxs(t *testing.T) { for _, f := range fixtures { connectors, forfeitTxs, err := builder.BuildForfeitTxs( - key, poolTx, f.payments, + key, fakePoolTx, f.payments, ) require.NoError(t, err) require.Len(t, connectors, f.expectedNumOfConnectors)