From b2e034cf0efc85009eff1311c6d47a20b9647fb4 Mon Sep 17 00:00:00 2001 From: Louis Singer <41042567+louisinger@users.noreply.github.com> Date: Thu, 8 Feb 2024 04:51:01 +0100 Subject: [PATCH] Refactor `tx-builder/covenant` (#88) * clean tx-builder/covenant/tree.go * rename "createRoot" --> "createBinaryTree" * replace node.psets by an iterative function "createTreeTransactions" * merge function * unspendable point as bytes + remove psetWithLevel * re-add extra leaf level in congestion tree * polishing tx-builder/covenant * remove emptyNonce var * cleaning tree.go * improve node.outputs() * Fix var type * Fixes * fix linting * Renaming and reordering --------- Co-authored-by: altafan <18440657+altafan@users.noreply.github.com> --- asp/go.mod | 2 + asp/go.sum | 1 + asp/internal/app-config/config.go | 5 +- asp/internal/core/application/service.go | 4 +- asp/internal/core/ports/tx_builder.go | 4 +- .../tx-builder/covenant/builder.go | 629 +++++++--------- .../tx-builder/covenant/builder_test.go | 609 +++++---------- .../tx-builder/covenant/connectors.go | 127 +--- .../tx-builder/covenant/forfeit.go | 155 ++-- .../tx-builder/covenant/mocks_test.go | 152 ++++ .../covenant/testdata/fixtures.json | 229 ++++++ .../tx-builder/covenant/tree.go | 706 ++++++++---------- .../tx-builder/covenant/utils.go | 167 +++++ .../tx-builder/dummy/builder.go | 22 +- .../tx-builder/dummy/builder_test.go | 6 +- common/tree/validation.go | 29 +- noah/common.go | 5 +- 17 files changed, 1470 insertions(+), 1382 deletions(-) create mode 100644 asp/internal/infrastructure/tx-builder/covenant/mocks_test.go create mode 100644 asp/internal/infrastructure/tx-builder/covenant/testdata/fixtures.json create mode 100644 asp/internal/infrastructure/tx-builder/covenant/utils.go diff --git a/asp/go.mod b/asp/go.mod index 1c688ee..03924d7 100644 --- a/asp/go.mod +++ b/asp/go.mod @@ -24,6 +24,8 @@ require ( google.golang.org/protobuf v1.31.0 ) +require github.com/stretchr/objx v0.5.0 // indirect + require ( github.com/btcsuite/btcd v0.23.1 github.com/btcsuite/btcd/btcec/v2 v2.3.2 diff --git a/asp/go.sum b/asp/go.sum index 72cc011..1ac5ef4 100644 --- a/asp/go.sum +++ b/asp/go.sum @@ -305,6 +305,7 @@ github.com/spf13/viper v1.17.0/go.mod h1:BmMMMLQXSbcHK6KAOiFLz0l5JHrU89OdIRHvsk0 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= diff --git a/asp/internal/app-config/config.go b/asp/internal/app-config/config.go index 2713191..80fa97a 100644 --- a/asp/internal/app-config/config.go +++ b/asp/internal/app-config/config.go @@ -125,11 +125,12 @@ func (c *Config) txBuilderService() error { var svc ports.TxBuilder var err error net := c.mainChain() + switch c.TxBuilderType { case "dummy": - svc = txbuilderdummy.NewTxBuilder(net) + svc = txbuilderdummy.NewTxBuilder(c.wallet, net) case "covenant": - svc = txbuilder.NewTxBuilder(net) + svc = txbuilder.NewTxBuilder(c.wallet, net) default: err = fmt.Errorf("unknown tx builder type") } diff --git a/asp/internal/core/application/service.go b/asp/internal/core/application/service.go index 113c47a..e394a2b 100644 --- a/asp/internal/core/application/service.go +++ b/asp/internal/core/application/service.go @@ -266,7 +266,7 @@ func (s *service) startFinalization() { return } - unsignedPoolTx, tree, err := s.builder.BuildPoolTx(s.pubkey, s.wallet, payments, s.minRelayFee) + unsignedPoolTx, tree, err := s.builder.BuildPoolTx(s.pubkey, 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") @@ -419,7 +419,7 @@ func (s *service) getNewVtxos(round *domain.Round) []domain.Vtxo { buf, _ := hex.DecodeString(r.Pubkey) pk, _ := secp256k1.ParsePubKey(buf) - script, _ := s.builder.GetLeafOutputScript(pk, s.pubkey) + script, _ := s.builder.GetVtxoOutputScript(pk, s.pubkey) if bytes.Equal(script, out.Script) { found = true pubkey = r.Pubkey diff --git a/asp/internal/core/ports/tx_builder.go b/asp/internal/core/ports/tx_builder.go index 1e16a58..5d550dd 100644 --- a/asp/internal/core/ports/tx_builder.go +++ b/asp/internal/core/ports/tx_builder.go @@ -8,10 +8,10 @@ import ( type TxBuilder interface { BuildPoolTx( - aspPubkey *secp256k1.PublicKey, wallet WalletService, payments []domain.Payment, minRelayFee uint64, + aspPubkey *secp256k1.PublicKey, payments []domain.Payment, minRelayFee uint64, ) (poolTx string, congestionTree tree.CongestionTree, err error) BuildForfeitTxs( aspPubkey *secp256k1.PublicKey, poolTx string, payments []domain.Payment, ) (connectors []string, forfeitTxs []string, err error) - GetLeafOutputScript(userPubkey, aspPubkey *secp256k1.PublicKey) ([]byte, error) + GetVtxoOutputScript(userPubkey, aspPubkey *secp256k1.PublicKey) ([]byte, error) } diff --git a/asp/internal/infrastructure/tx-builder/covenant/builder.go b/asp/internal/infrastructure/tx-builder/covenant/builder.go index d01673e..665cbd6 100644 --- a/asp/internal/infrastructure/tx-builder/covenant/builder.go +++ b/asp/internal/infrastructure/tx-builder/covenant/builder.go @@ -8,333 +8,91 @@ import ( "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" "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" ) const ( - connectorAmount = 450 + connectorAmount = uint64(450) ) -var emptyNonce = []byte{0x00} - type txBuilder struct { - net *network.Network + wallet ports.WalletService + net *network.Network } -func NewTxBuilder(net network.Network) ports.TxBuilder { - return &txBuilder{ - net: &net, - } +func NewTxBuilder( + wallet ports.WalletService, net network.Network, +) ports.TxBuilder { + return &txBuilder{wallet, &net} } -func p2wpkhScript(publicKey *secp256k1.PublicKey, net *network.Network) ([]byte, error) { - payment := payment.FromPublicKey(publicKey, net, nil) - addr, err := payment.WitnessPubKeyHash() - if err != nil { - return nil, err - } - - return address.ToOutputScript(addr) -} - -func getTxid(txStr string) (string, error) { - pset, err := psetv2.NewPsetFromBase64(txStr) - if err != nil { - return "", err - } - - utx, err := pset.UnsignedTx() - if err != nil { - return "", err - } - - return utx.TxHash().String(), nil -} - -func (b *txBuilder) GetLeafOutputScript(userPubkey, aspPubkey *secp256k1.PublicKey) ([]byte, error) { - outputScript, _, err := b.getLeafTaprootTree(userPubkey, aspPubkey) +func (b *txBuilder) GetVtxoOutputScript(userPubkey, aspPubkey *secp256k1.PublicKey) ([]byte, error) { + outputScript, _, err := b.getLeafScriptAndTree(userPubkey, aspPubkey) if err != nil { return nil, err } return outputScript, nil } -// BuildForfeitTxs implements ports.TxBuilder. func (b *txBuilder) BuildForfeitTxs( aspPubkey *secp256k1.PublicKey, poolTx string, payments []domain.Payment, ) (connectors []string, forfeitTxs []string, err error) { - poolTxID, err := getTxid(poolTx) + connectorTxs, err := b.createConnectors(poolTx, payments, aspPubkey) if err != nil { return nil, nil, err } - aspScript, err := p2wpkhScript(aspPubkey, b.net) + forfeitTxs, err = b.createForfeitTxs(aspPubkey, payments, connectorTxs) if err != nil { return nil, nil, err } - numberOfConnectors := numberOfVTXOs(payments) - - connectors, err = createConnectors( - poolTxID, - 1, - psetv2.OutputArgs{ - Asset: b.net.AssetID, - Amount: connectorAmount, - Script: aspScript, - }, - aspScript, - numberOfConnectors, - ) - if err != nil { - return nil, nil, err + for _, tx := range connectorTxs { + buf, _ := tx.ToBase64() + connectors = append(connectors, buf) } - - connectorsAsInputs, err := connectorsToInputArgs(connectors) - if err != nil { - return nil, nil, err - } - - lbtc, _ := elementsutil.AssetHashToBytes(b.net.AssetID) - - forfeitTxs = make([]string, 0) - - for _, payment := range payments { - for _, vtxo := range payment.Inputs { - vtxoAmount, err := elementsutil.ValueToBytes(vtxo.Amount) - if err != nil { - return nil, nil, err - } - - pubkeyBytes, err := hex.DecodeString(vtxo.Pubkey) - if err != nil { - return nil, nil, fmt.Errorf("failed to decode pubkey: %s", err) - } - - vtxoPubkey, err := secp256k1.ParsePubKey(pubkeyBytes) - if err != nil { - return nil, nil, err - } - - vtxoOutputScript, vtxoTaprootTree, err := b.getLeafTaprootTree(vtxoPubkey, aspPubkey) - if err != nil { - return nil, nil, err - } - - for _, connector := range connectorsAsInputs { - forfeitTx, err := createForfeitTx( - connector.input, - connector.witnessUtxo, - psetv2.InputArgs{ - Txid: vtxo.Txid, - TxIndex: vtxo.VOut, - }, - &transaction.TxOutput{ - Asset: lbtc, - Value: vtxoAmount, - Script: vtxoOutputScript, - }, - vtxoTaprootTree, - aspScript, - *b.net, - ) - if err != nil { - return nil, nil, err - } - - forfeitTxs = append(forfeitTxs, forfeitTx) - } - } - - } - return connectors, forfeitTxs, nil } -// BuildPoolTx implements ports.TxBuilder. func (b *txBuilder) BuildPoolTx( - aspPubkey *secp256k1.PublicKey, - wallet ports.WalletService, - payments []domain.Payment, - minRelayFee uint64, + aspPubkey *secp256k1.PublicKey, payments []domain.Payment, minRelayFee uint64, ) (poolTx string, congestionTree tree.CongestionTree, err error) { - aspScriptBytes, err := p2wpkhScript(aspPubkey, b.net) - if err != nil { - return - } - - offchainReceivers, onchainReceivers := receiversFromPayments(payments) - numberOfConnectors := numberOfVTXOs(payments) - connectorOutputAmount := connectorAmount * numberOfConnectors - - ctx := context.Background() - - makeTree, sharedOutputScript, sharedOutputAmount, err := buildCongestionTree( - b.net, - aspPubkey, - offchainReceivers, - minRelayFee, + // The creation of the tree and the pool tx are tightly coupled: + // - building the tree requires knowing the shared outpoint (txid:vout) + // - building the pool tx requires knowing the shared output script and amount + // The idea here is to first create all the data for the outputs of the txs + // of the congestion tree to calculate the shared output script and amount. + // With these data the pool tx can be created, and once the shared utxo + // outpoint is obtained, the congestion tree can be finally created. + // The factory function `treeFactoryFn` returned below holds all outputs data + // generated in the process and takes the shared utxo outpoint as argument. + // This is safe as the memory allocated for `craftCongestionTree` is freed + // only after `BuildPoolTx` returns. + treeFactoryFn, sharedOutputScript, sharedOutputAmount, err := craftCongestionTree( + b.net.AssetID, aspPubkey, payments, minRelayFee, ) if err != nil { return } - outputs := []psetv2.OutputArgs{ - { - Asset: b.net.AssetID, - Amount: sharedOutputAmount, - Script: sharedOutputScript, - }, - { - Asset: b.net.AssetID, - Amount: connectorOutputAmount, - Script: aspScriptBytes, - }, - } - - targetAmount := sharedOutputAmount + connectorOutputAmount - - for _, receiver := range onchainReceivers { - targetAmount += 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, targetAmount) + ptx, err := b.createPoolTx( + sharedOutputAmount, sharedOutputScript, payments, aspPubkey, + ) if err != nil { return } - if change > 0 { - outputs = append(outputs, psetv2.OutputArgs{ - Asset: b.net.AssetID, - Amount: change, - Script: aspScriptBytes, - }) - } - - ptx, err := psetv2.New(toInputArgs(utxos), outputs, nil) + unsignedTx, err := ptx.UnsignedTx() if err != nil { 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 - } - } - - 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 - } - - tree, err := makeTree(psetv2.InputArgs{ - Txid: utx.TxHash().String(), + tree, err := treeFactoryFn(psetv2.InputArgs{ + Txid: unsignedTx.TxHash().String(), TxIndex: 0, }) if err != nil { @@ -350,137 +108,262 @@ func (b *txBuilder) BuildPoolTx( return } -func (b *txBuilder) getLeafTaprootTree(userPubkey, aspPubkey *secp256k1.PublicKey) ([]byte, *taproot.IndexedElementsTapScriptTree, error) { - sweepTaprootLeaf, err := tree.SweepScript(aspPubkey, expirationTime) +func (b *txBuilder) getLeafScriptAndTree( + userPubkey, aspPubkey *secp256k1.PublicKey, +) ([]byte, *taproot.IndexedElementsTapScriptTree, error) { + redeemClosure, err := tree.VtxoScript(userPubkey) if err != nil { return nil, nil, err } - vtxoLeaf, err := tree.VtxoScript(userPubkey) + sweepClosure, err := tree.SweepScript(aspPubkey, expirationTime) if err != nil { return nil, nil, err } - leafTaprootTree := taproot.AssembleTaprootScriptTree(*vtxoLeaf, *sweepTaprootLeaf) - root := leafTaprootTree.RootNode.TapHash() - - unspendableKeyBytes, _ := hex.DecodeString(tree.UnspendablePoint) - unspendableKey, _ := secp256k1.ParsePubKey(unspendableKeyBytes) - - taprootKey := taproot.ComputeTaprootOutputKey( - unspendableKey, - root[:], + taprootTree := taproot.AssembleTaprootScriptTree( + *redeemClosure, *sweepClosure, ) + root := taprootTree.RootNode.TapHash() + unspendableKey := tree.UnspendableKey() + taprootKey := taproot.ComputeTaprootOutputKey(unspendableKey, root[:]) + outputScript, err := taprootOutputScript(taprootKey) if err != nil { return nil, nil, err } - return outputScript, leafTaprootTree, nil + return outputScript, taprootTree, nil } -type inputWithWitnessUtxo struct { - input psetv2.InputArgs - witnessUtxo *transaction.TxOutput -} +func (b *txBuilder) createPoolTx( + sharedOutputAmount uint64, sharedOutputScript []byte, + payments []domain.Payment, aspPubKey *secp256k1.PublicKey, +) (*psetv2.Pset, error) { + aspScript, err := p2wpkhScript(aspPubKey, b.net) + if err != nil { + return nil, err + } -func connectorsToInputArgs(connectors []string) ([]inputWithWitnessUtxo, error) { - inputs := make([]inputWithWitnessUtxo, 0, len(connectors)+1) - for i, psetb64 := range connectors { - pset, err := psetv2.NewPsetFromBase64(psetb64) + receivers := getOnchainReceivers(payments) + connectorsAmount := connectorAmount * countSpentVtxos(payments) + targetAmount := sharedOutputAmount + connectorsAmount + + outputs := []psetv2.OutputArgs{ + { + Asset: b.net.AssetID, + Amount: sharedOutputAmount, + Script: sharedOutputScript, + }, + { + Asset: b.net.AssetID, + Amount: connectorAmount, + Script: aspScript, + }, + } + + for _, receiver := range receivers { + targetAmount += receiver.Amount + + receiverScript, err := address.ToOutputScript(receiver.OnchainAddress) if err != nil { return nil, err } - utx, err := pset.UnsignedTx() - if err != nil { - return nil, err - } - - txID := utx.TxHash().String() - - input := psetv2.InputArgs{ - Txid: txID, - TxIndex: 0, - } - inputs = append(inputs, inputWithWitnessUtxo{ - input: input, - witnessUtxo: utx.Outputs[0], + outputs = append(outputs, psetv2.OutputArgs{ + Asset: b.net.AssetID, + Amount: receiver.Amount, + Script: receiverScript, }) + } - if i == len(connectors)-1 && len(utx.Outputs) > 1 { - input := psetv2.InputArgs{ - Txid: txID, - TxIndex: 1, + ctx := context.Background() + utxos, change, err := b.wallet.SelectUtxos(ctx, b.net.AssetID, targetAmount) + if err != nil { + return nil, err + } + + if change > 0 { + outputs = append(outputs, psetv2.OutputArgs{ + Asset: b.net.AssetID, + Amount: change, + Script: aspScript, + }) + } + + ptx, err := psetv2.New(nil, outputs, nil) + if err != nil { + return nil, err + } + + updater, err := psetv2.NewUpdater(ptx) + if err != nil { + return nil, err + } + + if err := addInputs(updater, utxos); err != nil { + return nil, err + } + + b64, err := ptx.ToBase64() + if err != nil { + return nil, err + } + + feeAmount, err := b.wallet.EstimateFees(ctx, b64) + if err != nil { + return nil, err + } + + if feeAmount == change { + // fees = change, remove change output + ptx.Outputs = ptx.Outputs[:len(ptx.Outputs)-1] + } else if feeAmount < change { + // change covers the fees, reduce change amount + ptx.Outputs[len(ptx.Outputs)-1].Value = change - feeAmount + } else { + // change is not enough to cover fees, re-select utxos + if change > 0 { + // remove change output if present + ptx.Outputs = ptx.Outputs[:len(ptx.Outputs)-1] + } + newUtxos, change, err := b.wallet.SelectUtxos(ctx, b.net.AssetID, feeAmount-change) + if err != nil { + return nil, err + } + + if change > 0 { + if err := updater.AddOutputs([]psetv2.OutputArgs{ + { + Asset: b.net.AssetID, + Amount: change, + Script: aspScript, + }, + }); err != nil { + return nil, err } - inputs = append(inputs, inputWithWitnessUtxo{ - input: input, - witnessUtxo: utx.Outputs[1], + } + + if err := addInputs(updater, newUtxos); err != nil { + return nil, err + } + } + + // add fee output + if err := updater.AddOutputs([]psetv2.OutputArgs{ + { + Asset: b.net.AssetID, + Amount: feeAmount, + }, + }); err != nil { + return nil, err + } + + return ptx, nil +} + +func (b *txBuilder) createConnectors( + poolTx string, payments []domain.Payment, aspPubkey *secp256k1.PublicKey, +) ([]*psetv2.Pset, error) { + txid, _ := getTxid(poolTx) + + aspScript, err := p2wpkhScript(aspPubkey, b.net) + if err != nil { + return nil, err + } + + connectorOutput := psetv2.OutputArgs{ + Asset: b.net.AssetID, + Script: aspScript, + Amount: connectorAmount, + } + + numberOfConnectors := countSpentVtxos(payments) + + previousInput := psetv2.InputArgs{ + Txid: txid, + TxIndex: 1, + } + + if numberOfConnectors == 1 { + outputs := []psetv2.OutputArgs{connectorOutput} + connectorTx, err := craftConnectorTx(previousInput, outputs) + if err != nil { + return nil, err + } + + return []*psetv2.Pset{connectorTx}, nil + } + + totalConnectorAmount := connectorAmount * numberOfConnectors + + connectors := make([]*psetv2.Pset, 0, numberOfConnectors-1) + for i := uint64(0); i < numberOfConnectors-1; i++ { + outputs := []psetv2.OutputArgs{connectorOutput} + totalConnectorAmount -= connectorAmount + if totalConnectorAmount > 0 { + outputs = append(outputs, psetv2.OutputArgs{ + Asset: b.net.AssetID, + Script: aspScript, + Amount: totalConnectorAmount, }) } + connectorTx, err := craftConnectorTx(previousInput, outputs) + if err != nil { + return nil, err + } + + txid, _ := getPsetId(connectorTx) + + previousInput = psetv2.InputArgs{ + Txid: txid, + TxIndex: 1, + } + + connectors = append(connectors, connectorTx) } - return inputs, nil + + return connectors, nil } -func numberOfVTXOs(payments []domain.Payment) uint64 { - var sum uint64 - for _, payment := range payments { - sum += uint64(len(payment.Inputs)) +func (b *txBuilder) createForfeitTxs( + aspPubkey *secp256k1.PublicKey, payments []domain.Payment, connectors []*psetv2.Pset, +) ([]string, error) { + aspScript, err := p2wpkhScript(aspPubkey, b.net) + if err != nil { + return nil, err } - return sum -} -func receiversFromPayments( - payments []domain.Payment, -) (offchainReceivers, onchainReceivers []domain.Receiver) { + forfeitTxs := make([]string, 0) for _, payment := range payments { - for _, receiver := range payment.Receivers { - if receiver.IsOnchain() { - onchainReceivers = append(onchainReceivers, receiver) - } else { - offchainReceivers = append(offchainReceivers, receiver) + 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) + if err != nil { + return nil, err + } + + vtxoScript, vtxoTaprootTree, err := b.getLeafScriptAndTree(vtxoPubkey, aspPubkey) + if err != nil { + return nil, err + } + + for _, connector := range connectors { + txs, err := craftForfeitTxs( + connector, vtxo, vtxoTaprootTree, vtxoScript, aspScript, + ) + if err != nil { + return nil, err + } + + forfeitTxs = append(forfeitTxs, txs...) } } } - return -} - -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(), - }) - } - 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 + return forfeitTxs, nil } diff --git a/asp/internal/infrastructure/tx-builder/covenant/builder_test.go b/asp/internal/infrastructure/tx-builder/covenant/builder_test.go index c7cfeda..89cd625 100644 --- a/asp/internal/infrastructure/tx-builder/covenant/builder_test.go +++ b/asp/internal/infrastructure/tx-builder/covenant/builder_test.go @@ -1,9 +1,10 @@ package txbuilder_test import ( - "context" "crypto/rand" "encoding/hex" + "encoding/json" + "os" "testing" "github.com/ark-network/ark/common" @@ -12,453 +13,217 @@ import ( "github.com/ark-network/ark/internal/core/ports" txbuilder "github.com/ark-network/ark/internal/infrastructure/tx-builder/covenant" "github.com/btcsuite/btcd/chaincfg/chainhash" - secp256k1 "github.com/decred/dcrd/dcrec/secp256k1/v4" + "github.com/decred/dcrd/dcrec/secp256k1/v4" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "github.com/vulpemventures/go-elements/network" "github.com/vulpemventures/go-elements/psetv2" ) const ( - testingKey = "apub1qgvdtj5ttpuhkldavhq8thtm5auyk0ec4dcmrfdgu0u5hgp9we22v3hrs4x" - fakePoolTx = "cHNldP8BAgQCAAAAAQQBAQEFAQMBBgEDAfsEAgAAAAABDiDk7dXxh4KQzgLO8i1ABtaLCe4aPL12GVhN1E9zM1ePLwEPBAAAAAABEAT/////AAEDCOgDAAAAAAAAAQQWABSNnpy01UJqd99eTg2M1IpdKId11gf8BHBzZXQCICWyUQcOKcoZBDzzPM1zJOLdqwPsxK4LXnfE/A5c9slaB/wEcHNldAgEAAAAAAABAwh4BQAAAAAAAAEEFgAUjZ6ctNVCanffXk4NjNSKXSiHddYH/ARwc2V0AiAlslEHDinKGQQ88zzNcyTi3asD7MSuC153xPwOXPbJWgf8BHBzZXQIBAAAAAAAAQMI9AEAAAAAAAABBAAH/ARwc2V0AiAlslEHDinKGQQ88zzNcyTi3asD7MSuC153xPwOXPbJWgf8BHBzZXQIBAAAAAAA" + testingKey = "apub1qgvdtj5ttpuhkldavhq8thtm5auyk0ec4dcmrfdgu0u5hgp9we22v3hrs4x" + minRelayFee = uint64(30) + roundLifetime = int64(1209344) ) -type mockedWalletService struct{} +var ( + wallet *mockedWallet + pubkey *secp256k1.PublicKey +) -type input struct { - txid string - vout uint32 +func TestMain(m *testing.M) { + wallet = &mockedWallet{} + wallet.On("EstimateFees", mock.Anything, mock.Anything). + Return(uint64(100), nil) + wallet.On("SelectUtxos", mock.Anything, mock.Anything, mock.Anything). + Return(randomInput, uint64(0), nil) + + _, pubkey, _ = common.DecodePubKey(testingKey) + + os.Exit(m.Run()) } -func (i *input) GetTxid() string { - return i.txid -} +func TestBuildPoolTx(t *testing.T) { + builder := txbuilder.NewTxBuilder(wallet, network.Liquid) -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") -} - -// Close implements ports.WalletService. -func (*mockedWalletService) Close() { - panic("unimplemented") -} - -// DeriveAddresses implements ports.WalletService. -func (*mockedWalletService) DeriveAddresses(ctx context.Context, num int) ([]string, error) { - panic("unimplemented") -} - -// GetPubkey implements ports.WalletService. -func (*mockedWalletService) GetPubkey(ctx context.Context) (*secp256k1.PublicKey, error) { - panic("unimplemented") -} - -// SignPset implements ports.WalletService. -func (*mockedWalletService) SignPset(ctx context.Context, pset string, extractRawTx bool) (string, error) { - panic("unimplemented") -} - -// Status implements ports.WalletService. -func (*mockedWalletService) Status(ctx context.Context) (ports.WalletStatus, error) { - panic("unimplemented") -} - -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: 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) - - fixtures := []struct { - payments []domain.Payment - expectedNodesNum int // 2*len(receivers) -1 - expectedLeavesNum int - }{ - { - payments: []domain.Payment{ - { - Id: "0", - Inputs: []domain.Vtxo{ - { - VtxoKey: domain.VtxoKey{ - Txid: "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6", - VOut: 0, - }, - Receiver: domain.Receiver{ - Pubkey: "020000000000000000000000000000000000000000000000000000000000000002", - Amount: 1100, - }, - }, - }, - Receivers: []domain.Receiver{ - { - Pubkey: "020000000000000000000000000000000000000000000000000000000000000002", - Amount: 1100, - }, - }, - }, - }, - expectedNodesNum: 1, - expectedLeavesNum: 1, - }, - { - payments: []domain.Payment{ - { - Id: "0", - Inputs: []domain.Vtxo{ - { - VtxoKey: domain.VtxoKey{ - Txid: "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6", - VOut: 0, - }, - Receiver: domain.Receiver{ - Pubkey: "020000000000000000000000000000000000000000000000000000000000000002", - Amount: 1100, - }, - }, - }, - Receivers: []domain.Receiver{ - { - Pubkey: "020000000000000000000000000000000000000000000000000000000000000002", - Amount: 600, - }, - { - Pubkey: "020000000000000000000000000000000000000000000000000000000000000002", - Amount: 500, - }, - }, - }, - }, - expectedNodesNum: 1, - expectedLeavesNum: 1, - }, - { - payments: []domain.Payment{ - { - Id: "0", - Inputs: []domain.Vtxo{ - { - VtxoKey: domain.VtxoKey{ - Txid: "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6", - VOut: 0, - }, - Receiver: domain.Receiver{ - Pubkey: "020000000000000000000000000000000000000000000000000000000000000002", - Amount: 1100, - }, - }, - }, - Receivers: []domain.Receiver{ - { - Pubkey: "020000000000000000000000000000000000000000000000000000000000000002", - Amount: 600, - }, - { - Pubkey: "020000000000000000000000000000000000000000000000000000000000000002", - Amount: 500, - }, - }, - }, - { - Id: "0", - Inputs: []domain.Vtxo{ - { - VtxoKey: domain.VtxoKey{ - Txid: "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6", - VOut: 0, - }, - Receiver: domain.Receiver{ - Pubkey: "020000000000000000000000000000000000000000000000000000000000000002", - Amount: 1100, - }, - }, - }, - Receivers: []domain.Receiver{ - { - Pubkey: "020000000000000000000000000000000000000000000000000000000000000002", - Amount: 600, - }, - { - Pubkey: "020000000000000000000000000000000000000000000000000000000000000002", - Amount: 500, - }, - }, - }, - { - Id: "0", - Inputs: []domain.Vtxo{ - { - VtxoKey: domain.VtxoKey{ - Txid: "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6", - VOut: 0, - }, - Receiver: domain.Receiver{ - Pubkey: "020000000000000000000000000000000000000000000000000000000000000002", - Amount: 1100, - }, - }, - }, - Receivers: []domain.Receiver{ - { - Pubkey: "020000000000000000000000000000000000000000000000000000000000000002", - Amount: 600, - }, - { - Pubkey: "020000000000000000000000000000000000000000000000000000000000000002", - Amount: 500, - }, - }, - }, - }, - expectedNodesNum: 5, - expectedLeavesNum: 3, - }, { - payments: []domain.Payment{ - { - Id: "a242cdd8-f3d5-46c0-ae98-94135a2bee3f", - Inputs: []domain.Vtxo{ - { - VtxoKey: domain.VtxoKey{ - Txid: "755c820771284d85ea4bbcc246565b4eddadc44237a7e57a0f9cb78a840d1d41", - VOut: 0, - }, - Receiver: domain.Receiver{ - Pubkey: "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5", - Amount: 1000, - }, - }, - { - VtxoKey: domain.VtxoKey{ - Txid: "66a0df86fcdeb84b8877adfe0b2c556dba30305d72ddbd4c49355f6930355357", - VOut: 0, - }, - Receiver: domain.Receiver{ - Pubkey: "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5", - Amount: 1000, - }, - }, - { - VtxoKey: domain.VtxoKey{ - Txid: "9913159bc7aa493ca53cbb9cbc88f97ba01137c814009dc7ef520c3fafc67909", - VOut: 1, - }, - Receiver: domain.Receiver{ - Pubkey: "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5", - Amount: 500, - }, - }, - { - VtxoKey: domain.VtxoKey{ - Txid: "5e10e77a7cdedc153be5193a4b6055a7802706ded4f2a9efefe86ed2f9a6ae60", - VOut: 0, - }, - Receiver: domain.Receiver{ - Pubkey: "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5", - Amount: 1000, - }, - }, - { - VtxoKey: domain.VtxoKey{ - Txid: "5e10e77a7cdedc153be5193a4b6055a7802706ded4f2a9efefe86ed2f9a6ae60", - VOut: 1, - }, - Receiver: domain.Receiver{ - Pubkey: "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5", - Amount: 1000, - }, - }, - }, - Receivers: []domain.Receiver{ - { - Pubkey: "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5", - Amount: 1000, - }, - { - Pubkey: "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5", - Amount: 1000, - }, - { - Pubkey: "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5", - Amount: 1000, - }, - { - Pubkey: "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5", - Amount: 1000, - }, - { - Pubkey: "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5", - Amount: 500, - }, - }, - }, - }, - expectedNodesNum: 4, - expectedLeavesNum: 3, - }, - } - - _, key, err := common.DecodePubKey(testingKey) + fixtures, err := parsePoolTxFixtures() require.NoError(t, err) - require.NotNil(t, key) + require.NotEmpty(t, fixtures) - for _, f := range fixtures { - poolTx, congestionTree, err := builder.BuildPoolTx(key, &mockedWalletService{}, f.payments, 30) - require.NoError(t, err) - require.Equal(t, f.expectedNodesNum, congestionTree.NumberOfNodes()) - require.Len(t, congestionTree.Leaves(), f.expectedLeavesNum) + if len(fixtures.Valid) > 0 { + t.Run("valid", func(t *testing.T) { + for _, f := range fixtures.Valid { + poolTx, congestionTree, err := builder.BuildPoolTx(pubkey, f.Payments, minRelayFee) + require.NoError(t, err) + require.NotEmpty(t, poolTx) + require.NotEmpty(t, congestionTree) + require.Equal(t, f.ExpectedNumOfNodes, congestionTree.NumberOfNodes()) + require.Len(t, congestionTree.Leaves(), f.ExpectedNumOfLeaves) - // check that the pool tx has the right number of inputs and outputs - err = tree.ValidateCongestionTree( - congestionTree, - poolTx, - key, - 1209344, // 2 weeks - 8 minutes - ) - require.NoError(t, err) + err = tree.ValidateCongestionTree(congestionTree, poolTx, pubkey, roundLifetime) + require.NoError(t, err) + } + }) + } + + if len(fixtures.Invalid) > 0 { + t.Run("invalid", func(t *testing.T) { + for _, f := range fixtures.Invalid { + poolTx, congestionTree, err := builder.BuildPoolTx(pubkey, f.Payments, minRelayFee) + require.EqualError(t, err, f.ExpectedErr) + require.Empty(t, poolTx) + require.Empty(t, congestionTree) + } + }) } } func TestBuildForfeitTxs(t *testing.T) { - builder := txbuilder.NewTxBuilder(network.Liquid) + builder := txbuilder.NewTxBuilder(wallet, network.Liquid) - poolTx, err := psetv2.NewPsetFromBase64(fakePoolTx) + fixtures, err := parseForfeitTxsFixtures() require.NoError(t, err) + require.NotEmpty(t, fixtures) - utx, err := poolTx.UnsignedTx() - require.NoError(t, err) + if len(fixtures.Valid) > 0 { + t.Run("valid", func(t *testing.T) { + for _, f := range fixtures.Valid { + connectors, forfeitTxs, err := builder.BuildForfeitTxs( + pubkey, f.PoolTx, f.Payments, + ) + require.NoError(t, err) + require.Len(t, connectors, f.ExpectedNumOfConnectors) + require.Len(t, forfeitTxs, f.ExpectedNumOfForfeitTxs) - poolTxid := utx.TxHash().String() + expectedInputTxid := f.PoolTxid + // Verify the chain of connectors + for _, connector := range connectors { + tx, err := psetv2.NewPsetFromBase64(connector) + require.NoError(t, err) + require.NotNil(t, tx) - fixtures := []struct { - payments []domain.Payment - expectedNumOfForfeitTxs int - expectedNumOfConnectors int - }{ - { - payments: []domain.Payment{ - { - Id: "0", - Inputs: []domain.Vtxo{ - { - VtxoKey: domain.VtxoKey{ - Txid: "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6", - VOut: 0, - }, - Receiver: domain.Receiver{ - Pubkey: "020000000000000000000000000000000000000000000000000000000000000002", - Amount: 600, - }, - }, - { - VtxoKey: domain.VtxoKey{ - Txid: "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6", - VOut: 1, - }, - Receiver: domain.Receiver{ - Pubkey: "020000000000000000000000000000000000000000000000000000000000000002", - Amount: 500, - }, - }, - }, - Receivers: []domain.Receiver{ - { - Pubkey: "020000000000000000000000000000000000000000000000000000000000000002", - Amount: 600, - }, - { - Pubkey: "020000000000000000000000000000000000000000000000000000000000000002", - Amount: 500, - }, - }, - }, - }, - expectedNumOfForfeitTxs: 4, - expectedNumOfConnectors: 1, - }, + require.Len(t, tx.Inputs, 1) + require.Len(t, tx.Outputs, 2) + + inputTxid := chainhash.Hash(tx.Inputs[0].PreviousTxid).String() + require.Equal(t, expectedInputTxid, inputTxid) + require.Equal(t, 1, int(tx.Inputs[0].PreviousTxIndex)) + + expectedInputTxid = getTxid(tx) + } + + // decode and check forfeit txs + for _, forfeitTx := range forfeitTxs { + tx, err := psetv2.NewPsetFromBase64(forfeitTx) + require.NoError(t, err) + require.Len(t, tx.Inputs, 2) + require.Len(t, tx.Outputs, 2) + } + } + }) } - _, key, err := common.DecodePubKey(testingKey) - require.NoError(t, err) - require.NotNil(t, key) - - for _, f := range fixtures { - connectors, forfeitTxs, err := builder.BuildForfeitTxs( - key, fakePoolTx, f.payments, - ) - require.NoError(t, err) - require.Len(t, connectors, f.expectedNumOfConnectors) - require.Len(t, forfeitTxs, f.expectedNumOfForfeitTxs) - - // decode and check connectors - connectorsPsets := make([]*psetv2.Pset, 0, f.expectedNumOfConnectors) - for _, pset := range connectors { - p, err := psetv2.NewPsetFromBase64(pset) - require.NoError(t, err) - connectorsPsets = append(connectorsPsets, p) - } - - for i, pset := range connectorsPsets { - require.Len(t, pset.Inputs, 1) - require.Len(t, pset.Outputs, 2) - - expectedInputTxid := poolTxid - expectedInputVout := uint32(1) - if i > 0 { - tx, err := connectorsPsets[i-1].UnsignedTx() - require.NoError(t, err) - require.NotNil(t, tx) - expectedInputTxid = tx.TxHash().String() + if len(fixtures.Invalid) > 0 { + t.Run("invalid", func(t *testing.T) { + for _, f := range fixtures.Invalid { + connectors, forfeitTxs, err := builder.BuildForfeitTxs( + pubkey, f.PoolTx, f.Payments, + ) + require.EqualError(t, err, f.ExpectedErr) + require.Empty(t, connectors) + require.Empty(t, forfeitTxs) } - - inputTxid := chainhash.Hash(pset.Inputs[0].PreviousTxid).String() - require.Equal(t, expectedInputTxid, inputTxid) - require.Equal(t, expectedInputVout, pset.Inputs[0].PreviousTxIndex) - } - - // decode and check forfeit txs - forfeitTxsPsets := make([]*psetv2.Pset, 0, f.expectedNumOfForfeitTxs) - for _, pset := range forfeitTxs { - p, err := psetv2.NewPsetFromBase64(pset) - require.NoError(t, err) - forfeitTxsPsets = append(forfeitTxsPsets, p) - } - - // each forfeit tx should have 2 inputs and 2 outputs - for _, pset := range forfeitTxsPsets { - require.Len(t, pset.Inputs, 2) - require.Len(t, pset.Outputs, 2) - } + }) } } + +func randomInput() []ports.TxInput { + txid := randomHex(32) + input := &mockedInput{} + input.On("GetAsset").Return("5ac9f65c0efcc4775e0baec4ec03abdde22473cd3cf33c0419ca290e0751b225") + input.On("GetValue").Return(uint64(1000)) + input.On("GetScript").Return("a914ea9f486e82efb3dd83a69fd96e3f0113757da03c87") + input.On("GetTxid").Return(txid) + input.On("GetIndex").Return(uint32(0)) + + return []ports.TxInput{input} +} + +func randomHex(len int) string { + buf := make([]byte, len) + // nolint + rand.Read(buf) + return hex.EncodeToString(buf) +} + +type poolTxFixtures struct { + Valid []struct { + Payments []domain.Payment + ExpectedNumOfNodes int + ExpectedNumOfLeaves int + } + Invalid []struct { + Payments []domain.Payment + ExpectedErr string + } +} + +func parsePoolTxFixtures() (*poolTxFixtures, error) { + file, err := os.ReadFile("testdata/fixtures.json") + if err != nil { + return nil, err + } + v := map[string]interface{}{} + if err := json.Unmarshal(file, &v); err != nil { + return nil, err + } + + vv := v["buildPoolTx"].(map[string]interface{}) + file, _ = json.Marshal(vv) + var fixtures poolTxFixtures + if err := json.Unmarshal(file, &fixtures); err != nil { + return nil, err + } + + return &fixtures, nil +} + +type forfeitTxsFixtures struct { + Valid []struct { + Payments []domain.Payment + ExpectedNumOfConnectors int + ExpectedNumOfForfeitTxs int + PoolTx string + PoolTxid string + } + Invalid []struct { + Payments []domain.Payment + ExpectedErr string + PoolTx string + } +} + +func parseForfeitTxsFixtures() (*forfeitTxsFixtures, error) { + file, err := os.ReadFile("testdata/fixtures.json") + if err != nil { + return nil, err + } + v := map[string]interface{}{} + if err := json.Unmarshal(file, &v); err != nil { + return nil, err + } + + vv := v["buildForfeitTxs"].(map[string]interface{}) + file, _ = json.Marshal(vv) + var fixtures forfeitTxsFixtures + if err := json.Unmarshal(file, &fixtures); err != nil { + return nil, err + } + + return &fixtures, nil +} + +func getTxid(tx *psetv2.Pset) string { + utx, _ := tx.UnsignedTx() + return utx.TxHash().String() +} diff --git a/asp/internal/infrastructure/tx-builder/covenant/connectors.go b/asp/internal/infrastructure/tx-builder/covenant/connectors.go index 8008295..0d9d02f 100644 --- a/asp/internal/infrastructure/tx-builder/covenant/connectors.go +++ b/asp/internal/infrastructure/tx-builder/covenant/connectors.go @@ -2,102 +2,47 @@ package txbuilder import ( "github.com/vulpemventures/go-elements/psetv2" + "github.com/vulpemventures/go-elements/transaction" ) -func createConnectors( - poolTxID string, - connectorOutputIndex uint32, - connectorOutput psetv2.OutputArgs, - changeScript []byte, - numberOfConnectors uint64, -) (connectorsPsets []string, err error) { - previousInput := psetv2.InputArgs{ - Txid: poolTxID, - TxIndex: connectorOutputIndex, +func craftConnectorTx( + input psetv2.InputArgs, outputs []psetv2.OutputArgs, +) (*psetv2.Pset, error) { + ptx, _ := psetv2.New(nil, nil, nil) + updater, _ := psetv2.NewUpdater(ptx) + + if err := updater.AddInputs( + []psetv2.InputArgs{input}, + ); err != nil { + return nil, err } - if numberOfConnectors == 1 { - pset, err := psetv2.New(nil, nil, nil) - if err != nil { - return nil, err - } - updater, err := psetv2.NewUpdater(pset) - if err != nil { - return nil, err - } + // TODO: add prevout. - err = updater.AddInputs([]psetv2.InputArgs{previousInput}) - if err != nil { - return nil, err - } - - err = updater.AddOutputs([]psetv2.OutputArgs{connectorOutput}) - if err != nil { - return nil, err - } - - base64, err := pset.ToBase64() - if err != nil { - return nil, err - } - - return []string{base64}, nil + if err := updater.AddOutputs(outputs); err != nil { + return nil, err } - // compute the initial amount of the connectors output in pool transaction - remainingAmount := connectorAmount * numberOfConnectors - - connectorsPset := make([]string, 0, numberOfConnectors-1) - for i := uint64(0); i < numberOfConnectors-1; i++ { - // create a new pset - pset, err := psetv2.New(nil, nil, nil) - if err != nil { - return nil, err - } - - updater, err := psetv2.NewUpdater(pset) - if err != nil { - return nil, err - } - - err = updater.AddInputs([]psetv2.InputArgs{previousInput}) - if err != nil { - return nil, err - } - - err = updater.AddOutputs([]psetv2.OutputArgs{connectorOutput}) - if err != nil { - return nil, err - } - - changeAmount := remainingAmount - connectorOutput.Amount - if changeAmount > 0 { - changeOutput := psetv2.OutputArgs{ - Asset: connectorOutput.Asset, - Amount: changeAmount, - Script: changeScript, - } - err = updater.AddOutputs([]psetv2.OutputArgs{changeOutput}) - if err != nil { - return nil, err - } - tx, _ := pset.UnsignedTx() - txid := tx.TxHash().String() - - // make the change the next previousInput - previousInput = psetv2.InputArgs{ - Txid: txid, - TxIndex: 1, - } - } - - base64, err := pset.ToBase64() - if err != nil { - return nil, err - } - - connectorsPset = append(connectorsPset, base64) - } - - return connectorsPset, nil + return ptx, nil +} + +func getConnectorInputs(pset *psetv2.Pset) ([]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 } diff --git a/asp/internal/infrastructure/tx-builder/covenant/forfeit.go b/asp/internal/infrastructure/tx-builder/covenant/forfeit.go index 45160ab..7cbb76a 100644 --- a/asp/internal/infrastructure/tx-builder/covenant/forfeit.go +++ b/asp/internal/infrastructure/tx-builder/covenant/forfeit.go @@ -1,91 +1,104 @@ package txbuilder import ( - "encoding/hex" - "github.com/ark-network/ark/common/tree" + "github.com/ark-network/ark/internal/core/domain" "github.com/btcsuite/btcd/txscript" - "github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/vulpemventures/go-elements/elementsutil" - "github.com/vulpemventures/go-elements/network" "github.com/vulpemventures/go-elements/psetv2" "github.com/vulpemventures/go-elements/taproot" "github.com/vulpemventures/go-elements/transaction" ) -func createForfeitTx( - connectorInput psetv2.InputArgs, - connectorWitnessUtxo *transaction.TxOutput, - vtxoInput psetv2.InputArgs, - vtxoWitnessUtxo *transaction.TxOutput, +func craftForfeitTxs( + connectorTx *psetv2.Pset, + vtxo domain.Vtxo, vtxoTaprootTree *taproot.IndexedElementsTapScriptTree, - aspScript []byte, - net network.Network, -) (forfeitTx string, err error) { - pset, err := psetv2.New(nil, nil, nil) - if err != nil { - return "", err - } + vtxoScript, aspScript []byte, +) (forfeitTxs []string, err error) { + connectors, prevouts := getConnectorInputs(connectorTx) - updater, err := psetv2.NewUpdater(pset) - if err != nil { - return "", err - } + for i, connectorInput := range connectors { + connectorPrevout := prevouts[i] + asset := elementsutil.AssetHashFromBytes(connectorPrevout.Asset) - if err = updater.AddInputs([]psetv2.InputArgs{connectorInput, vtxoInput}); err != nil { - return "", err - } - - if err = updater.AddInWitnessUtxo(0, connectorWitnessUtxo); err != nil { - return "", err - } - - if err := updater.AddInSighashType(0, txscript.SigHashAll); err != nil { - return "", err - } - - if err = updater.AddInWitnessUtxo(1, vtxoWitnessUtxo); err != nil { - return "", err - } - - if err := updater.AddInSighashType(1, txscript.SigHashDefault); err != nil { - return "", err - } - - unspendableKeyBytes, _ := hex.DecodeString(tree.UnspendablePoint) - unspendableKey, _ := secp256k1.ParsePubKey(unspendableKeyBytes) - - for _, proof := range vtxoTaprootTree.LeafMerkleProofs { - tapScript := psetv2.NewTapLeafScript(proof, unspendableKey) - if err := updater.AddInTapLeafScript(1, tapScript); err != nil { - return "", err + pset, err := psetv2.New(nil, nil, nil) + if err != nil { + return nil, err } - } - vtxoAmount, err := elementsutil.ValueFromBytes(vtxoWitnessUtxo.Value) - if err != nil { - return "", err - } + updater, err := psetv2.NewUpdater(pset) + if err != nil { + return nil, err + } - connectorAmount, err := elementsutil.ValueFromBytes(connectorWitnessUtxo.Value) - if err != nil { - return "", err - } + vtxoInput := psetv2.InputArgs{ + Txid: vtxo.Txid, + TxIndex: vtxo.VOut, + } - err = updater.AddOutputs([]psetv2.OutputArgs{ - { - Asset: net.AssetID, - Amount: vtxoAmount + connectorAmount - 30, - Script: aspScript, - }, - { - Asset: net.AssetID, - Amount: 30, - }, - }) - if err != nil { - return "", err - } + vtxoAmount, _ := elementsutil.ValueToBytes(vtxo.Amount) + vtxoPrevout := &transaction.TxOutput{ + Asset: connectorPrevout.Asset, + Value: vtxoAmount, + Script: vtxoScript, + } - return pset.ToBase64() + if err := updater.AddInputs([]psetv2.InputArgs{connectorInput, vtxoInput}); err != nil { + return nil, err + } + + if err = updater.AddInWitnessUtxo(0, connectorPrevout); err != nil { + return nil, err + } + + if err := updater.AddInSighashType(0, txscript.SigHashAll); err != nil { + return nil, err + } + + if err = updater.AddInWitnessUtxo(1, vtxoPrevout); err != nil { + return nil, err + } + + if err := updater.AddInSighashType(1, txscript.SigHashDefault); err != nil { + return nil, err + } + + unspendableKey := tree.UnspendableKey() + + for _, proof := range vtxoTaprootTree.LeafMerkleProofs { + tapScript := psetv2.NewTapLeafScript(proof, unspendableKey) + if err := updater.AddInTapLeafScript(1, tapScript); err != nil { + return nil, err + } + } + + connectorAmount, err := elementsutil.ValueFromBytes(connectorPrevout.Value) + if err != nil { + return nil, err + } + + err = updater.AddOutputs([]psetv2.OutputArgs{ + { + Asset: asset, + Amount: vtxo.Amount + connectorAmount - 30, + Script: aspScript, + }, + { + Asset: asset, + Amount: 30, + }, + }) + if err != nil { + return nil, err + } + + tx, err := pset.ToBase64() + if err != nil { + return nil, err + } + + forfeitTxs = append(forfeitTxs, tx) + } + return forfeitTxs, nil } diff --git a/asp/internal/infrastructure/tx-builder/covenant/mocks_test.go b/asp/internal/infrastructure/tx-builder/covenant/mocks_test.go new file mode 100644 index 0000000..8fbe5a7 --- /dev/null +++ b/asp/internal/infrastructure/tx-builder/covenant/mocks_test.go @@ -0,0 +1,152 @@ +package txbuilder_test + +import ( + "context" + + "github.com/ark-network/ark/internal/core/ports" + "github.com/decred/dcrd/dcrec/secp256k1/v4" + "github.com/stretchr/testify/mock" +) + +type mockedWallet struct { + mock.Mock +} + +// BroadcastTransaction implements ports.WalletService. +func (m *mockedWallet) BroadcastTransaction(ctx context.Context, txHex string) (string, error) { + args := m.Called(ctx, txHex) + + var res string + if a := args.Get(0); a != nil { + res = a.(string) + } + return res, args.Error(1) +} + +// Close implements ports.WalletService. +func (m *mockedWallet) Close() { + m.Called() +} + +// DeriveAddresses implements ports.WalletService. +func (m *mockedWallet) DeriveAddresses(ctx context.Context, num int) ([]string, error) { + args := m.Called(ctx, num) + + var res []string + if a := args.Get(0); a != nil { + res = a.([]string) + } + return res, args.Error(1) +} + +// GetPubkey implements ports.WalletService. +func (m *mockedWallet) GetPubkey(ctx context.Context) (*secp256k1.PublicKey, error) { + args := m.Called(ctx) + + var res *secp256k1.PublicKey + if a := args.Get(0); a != nil { + res = a.(*secp256k1.PublicKey) + } + return res, args.Error(1) +} + +// SignPset implements ports.WalletService. +func (m *mockedWallet) SignPset(ctx context.Context, pset string, extractRawTx bool) (string, error) { + args := m.Called(ctx, pset, extractRawTx) + + var res string + if a := args.Get(0); a != nil { + res = a.(string) + } + return res, args.Error(1) +} + +// Status implements ports.WalletService. +func (m *mockedWallet) Status(ctx context.Context) (ports.WalletStatus, error) { + args := m.Called(ctx) + + var res ports.WalletStatus + if a := args.Get(0); a != nil { + res = a.(ports.WalletStatus) + } + return res, args.Error(1) +} + +func (m *mockedWallet) SelectUtxos(ctx context.Context, asset string, amount uint64) ([]ports.TxInput, uint64, error) { + args := m.Called(ctx, asset, amount) + + var res0 func() []ports.TxInput + if a := args.Get(0); a != nil { + res0 = a.(func() []ports.TxInput) + } + var res1 uint64 + if a := args.Get(1); a != nil { + res1 = a.(uint64) + } + return res0(), res1, args.Error(2) +} + +func (m *mockedWallet) EstimateFees(ctx context.Context, pset string) (uint64, error) { + args := m.Called(ctx, pset) + + var res uint64 + if a := args.Get(0); a != nil { + res = a.(uint64) + } + return res, args.Error(1) +} + +type mockedInput struct { + mock.Mock +} + +func (m *mockedInput) GetTxid() string { + args := m.Called() + + var res string + if a := args.Get(0); a != nil { + res = a.(string) + } + + return res +} + +func (m *mockedInput) GetIndex() uint32 { + args := m.Called() + + var res uint32 + if a := args.Get(0); a != nil { + res = a.(uint32) + } + return res +} + +func (m *mockedInput) GetScript() string { + args := m.Called() + + var res string + if a := args.Get(0); a != nil { + res = a.(string) + } + return res +} + +func (m *mockedInput) GetAsset() string { + args := m.Called() + + var res string + if a := args.Get(0); a != nil { + res = a.(string) + } + return res +} + +func (m *mockedInput) GetValue() uint64 { + args := m.Called() + + var res uint64 + if a := args.Get(0); a != nil { + res = a.(uint64) + } + return res +} diff --git a/asp/internal/infrastructure/tx-builder/covenant/testdata/fixtures.json b/asp/internal/infrastructure/tx-builder/covenant/testdata/fixtures.json new file mode 100644 index 0000000..4dbdef0 --- /dev/null +++ b/asp/internal/infrastructure/tx-builder/covenant/testdata/fixtures.json @@ -0,0 +1,229 @@ +{ + "buildPoolTx": { + "valid": [ + { + "payments": [ + { + "id": "0", + "inputs": [ + { + "txid": "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6", + "vout": 0, + "pubkey": "020000000000000000000000000000000000000000000000000000000000000002", + "amount": 1100 + } + ], + "receivers": [ + { + "pubkey": "020000000000000000000000000000000000000000000000000000000000000002", + "amount": 1100 + } + ] + } + ], + "expectedNumOfNodes": 1, + "expectedNumOfLeaves": 1 + }, + { + "payments": [ + { + "id": "0", + "inputs": [ + { + "txid": "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6", + "vout": 0, + "pubkey": "020000000000000000000000000000000000000000000000000000000000000002", + "amount": 1100 + } + ], + "receivers": [ + { + "pubkey": "020000000000000000000000000000000000000000000000000000000000000002", + "amount": 600 + }, + { + "pubkey": "020000000000000000000000000000000000000000000000000000000000000002", + "amount": 500 + } + ] + } + ], + "expectedNumOfNodes": 3, + "expectedNumOfLeaves": 2 + }, + { + "payments": [ + { + "id": "0", + "inputs": [ + { + "txid": "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6", + "vout": 0, + "pubkey": "020000000000000000000000000000000000000000000000000000000000000002", + "amount": 1100 + } + ], + "receivers": [ + { + "pubkey": "020000000000000000000000000000000000000000000000000000000000000002", + "amount": 600 + }, + { + "pubkey": "020000000000000000000000000000000000000000000000000000000000000002", + "amount": 500 + } + ] + }, + { + "id": "0", + "inputs": [ + { + "txid": "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6", + "vout": 0, + "pubkey": "020000000000000000000000000000000000000000000000000000000000000002", + "amount": 1100 + } + ], + "receivers": [ + { + "pubkey": "020000000000000000000000000000000000000000000000000000000000000002", + "amount": 600 + }, + { + "pubkey": "020000000000000000000000000000000000000000000000000000000000000002", + "amount": 500 + } + ] + }, + { + "id": "0", + "inputs": [ + { + "txid": "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6", + "vout": 0, + "pubkey": "020000000000000000000000000000000000000000000000000000000000000002", + "amount": 1100 + } + ], + "receivers": [ + { + "pubkey": "020000000000000000000000000000000000000000000000000000000000000002", + "amount": 600 + }, + { + "pubkey": "020000000000000000000000000000000000000000000000000000000000000002", + "amount": 500 + } + ] + } + ], + "expectedNumOfNodes": 11, + "expectedNumOfLeaves": 6 + }, + { + "payments": [ + { + "id": "a242cdd8-f3d5-46c0-ae98-94135a2bee3f", + "inputs": [ + { + "txid": "755c820771284d85ea4bbcc246565b4eddadc44237a7e57a0f9cb78a840d1d41", + "vout": 0, + "pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5", + "amount": 1000 + }, + { + "txid": "66a0df86fcdeb84b8877adfe0b2c556dba30305d72ddbd4c49355f6930355357", + "vout": 0, + "pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5", + "amount": 1000 + }, + { + "txid": "9913159bc7aa493ca53cbb9cbc88f97ba01137c814009dc7ef520c3fafc67909", + "vout": 1, + "pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5", + "amount": 500 + }, + { + "txid": "5e10e77a7cdedc153be5193a4b6055a7802706ded4f2a9efefe86ed2f9a6ae60", + "vout": 0, + "pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5", + "amount": 1000 + }, + { + "txid": "5e10e77a7cdedc153be5193a4b6055a7802706ded4f2a9efefe86ed2f9a6ae60", + "vout": 1, + "pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5", + "amount": 1000 + } + ], + "receivers": [ + { + "pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5", + "amount": 1000 + }, + { + "pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5", + "amount": 1000 + }, + { + "pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5", + "amount": 1000 + }, + { + "pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5", + "amount": 1000 + }, + { + "pubkey": "02c87e5c1758df5ad42a918ec507b6e8dfcdcebf22f64f58eb4ad5804257d658a5", + "amount": 500 + } + ] + } + ], + "expectedNumOfNodes": 9, + "expectedNumOfLeaves": 5 + } + ], + "invalid": [] + }, + "buildForfeitTxs": { + "valid": [ + { + "payments": [ + { + "id": "0", + "inputs": [ + { + "txid": "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6", + "vout": 0, + "pubkey": "020000000000000000000000000000000000000000000000000000000000000002", + "amount": 600 + }, + { + "txid": "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6", + "vout": 1, + "pubkey": "020000000000000000000000000000000000000000000000000000000000000002", + "amount": 500 + } + ], + "receivers": [ + { + "pubkey": "020000000000000000000000000000000000000000000000000000000000000002", + "amount": 600 + }, + { + "pubkey": "020000000000000000000000000000000000000000000000000000000000000002", + "amount": 500 + } + ] + } + ], + "poolTx": "cHNldP8BAgQCAAAAAQQBAQEFAQMBBgEDAfsEAgAAAAABDiDk7dXxh4KQzgLO8i1ABtaLCe4aPL12GVhN1E9zM1ePLwEPBAAAAAABEAT/////AAEDCOgDAAAAAAAAAQQWABSNnpy01UJqd99eTg2M1IpdKId11gf8BHBzZXQCICWyUQcOKcoZBDzzPM1zJOLdqwPsxK4LXnfE/A5c9slaB/wEcHNldAgEAAAAAAABAwh4BQAAAAAAAAEEFgAUjZ6ctNVCanffXk4NjNSKXSiHddYH/ARwc2V0AiAlslEHDinKGQQ88zzNcyTi3asD7MSuC153xPwOXPbJWgf8BHBzZXQIBAAAAAAAAQMI9AEAAAAAAAABBAAH/ARwc2V0AiAlslEHDinKGQQ88zzNcyTi3asD7MSuC153xPwOXPbJWgf8BHBzZXQIBAAAAAAA", + "poolTxid": "7981fce656f266472cc742444527cb32a8bed8c90fed6d47adbfc4c8780d4d9a", + "expectedNumOfForfeitTxs": 4, + "expectedNumOfConnectors": 1 + } + ], + "invalid": [] + } +} \ No newline at end of file diff --git a/asp/internal/infrastructure/tx-builder/covenant/tree.go b/asp/internal/infrastructure/tx-builder/covenant/tree.go index 3aa1949..d049803 100644 --- a/asp/internal/infrastructure/tx-builder/covenant/tree.go +++ b/asp/internal/infrastructure/tx-builder/covenant/tree.go @@ -6,11 +6,8 @@ import ( "github.com/ark-network/ark/common/tree" "github.com/ark-network/ark/internal/core/domain" - "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" - "github.com/vulpemventures/go-elements/network" "github.com/vulpemventures/go-elements/psetv2" "github.com/vulpemventures/go-elements/taproot" ) @@ -19,255 +16,46 @@ const ( expirationTime = 60 * 60 * 24 * 14 // 14 days in seconds ) -// the private method buildCongestionTree returns a function letting to plug in the pool transaction output as input of the tree's root node -type pluggableCongestionTree func(outpoint psetv2.InputArgs) (tree.CongestionTree, error) +type treeFactory func(outpoint psetv2.InputArgs) (tree.CongestionTree, error) -func taprootOutputScript(taprootKey *secp256k1.PublicKey) ([]byte, error) { - return txscript.NewScriptBuilder().AddOp(txscript.OP_1).AddData(schnorr.SerializePubKey(taprootKey)).Script() -} - -// wrapper of updater methods adding a taproot input to the pset with all the necessary data to spend it via any taproot script -func addTaprootInput( - updater *psetv2.Updater, - input psetv2.InputArgs, - internalTaprootKey *secp256k1.PublicKey, - taprootTree *taproot.IndexedElementsTapScriptTree, -) error { - if err := updater.AddInputs([]psetv2.InputArgs{input}); err != nil { - return err - } - - if err := updater.AddInTapInternalKey(0, schnorr.SerializePubKey(internalTaprootKey)); err != nil { - return err - } - - for _, proof := range taprootTree.LeafMerkleProofs { - controlBlock := proof.ToControlBlock(internalTaprootKey) - - if err := updater.AddInTapLeafScript(0, psetv2.TapLeafScript{ - TapElementsLeaf: taproot.NewBaseTapElementsLeaf(proof.Script), - ControlBlock: controlBlock, - }); err != nil { - return err - } - } - - return nil -} - -// buildCongestionTree builder iteratively creates a binary tree of Pset from a set of receivers -// it returns a factory function creating a CongestionTree and the associated output script to be used in the pool transaction -func buildCongestionTree( - net *network.Network, - aspPublicKey *secp256k1.PublicKey, - receivers []domain.Receiver, - feeSatsPerNode uint64, -) (pluggableTree pluggableCongestionTree, sharedOutputScript []byte, sharedOutputAmount uint64, err error) { - unspendableKeyBytes, err := hex.DecodeString(tree.UnspendablePoint) - if err != nil { - return nil, nil, 0, err - } - - unspendableKey, err := secp256k1.ParsePubKey(unspendableKeyBytes) - if err != nil { - return nil, nil, 0, err - } - - var nodes []*node - - for _, r := range receivers { - nodes = append(nodes, newLeaf(net, unspendableKey, aspPublicKey, r, feeSatsPerNode)) - } - - for len(nodes) > 1 { - nodes, err = createTreeLevel(nodes) - if err != nil { - return nil, nil, 0, err - } - } - - psets, err := nodes[0].psets(nil, 0) - if err != nil { - return nil, nil, 0, err - } - - // find the root - var rootPset *psetv2.Pset - for _, psetWithLevel := range psets { - if psetWithLevel.level == 0 { - rootPset = psetWithLevel.pset - break - } - } - - // compute the shared output script - sweepLeaf, err := tree.VtxoScript(aspPublicKey) - if err != nil { - return nil, nil, 0, err - } - leftOutput := rootPset.Outputs[0] - leftWitnessProgram := leftOutput.Script[2:] - leftKey, err := schnorr.ParsePubKey(leftWitnessProgram) - if err != nil { - return nil, nil, 0, err - } - - var rightAmount uint64 - var rightKey *secp256k1.PublicKey - - if len(rootPset.Outputs) > 2 { - rightAmount = rootPset.Outputs[1].Value - rightKey, err = schnorr.ParsePubKey(rootPset.Outputs[1].Script[2:]) - if err != nil { - return nil, nil, 0, err - } - } - - goToTreeScript := tree.BranchScript( - leftKey, rightKey, leftOutput.Value, rightAmount, - ) - - taprootTree := taproot.AssembleTaprootScriptTree(goToTreeScript, *sweepLeaf) - root := taprootTree.RootNode.TapHash() - taprootKey := taproot.ComputeTaprootOutputKey(unspendableKey, root[:]) - outputScript, err := taprootOutputScript(taprootKey) - if err != nil { - return nil, nil, 0, err - } - - return func(outpoint psetv2.InputArgs) (tree.CongestionTree, error) { - psets, err := nodes[0].psets(&psetArgs{ - input: outpoint, - taprootTree: taprootTree, - }, 0) - if err != nil { - return nil, err - } - - maxLevel := 0 - for _, p := range psets { - if p.level > maxLevel { - maxLevel = p.level - } - } - - congestionTree := make(tree.CongestionTree, maxLevel+1) - - for _, psetWithLevel := range psets { - utx, err := psetWithLevel.pset.UnsignedTx() - if err != nil { - return nil, err - } - - txid := utx.TxHash().String() - - psetB64, err := psetWithLevel.pset.ToBase64() - if err != nil { - return nil, err - } - - parentTxid := chainhash.Hash(psetWithLevel.pset.Inputs[0].PreviousTxid).String() - - congestionTree[psetWithLevel.level] = append(congestionTree[psetWithLevel.level], tree.Node{ - Txid: txid, - Tx: psetB64, - ParentTxid: parentTxid, - Leaf: psetWithLevel.leaf, - }) - } - - return congestionTree, nil - }, outputScript, uint64(rightAmount) + leftOutput.Value + uint64(feeSatsPerNode), nil -} - -func createTreeLevel(nodes []*node) ([]*node, error) { - if len(nodes)%2 != 0 { - last := nodes[len(nodes)-1] - pairs, err := createTreeLevel(nodes[:len(nodes)-1]) - if err != nil { - return nil, err - } - - return append(pairs, last), nil - } - - pairs := make([]*node, 0, len(nodes)/2) - for i := 0; i < len(nodes); i += 2 { - pairs = append(pairs, newBranch(nodes[i], nodes[i+1])) - } - return pairs, nil -} - -// internal struct to build a binary tree of Pset type node struct { - internalTaprootKey *secp256k1.PublicKey - sweepKey *secp256k1.PublicKey - receivers []domain.Receiver - left *node - right *node - network *network.Network - feeSats uint64 + sweepKey *secp256k1.PublicKey + receivers []domain.Receiver + left *node + right *node + asset string + feeSats uint64 - // cached values - _taprootKey *secp256k1.PublicKey - _taprootTree *taproot.IndexedElementsTapScriptTree -} - -// create a node from a single receiver -func newLeaf( - network *network.Network, - internalKey *secp256k1.PublicKey, - sweepKey *secp256k1.PublicKey, - receiver domain.Receiver, - feeSats uint64, -) *node { - return &node{ - sweepKey: sweepKey, - internalTaprootKey: internalKey, - receivers: []domain.Receiver{receiver}, - network: network, - feeSats: feeSats, - } -} - -// aggregate two nodes into a branch node -func newBranch( - left *node, - right *node, -) *node { - return &node{ - internalTaprootKey: left.internalTaprootKey, - sweepKey: left.sweepKey, - receivers: append(left.receivers, right.receivers...), - left: left, - right: right, - network: left.network, - feeSats: left.feeSats, - } + _inputTaprootKey *secp256k1.PublicKey + _inputTaprootTree *taproot.IndexedElementsTapScriptTree } func (n *node) isLeaf() bool { - return (n.left == nil || n.left.isEmpty()) && (n.right == nil || n.right.isEmpty()) + return len(n.receivers) == 1 } -// is it the final node of the tree -func (n *node) isEmpty() bool { - return n.left == nil && n.right == nil +func (n *node) getAmount() uint64 { + var amount uint64 + for _, r := range n.receivers { + amount += r.Amount + } + + if n.isLeaf() { + return amount + } + + return amount + n.feeSats*uint64(n.countChildren()) } func (n *node) countChildren() int { - if n.isEmpty() { - return 0 - } - result := 0 - if n.left != nil && !n.left.isEmpty() { + if n.left != nil { result++ result += n.left.countChildren() } - if n.right != nil && !n.right.isEmpty() { + if n.right != nil { result++ result += n.right.countChildren() } @@ -275,123 +63,211 @@ func (n *node) countChildren() int { return result } -// compute the output amount of a node -func (n *node) amount() uint64 { - var amount uint64 - for _, r := range n.receivers { - amount += r.Amount - } - if n.isEmpty() { - return amount +func (n *node) getChildren() []*node { + if n.isLeaf() { + return nil } - nb := uint64(n.countChildren()) + children := make([]*node, 0, 2) - return amount + (nb+1)*n.feeSats + if n.left != nil { + children = append(children, n.left) + } + if n.right != nil { + children = append(children, n.right) + } + + return children } -func (n *node) taprootKey() (*secp256k1.PublicKey, *taproot.IndexedElementsTapScriptTree, error) { - if n._taprootKey != nil && n._taprootTree != nil { - return n._taprootKey, n._taprootTree, nil +func (n *node) getOutputs() ([]psetv2.OutputArgs, error) { + if n.isLeaf() { + taprootKey, _, err := n.getVtxoWitnessData() + if err != nil { + return nil, err + } + + script, err := taprootOutputScript(taprootKey) + if err != nil { + return nil, err + } + + output := &psetv2.OutputArgs{ + Asset: n.asset, + Amount: uint64(n.getAmount()), + Script: script, + } + + return []psetv2.OutputArgs{*output}, nil } - sweepTaprootLeaf, err := tree.SweepScript(n.sweepKey, expirationTime) + outputs := make([]psetv2.OutputArgs, 0, 2) + children := n.getChildren() + + for _, child := range children { + childWitnessProgram, _, err := child.getWitnessData() + if err != nil { + return nil, err + } + + script, err := taprootOutputScript(childWitnessProgram) + if err != nil { + return nil, err + } + + outputs = append(outputs, psetv2.OutputArgs{ + Asset: n.asset, + Amount: child.getAmount() + child.feeSats, + Script: script, + }) + } + + return outputs, nil +} + +func (n *node) getWitnessData() ( + *secp256k1.PublicKey, *taproot.IndexedElementsTapScriptTree, error, +) { + if n._inputTaprootKey != nil && n._inputTaprootTree != nil { + return n._inputTaprootKey, n._inputTaprootTree, nil + } + + sweepClosure, err := tree.SweepScript(n.sweepKey, expirationTime) if err != nil { return nil, nil, err } - if n.isEmpty() { - key, err := hex.DecodeString(n.receivers[0].Pubkey) + if n.isLeaf() { + taprootKey, _, err := n.getVtxoWitnessData() if err != nil { return nil, nil, err } - pubkey, err := secp256k1.ParsePubKey(key) - if err != nil { - return nil, nil, err - } + branchTaprootScript := tree.BranchScript( + taprootKey, nil, n.getAmount(), 0, + ) - vtxoLeaf, err := tree.VtxoScript(pubkey) - if err != nil { - return nil, nil, err - } + branchTaprootTree := taproot.AssembleTaprootScriptTree( + branchTaprootScript, *sweepClosure, + ) + root := branchTaprootTree.RootNode.TapHash() - leafTaprootTree := taproot.AssembleTaprootScriptTree(*vtxoLeaf, *sweepTaprootLeaf) - root := leafTaprootTree.RootNode.TapHash() - - taprootKey := taproot.ComputeTaprootOutputKey( - n.internalTaprootKey, + inputTapkey := taproot.ComputeTaprootOutputKey( + tree.UnspendableKey(), root[:], ) - n._taprootKey = taprootKey - n._taprootTree = leafTaprootTree + n._inputTaprootKey = inputTapkey + n._inputTaprootTree = branchTaprootTree - return taprootKey, leafTaprootTree, nil + return inputTapkey, branchTaprootTree, nil } - leftKey, _, err := n.left.taprootKey() + leftKey, _, err := n.left.getWitnessData() if err != nil { return nil, nil, err } - rightKey, _, err := n.right.taprootKey() + rightKey, _, err := n.right.getWitnessData() if err != nil { return nil, nil, err } + leftAmount := n.left.getAmount() + n.feeSats + rightAmount := n.right.getAmount() + n.feeSats branchTaprootLeaf := tree.BranchScript( - leftKey, rightKey, n.left.amount(), n.right.amount(), + leftKey, rightKey, leftAmount, rightAmount, ) - branchTaprootTree := taproot.AssembleTaprootScriptTree(branchTaprootLeaf, *sweepTaprootLeaf) + branchTaprootTree := taproot.AssembleTaprootScriptTree( + branchTaprootLeaf, *sweepClosure, + ) root := branchTaprootTree.RootNode.TapHash() taprootKey := taproot.ComputeTaprootOutputKey( - n.internalTaprootKey, + tree.UnspendableKey(), root[:], ) - n._taprootKey = taprootKey - n._taprootTree = branchTaprootTree + n._inputTaprootKey = taprootKey + n._inputTaprootTree = branchTaprootTree return taprootKey, branchTaprootTree, nil } -// compute the output script of a node -func (n *node) script() ([]byte, error) { - taprootKey, _, err := n.taprootKey() - if err != nil { - return nil, err +func (n *node) getVtxoWitnessData() ( + *secp256k1.PublicKey, *taproot.IndexedElementsTapScriptTree, error, +) { + if !n.isLeaf() { + return nil, nil, fmt.Errorf("cannot call vtxoWitness on a non-leaf node") } - return taprootOutputScript(taprootKey) + sweepClosure, err := tree.SweepScript(n.sweepKey, expirationTime) + if err != nil { + return nil, nil, err + } + + key, err := hex.DecodeString(n.receivers[0].Pubkey) + if err != nil { + return nil, nil, err + } + + pubkey, err := secp256k1.ParsePubKey(key) + if err != nil { + return nil, nil, err + } + + vtxoLeaf, err := tree.VtxoScript(pubkey) + if err != nil { + return nil, nil, err + } + + // TODO: add forfeit path + leafTaprootTree := taproot.AssembleTaprootScriptTree( + *vtxoLeaf, *sweepClosure, + ) + root := leafTaprootTree.RootNode.TapHash() + + taprootKey := taproot.ComputeTaprootOutputKey( + tree.UnspendableKey(), + root[:], + ) + + return taprootKey, leafTaprootTree, nil } -// use script & amount() to create OutputArgs -func (n *node) output() (*psetv2.OutputArgs, error) { - script, err := n.script() +func (n *node) getTreeNode( + input psetv2.InputArgs, tapTree *taproot.IndexedElementsTapScriptTree, +) (tree.Node, error) { + pset, err := n.getTx(input, tapTree) if err != nil { - return nil, err + return tree.Node{}, err } - return &psetv2.OutputArgs{ - Asset: n.network.AssetID, - Amount: uint64(n.amount()), - Script: script, + txid, err := getPsetId(pset) + if err != nil { + return tree.Node{}, err + } + + tx, err := pset.ToBase64() + if err != nil { + return tree.Node{}, err + } + + parentTxid := chainhash.Hash(pset.Inputs[0].PreviousTxid).String() + + return tree.Node{ + Txid: txid, + Tx: tx, + ParentTxid: parentTxid, + Leaf: n.isLeaf(), }, nil } -type psetArgs struct { - input psetv2.InputArgs - taprootTree *taproot.IndexedElementsTapScriptTree -} - -// create the node Pset from the previous node Pset represented by input arg -// if node is a branch, it adds two outputs to the Pset, one for the left branch and one for the right branch -// if node is a leaf, it only adds one output to the Pset (the node output) -func (n *node) pset(args *psetArgs) (*psetv2.Pset, error) { +func (n *node) getTx( + input psetv2.InputArgs, inputTapTree *taproot.IndexedElementsTapScriptTree, +) (*psetv2.Pset, error) { pset, err := psetv2.New(nil, nil, nil) if err != nil { return nil, err @@ -402,125 +278,171 @@ func (n *node) pset(args *psetArgs) (*psetv2.Pset, error) { return nil, err } - if args != nil { - if err := addTaprootInput(updater, args.input, n.internalTaprootKey, args.taprootTree); err != nil { - return nil, err - } + if err := addTaprootInput( + updater, input, tree.UnspendableKey(), inputTapTree, + ); err != nil { + return nil, err } feeOutput := psetv2.OutputArgs{ Amount: uint64(n.feeSats), - Asset: n.network.AssetID, + Asset: n.asset, } - if n.isEmpty() { - output, err := n.output() - if err != nil { - return nil, err - } - - err = updater.AddOutputs([]psetv2.OutputArgs{*output, feeOutput}) - if err != nil { - return nil, err - } - return pset, nil - } - - outputLeft, err := n.left.output() + outputs, err := n.getOutputs() if err != nil { return nil, err } - outputRight, err := n.right.output() - if err != nil { - return nil, err - } - - err = updater.AddOutputs([]psetv2.OutputArgs{*outputLeft, *outputRight, feeOutput}) - if err != nil { + if err := updater.AddOutputs(append(outputs, feeOutput)); err != nil { return nil, err } return pset, nil } -type psetWithLevel struct { - pset *psetv2.Pset - level int - leaf bool +func (n *node) createFinalCongestionTree() treeFactory { + return func(poolTxInput psetv2.InputArgs) (tree.CongestionTree, error) { + congestionTree := make(tree.CongestionTree, 0) + + _, taprootTree, err := n.getWitnessData() + if err != nil { + return nil, err + } + + ins := []psetv2.InputArgs{poolTxInput} + inTrees := []*taproot.IndexedElementsTapScriptTree{taprootTree} + nodes := []*node{n} + + for len(nodes) > 0 { + nextNodes := make([]*node, 0) + nextInputsArgs := make([]psetv2.InputArgs, 0) + nextTaprootTrees := make([]*taproot.IndexedElementsTapScriptTree, 0) + + treeLevel := make([]tree.Node, 0) + + for i, node := range nodes { + treeNode, err := node.getTreeNode(ins[i], inTrees[i]) + if err != nil { + return nil, err + } + + treeLevel = append(treeLevel, treeNode) + + children := node.getChildren() + + for i, child := range children { + _, taprootTree, err := child.getWitnessData() + if err != nil { + return nil, err + } + + nextNodes = append(nextNodes, child) + nextInputsArgs = append(nextInputsArgs, psetv2.InputArgs{ + Txid: treeNode.Txid, + TxIndex: uint32(i), + }) + nextTaprootTrees = append(nextTaprootTrees, taprootTree) + } + } + + congestionTree = append(congestionTree, treeLevel) + nodes = append([]*node{}, nextNodes...) + ins = append([]psetv2.InputArgs{}, nextInputsArgs...) + inTrees = append( + []*taproot.IndexedElementsTapScriptTree{}, nextTaprootTrees..., + ) + } + + return congestionTree, nil + } } -// create the node pset and all the psets of its children recursively, updating the input arg at each step -// the function stops when it reaches a leaf node -func (n *node) psets(inputArgs *psetArgs, level int) ([]psetWithLevel, error) { - if inputArgs == nil && level != 0 { - return nil, fmt.Errorf("only the first level must be pluggable") - } - - pset, err := n.pset(inputArgs) +func craftCongestionTree( + asset string, aspPublicKey *secp256k1.PublicKey, + payments []domain.Payment, feeSatsPerNode uint64, +) ( + buildCongestionTree treeFactory, + sharedOutputScript []byte, sharedOutputAmount uint64, err error, +) { + receivers := getOffchainReceivers(payments) + root, err := createPartialCongestionTree( + receivers, aspPublicKey, asset, feeSatsPerNode, + ) if err != nil { - return nil, err + return } - nodeResult := []psetWithLevel{ - {pset, level, n.isLeaf() || (n.left.isEmpty() || n.right.isEmpty())}, - } - - if n.isLeaf() { - return nodeResult, nil - } - - if n.isEmpty() { - return nodeResult, nil - } - - unsignedTx, err := pset.UnsignedTx() + taprootKey, _, err := root.getWitnessData() if err != nil { - return nil, err + return } - txID := unsignedTx.TxHash().String() - - if !n.left.isEmpty() { - _, leftTaprootTree, err := n.left.taprootKey() - if err != nil { - return nil, err - } - - psetsLeft, err := n.left.psets(&psetArgs{ - input: psetv2.InputArgs{ - Txid: txID, - TxIndex: 0, - }, - taprootTree: leftTaprootTree, - }, level+1) - if err != nil { - return nil, err - } - - nodeResult = append(nodeResult, psetsLeft...) + sharedOutputScript, err = taprootOutputScript(taprootKey) + if err != nil { + return } + sharedOutputAmount = root.getAmount() + root.feeSats + buildCongestionTree = root.createFinalCongestionTree() - if !n.right.isEmpty() { - - _, rightTaprootTree, err := n.right.taprootKey() - if err != nil { - return nil, err - } - - psetsRight, err := n.right.psets(&psetArgs{ - input: psetv2.InputArgs{ - Txid: txID, - TxIndex: 1, - }, - taprootTree: rightTaprootTree, - }, level+1) - if err != nil { - return nil, err - } - - nodeResult = append(nodeResult, psetsRight...) - } - - return nodeResult, nil + return +} + +func createPartialCongestionTree( + receivers []domain.Receiver, + aspPublicKey *secp256k1.PublicKey, + asset string, + feeSatsPerNode uint64, +) (root *node, err error) { + if len(receivers) == 0 { + return nil, fmt.Errorf("no receivers provided") + } + + nodes := make([]*node, 0, len(receivers)) + for _, r := range receivers { + leafNode := &node{ + sweepKey: aspPublicKey, + receivers: []domain.Receiver{r}, + asset: asset, + feeSats: feeSatsPerNode, + } + nodes = append(nodes, leafNode) + } + + for len(nodes) > 1 { + nodes, err = createUpperLevel(nodes) + if err != nil { + return + } + } + + return nodes[0], nil +} + +func createUpperLevel(nodes []*node) ([]*node, error) { + if len(nodes)%2 != 0 { + last := nodes[len(nodes)-1] + pairs, err := createUpperLevel(nodes[:len(nodes)-1]) + if err != nil { + return nil, err + } + + return append(pairs, last), nil + } + + pairs := make([]*node, 0, len(nodes)/2) + for i := 0; i < len(nodes); i += 2 { + left := nodes[i] + right := nodes[i+1] + branchNode := &node{ + sweepKey: left.sweepKey, + receivers: append(left.receivers, right.receivers...), + left: left, + right: right, + asset: left.asset, + feeSats: left.feeSats, + } + pairs = append(pairs, branchNode) + } + return pairs, nil } diff --git a/asp/internal/infrastructure/tx-builder/covenant/utils.go b/asp/internal/infrastructure/tx-builder/covenant/utils.go new file mode 100644 index 0000000..07e805e --- /dev/null +++ b/asp/internal/infrastructure/tx-builder/covenant/utils.go @@ -0,0 +1,167 @@ +package txbuilder + +import ( + "encoding/hex" + "fmt" + + "github.com/ark-network/ark/internal/core/domain" + "github.com/ark-network/ark/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" + "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" +) + +func p2wpkhScript(publicKey *secp256k1.PublicKey, net *network.Network) ([]byte, error) { + payment := payment.FromPublicKey(publicKey, net, nil) + addr, err := payment.WitnessPubKeyHash() + if err != nil { + return nil, err + } + + return address.ToOutputScript(addr) +} + +func getTxid(txStr string) (string, error) { + pset, err := psetv2.NewPsetFromBase64(txStr) + if err != nil { + return "", err + } + + return getPsetId(pset) +} + +func getPsetId(pset *psetv2.Pset) (string, error) { + utx, err := pset.UnsignedTx() + if err != nil { + return "", err + } + + return utx.TxHash().String(), nil +} + +func getOnchainReceivers( + payments []domain.Payment, +) []domain.Receiver { + receivers := make([]domain.Receiver, 0) + for _, payment := range payments { + for _, receiver := range payment.Receivers { + if receiver.IsOnchain() { + receivers = append(receivers, receiver) + } + } + } + return receivers +} + +func getOffchainReceivers( + payments []domain.Payment, +) []domain.Receiver { + receivers := make([]domain.Receiver, 0) + for _, payment := range payments { + for _, receiver := range payment.Receivers { + if !receiver.IsOnchain() { + receivers = append(receivers, receiver) + } + } + } + return receivers +} + +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.NewTxOutput(assetBytes, valueBytes, scriptBytes), nil +} + +func countSpentVtxos(payments []domain.Payment) uint64 { + var sum uint64 + for _, payment := range payments { + sum += uint64(len(payment.Inputs)) + } + return sum +} + +func addInputs( + updater *psetv2.Updater, + inputs []ports.TxInput, +) error { + for _, in := range inputs { + inputArg := psetv2.InputArgs{ + Txid: in.GetTxid(), + TxIndex: in.GetIndex(), + } + + witnessUtxo, err := toWitnessUtxo(in) + if err != nil { + return err + } + + if err := updater.AddInputs([]psetv2.InputArgs{inputArg}); err != nil { + return err + } + + index := int(updater.Pset.Global.InputCount) - 1 + if err := updater.AddInWitnessUtxo(index, witnessUtxo); err != nil { + return err + } + + if err := updater.AddInSighashType(index, txscript.SigHashAll); err != nil { + return err + } + } + + return nil +} + +// wrapper of updater methods adding a taproot input to the pset with all the necessary data to spend it via any taproot script +func addTaprootInput( + updater *psetv2.Updater, + input psetv2.InputArgs, + internalTaprootKey *secp256k1.PublicKey, + taprootTree *taproot.IndexedElementsTapScriptTree, +) error { + if err := updater.AddInputs([]psetv2.InputArgs{input}); err != nil { + return err + } + + if err := updater.AddInTapInternalKey(0, schnorr.SerializePubKey(internalTaprootKey)); err != nil { + return err + } + + for _, proof := range taprootTree.LeafMerkleProofs { + controlBlock := proof.ToControlBlock(internalTaprootKey) + + if err := updater.AddInTapLeafScript(0, psetv2.TapLeafScript{ + TapElementsLeaf: taproot.NewBaseTapElementsLeaf(proof.Script), + ControlBlock: controlBlock, + }); err != nil { + return err + } + } + + return nil +} + +func taprootOutputScript(taprootKey *secp256k1.PublicKey) ([]byte, error) { + return txscript.NewScriptBuilder().AddOp(txscript.OP_1).AddData(schnorr.SerializePubKey(taprootKey)).Script() +} diff --git a/asp/internal/infrastructure/tx-builder/dummy/builder.go b/asp/internal/infrastructure/tx-builder/dummy/builder.go index 56f5585..4ae2142 100644 --- a/asp/internal/infrastructure/tx-builder/dummy/builder.go +++ b/asp/internal/infrastructure/tx-builder/dummy/builder.go @@ -19,11 +19,14 @@ const ( ) type txBuilder struct { - net network.Network + wallet ports.WalletService + net network.Network } -func NewTxBuilder(net network.Network) ports.TxBuilder { - return &txBuilder{net} +func NewTxBuilder( + wallet ports.WalletService, net network.Network, +) ports.TxBuilder { + return &txBuilder{wallet, net} } // BuildForfeitTxs implements ports.TxBuilder. @@ -40,7 +43,7 @@ func (b *txBuilder) BuildForfeitTxs( return nil, nil, err } - numberOfConnectors := numberOfVTXOs(payments) + numberOfConnectors := countSpentVtxos(payments) connectors, err = createConnectors( poolTxID, @@ -90,8 +93,7 @@ func (b *txBuilder) BuildForfeitTxs( // BuildPoolTx implements ports.TxBuilder. func (b *txBuilder) BuildPoolTx( - aspPubkey *secp256k1.PublicKey, wallet ports.WalletService, payments []domain.Payment, - minRelayFee uint64, + aspPubkey *secp256k1.PublicKey, payments []domain.Payment, minRelayFee uint64, ) (poolTx string, congestionTree tree.CongestionTree, err error) { aspScriptBytes, err := p2wpkhScript(aspPubkey, b.net) if err != nil { @@ -101,7 +103,7 @@ func (b *txBuilder) BuildPoolTx( offchainReceivers, onchainReceivers := receiversFromPayments(payments) sharedOutputAmount := sumReceivers(offchainReceivers) - numberOfConnectors := numberOfVTXOs(payments) + numberOfConnectors := countSpentVtxos(payments) connectorOutputAmount := connectorAmount * numberOfConnectors ctx := context.Background() @@ -136,7 +138,7 @@ func (b *txBuilder) BuildPoolTx( }) } - utxos, change, err := wallet.SelectUtxos(ctx, b.net.AssetID, amountToSelect) + utxos, change, err := b.wallet.SelectUtxos(ctx, b.net.AssetID, amountToSelect) if err != nil { return } @@ -177,7 +179,7 @@ func (b *txBuilder) BuildPoolTx( return poolTx, congestionTree, err } -func (b *txBuilder) GetLeafOutputScript(userPubkey, _ *secp256k1.PublicKey) ([]byte, error) { +func (b *txBuilder) GetVtxoOutputScript(userPubkey, _ *secp256k1.PublicKey) ([]byte, error) { p2wpkh := payment.FromPublicKey(userPubkey, &b.net, nil) addr, _ := p2wpkh.WitnessPubKeyHash() return address.ToOutputScript(addr) @@ -226,7 +228,7 @@ func getTxid(txStr string) (string, error) { return utx.TxHash().String(), nil } -func numberOfVTXOs(payments []domain.Payment) uint64 { +func countSpentVtxos(payments []domain.Payment) uint64 { var sum uint64 for _, payment := range payments { sum += uint64(len(payment.Inputs)) diff --git a/asp/internal/infrastructure/tx-builder/dummy/builder_test.go b/asp/internal/infrastructure/tx-builder/dummy/builder_test.go index 41ce9d1..c4ec1ba 100644 --- a/asp/internal/infrastructure/tx-builder/dummy/builder_test.go +++ b/asp/internal/infrastructure/tx-builder/dummy/builder_test.go @@ -98,7 +98,7 @@ func (*mockedWalletService) EstimateFees(ctx context.Context, pset string) (uint } func TestBuildCongestionTree(t *testing.T) { - builder := txbuilder.NewTxBuilder(network.Liquid) + builder := txbuilder.NewTxBuilder(&mockedWalletService{}, network.Liquid) fixtures := []struct { payments []domain.Payment @@ -224,7 +224,7 @@ func TestBuildCongestionTree(t *testing.T) { require.NotNil(t, key) for _, f := range fixtures { - poolTx, tree, err := builder.BuildPoolTx(key, &mockedWalletService{}, f.payments, 30) + poolTx, tree, err := builder.BuildPoolTx(key, f.payments, 30) require.NoError(t, err) require.Equal(t, f.expectedNodesNum, tree.NumberOfNodes()) @@ -274,7 +274,7 @@ func TestBuildCongestionTree(t *testing.T) { } func TestBuildForfeitTxs(t *testing.T) { - builder := txbuilder.NewTxBuilder(network.Liquid) + builder := txbuilder.NewTxBuilder(&mockedWalletService{}, network.Liquid) poolPset, err := psetv2.NewPsetFromBase64(fakePoolTx) require.NoError(t, err) diff --git a/common/tree/validation.go b/common/tree/validation.go index 8376ec0..1f23a91 100644 --- a/common/tree/validation.go +++ b/common/tree/validation.go @@ -2,7 +2,6 @@ package tree import ( "bytes" - "encoding/hex" "errors" "fmt" @@ -32,6 +31,7 @@ var ( ErrNumberOfTapscripts = errors.New("input should have two tapscripts leaves") ErrInternalKey = errors.New("taproot internal key is not unspendable") ErrInvalidTaprootScript = errors.New("invalid taproot script") + ErrInvalidTaprootScriptLen = errors.New("invalid taproot script length (expected 32 bytes)") ErrInvalidLeafTaprootScript = errors.New("invalid leaf taproot script") ErrInvalidAmount = errors.New("children amount is different from parent amount") ErrInvalidAsset = errors.New("invalid output asset") @@ -46,11 +46,21 @@ var ( ErrWrongPoolTxID = errors.New("root input should be the pool tx outpoint") ) +// 0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0 +var unspendablePoint = []byte{ + 0x02, 0x50, 0x92, 0x9b, 0x74, 0xc1, 0xa0, 0x49, 0x54, 0xb7, 0x8b, 0x4b, 0x60, 0x35, 0xe9, 0x7a, + 0x5e, 0x07, 0x8a, 0x5a, 0x0f, 0x28, 0xec, 0x96, 0xd5, 0x47, 0xbf, 0xee, 0x9a, 0xce, 0x80, 0x3a, 0xc0, +} + const ( - UnspendablePoint = "0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0" sharedOutputIndex = 0 ) +func UnspendableKey() *secp256k1.PublicKey { + key, _ := secp256k1.ParsePubKey(unspendablePoint) + return key +} + // ValidateCongestionTree checks if the given congestion tree is valid // poolTxID & poolTxIndex & poolTxAmount are used to validate the root input outpoint // aspPublicKey & roundLifetimeSeconds are used to validate the sweep tapscript leaves @@ -64,11 +74,8 @@ func ValidateCongestionTree( tree CongestionTree, poolTx string, aspPublicKey *secp256k1.PublicKey, - roundLifetimeSeconds uint, + roundLifetimeSeconds int64, ) error { - unspendableKeyBytes, _ := hex.DecodeString(UnspendablePoint) - unspendableKey, _ := secp256k1.ParsePubKey(unspendableKeyBytes) - poolTransaction, err := psetv2.NewPsetFromBase64(poolTx) if err != nil { return ErrInvalidPoolTransaction @@ -128,7 +135,7 @@ func ValidateCongestionTree( // iterates over all the nodes of the tree for _, level := range tree { for _, node := range level { - if err := validateNodeTransaction(node, tree, unspendableKey, aspPublicKey, roundLifetimeSeconds); err != nil { + if err := validateNodeTransaction(node, tree, UnspendableKey(), aspPublicKey, roundLifetimeSeconds); err != nil { return err } } @@ -142,7 +149,7 @@ func validateNodeTransaction( tree CongestionTree, expectedInternalKey, expectedPublicKeyASP *secp256k1.PublicKey, - expectedSequenceSeconds uint, + expectedSequenceSeconds int64, ) error { if node.Tx == "" { return ErrNodeTransactionEmpty @@ -190,7 +197,7 @@ func validateNodeTransaction( children := tree.Children(node.Txid) - if node.Leaf && len(children) > 1 { + if node.Leaf && len(children) >= 1 { return ErrLeafChildren } @@ -232,7 +239,7 @@ func validateNodeTransaction( return ErrInvalidASP } - if seconds != expectedSequenceSeconds { + if int64(seconds) != expectedSequenceSeconds { return ErrInvalidSweepSequence } @@ -242,7 +249,7 @@ func validateNodeTransaction( isBranchLeaf, leftKey, rightKey, leftAmount, rightAmount, err := decodeBranchScript(tapLeaf.Script) if err != nil { - return fmt.Errorf("invalid vtxo script: %w", err) + return fmt.Errorf("invalid branch script: %w", err) } if isBranchLeaf { diff --git a/noah/common.go b/noah/common.go index 1268b10..d347b17 100644 --- a/noah/common.go +++ b/noah/common.go @@ -456,8 +456,7 @@ func handleRoundStream( vtxoTaprootTree := taproot.AssembleTaprootScriptTree(*vtxoScript, *sweepLeaf) root := vtxoTaprootTree.RootNode.TapHash() - unspendableKeyBytes, _ := hex.DecodeString(tree.UnspendablePoint) - unspendableKey, _ := secp256k1.ParsePubKey(unspendableKeyBytes) + unspendableKey := tree.UnspendableKey() vtxoTaprootKey := schnorr.SerializePubKey(taproot.ComputeTaprootOutputKey(unspendableKey, root[:])) leaves := congestionTree.Leaves() @@ -596,7 +595,7 @@ func toCongestionTree(treeFromProto *arkv1.Tree) (tree.CongestionTree, error) { for j, treeLvl := range levels { for i, node := range treeLvl { - if len(levels.Children(node.Txid)) < 2 { + if len(levels.Children(node.Txid)) == 0 { levels[j][i].Leaf = true } }