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>
This commit is contained in:
Louis Singer
2024-02-08 04:51:01 +01:00
committed by GitHub
parent d4ee064245
commit b2e034cf0e
17 changed files with 1470 additions and 1382 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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": []
}
}

View File

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

View File

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

View File

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

View File

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