diff --git a/asp/internal/app-config/config.go b/asp/internal/app-config/config.go index 827a502..bbb4a7c 100644 --- a/asp/internal/app-config/config.go +++ b/asp/internal/app-config/config.go @@ -9,7 +9,8 @@ import ( "github.com/ark-network/ark/internal/core/ports" "github.com/ark-network/ark/internal/infrastructure/db" oceanwallet "github.com/ark-network/ark/internal/infrastructure/ocean-wallet" - txbuilder "github.com/ark-network/ark/internal/infrastructure/tx-builder/dummy" + txbuilder "github.com/ark-network/ark/internal/infrastructure/tx-builder/covenant" + txbuilderdummy "github.com/ark-network/ark/internal/infrastructure/tx-builder/dummy" log "github.com/sirupsen/logrus" "github.com/vulpemventures/go-elements/network" ) @@ -22,7 +23,8 @@ var ( "gocron": {}, } supportedTxBuilders = supportedType{ - "dummy": {}, + "dummy": {}, + "covenant": {}, } ) @@ -121,9 +123,11 @@ func (c *Config) txBuilderService() error { net := c.mainChain() switch c.TxBuilderType { case "dummy": + svc = txbuilderdummy.NewTxBuilder(net) + case "covenant": svc = txbuilder.NewTxBuilder(net) default: - err = fmt.Errorf("unknown db type") + err = fmt.Errorf("unknown tx builder type") } if err != nil { return err diff --git a/asp/internal/config/config.go b/asp/internal/config/config.go index 8428c13..cfe15e6 100644 --- a/asp/internal/config/config.go +++ b/asp/internal/config/config.go @@ -40,7 +40,7 @@ var ( defaultPort = 6000 defaultDbType = "badger" defaultSchedulerType = "gocron" - defaultTxBuilderType = "dummy" + defaultTxBuilderType = "covenant" defaultInsecure = true defaultNetwork = "testnet" defaultLogLevel = 5 diff --git a/asp/internal/core/application/service.go b/asp/internal/core/application/service.go index 7159e48..7b517e6 100644 --- a/asp/internal/core/application/service.go +++ b/asp/internal/core/application/service.go @@ -12,9 +12,7 @@ import ( "github.com/ark-network/ark/internal/core/ports" "github.com/decred/dcrd/dcrec/secp256k1/v4" log "github.com/sirupsen/logrus" - "github.com/vulpemventures/go-elements/address" "github.com/vulpemventures/go-elements/network" - "github.com/vulpemventures/go-elements/payment" "github.com/vulpemventures/go-elements/psetv2" ) @@ -267,26 +265,20 @@ func (s *service) startFinalization() { return } - signedPoolTx, err := s.builder.BuildPoolTx(s.pubkey, s.wallet, payments) + signedPoolTx, tree, err := s.builder.BuildPoolTx(s.pubkey, s.wallet, payments) 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 } - tree, err := s.builder.BuildCongestionTree(s.pubkey, signedPoolTx, payments) - if err != nil { - changes = round.Fail(fmt.Errorf("failed to create congestion tree: %s", err)) - log.WithError(err).Warn("failed to create congestion tree") - return - } - connectors, forfeitTxs, err := s.builder.BuildForfeitTxs(s.pubkey, signedPoolTx, 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) changes = append(changes, events...) @@ -354,7 +346,7 @@ func (s *service) updateProjectionStore(round *domain.Round) { } } - newVtxos := getNewVtxos(s.onchainNework, round) + newVtxos := s.getNewVtxos(round) for { if err := repo.AddVtxos(ctx, newVtxos); err != nil { log.WithError(err).Warn("failed to add new vtxos, retrying soon") @@ -393,7 +385,7 @@ func (s *service) propagateEvents(round *domain.Round) { } } -func getNewVtxos(net network.Network, round *domain.Round) []domain.Vtxo { +func (s *service) getNewVtxos(round *domain.Round) []domain.Vtxo { leaves := round.CongestionTree.Leaves() vtxos := make([]domain.Vtxo, 0) for _, node := range leaves { @@ -405,9 +397,7 @@ func getNewVtxos(net network.Network, round *domain.Round) []domain.Vtxo { for _, r := range p.Receivers { buf, _ := hex.DecodeString(r.Pubkey) pk, _ := secp256k1.ParsePubKey(buf) - p2wpkh := payment.FromPublicKey(pk, &net, nil) - addr, _ := p2wpkh.WitnessPubKeyHash() - script, _ := address.ToOutputScript(addr) + script, _ := s.builder.GetLeafOutputScript(pk, s.pubkey) if bytes.Equal(script, out.Script) { found = true pubkey = r.Pubkey diff --git a/asp/internal/core/domain/congestion_tree.go b/asp/internal/core/domain/congestion_tree.go index c7be516..0b7fe4a 100644 --- a/asp/internal/core/domain/congestion_tree.go +++ b/asp/internal/core/domain/congestion_tree.go @@ -22,6 +22,19 @@ func (c CongestionTree) Leaves() []Node { return leaves } +func (c CongestionTree) Children(nodeTxid string) []Node { + var children []Node + for _, level := range c { + for _, node := range level { + if node.ParentTxid == nodeTxid { + children = append(children, node) + } + } + } + + return children +} + func (c CongestionTree) NumberOfNodes() int { var count int for _, level := range c { diff --git a/asp/internal/core/ports/tx_builder.go b/asp/internal/core/ports/tx_builder.go index 9adfb95..706a301 100644 --- a/asp/internal/core/ports/tx_builder.go +++ b/asp/internal/core/ports/tx_builder.go @@ -8,11 +8,9 @@ import ( type TxBuilder interface { BuildPoolTx( aspPubkey *secp256k1.PublicKey, wallet WalletService, payments []domain.Payment, - ) (poolTx string, err error) - BuildCongestionTree( - aspPubkey *secp256k1.PublicKey, poolTx string, payments []domain.Payment, - ) (congestionTree domain.CongestionTree, err error) + ) (poolTx string, congestionTree domain.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) } diff --git a/asp/internal/infrastructure/tx-builder/covenant/builder.go b/asp/internal/infrastructure/tx-builder/covenant/builder.go new file mode 100644 index 0000000..216f5fa --- /dev/null +++ b/asp/internal/infrastructure/tx-builder/covenant/builder.go @@ -0,0 +1,291 @@ +package txbuilder + +import ( + "context" + "encoding/hex" + + "github.com/ark-network/ark/internal/core/domain" + "github.com/ark-network/ark/internal/core/ports" + "github.com/decred/dcrd/dcrec/secp256k1/v4" + "github.com/vulpemventures/go-elements/address" + "github.com/vulpemventures/go-elements/network" + "github.com/vulpemventures/go-elements/payment" + "github.com/vulpemventures/go-elements/psetv2" + "github.com/vulpemventures/go-elements/taproot" + "github.com/vulpemventures/go-elements/transaction" +) + +const ( + connectorAmount = 450 +) + +type txBuilder struct { + net *network.Network +} + +func NewTxBuilder(net network.Network) ports.TxBuilder { + return &txBuilder{ + net: &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 { + tx, err := transaction.NewTxFromHex(txStr) + if err != nil { + return "", err + } + return tx.TxHash().String(), nil + } + + utx, err := pset.UnsignedTx() + if err != nil { + return "", err + } + + return utx.TxHash().String(), nil +} + +func (b *txBuilder) GetLeafOutputScript(userPubkey, aspPubkey *secp256k1.PublicKey) ([]byte, error) { + unspendableKeyBytes, _ := hex.DecodeString(unspendablePoint) + unspendableKey, _ := secp256k1.ParsePubKey(unspendableKeyBytes) + + sweepTaprootLeaf, err := sweepTapLeaf(aspPubkey) + if err != nil { + return nil, err + } + + leafScript, err := checksigScript(userPubkey) + if err != nil { + return nil, err + } + + leafTaprootLeaf := taproot.NewBaseTapElementsLeaf(leafScript) + leafTaprootTree := taproot.AssembleTaprootScriptTree(leafTaprootLeaf, *sweepTaprootLeaf) + root := leafTaprootTree.RootNode.TapHash() + + taprootKey := taproot.ComputeTaprootOutputKey( + unspendableKey, + root[:], + ) + + return taprootOutputScript(taprootKey) +} + +// 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) + if err != nil { + return nil, nil, err + } + + aspScript, err := p2wpkhScript(aspPubkey, b.net) + 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 + } + + connectorsAsInputs, err := connectorsToInputArgs(connectors) + if err != nil { + return nil, nil, err + } + + forfeitTxs = make([]string, 0) + for _, payment := range payments { + for _, vtxo := range payment.Inputs { + for _, connector := range connectorsAsInputs { + forfeitTx, err := createForfeitTx( + connector, + psetv2.InputArgs{ + Txid: vtxo.Txid, + TxIndex: vtxo.VOut, + }, + vtxo.Amount, + 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, +) (poolTx string, congestionTree domain.CongestionTree, err error) { + aspScriptBytes, err := p2wpkhScript(aspPubkey, b.net) + if err != nil { + return "", nil, err + } + + aspScript := hex.EncodeToString(aspScriptBytes) + + receivers := receiversFromPayments(payments) + sharedOutputAmount := sumReceivers(receivers) + + numberOfConnectors := numberOfVTXOs(payments) + connectorOutputAmount := connectorAmount * numberOfConnectors + + ctx := context.Background() + + makeTree, sharedOutputScript, err := buildCongestionTree( + b.net, + aspPubkey, + receivers, + ) + if err != nil { + return "", nil, err + } + + sharedOutputScriptHex := hex.EncodeToString(sharedOutputScript) + + poolTx, err = wallet.Transfer(ctx, []ports.TxOutput{ + newOutput(sharedOutputScriptHex, sharedOutputAmount, b.net.AssetID), + newOutput(aspScript, connectorOutputAmount, b.net.AssetID), + }) + if err != nil { + return "", nil, err + } + + poolTransaction, err := transaction.NewTxFromHex(poolTx) + if err != nil { + return "", nil, err + } + + congestionTree, err = makeTree(psetv2.InputArgs{ + Txid: poolTransaction.TxHash().String(), + TxIndex: 0, + }) + if err != nil { + return "", nil, err + } + + return poolTx, congestionTree, nil +} + +func connectorsToInputArgs(connectors []string) ([]psetv2.InputArgs, error) { + inputs := make([]psetv2.InputArgs, 0, len(connectors)+1) + for i, psetb64 := range connectors { + txID, err := getTxID(psetb64) + if err != nil { + return nil, err + } + + input := psetv2.InputArgs{ + Txid: txID, + TxIndex: 0, + } + inputs = append(inputs, input) + + if i == len(connectors)-1 { + input := psetv2.InputArgs{ + Txid: txID, + TxIndex: 1, + } + inputs = append(inputs, input) + } + } + return inputs, nil +} + +func getTxID(psetBase64 string) (string, error) { + pset, err := psetv2.NewPsetFromBase64(psetBase64) + if err != nil { + return "", err + } + + utx, err := pset.UnsignedTx() + if err != nil { + return "", err + } + + return utx.TxHash().String(), nil +} + +func numberOfVTXOs(payments []domain.Payment) uint64 { + var sum uint64 + for _, payment := range payments { + sum += uint64(len(payment.Inputs)) + } + return sum +} + +func receiversFromPayments(payments []domain.Payment) []domain.Receiver { + receivers := make([]domain.Receiver, 0) + for _, payment := range payments { + receivers = append(receivers, payment.Receivers...) + } + return receivers +} + +func sumReceivers(receivers []domain.Receiver) uint64 { + var sum uint64 + for _, r := range receivers { + sum += r.Amount + } + 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 (o *output) GetAsset() string { + return o.asset +} + +func (o *output) GetAmount() uint64 { + return o.amount +} + +func (o *output) GetScript() string { + return o.script +} diff --git a/asp/internal/infrastructure/tx-builder/covenant/builder_test.go b/asp/internal/infrastructure/tx-builder/covenant/builder_test.go new file mode 100644 index 0000000..ff97a2d --- /dev/null +++ b/asp/internal/infrastructure/tx-builder/covenant/builder_test.go @@ -0,0 +1,406 @@ +package txbuilder_test + +import ( + "context" + "testing" + + "github.com/ark-network/ark/common" + "github.com/ark-network/ark/internal/core/domain" + "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/stretchr/testify/require" + "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 ( + testingKey = "apub1qgvdtj5ttpuhkldavhq8thtm5auyk0ec4dcmrfdgu0u5hgp9we22v3hrs4x" +) + +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 + } + + utx, err := pset.UnsignedTx() + if err != nil { + return "", err + } + + return utx.ToHex() +} + +type mockedWalletService struct{} + +// 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") +} + +// Transfer implements ports.WalletService. +func (*mockedWalletService) Transfer(ctx context.Context, outs []ports.TxOutput) (string, error) { + return createTestPoolTx(1000, (450+500)*1) +} + +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: 600, + }, + }, + }, + Receivers: []domain.Receiver{ + { + Pubkey: "020000000000000000000000000000000000000000000000000000000000000002", + Amount: 600, + }, + { + Pubkey: "020000000000000000000000000000000000000000000000000000000000000002", + Amount: 400, + }, + }, + }, + }, + expectedNodesNum: 3, + expectedLeavesNum: 2, + }, + { + payments: []domain.Payment{ + { + Id: "0", + Inputs: []domain.Vtxo{ + { + VtxoKey: domain.VtxoKey{ + Txid: "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6", + VOut: 0, + }, + Receiver: domain.Receiver{ + Pubkey: "020000000000000000000000000000000000000000000000000000000000000002", + Amount: 600, + }, + }, + }, + Receivers: []domain.Receiver{ + { + Pubkey: "020000000000000000000000000000000000000000000000000000000000000002", + Amount: 600, + }, + { + Pubkey: "020000000000000000000000000000000000000000000000000000000000000002", + Amount: 400, + }, + }, + }, + { + Id: "0", + Inputs: []domain.Vtxo{ + { + VtxoKey: domain.VtxoKey{ + Txid: "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6", + VOut: 0, + }, + Receiver: domain.Receiver{ + Pubkey: "020000000000000000000000000000000000000000000000000000000000000002", + Amount: 600, + }, + }, + }, + Receivers: []domain.Receiver{ + { + Pubkey: "020000000000000000000000000000000000000000000000000000000000000002", + Amount: 600, + }, + { + Pubkey: "020000000000000000000000000000000000000000000000000000000000000002", + Amount: 400, + }, + }, + }, + { + Id: "0", + Inputs: []domain.Vtxo{ + { + VtxoKey: domain.VtxoKey{ + Txid: "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6", + VOut: 0, + }, + Receiver: domain.Receiver{ + Pubkey: "020000000000000000000000000000000000000000000000000000000000000002", + Amount: 600, + }, + }, + }, + Receivers: []domain.Receiver{ + { + Pubkey: "020000000000000000000000000000000000000000000000000000000000000002", + Amount: 600, + }, + { + Pubkey: "020000000000000000000000000000000000000000000000000000000000000002", + Amount: 400, + }, + }, + }, + }, + expectedNodesNum: 11, + expectedLeavesNum: 6, + }, + } + + _, key, err := common.DecodePubKey(testingKey) + require.NoError(t, err) + require.NotNil(t, key) + + for _, f := range fixtures { + poolTx, tree, err := builder.BuildPoolTx(key, &mockedWalletService{}, f.payments) + require.NoError(t, err) + require.Equal(t, f.expectedNodesNum, tree.NumberOfNodes()) + require.Len(t, tree.Leaves(), f.expectedLeavesNum) + + poolTransaction, err := transaction.NewTxFromHex(poolTx) + require.NoError(t, err) + + poolTxID := poolTransaction.TxHash().String() + + // check the root + require.Len(t, tree[0], 1) + require.Equal(t, poolTxID, tree[0][0].ParentTxid) + + // check the leaves + for _, leaf := range tree.Leaves() { + pset, err := psetv2.NewPsetFromBase64(leaf.Tx) + require.NoError(t, err) + + require.Len(t, pset.Inputs, 1) + require.Len(t, pset.Outputs, 1) + + inputTxID := chainhash.Hash(pset.Inputs[0].PreviousTxid).String() + require.Equal(t, leaf.ParentTxid, inputTxID) + } + + // check the nodes + for _, level := range tree[:len(tree)-2] { + for _, node := range level { + pset, err := psetv2.NewPsetFromBase64(node.Tx) + require.NoError(t, err) + + require.Len(t, pset.Inputs, 1) + require.Len(t, pset.Outputs, 2) + + inputTxID := chainhash.Hash(pset.Inputs[0].PreviousTxid).String() + require.Equal(t, node.ParentTxid, inputTxID) + + children := tree.Children(node.Txid) + require.Len(t, children, 2) + } + } + } +} + +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) + require.NoError(t, err) + + poolTxUnsigned, err := poolPset.UnsignedTx() + require.NoError(t, err) + + poolTxID := poolTxUnsigned.TxHash().String() + + 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: 400, + }, + }, + }, + Receivers: []domain.Receiver{ + { + Pubkey: "020000000000000000000000000000000000000000000000000000000000000002", + Amount: 600, + }, + { + Pubkey: "020000000000000000000000000000000000000000000000000000000000000002", + Amount: 400, + }, + }, + }, + }, + expectedNumOfForfeitTxs: 4, + expectedNumOfConnectors: 1, + }, + } + + _, key, err := common.DecodePubKey(testingKey) + require.NoError(t, err) + require.NotNil(t, key) + + for _, f := range fixtures { + connectors, forfeitTxs, err := builder.BuildForfeitTxs( + key, poolTx, 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() + } + + 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, 1) + } + } +} diff --git a/asp/internal/infrastructure/tx-builder/covenant/connectors.go b/asp/internal/infrastructure/tx-builder/covenant/connectors.go new file mode 100644 index 0000000..8008295 --- /dev/null +++ b/asp/internal/infrastructure/tx-builder/covenant/connectors.go @@ -0,0 +1,103 @@ +package txbuilder + +import ( + "github.com/vulpemventures/go-elements/psetv2" +) + +func createConnectors( + poolTxID string, + connectorOutputIndex uint32, + connectorOutput psetv2.OutputArgs, + changeScript []byte, + numberOfConnectors uint64, +) (connectorsPsets []string, err error) { + previousInput := psetv2.InputArgs{ + Txid: poolTxID, + TxIndex: connectorOutputIndex, + } + + 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 + } + + 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 + } + + // 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 +} diff --git a/asp/internal/infrastructure/tx-builder/covenant/forfeit.go b/asp/internal/infrastructure/tx-builder/covenant/forfeit.go new file mode 100644 index 0000000..ff15c68 --- /dev/null +++ b/asp/internal/infrastructure/tx-builder/covenant/forfeit.go @@ -0,0 +1,42 @@ +package txbuilder + +import ( + "github.com/vulpemventures/go-elements/network" + "github.com/vulpemventures/go-elements/psetv2" +) + +func createForfeitTx( + connectorInput psetv2.InputArgs, + vtxoInput psetv2.InputArgs, + vtxoAmount uint64, + aspScript []byte, + net network.Network, +) (forfeitTx string, err error) { + 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{connectorInput, vtxoInput}) + if err != nil { + return "", err + } + + err = updater.AddOutputs([]psetv2.OutputArgs{ + { + Asset: net.AssetID, + Amount: vtxoAmount, + Script: aspScript, + }, + }) + if err != nil { + return "", err + } + + return pset.ToBase64() +} diff --git a/asp/internal/infrastructure/tx-builder/covenant/scriptnum.go b/asp/internal/infrastructure/tx-builder/covenant/scriptnum.go new file mode 100644 index 0000000..f0a7ccc --- /dev/null +++ b/asp/internal/infrastructure/tx-builder/covenant/scriptnum.go @@ -0,0 +1,106 @@ +// Copyright (c) 2015-2017 The btcsuite developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package txbuilder + +const ( + maxInt32 = 1<<31 - 1 + minInt32 = -1 << 31 +) + +// scriptNum represents a numeric value used in the scripting engine with +// special handling to deal with the subtle semantics required by consensus. +// +// All numbers are stored on the data and alternate stacks encoded as little +// endian with a sign bit. All numeric opcodes such as OP_ADD, OP_SUB, +// and OP_MUL, are only allowed to operate on 4-byte integers in the range +// [-2^31 + 1, 2^31 - 1], however the results of numeric operations may overflow +// and remain valid so long as they are not used as inputs to other numeric +// operations or otherwise interpreted as an integer. +// +// For example, it is possible for OP_ADD to have 2^31 - 1 for its two operands +// resulting 2^32 - 2, which overflows, but is still pushed to the stack as the +// result of the addition. That value can then be used as input to OP_VERIFY +// which will succeed because the data is being interpreted as a boolean. +// However, if that same value were to be used as input to another numeric +// opcode, such as OP_SUB, it must fail. +// +// This type handles the aforementioned requirements by storing all numeric +// operation results as an int64 to handle overflow and provides the Bytes +// method to get the serialized representation (including values that overflow). +// +// Then, whenever data is interpreted as an integer, it is converted to this +// type by using the MakeScriptNum function which will return an error if the +// number is out of range or not minimally encoded depending on parameters. +// Since all numeric opcodes involve pulling data from the stack and +// interpreting it as an integer, it provides the required behavior. +type scriptNum int64 + +// Bytes returns the number serialized as a little endian with a sign bit. +func (n scriptNum) Bytes() []byte { + // Zero encodes as an empty byte slice. + if n == 0 { + return nil + } + + // Take the absolute value and keep track of whether it was originally + // negative. + isNegative := n < 0 + if isNegative { + n = -n + } + + // Encode to little endian. The maximum number of encoded bytes is 9 + // (8 bytes for max int64 plus a potential byte for sign extension). + result := make([]byte, 0, 9) + for n > 0 { + result = append(result, byte(n&0xff)) + n >>= 8 + } + + // When the most significant byte already has the high bit set, an + // additional high byte is required to indicate whether the number is + // negative or positive. The additional byte is removed when converting + // back to an integral and its high bit is used to denote the sign. + // + // Otherwise, when the most significant byte does not already have the + // high bit set, use it to indicate the value is negative, if needed. + if result[len(result)-1]&0x80 != 0 { + extraByte := byte(0x00) + if isNegative { + extraByte = 0x80 + } + result = append(result, extraByte) + + } else if isNegative { + result[len(result)-1] |= 0x80 + } + + return result +} + +// Int32 returns the script number clamped to a valid int32. That is to say +// when the script number is higher than the max allowed int32, the max int32 +// value is returned and vice versa for the minimum value. Note that this +// behavior is different from a simple int32 cast because that truncates +// and the consensus rules dictate numbers which are directly cast to ints +// provide this behavior. +// +// In practice, for most opcodes, the number should never be out of range since +// it will have been created with MakeScriptNum using the defaultScriptLen +// value, which rejects them. In case something in the future ends up calling +// this function against the result of some arithmetic, which IS allowed to be +// out of range before being reinterpreted as an integer, this will provide the +// correct behavior. +func (n scriptNum) Int32() int32 { + if n > maxInt32 { + return maxInt32 + } + + if n < minInt32 { + return minInt32 + } + + return int32(n) +} diff --git a/asp/internal/infrastructure/tx-builder/covenant/tree.go b/asp/internal/infrastructure/tx-builder/covenant/tree.go new file mode 100644 index 0000000..e3bb69d --- /dev/null +++ b/asp/internal/infrastructure/tx-builder/covenant/tree.go @@ -0,0 +1,565 @@ +package txbuilder + +import ( + "encoding/binary" + "encoding/hex" + "fmt" + + "github.com/ark-network/ark/common" + "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" +) + +const ( + OP_INSPECTOUTPUTSCRIPTPUBKEY = 0xd1 + OP_INSPECTOUTPUTVALUE = 0xcf + OP_PUSHCURRENTINPUTINDEX = 0xcd + unspendablePoint = "0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0" + timeDelta = 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) (domain.CongestionTree, error) + +// withOutput returns an introspection script that checks the script and the amount of the output at the given index +// verify will add an OP_EQUALVERIFY at the end of the script, otherwise it will add an OP_EQUAL +func withOutput(outputIndex uint64, taprootWitnessProgram []byte, amount uint32, verify bool) []byte { + amountBuffer := make([]byte, 8) + binary.LittleEndian.PutUint32(amountBuffer, amount) + + index := scriptNum(outputIndex).Bytes() + + script := append(index, []byte{ + OP_INSPECTOUTPUTSCRIPTPUBKEY, + txscript.OP_1, + txscript.OP_EQUALVERIFY, + txscript.OP_DATA_32, + }...) + + script = append(script, taprootWitnessProgram...) + script = append(script, []byte{ + txscript.OP_EQUALVERIFY, + }...) + script = append(script, index...) + script = append(script, []byte{ + OP_INSPECTOUTPUTVALUE, + txscript.OP_1, + txscript.OP_EQUALVERIFY, + txscript.OP_DATA_8, + }...) + script = append(script, amountBuffer...) + if verify { + script = append(script, []byte{ + txscript.OP_EQUALVERIFY, + }...) + } else { + script = append(script, []byte{ + txscript.OP_EQUAL, + }...) + } + + return script +} + +func checksigScript(pubkey *secp256k1.PublicKey) ([]byte, error) { + key := schnorr.SerializePubKey(pubkey) + return txscript.NewScriptBuilder().AddData(key).AddOp(txscript.OP_CHECKSIG).Script() +} + +// checkSequenceVerifyScript without checksig +func checkSequenceVerifyScript(seconds uint) ([]byte, error) { + sequence, err := common.BIP68Encode(seconds) + if err != nil { + return nil, err + } + + return append(sequence, []byte{ + txscript.OP_CHECKSEQUENCEVERIFY, + txscript.OP_DROP, + }...), nil +} + +// checkSequenceVerifyScript + checksig +func csvChecksigScript(pubkey *secp256k1.PublicKey, seconds uint) ([]byte, error) { + script, err := checksigScript(pubkey) + if err != nil { + return nil, err + } + + csvScript, err := checkSequenceVerifyScript(seconds) + if err != nil { + return nil, err + } + + return append(csvScript, script...), nil +} + +// sweepTapLeaf returns a taproot leaf letting the owner of the key to spend the output after a given timeDelta +func sweepTapLeaf(sweepKey *secp256k1.PublicKey) (*taproot.TapElementsLeaf, error) { + sweepScript, err := csvChecksigScript(sweepKey, timeDelta) + if err != nil { + return nil, err + } + + tapLeaf := taproot.NewBaseTapElementsLeaf(sweepScript) + return &tapLeaf, nil +} + +// forceSplitCoinTapLeaf returns a taproot leaf that enforces a split into two outputs +// each output (left and right) will have the given amount and the given taproot key as witness program +func forceSplitCoinTapLeaf( + leftKey, rightKey *secp256k1.PublicKey, leftAmount, rightAmount uint32, +) taproot.TapElementsLeaf { + nextScriptLeft := withOutput(0, schnorr.SerializePubKey(leftKey), leftAmount, true) + nextScriptRight := withOutput(1, schnorr.SerializePubKey(rightKey), rightAmount, false) + branchScript := append(nextScriptLeft, nextScriptRight...) + return taproot.NewBaseTapElementsLeaf(branchScript) +} + +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, +) (pluggableTree pluggableCongestionTree, sharedOutputScript []byte, err error) { + unspendableKeyBytes, err := hex.DecodeString(unspendablePoint) + if err != nil { + return nil, nil, err + } + + unspendableKey, err := secp256k1.ParsePubKey(unspendableKeyBytes) + if err != nil { + return nil, nil, err + } + + var nodes []*node + + for _, r := range receivers { + nodes = append(nodes, newLeaf(net, unspendableKey, aspPublicKey, r)) + } + + for len(nodes) > 1 { + nodes, err = createTreeLevel(nodes) + if err != nil { + return nil, nil, err + } + } + + psets, err := nodes[0].psets(nil, 0) + if err != nil { + return nil, nil, 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 := sweepTapLeaf(aspPublicKey) + if err != nil { + return nil, nil, err + } + + leftOutput := rootPset.Outputs[0] + rightOutput := rootPset.Outputs[1] + + leftWitnessProgram := leftOutput.Script[2:] + leftKey, err := schnorr.ParsePubKey(leftWitnessProgram) + if err != nil { + return nil, nil, err + } + + rightWitnessProgram := rightOutput.Script[2:] + rightKey, err := schnorr.ParsePubKey(rightWitnessProgram) + if err != nil { + return nil, nil, err + } + + goToTreeScript := forceSplitCoinTapLeaf( + leftKey, rightKey, uint32(leftOutput.Value), uint32(rightOutput.Value), + ) + + taprootTree := taproot.AssembleTaprootScriptTree(goToTreeScript, *sweepLeaf) + root := taprootTree.RootNode.TapHash() + taprootKey := taproot.ComputeTaprootOutputKey(unspendableKey, root[:]) + outputScript, err := taprootOutputScript(taprootKey) + if err != nil { + return nil, nil, err + } + + return func(outpoint psetv2.InputArgs) (domain.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 + } + } + + tree := make(domain.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() + + tree[psetWithLevel.level] = append(tree[psetWithLevel.level], domain.Node{ + Txid: txid, + Tx: psetB64, + ParentTxid: parentTxid, + Leaf: psetWithLevel.leaf, + }) + } + return tree, nil + }, outputScript, 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 + + // 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, +) *node { + return &node{ + sweepKey: sweepKey, + internalTaprootKey: internalKey, + receivers: []domain.Receiver{receiver}, + network: network, + } +} + +// 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, + } +} + +// is it the final node of the tree +func (n *node) isLeaf() bool { + return len(n.receivers) == 1 +} + +// compute the output amount of a node +func (n *node) amount() uint32 { + var amount uint32 + for _, r := range n.receivers { + amount += uint32(r.Amount) + } + return amount +} + +func (n *node) taprootKey() (*secp256k1.PublicKey, *taproot.IndexedElementsTapScriptTree, error) { + if n._taprootKey != nil && n._taprootTree != nil { + return n._taprootKey, n._taprootTree, nil + } + + sweepTaprootLeaf, err := sweepTapLeaf(n.sweepKey) + if err != nil { + return nil, nil, err + } + + if n.isLeaf() { + 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 + } + + leafScript, err := checksigScript(pubkey) + if err != nil { + return nil, nil, err + } + + leafTaprootLeaf := taproot.NewBaseTapElementsLeaf(leafScript) + leafTaprootTree := taproot.AssembleTaprootScriptTree(leafTaprootLeaf, *sweepTaprootLeaf) + root := leafTaprootTree.RootNode.TapHash() + + taprootKey := taproot.ComputeTaprootOutputKey( + n.internalTaprootKey, + root[:], + ) + + n._taprootKey = taprootKey + n._taprootTree = leafTaprootTree + + return taprootKey, leafTaprootTree, nil + } + + leftKey, _, err := n.left.taprootKey() + if err != nil { + return nil, nil, err + } + + rightKey, _, err := n.right.taprootKey() + if err != nil { + return nil, nil, err + } + + branchTaprootLeaf := forceSplitCoinTapLeaf( + leftKey, rightKey, n.left.amount(), n.right.amount(), + ) + + branchTaprootTree := taproot.AssembleTaprootScriptTree(branchTaprootLeaf, *sweepTaprootLeaf) + root := branchTaprootTree.RootNode.TapHash() + + taprootKey := taproot.ComputeTaprootOutputKey( + n.internalTaprootKey, + root[:], + ) + + n._taprootKey = taprootKey + n._taprootTree = 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 + } + + return taprootOutputScript(taprootKey) +} + +// use script & amount to create OutputArgs +func (n *node) output() (*psetv2.OutputArgs, error) { + script, err := n.script() + if err != nil { + return nil, err + } + + return &psetv2.OutputArgs{ + Asset: n.network.AssetID, + Amount: uint64(n.amount()), + Script: script, + }, 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) { + pset, err := psetv2.New(nil, nil, nil) + if err != nil { + return nil, err + } + + updater, err := psetv2.NewUpdater(pset) + if err != nil { + return nil, err + } + + if args != nil { + if err := addTaprootInput(updater, args.input, n.internalTaprootKey, args.taprootTree); err != nil { + return nil, err + } + } + + if n.isLeaf() { + output, err := n.output() + if err != nil { + return nil, err + } + + err = updater.AddOutputs([]psetv2.OutputArgs{*output}) + if err != nil { + return nil, err + } + return pset, nil + } + + outputLeft, err := n.left.output() + if err != nil { + return nil, err + } + + outputRight, err := n.right.output() + if err != nil { + return nil, err + } + + err = updater.AddOutputs([]psetv2.OutputArgs{*outputLeft, *outputRight}) + if err != nil { + return nil, err + } + + return pset, nil +} + +type psetWithLevel struct { + pset *psetv2.Pset + level int + leaf bool +} + +// 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) + if err != nil { + return nil, err + } + + nodeResult := []psetWithLevel{ + {pset, level, n.isLeaf()}, + } + + if n.isLeaf() { + return nodeResult, nil + } + + unsignedTx, err := pset.UnsignedTx() + if err != nil { + return nil, err + } + + txID := unsignedTx.TxHash().String() + + _, taprootTree, err := n.taprootKey() + if err != nil { + return nil, err + } + + psetsLeft, err := n.left.psets(&psetArgs{ + input: psetv2.InputArgs{ + Txid: txID, + TxIndex: 0, + }, + taprootTree: taprootTree, + }, level+1) + if err != nil { + return nil, err + } + + psetsRight, err := n.right.psets(&psetArgs{ + input: psetv2.InputArgs{ + Txid: txID, + TxIndex: 1, + }, + taprootTree: taprootTree, + }, level+1) + if err != nil { + return nil, err + } + + return append(nodeResult, append(psetsLeft, psetsRight...)...), nil +} diff --git a/asp/internal/infrastructure/tx-builder/dummy/builder.go b/asp/internal/infrastructure/tx-builder/dummy/builder.go index 36529f3..e74aa3d 100644 --- a/asp/internal/infrastructure/tx-builder/dummy/builder.go +++ b/asp/internal/infrastructure/tx-builder/dummy/builder.go @@ -7,7 +7,9 @@ import ( "github.com/ark-network/ark/internal/core/domain" "github.com/ark-network/ark/internal/core/ports" "github.com/decred/dcrd/dcrec/secp256k1/v4" + "github.com/vulpemventures/go-elements/address" "github.com/vulpemventures/go-elements/network" + "github.com/vulpemventures/go-elements/payment" "github.com/vulpemventures/go-elements/psetv2" "github.com/vulpemventures/go-elements/transaction" ) @@ -24,25 +26,6 @@ func NewTxBuilder(net network.Network) ports.TxBuilder { return &txBuilder{net} } -// BuildCongestionTree implements ports.TxBuilder. -func (b *txBuilder) BuildCongestionTree( - aspPubkey *secp256k1.PublicKey, poolTx string, payments []domain.Payment, -) (congestionTree domain.CongestionTree, err error) { - poolTxID, err := getTxid(poolTx) - if err != nil { - return nil, err - } - - receivers := receiversFromPayments(payments) - - return buildCongestionTree( - newOutputScriptFactory(aspPubkey, b.net), - b.net, - poolTxID, - receivers, - ) -} - // BuildForfeitTxs implements ports.TxBuilder. func (b *txBuilder) BuildForfeitTxs( aspPubkey *secp256k1.PublicKey, poolTx string, payments []domain.Payment, @@ -108,10 +91,10 @@ func (b *txBuilder) BuildForfeitTxs( // BuildPoolTx implements ports.TxBuilder. func (b *txBuilder) BuildPoolTx( aspPubkey *secp256k1.PublicKey, wallet ports.WalletService, payments []domain.Payment, -) (poolTx string, err error) { +) (poolTx string, congestionTree domain.CongestionTree, err error) { aspScriptBytes, err := p2wpkhScript(aspPubkey, b.net) if err != nil { - return "", err + return "", nil, err } aspScript := hex.EncodeToString(aspScriptBytes) @@ -124,10 +107,33 @@ func (b *txBuilder) BuildPoolTx( ctx := context.Background() - return wallet.Transfer(ctx, []ports.TxOutput{ + poolTx, err = wallet.Transfer(ctx, []ports.TxOutput{ newOutput(aspScript, sharedOutputAmount, b.net.AssetID), newOutput(aspScript, connectorOutputAmount, b.net.AssetID), }) + if err != nil { + return "", nil, err + } + + poolTxID, err := getTxid(poolTx) + if err != nil { + return "", nil, err + } + + congestionTree, err = buildCongestionTree( + newOutputScriptFactory(aspPubkey, b.net), + b.net, + poolTxID, + receivers, + ) + + return poolTx, congestionTree, err +} + +func (b *txBuilder) GetLeafOutputScript(userPubkey, _ *secp256k1.PublicKey) ([]byte, error) { + p2wpkh := payment.FromPublicKey(userPubkey, &b.net, nil) + addr, _ := p2wpkh.WitnessPubKeyHash() + return address.ToOutputScript(addr) } func connectorsToInputArgs(connectors []string) ([]psetv2.InputArgs, error) { diff --git a/asp/internal/infrastructure/tx-builder/dummy/builder_test.go b/asp/internal/infrastructure/tx-builder/dummy/builder_test.go index 0580f46..3ef0490 100644 --- a/asp/internal/infrastructure/tx-builder/dummy/builder_test.go +++ b/asp/internal/infrastructure/tx-builder/dummy/builder_test.go @@ -1,6 +1,7 @@ package txbuilder_test import ( + "context" "testing" "github.com/ark-network/ark/common" @@ -8,6 +9,7 @@ import ( "github.com/ark-network/ark/internal/core/ports" txbuilder "github.com/ark-network/ark/internal/infrastructure/tx-builder/dummy" "github.com/btcsuite/btcd/chaincfg/chainhash" + 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" @@ -18,10 +20,6 @@ const ( testingKey = "apub1qgvdtj5ttpuhkldavhq8thtm5auyk0ec4dcmrfdgu0u5hgp9we22v3hrs4x" ) -func createTestTxBuilder() (ports.TxBuilder, error) { - return txbuilder.NewTxBuilder(network.Liquid), nil -} - func createTestPoolTx(sharedOutputAmount, numberOfInputs uint64) (string, error) { _, key, err := common.DecodePubKey(testingKey) if err != nil { @@ -76,20 +74,45 @@ func createTestPoolTx(sharedOutputAmount, numberOfInputs uint64) (string, error) return pset.ToBase64() } +type mockedWalletService struct{} + +// 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") +} + +// Transfer implements ports.WalletService. +func (*mockedWalletService) Transfer(ctx context.Context, outs []ports.TxOutput) (string, error) { + return createTestPoolTx(1000, (450+500)*1) +} + func TestBuildCongestionTree(t *testing.T) { - builder, err := createTestTxBuilder() - require.NoError(t, err) - - poolTx, err := createTestPoolTx(1000, (450+500)*1) - require.NoError(t, err) - - poolPset, err := psetv2.NewPsetFromBase64(poolTx) - require.NoError(t, err) - - poolTxUnsigned, err := poolPset.UnsignedTx() - require.NoError(t, err) - - poolTxID := poolTxUnsigned.TxHash().String() + builder := txbuilder.NewTxBuilder(network.Liquid) fixtures := []struct { payments []domain.Payment @@ -215,11 +238,19 @@ func TestBuildCongestionTree(t *testing.T) { require.NotNil(t, key) for _, f := range fixtures { - tree, err := builder.BuildCongestionTree(key, poolTx, f.payments) + poolTx, tree, err := builder.BuildPoolTx(key, &mockedWalletService{}, f.payments) require.NoError(t, err) require.Equal(t, f.expectedNodesNum, tree.NumberOfNodes()) require.Len(t, tree.Leaves(), f.expectedLeavesNum) + poolPset, err := psetv2.NewPsetFromBase64(poolTx) + require.NoError(t, err) + + poolTxUnsigned, err := poolPset.UnsignedTx() + require.NoError(t, err) + + poolTxID := poolTxUnsigned.TxHash().String() + // check the root require.Len(t, tree[0], 1) require.Equal(t, poolTxID, tree[0][0].ParentTxid) @@ -237,7 +268,7 @@ func TestBuildCongestionTree(t *testing.T) { } // check the nodes - for i, level := range tree[:len(tree)-2] { + for _, level := range tree[:len(tree)-2] { for _, node := range level { pset, err := psetv2.NewPsetFromBase64(node.Tx) require.NoError(t, err) @@ -248,22 +279,15 @@ func TestBuildCongestionTree(t *testing.T) { inputTxID := chainhash.Hash(pset.Inputs[0].PreviousTxid).String() require.Equal(t, node.ParentTxid, inputTxID) - nextLevel := tree[i+1] - childs := 0 - for _, n := range nextLevel { - if n.ParentTxid == node.Txid { - childs++ - } - } - require.Equal(t, 2, childs) + children := tree.Children(node.Txid) + require.Len(t, children, 2) } } } } func TestBuildForfeitTxs(t *testing.T) { - builder, err := createTestTxBuilder() - require.NoError(t, err) + builder := txbuilder.NewTxBuilder(network.Liquid) poolTx, err := createTestPoolTx(1000, 450*2) require.NoError(t, err)