mirror of
https://github.com/aljazceru/ark.git
synced 2026-01-31 09:04:46 +01:00
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:
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
152
asp/internal/infrastructure/tx-builder/covenant/mocks_test.go
Normal file
152
asp/internal/infrastructure/tx-builder/covenant/mocks_test.go
Normal 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
|
||||
}
|
||||
229
asp/internal/infrastructure/tx-builder/covenant/testdata/fixtures.json
vendored
Normal file
229
asp/internal/infrastructure/tx-builder/covenant/testdata/fixtures.json
vendored
Normal 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": []
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
167
asp/internal/infrastructure/tx-builder/covenant/utils.go
Normal file
167
asp/internal/infrastructure/tx-builder/covenant/utils.go
Normal 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()
|
||||
}
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user