Add covenant-based congestion tree (#62)

* covenant based tx builder

* remove relative time delta

* txbuilder/covenant add leaf boolean in node

* txbuilder/covenant final version

* support covenantType

* add GetLeafOutputScript

* remove printLn

* fix linting

* Update asp/internal/app-config/config.go

Co-authored-by: João Bordalo <bordalix@users.noreply.github.com>
Signed-off-by: Louis Singer <41042567+louisinger@users.noreply.github.com>

---------

Signed-off-by: Louis Singer <41042567+louisinger@users.noreply.github.com>
Co-authored-by: João Bordalo <bordalix@users.noreply.github.com>
This commit is contained in:
Louis Singer
2023-12-14 14:02:37 +01:00
committed by GitHub
parent 325ef38197
commit 51bc673e66
13 changed files with 1622 additions and 74 deletions

View File

@@ -9,7 +9,8 @@ import (
"github.com/ark-network/ark/internal/core/ports" "github.com/ark-network/ark/internal/core/ports"
"github.com/ark-network/ark/internal/infrastructure/db" "github.com/ark-network/ark/internal/infrastructure/db"
oceanwallet "github.com/ark-network/ark/internal/infrastructure/ocean-wallet" oceanwallet "github.com/ark-network/ark/internal/infrastructure/ocean-wallet"
txbuilder "github.com/ark-network/ark/internal/infrastructure/tx-builder/dummy" txbuilder "github.com/ark-network/ark/internal/infrastructure/tx-builder/covenant"
txbuilderdummy "github.com/ark-network/ark/internal/infrastructure/tx-builder/dummy"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/vulpemventures/go-elements/network" "github.com/vulpemventures/go-elements/network"
) )
@@ -22,7 +23,8 @@ var (
"gocron": {}, "gocron": {},
} }
supportedTxBuilders = supportedType{ supportedTxBuilders = supportedType{
"dummy": {}, "dummy": {},
"covenant": {},
} }
) )
@@ -121,9 +123,11 @@ func (c *Config) txBuilderService() error {
net := c.mainChain() net := c.mainChain()
switch c.TxBuilderType { switch c.TxBuilderType {
case "dummy": case "dummy":
svc = txbuilderdummy.NewTxBuilder(net)
case "covenant":
svc = txbuilder.NewTxBuilder(net) svc = txbuilder.NewTxBuilder(net)
default: default:
err = fmt.Errorf("unknown db type") err = fmt.Errorf("unknown tx builder type")
} }
if err != nil { if err != nil {
return err return err

View File

@@ -40,7 +40,7 @@ var (
defaultPort = 6000 defaultPort = 6000
defaultDbType = "badger" defaultDbType = "badger"
defaultSchedulerType = "gocron" defaultSchedulerType = "gocron"
defaultTxBuilderType = "dummy" defaultTxBuilderType = "covenant"
defaultInsecure = true defaultInsecure = true
defaultNetwork = "testnet" defaultNetwork = "testnet"
defaultLogLevel = 5 defaultLogLevel = 5

View File

@@ -12,9 +12,7 @@ import (
"github.com/ark-network/ark/internal/core/ports" "github.com/ark-network/ark/internal/core/ports"
"github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/decred/dcrd/dcrec/secp256k1/v4"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/vulpemventures/go-elements/address"
"github.com/vulpemventures/go-elements/network" "github.com/vulpemventures/go-elements/network"
"github.com/vulpemventures/go-elements/payment"
"github.com/vulpemventures/go-elements/psetv2" "github.com/vulpemventures/go-elements/psetv2"
) )
@@ -267,26 +265,20 @@ func (s *service) startFinalization() {
return return
} }
signedPoolTx, err := s.builder.BuildPoolTx(s.pubkey, s.wallet, payments) signedPoolTx, tree, err := s.builder.BuildPoolTx(s.pubkey, s.wallet, payments)
if err != nil { if err != nil {
changes = round.Fail(fmt.Errorf("failed to create pool tx: %s", err)) changes = round.Fail(fmt.Errorf("failed to create pool tx: %s", err))
log.WithError(err).Warn("failed to create pool tx") log.WithError(err).Warn("failed to create pool tx")
return return
} }
tree, err := s.builder.BuildCongestionTree(s.pubkey, signedPoolTx, payments)
if err != nil {
changes = round.Fail(fmt.Errorf("failed to create congestion tree: %s", err))
log.WithError(err).Warn("failed to create congestion tree")
return
}
connectors, forfeitTxs, err := s.builder.BuildForfeitTxs(s.pubkey, signedPoolTx, payments) connectors, forfeitTxs, err := s.builder.BuildForfeitTxs(s.pubkey, signedPoolTx, payments)
if err != nil { if err != nil {
changes = round.Fail(fmt.Errorf("failed to create connectors and forfeit txs: %s", err)) changes = round.Fail(fmt.Errorf("failed to create connectors and forfeit txs: %s", err))
log.WithError(err).Warn("failed to create connectors and forfeit txs") log.WithError(err).Warn("failed to create connectors and forfeit txs")
return return
} }
events, _ := round.StartFinalization(connectors, tree, signedPoolTx) events, _ := round.StartFinalization(connectors, tree, signedPoolTx)
changes = append(changes, events...) changes = append(changes, events...)
@@ -354,7 +346,7 @@ func (s *service) updateProjectionStore(round *domain.Round) {
} }
} }
newVtxos := getNewVtxos(s.onchainNework, round) newVtxos := s.getNewVtxos(round)
for { for {
if err := repo.AddVtxos(ctx, newVtxos); err != nil { if err := repo.AddVtxos(ctx, newVtxos); err != nil {
log.WithError(err).Warn("failed to add new vtxos, retrying soon") log.WithError(err).Warn("failed to add new vtxos, retrying soon")
@@ -393,7 +385,7 @@ func (s *service) propagateEvents(round *domain.Round) {
} }
} }
func getNewVtxos(net network.Network, round *domain.Round) []domain.Vtxo { func (s *service) getNewVtxos(round *domain.Round) []domain.Vtxo {
leaves := round.CongestionTree.Leaves() leaves := round.CongestionTree.Leaves()
vtxos := make([]domain.Vtxo, 0) vtxos := make([]domain.Vtxo, 0)
for _, node := range leaves { for _, node := range leaves {
@@ -405,9 +397,7 @@ func getNewVtxos(net network.Network, round *domain.Round) []domain.Vtxo {
for _, r := range p.Receivers { for _, r := range p.Receivers {
buf, _ := hex.DecodeString(r.Pubkey) buf, _ := hex.DecodeString(r.Pubkey)
pk, _ := secp256k1.ParsePubKey(buf) pk, _ := secp256k1.ParsePubKey(buf)
p2wpkh := payment.FromPublicKey(pk, &net, nil) script, _ := s.builder.GetLeafOutputScript(pk, s.pubkey)
addr, _ := p2wpkh.WitnessPubKeyHash()
script, _ := address.ToOutputScript(addr)
if bytes.Equal(script, out.Script) { if bytes.Equal(script, out.Script) {
found = true found = true
pubkey = r.Pubkey pubkey = r.Pubkey

View File

@@ -22,6 +22,19 @@ func (c CongestionTree) Leaves() []Node {
return leaves return leaves
} }
func (c CongestionTree) Children(nodeTxid string) []Node {
var children []Node
for _, level := range c {
for _, node := range level {
if node.ParentTxid == nodeTxid {
children = append(children, node)
}
}
}
return children
}
func (c CongestionTree) NumberOfNodes() int { func (c CongestionTree) NumberOfNodes() int {
var count int var count int
for _, level := range c { for _, level := range c {

View File

@@ -8,11 +8,9 @@ import (
type TxBuilder interface { type TxBuilder interface {
BuildPoolTx( BuildPoolTx(
aspPubkey *secp256k1.PublicKey, wallet WalletService, payments []domain.Payment, aspPubkey *secp256k1.PublicKey, wallet WalletService, payments []domain.Payment,
) (poolTx string, err error) ) (poolTx string, congestionTree domain.CongestionTree, err error)
BuildCongestionTree(
aspPubkey *secp256k1.PublicKey, poolTx string, payments []domain.Payment,
) (congestionTree domain.CongestionTree, err error)
BuildForfeitTxs( BuildForfeitTxs(
aspPubkey *secp256k1.PublicKey, poolTx string, payments []domain.Payment, aspPubkey *secp256k1.PublicKey, poolTx string, payments []domain.Payment,
) (connectors []string, forfeitTxs []string, err error) ) (connectors []string, forfeitTxs []string, err error)
GetLeafOutputScript(userPubkey, aspPubkey *secp256k1.PublicKey) ([]byte, error)
} }

View File

@@ -0,0 +1,291 @@
package txbuilder
import (
"context"
"encoding/hex"
"github.com/ark-network/ark/internal/core/domain"
"github.com/ark-network/ark/internal/core/ports"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/vulpemventures/go-elements/address"
"github.com/vulpemventures/go-elements/network"
"github.com/vulpemventures/go-elements/payment"
"github.com/vulpemventures/go-elements/psetv2"
"github.com/vulpemventures/go-elements/taproot"
"github.com/vulpemventures/go-elements/transaction"
)
const (
connectorAmount = 450
)
type txBuilder struct {
net *network.Network
}
func NewTxBuilder(net network.Network) ports.TxBuilder {
return &txBuilder{
net: &net,
}
}
func p2wpkhScript(publicKey *secp256k1.PublicKey, net *network.Network) ([]byte, error) {
payment := payment.FromPublicKey(publicKey, net, nil)
addr, err := payment.WitnessPubKeyHash()
if err != nil {
return nil, err
}
return address.ToOutputScript(addr)
}
func getTxid(txStr string) (string, error) {
pset, err := psetv2.NewPsetFromBase64(txStr)
if err != nil {
tx, err := transaction.NewTxFromHex(txStr)
if err != nil {
return "", err
}
return tx.TxHash().String(), nil
}
utx, err := pset.UnsignedTx()
if err != nil {
return "", err
}
return utx.TxHash().String(), nil
}
func (b *txBuilder) GetLeafOutputScript(userPubkey, aspPubkey *secp256k1.PublicKey) ([]byte, error) {
unspendableKeyBytes, _ := hex.DecodeString(unspendablePoint)
unspendableKey, _ := secp256k1.ParsePubKey(unspendableKeyBytes)
sweepTaprootLeaf, err := sweepTapLeaf(aspPubkey)
if err != nil {
return nil, err
}
leafScript, err := checksigScript(userPubkey)
if err != nil {
return nil, err
}
leafTaprootLeaf := taproot.NewBaseTapElementsLeaf(leafScript)
leafTaprootTree := taproot.AssembleTaprootScriptTree(leafTaprootLeaf, *sweepTaprootLeaf)
root := leafTaprootTree.RootNode.TapHash()
taprootKey := taproot.ComputeTaprootOutputKey(
unspendableKey,
root[:],
)
return taprootOutputScript(taprootKey)
}
// BuildForfeitTxs implements ports.TxBuilder.
func (b *txBuilder) BuildForfeitTxs(
aspPubkey *secp256k1.PublicKey, poolTx string, payments []domain.Payment,
) (connectors []string, forfeitTxs []string, err error) {
poolTxID, err := getTxid(poolTx)
if err != nil {
return nil, nil, err
}
aspScript, err := p2wpkhScript(aspPubkey, b.net)
if err != nil {
return nil, nil, err
}
numberOfConnectors := numberOfVTXOs(payments)
connectors, err = createConnectors(
poolTxID,
1,
psetv2.OutputArgs{
Asset: b.net.AssetID,
Amount: connectorAmount,
Script: aspScript,
},
aspScript,
numberOfConnectors,
)
if err != nil {
return nil, nil, err
}
connectorsAsInputs, err := connectorsToInputArgs(connectors)
if err != nil {
return nil, nil, err
}
forfeitTxs = make([]string, 0)
for _, payment := range payments {
for _, vtxo := range payment.Inputs {
for _, connector := range connectorsAsInputs {
forfeitTx, err := createForfeitTx(
connector,
psetv2.InputArgs{
Txid: vtxo.Txid,
TxIndex: vtxo.VOut,
},
vtxo.Amount,
aspScript,
*b.net,
)
if err != nil {
return nil, nil, err
}
forfeitTxs = append(forfeitTxs, forfeitTx)
}
}
}
return connectors, forfeitTxs, nil
}
// BuildPoolTx implements ports.TxBuilder.
func (b *txBuilder) BuildPoolTx(
aspPubkey *secp256k1.PublicKey,
wallet ports.WalletService,
payments []domain.Payment,
) (poolTx string, congestionTree domain.CongestionTree, err error) {
aspScriptBytes, err := p2wpkhScript(aspPubkey, b.net)
if err != nil {
return "", nil, err
}
aspScript := hex.EncodeToString(aspScriptBytes)
receivers := receiversFromPayments(payments)
sharedOutputAmount := sumReceivers(receivers)
numberOfConnectors := numberOfVTXOs(payments)
connectorOutputAmount := connectorAmount * numberOfConnectors
ctx := context.Background()
makeTree, sharedOutputScript, err := buildCongestionTree(
b.net,
aspPubkey,
receivers,
)
if err != nil {
return "", nil, err
}
sharedOutputScriptHex := hex.EncodeToString(sharedOutputScript)
poolTx, err = wallet.Transfer(ctx, []ports.TxOutput{
newOutput(sharedOutputScriptHex, sharedOutputAmount, b.net.AssetID),
newOutput(aspScript, connectorOutputAmount, b.net.AssetID),
})
if err != nil {
return "", nil, err
}
poolTransaction, err := transaction.NewTxFromHex(poolTx)
if err != nil {
return "", nil, err
}
congestionTree, err = makeTree(psetv2.InputArgs{
Txid: poolTransaction.TxHash().String(),
TxIndex: 0,
})
if err != nil {
return "", nil, err
}
return poolTx, congestionTree, nil
}
func connectorsToInputArgs(connectors []string) ([]psetv2.InputArgs, error) {
inputs := make([]psetv2.InputArgs, 0, len(connectors)+1)
for i, psetb64 := range connectors {
txID, err := getTxID(psetb64)
if err != nil {
return nil, err
}
input := psetv2.InputArgs{
Txid: txID,
TxIndex: 0,
}
inputs = append(inputs, input)
if i == len(connectors)-1 {
input := psetv2.InputArgs{
Txid: txID,
TxIndex: 1,
}
inputs = append(inputs, input)
}
}
return inputs, nil
}
func getTxID(psetBase64 string) (string, error) {
pset, err := psetv2.NewPsetFromBase64(psetBase64)
if err != nil {
return "", err
}
utx, err := pset.UnsignedTx()
if err != nil {
return "", err
}
return utx.TxHash().String(), nil
}
func numberOfVTXOs(payments []domain.Payment) uint64 {
var sum uint64
for _, payment := range payments {
sum += uint64(len(payment.Inputs))
}
return sum
}
func receiversFromPayments(payments []domain.Payment) []domain.Receiver {
receivers := make([]domain.Receiver, 0)
for _, payment := range payments {
receivers = append(receivers, payment.Receivers...)
}
return receivers
}
func sumReceivers(receivers []domain.Receiver) uint64 {
var sum uint64
for _, r := range receivers {
sum += r.Amount
}
return sum
}
type output struct {
script string
amount uint64
asset string
}
func newOutput(script string, amount uint64, asset string) ports.TxOutput {
return &output{
script: script,
amount: amount,
asset: asset,
}
}
func (o *output) GetAsset() string {
return o.asset
}
func (o *output) GetAmount() uint64 {
return o.amount
}
func (o *output) GetScript() string {
return o.script
}

View File

@@ -0,0 +1,406 @@
package txbuilder_test
import (
"context"
"testing"
"github.com/ark-network/ark/common"
"github.com/ark-network/ark/internal/core/domain"
"github.com/ark-network/ark/internal/core/ports"
txbuilder "github.com/ark-network/ark/internal/infrastructure/tx-builder/covenant"
"github.com/btcsuite/btcd/chaincfg/chainhash"
secp256k1 "github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/stretchr/testify/require"
"github.com/vulpemventures/go-elements/network"
"github.com/vulpemventures/go-elements/payment"
"github.com/vulpemventures/go-elements/psetv2"
"github.com/vulpemventures/go-elements/transaction"
)
const (
testingKey = "apub1qgvdtj5ttpuhkldavhq8thtm5auyk0ec4dcmrfdgu0u5hgp9we22v3hrs4x"
)
func createTestPoolTx(sharedOutputAmount, numberOfInputs uint64) (string, error) {
_, key, err := common.DecodePubKey(testingKey)
if err != nil {
return "", err
}
payment := payment.FromPublicKey(key, &network.Testnet, nil)
script := payment.WitnessScript
pset, err := psetv2.New(nil, nil, nil)
if err != nil {
return "", err
}
updater, err := psetv2.NewUpdater(pset)
if err != nil {
return "", err
}
err = updater.AddInputs([]psetv2.InputArgs{
{
Txid: "2f8f5733734fd44d581976bd3c1aee098bd606402df2ce02ce908287f1d5ede4",
TxIndex: 0,
},
})
if err != nil {
return "", err
}
connectorsAmount := numberOfInputs * (450 + 500)
err = updater.AddOutputs([]psetv2.OutputArgs{
{
Asset: network.Regtest.AssetID,
Amount: sharedOutputAmount,
Script: script,
},
{
Asset: network.Regtest.AssetID,
Amount: connectorsAmount,
Script: script,
},
{
Asset: network.Regtest.AssetID,
Amount: 500,
},
})
if err != nil {
return "", err
}
utx, err := pset.UnsignedTx()
if err != nil {
return "", err
}
return utx.ToHex()
}
type mockedWalletService struct{}
// BroadcastTransaction implements ports.WalletService.
func (*mockedWalletService) BroadcastTransaction(ctx context.Context, txHex string) (string, error) {
panic("unimplemented")
}
// Close implements ports.WalletService.
func (*mockedWalletService) Close() {
panic("unimplemented")
}
// DeriveAddresses implements ports.WalletService.
func (*mockedWalletService) DeriveAddresses(ctx context.Context, num int) ([]string, error) {
panic("unimplemented")
}
// GetPubkey implements ports.WalletService.
func (*mockedWalletService) GetPubkey(ctx context.Context) (*secp256k1.PublicKey, error) {
panic("unimplemented")
}
// SignPset implements ports.WalletService.
func (*mockedWalletService) SignPset(ctx context.Context, pset string, extractRawTx bool) (string, error) {
panic("unimplemented")
}
// Status implements ports.WalletService.
func (*mockedWalletService) Status(ctx context.Context) (ports.WalletStatus, error) {
panic("unimplemented")
}
// Transfer implements ports.WalletService.
func (*mockedWalletService) Transfer(ctx context.Context, outs []ports.TxOutput) (string, error) {
return createTestPoolTx(1000, (450+500)*1)
}
func TestBuildCongestionTree(t *testing.T) {
builder := txbuilder.NewTxBuilder(network.Liquid)
fixtures := []struct {
payments []domain.Payment
expectedNodesNum int // 2*len(receivers)-1
expectedLeavesNum int
}{
{
payments: []domain.Payment{
{
Id: "0",
Inputs: []domain.Vtxo{
{
VtxoKey: domain.VtxoKey{
Txid: "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6",
VOut: 0,
},
Receiver: domain.Receiver{
Pubkey: "020000000000000000000000000000000000000000000000000000000000000002",
Amount: 600,
},
},
},
Receivers: []domain.Receiver{
{
Pubkey: "020000000000000000000000000000000000000000000000000000000000000002",
Amount: 600,
},
{
Pubkey: "020000000000000000000000000000000000000000000000000000000000000002",
Amount: 400,
},
},
},
},
expectedNodesNum: 3,
expectedLeavesNum: 2,
},
{
payments: []domain.Payment{
{
Id: "0",
Inputs: []domain.Vtxo{
{
VtxoKey: domain.VtxoKey{
Txid: "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6",
VOut: 0,
},
Receiver: domain.Receiver{
Pubkey: "020000000000000000000000000000000000000000000000000000000000000002",
Amount: 600,
},
},
},
Receivers: []domain.Receiver{
{
Pubkey: "020000000000000000000000000000000000000000000000000000000000000002",
Amount: 600,
},
{
Pubkey: "020000000000000000000000000000000000000000000000000000000000000002",
Amount: 400,
},
},
},
{
Id: "0",
Inputs: []domain.Vtxo{
{
VtxoKey: domain.VtxoKey{
Txid: "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6",
VOut: 0,
},
Receiver: domain.Receiver{
Pubkey: "020000000000000000000000000000000000000000000000000000000000000002",
Amount: 600,
},
},
},
Receivers: []domain.Receiver{
{
Pubkey: "020000000000000000000000000000000000000000000000000000000000000002",
Amount: 600,
},
{
Pubkey: "020000000000000000000000000000000000000000000000000000000000000002",
Amount: 400,
},
},
},
{
Id: "0",
Inputs: []domain.Vtxo{
{
VtxoKey: domain.VtxoKey{
Txid: "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6",
VOut: 0,
},
Receiver: domain.Receiver{
Pubkey: "020000000000000000000000000000000000000000000000000000000000000002",
Amount: 600,
},
},
},
Receivers: []domain.Receiver{
{
Pubkey: "020000000000000000000000000000000000000000000000000000000000000002",
Amount: 600,
},
{
Pubkey: "020000000000000000000000000000000000000000000000000000000000000002",
Amount: 400,
},
},
},
},
expectedNodesNum: 11,
expectedLeavesNum: 6,
},
}
_, key, err := common.DecodePubKey(testingKey)
require.NoError(t, err)
require.NotNil(t, key)
for _, f := range fixtures {
poolTx, tree, err := builder.BuildPoolTx(key, &mockedWalletService{}, f.payments)
require.NoError(t, err)
require.Equal(t, f.expectedNodesNum, tree.NumberOfNodes())
require.Len(t, tree.Leaves(), f.expectedLeavesNum)
poolTransaction, err := transaction.NewTxFromHex(poolTx)
require.NoError(t, err)
poolTxID := poolTransaction.TxHash().String()
// check the root
require.Len(t, tree[0], 1)
require.Equal(t, poolTxID, tree[0][0].ParentTxid)
// check the leaves
for _, leaf := range tree.Leaves() {
pset, err := psetv2.NewPsetFromBase64(leaf.Tx)
require.NoError(t, err)
require.Len(t, pset.Inputs, 1)
require.Len(t, pset.Outputs, 1)
inputTxID := chainhash.Hash(pset.Inputs[0].PreviousTxid).String()
require.Equal(t, leaf.ParentTxid, inputTxID)
}
// check the nodes
for _, level := range tree[:len(tree)-2] {
for _, node := range level {
pset, err := psetv2.NewPsetFromBase64(node.Tx)
require.NoError(t, err)
require.Len(t, pset.Inputs, 1)
require.Len(t, pset.Outputs, 2)
inputTxID := chainhash.Hash(pset.Inputs[0].PreviousTxid).String()
require.Equal(t, node.ParentTxid, inputTxID)
children := tree.Children(node.Txid)
require.Len(t, children, 2)
}
}
}
}
func TestBuildForfeitTxs(t *testing.T) {
builder := txbuilder.NewTxBuilder(network.Liquid)
poolTx, err := createTestPoolTx(1000, 450*2)
require.NoError(t, err)
poolPset, err := psetv2.NewPsetFromBase64(poolTx)
require.NoError(t, err)
poolTxUnsigned, err := poolPset.UnsignedTx()
require.NoError(t, err)
poolTxID := poolTxUnsigned.TxHash().String()
fixtures := []struct {
payments []domain.Payment
expectedNumOfForfeitTxs int
expectedNumOfConnectors int
}{
{
payments: []domain.Payment{
{
Id: "0",
Inputs: []domain.Vtxo{
{
VtxoKey: domain.VtxoKey{
Txid: "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6",
VOut: 0,
},
Receiver: domain.Receiver{
Pubkey: "020000000000000000000000000000000000000000000000000000000000000002",
Amount: 600,
},
},
{
VtxoKey: domain.VtxoKey{
Txid: "fd68e3c5796cc7db0a8036d486d5f625b6b2f2c014810ac020e1ac23e82c59d6",
VOut: 1,
},
Receiver: domain.Receiver{
Pubkey: "020000000000000000000000000000000000000000000000000000000000000002",
Amount: 400,
},
},
},
Receivers: []domain.Receiver{
{
Pubkey: "020000000000000000000000000000000000000000000000000000000000000002",
Amount: 600,
},
{
Pubkey: "020000000000000000000000000000000000000000000000000000000000000002",
Amount: 400,
},
},
},
},
expectedNumOfForfeitTxs: 4,
expectedNumOfConnectors: 1,
},
}
_, key, err := common.DecodePubKey(testingKey)
require.NoError(t, err)
require.NotNil(t, key)
for _, f := range fixtures {
connectors, forfeitTxs, err := builder.BuildForfeitTxs(
key, poolTx, f.payments,
)
require.NoError(t, err)
require.Len(t, connectors, f.expectedNumOfConnectors)
require.Len(t, forfeitTxs, f.expectedNumOfForfeitTxs)
// decode and check connectors
connectorsPsets := make([]*psetv2.Pset, 0, f.expectedNumOfConnectors)
for _, pset := range connectors {
p, err := psetv2.NewPsetFromBase64(pset)
require.NoError(t, err)
connectorsPsets = append(connectorsPsets, p)
}
for i, pset := range connectorsPsets {
require.Len(t, pset.Inputs, 1)
require.Len(t, pset.Outputs, 2)
expectedInputTxid := poolTxID
expectedInputVout := uint32(1)
if i > 0 {
tx, err := connectorsPsets[i-1].UnsignedTx()
require.NoError(t, err)
require.NotNil(t, tx)
expectedInputTxid = tx.TxHash().String()
}
inputTxid := chainhash.Hash(pset.Inputs[0].PreviousTxid).String()
require.Equal(t, expectedInputTxid, inputTxid)
require.Equal(t, expectedInputVout, pset.Inputs[0].PreviousTxIndex)
}
// decode and check forfeit txs
forfeitTxsPsets := make([]*psetv2.Pset, 0, f.expectedNumOfForfeitTxs)
for _, pset := range forfeitTxs {
p, err := psetv2.NewPsetFromBase64(pset)
require.NoError(t, err)
forfeitTxsPsets = append(forfeitTxsPsets, p)
}
// each forfeit tx should have 2 inputs and 2 outputs
for _, pset := range forfeitTxsPsets {
require.Len(t, pset.Inputs, 2)
require.Len(t, pset.Outputs, 1)
}
}
}

View File

@@ -0,0 +1,103 @@
package txbuilder
import (
"github.com/vulpemventures/go-elements/psetv2"
)
func createConnectors(
poolTxID string,
connectorOutputIndex uint32,
connectorOutput psetv2.OutputArgs,
changeScript []byte,
numberOfConnectors uint64,
) (connectorsPsets []string, err error) {
previousInput := psetv2.InputArgs{
Txid: poolTxID,
TxIndex: connectorOutputIndex,
}
if numberOfConnectors == 1 {
pset, err := psetv2.New(nil, nil, nil)
if err != nil {
return nil, err
}
updater, err := psetv2.NewUpdater(pset)
if err != nil {
return nil, err
}
err = updater.AddInputs([]psetv2.InputArgs{previousInput})
if err != nil {
return nil, err
}
err = updater.AddOutputs([]psetv2.OutputArgs{connectorOutput})
if err != nil {
return nil, err
}
base64, err := pset.ToBase64()
if err != nil {
return nil, err
}
return []string{base64}, nil
}
// compute the initial amount of the connectors output in pool transaction
remainingAmount := connectorAmount * numberOfConnectors
connectorsPset := make([]string, 0, numberOfConnectors-1)
for i := uint64(0); i < numberOfConnectors-1; i++ {
// create a new pset
pset, err := psetv2.New(nil, nil, nil)
if err != nil {
return nil, err
}
updater, err := psetv2.NewUpdater(pset)
if err != nil {
return nil, err
}
err = updater.AddInputs([]psetv2.InputArgs{previousInput})
if err != nil {
return nil, err
}
err = updater.AddOutputs([]psetv2.OutputArgs{connectorOutput})
if err != nil {
return nil, err
}
changeAmount := remainingAmount - connectorOutput.Amount
if changeAmount > 0 {
changeOutput := psetv2.OutputArgs{
Asset: connectorOutput.Asset,
Amount: changeAmount,
Script: changeScript,
}
err = updater.AddOutputs([]psetv2.OutputArgs{changeOutput})
if err != nil {
return nil, err
}
tx, _ := pset.UnsignedTx()
txid := tx.TxHash().String()
// make the change the next previousInput
previousInput = psetv2.InputArgs{
Txid: txid,
TxIndex: 1,
}
}
base64, err := pset.ToBase64()
if err != nil {
return nil, err
}
connectorsPset = append(connectorsPset, base64)
}
return connectorsPset, nil
}

View File

@@ -0,0 +1,42 @@
package txbuilder
import (
"github.com/vulpemventures/go-elements/network"
"github.com/vulpemventures/go-elements/psetv2"
)
func createForfeitTx(
connectorInput psetv2.InputArgs,
vtxoInput psetv2.InputArgs,
vtxoAmount uint64,
aspScript []byte,
net network.Network,
) (forfeitTx string, err error) {
pset, err := psetv2.New(nil, nil, nil)
if err != nil {
return "", err
}
updater, err := psetv2.NewUpdater(pset)
if err != nil {
return "", err
}
err = updater.AddInputs([]psetv2.InputArgs{connectorInput, vtxoInput})
if err != nil {
return "", err
}
err = updater.AddOutputs([]psetv2.OutputArgs{
{
Asset: net.AssetID,
Amount: vtxoAmount,
Script: aspScript,
},
})
if err != nil {
return "", err
}
return pset.ToBase64()
}

View File

@@ -0,0 +1,106 @@
// Copyright (c) 2015-2017 The btcsuite developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package txbuilder
const (
maxInt32 = 1<<31 - 1
minInt32 = -1 << 31
)
// scriptNum represents a numeric value used in the scripting engine with
// special handling to deal with the subtle semantics required by consensus.
//
// All numbers are stored on the data and alternate stacks encoded as little
// endian with a sign bit. All numeric opcodes such as OP_ADD, OP_SUB,
// and OP_MUL, are only allowed to operate on 4-byte integers in the range
// [-2^31 + 1, 2^31 - 1], however the results of numeric operations may overflow
// and remain valid so long as they are not used as inputs to other numeric
// operations or otherwise interpreted as an integer.
//
// For example, it is possible for OP_ADD to have 2^31 - 1 for its two operands
// resulting 2^32 - 2, which overflows, but is still pushed to the stack as the
// result of the addition. That value can then be used as input to OP_VERIFY
// which will succeed because the data is being interpreted as a boolean.
// However, if that same value were to be used as input to another numeric
// opcode, such as OP_SUB, it must fail.
//
// This type handles the aforementioned requirements by storing all numeric
// operation results as an int64 to handle overflow and provides the Bytes
// method to get the serialized representation (including values that overflow).
//
// Then, whenever data is interpreted as an integer, it is converted to this
// type by using the MakeScriptNum function which will return an error if the
// number is out of range or not minimally encoded depending on parameters.
// Since all numeric opcodes involve pulling data from the stack and
// interpreting it as an integer, it provides the required behavior.
type scriptNum int64
// Bytes returns the number serialized as a little endian with a sign bit.
func (n scriptNum) Bytes() []byte {
// Zero encodes as an empty byte slice.
if n == 0 {
return nil
}
// Take the absolute value and keep track of whether it was originally
// negative.
isNegative := n < 0
if isNegative {
n = -n
}
// Encode to little endian. The maximum number of encoded bytes is 9
// (8 bytes for max int64 plus a potential byte for sign extension).
result := make([]byte, 0, 9)
for n > 0 {
result = append(result, byte(n&0xff))
n >>= 8
}
// When the most significant byte already has the high bit set, an
// additional high byte is required to indicate whether the number is
// negative or positive. The additional byte is removed when converting
// back to an integral and its high bit is used to denote the sign.
//
// Otherwise, when the most significant byte does not already have the
// high bit set, use it to indicate the value is negative, if needed.
if result[len(result)-1]&0x80 != 0 {
extraByte := byte(0x00)
if isNegative {
extraByte = 0x80
}
result = append(result, extraByte)
} else if isNegative {
result[len(result)-1] |= 0x80
}
return result
}
// Int32 returns the script number clamped to a valid int32. That is to say
// when the script number is higher than the max allowed int32, the max int32
// value is returned and vice versa for the minimum value. Note that this
// behavior is different from a simple int32 cast because that truncates
// and the consensus rules dictate numbers which are directly cast to ints
// provide this behavior.
//
// In practice, for most opcodes, the number should never be out of range since
// it will have been created with MakeScriptNum using the defaultScriptLen
// value, which rejects them. In case something in the future ends up calling
// this function against the result of some arithmetic, which IS allowed to be
// out of range before being reinterpreted as an integer, this will provide the
// correct behavior.
func (n scriptNum) Int32() int32 {
if n > maxInt32 {
return maxInt32
}
if n < minInt32 {
return minInt32
}
return int32(n)
}

View File

@@ -0,0 +1,565 @@
package txbuilder
import (
"encoding/binary"
"encoding/hex"
"fmt"
"github.com/ark-network/ark/common"
"github.com/ark-network/ark/internal/core/domain"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/vulpemventures/go-elements/network"
"github.com/vulpemventures/go-elements/psetv2"
"github.com/vulpemventures/go-elements/taproot"
)
const (
OP_INSPECTOUTPUTSCRIPTPUBKEY = 0xd1
OP_INSPECTOUTPUTVALUE = 0xcf
OP_PUSHCURRENTINPUTINDEX = 0xcd
unspendablePoint = "0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0"
timeDelta = 60 * 60 * 24 * 14 // 14 days in seconds
)
// the private method buildCongestionTree returns a function letting to plug in the pool transaction output as input of the tree's root node
type pluggableCongestionTree func(outpoint psetv2.InputArgs) (domain.CongestionTree, error)
// withOutput returns an introspection script that checks the script and the amount of the output at the given index
// verify will add an OP_EQUALVERIFY at the end of the script, otherwise it will add an OP_EQUAL
func withOutput(outputIndex uint64, taprootWitnessProgram []byte, amount uint32, verify bool) []byte {
amountBuffer := make([]byte, 8)
binary.LittleEndian.PutUint32(amountBuffer, amount)
index := scriptNum(outputIndex).Bytes()
script := append(index, []byte{
OP_INSPECTOUTPUTSCRIPTPUBKEY,
txscript.OP_1,
txscript.OP_EQUALVERIFY,
txscript.OP_DATA_32,
}...)
script = append(script, taprootWitnessProgram...)
script = append(script, []byte{
txscript.OP_EQUALVERIFY,
}...)
script = append(script, index...)
script = append(script, []byte{
OP_INSPECTOUTPUTVALUE,
txscript.OP_1,
txscript.OP_EQUALVERIFY,
txscript.OP_DATA_8,
}...)
script = append(script, amountBuffer...)
if verify {
script = append(script, []byte{
txscript.OP_EQUALVERIFY,
}...)
} else {
script = append(script, []byte{
txscript.OP_EQUAL,
}...)
}
return script
}
func checksigScript(pubkey *secp256k1.PublicKey) ([]byte, error) {
key := schnorr.SerializePubKey(pubkey)
return txscript.NewScriptBuilder().AddData(key).AddOp(txscript.OP_CHECKSIG).Script()
}
// checkSequenceVerifyScript without checksig
func checkSequenceVerifyScript(seconds uint) ([]byte, error) {
sequence, err := common.BIP68Encode(seconds)
if err != nil {
return nil, err
}
return append(sequence, []byte{
txscript.OP_CHECKSEQUENCEVERIFY,
txscript.OP_DROP,
}...), nil
}
// checkSequenceVerifyScript + checksig
func csvChecksigScript(pubkey *secp256k1.PublicKey, seconds uint) ([]byte, error) {
script, err := checksigScript(pubkey)
if err != nil {
return nil, err
}
csvScript, err := checkSequenceVerifyScript(seconds)
if err != nil {
return nil, err
}
return append(csvScript, script...), nil
}
// sweepTapLeaf returns a taproot leaf letting the owner of the key to spend the output after a given timeDelta
func sweepTapLeaf(sweepKey *secp256k1.PublicKey) (*taproot.TapElementsLeaf, error) {
sweepScript, err := csvChecksigScript(sweepKey, timeDelta)
if err != nil {
return nil, err
}
tapLeaf := taproot.NewBaseTapElementsLeaf(sweepScript)
return &tapLeaf, nil
}
// forceSplitCoinTapLeaf returns a taproot leaf that enforces a split into two outputs
// each output (left and right) will have the given amount and the given taproot key as witness program
func forceSplitCoinTapLeaf(
leftKey, rightKey *secp256k1.PublicKey, leftAmount, rightAmount uint32,
) taproot.TapElementsLeaf {
nextScriptLeft := withOutput(0, schnorr.SerializePubKey(leftKey), leftAmount, true)
nextScriptRight := withOutput(1, schnorr.SerializePubKey(rightKey), rightAmount, false)
branchScript := append(nextScriptLeft, nextScriptRight...)
return taproot.NewBaseTapElementsLeaf(branchScript)
}
func taprootOutputScript(taprootKey *secp256k1.PublicKey) ([]byte, error) {
return txscript.NewScriptBuilder().AddOp(txscript.OP_1).AddData(schnorr.SerializePubKey(taprootKey)).Script()
}
// wrapper of updater methods adding a taproot input to the pset with all the necessary data to spend it via any taproot script
func addTaprootInput(
updater *psetv2.Updater,
input psetv2.InputArgs,
internalTaprootKey *secp256k1.PublicKey,
taprootTree *taproot.IndexedElementsTapScriptTree,
) error {
if err := updater.AddInputs([]psetv2.InputArgs{input}); err != nil {
return err
}
if err := updater.AddInTapInternalKey(0, schnorr.SerializePubKey(internalTaprootKey)); err != nil {
return err
}
for _, proof := range taprootTree.LeafMerkleProofs {
controlBlock := proof.ToControlBlock(internalTaprootKey)
if err := updater.AddInTapLeafScript(0, psetv2.TapLeafScript{
TapElementsLeaf: taproot.NewBaseTapElementsLeaf(proof.Script),
ControlBlock: controlBlock,
}); err != nil {
return err
}
}
return nil
}
// buildCongestionTree builder iteratively creates a binary tree of Pset from a set of receivers
// it returns a factory function creating a CongestionTree and the associated output script to be used in the pool transaction
func buildCongestionTree(
net *network.Network,
aspPublicKey *secp256k1.PublicKey,
receivers []domain.Receiver,
) (pluggableTree pluggableCongestionTree, sharedOutputScript []byte, err error) {
unspendableKeyBytes, err := hex.DecodeString(unspendablePoint)
if err != nil {
return nil, nil, err
}
unspendableKey, err := secp256k1.ParsePubKey(unspendableKeyBytes)
if err != nil {
return nil, nil, err
}
var nodes []*node
for _, r := range receivers {
nodes = append(nodes, newLeaf(net, unspendableKey, aspPublicKey, r))
}
for len(nodes) > 1 {
nodes, err = createTreeLevel(nodes)
if err != nil {
return nil, nil, err
}
}
psets, err := nodes[0].psets(nil, 0)
if err != nil {
return nil, nil, err
}
// find the root
var rootPset *psetv2.Pset
for _, psetWithLevel := range psets {
if psetWithLevel.level == 0 {
rootPset = psetWithLevel.pset
break
}
}
// compute the shared output script
sweepLeaf, err := sweepTapLeaf(aspPublicKey)
if err != nil {
return nil, nil, err
}
leftOutput := rootPset.Outputs[0]
rightOutput := rootPset.Outputs[1]
leftWitnessProgram := leftOutput.Script[2:]
leftKey, err := schnorr.ParsePubKey(leftWitnessProgram)
if err != nil {
return nil, nil, err
}
rightWitnessProgram := rightOutput.Script[2:]
rightKey, err := schnorr.ParsePubKey(rightWitnessProgram)
if err != nil {
return nil, nil, err
}
goToTreeScript := forceSplitCoinTapLeaf(
leftKey, rightKey, uint32(leftOutput.Value), uint32(rightOutput.Value),
)
taprootTree := taproot.AssembleTaprootScriptTree(goToTreeScript, *sweepLeaf)
root := taprootTree.RootNode.TapHash()
taprootKey := taproot.ComputeTaprootOutputKey(unspendableKey, root[:])
outputScript, err := taprootOutputScript(taprootKey)
if err != nil {
return nil, nil, err
}
return func(outpoint psetv2.InputArgs) (domain.CongestionTree, error) {
psets, err := nodes[0].psets(&psetArgs{
input: outpoint,
taprootTree: taprootTree,
}, 0)
if err != nil {
return nil, err
}
maxLevel := 0
for _, p := range psets {
if p.level > maxLevel {
maxLevel = p.level
}
}
tree := make(domain.CongestionTree, maxLevel+1)
for _, psetWithLevel := range psets {
utx, err := psetWithLevel.pset.UnsignedTx()
if err != nil {
return nil, err
}
txid := utx.TxHash().String()
psetB64, err := psetWithLevel.pset.ToBase64()
if err != nil {
return nil, err
}
parentTxid := chainhash.Hash(psetWithLevel.pset.Inputs[0].PreviousTxid).String()
tree[psetWithLevel.level] = append(tree[psetWithLevel.level], domain.Node{
Txid: txid,
Tx: psetB64,
ParentTxid: parentTxid,
Leaf: psetWithLevel.leaf,
})
}
return tree, nil
}, outputScript, nil
}
func createTreeLevel(nodes []*node) ([]*node, error) {
if len(nodes)%2 != 0 {
last := nodes[len(nodes)-1]
pairs, err := createTreeLevel(nodes[:len(nodes)-1])
if err != nil {
return nil, err
}
return append(pairs, last), nil
}
pairs := make([]*node, 0, len(nodes)/2)
for i := 0; i < len(nodes); i += 2 {
pairs = append(pairs, newBranch(nodes[i], nodes[i+1]))
}
return pairs, nil
}
// internal struct to build a binary tree of Pset
type node struct {
internalTaprootKey *secp256k1.PublicKey
sweepKey *secp256k1.PublicKey
receivers []domain.Receiver
left *node
right *node
network *network.Network
// cached values
_taprootKey *secp256k1.PublicKey
_taprootTree *taproot.IndexedElementsTapScriptTree
}
// create a node from a single receiver
func newLeaf(
network *network.Network,
internalKey *secp256k1.PublicKey,
sweepKey *secp256k1.PublicKey,
receiver domain.Receiver,
) *node {
return &node{
sweepKey: sweepKey,
internalTaprootKey: internalKey,
receivers: []domain.Receiver{receiver},
network: network,
}
}
// aggregate two nodes into a branch node
func newBranch(
left *node,
right *node,
) *node {
return &node{
internalTaprootKey: left.internalTaprootKey,
sweepKey: left.sweepKey,
receivers: append(left.receivers, right.receivers...),
left: left,
right: right,
network: left.network,
}
}
// is it the final node of the tree
func (n *node) isLeaf() bool {
return len(n.receivers) == 1
}
// compute the output amount of a node
func (n *node) amount() uint32 {
var amount uint32
for _, r := range n.receivers {
amount += uint32(r.Amount)
}
return amount
}
func (n *node) taprootKey() (*secp256k1.PublicKey, *taproot.IndexedElementsTapScriptTree, error) {
if n._taprootKey != nil && n._taprootTree != nil {
return n._taprootKey, n._taprootTree, nil
}
sweepTaprootLeaf, err := sweepTapLeaf(n.sweepKey)
if err != nil {
return nil, nil, err
}
if n.isLeaf() {
key, err := hex.DecodeString(n.receivers[0].Pubkey)
if err != nil {
return nil, nil, err
}
pubkey, err := secp256k1.ParsePubKey(key)
if err != nil {
return nil, nil, err
}
leafScript, err := checksigScript(pubkey)
if err != nil {
return nil, nil, err
}
leafTaprootLeaf := taproot.NewBaseTapElementsLeaf(leafScript)
leafTaprootTree := taproot.AssembleTaprootScriptTree(leafTaprootLeaf, *sweepTaprootLeaf)
root := leafTaprootTree.RootNode.TapHash()
taprootKey := taproot.ComputeTaprootOutputKey(
n.internalTaprootKey,
root[:],
)
n._taprootKey = taprootKey
n._taprootTree = leafTaprootTree
return taprootKey, leafTaprootTree, nil
}
leftKey, _, err := n.left.taprootKey()
if err != nil {
return nil, nil, err
}
rightKey, _, err := n.right.taprootKey()
if err != nil {
return nil, nil, err
}
branchTaprootLeaf := forceSplitCoinTapLeaf(
leftKey, rightKey, n.left.amount(), n.right.amount(),
)
branchTaprootTree := taproot.AssembleTaprootScriptTree(branchTaprootLeaf, *sweepTaprootLeaf)
root := branchTaprootTree.RootNode.TapHash()
taprootKey := taproot.ComputeTaprootOutputKey(
n.internalTaprootKey,
root[:],
)
n._taprootKey = taprootKey
n._taprootTree = branchTaprootTree
return taprootKey, branchTaprootTree, nil
}
// compute the output script of a node
func (n *node) script() ([]byte, error) {
taprootKey, _, err := n.taprootKey()
if err != nil {
return nil, err
}
return taprootOutputScript(taprootKey)
}
// use script & amount to create OutputArgs
func (n *node) output() (*psetv2.OutputArgs, error) {
script, err := n.script()
if err != nil {
return nil, err
}
return &psetv2.OutputArgs{
Asset: n.network.AssetID,
Amount: uint64(n.amount()),
Script: script,
}, nil
}
type psetArgs struct {
input psetv2.InputArgs
taprootTree *taproot.IndexedElementsTapScriptTree
}
// create the node Pset from the previous node Pset represented by input arg
// if node is a branch, it adds two outputs to the Pset, one for the left branch and one for the right branch
// if node is a leaf, it only adds one output to the Pset (the node output)
func (n *node) pset(args *psetArgs) (*psetv2.Pset, error) {
pset, err := psetv2.New(nil, nil, nil)
if err != nil {
return nil, err
}
updater, err := psetv2.NewUpdater(pset)
if err != nil {
return nil, err
}
if args != nil {
if err := addTaprootInput(updater, args.input, n.internalTaprootKey, args.taprootTree); err != nil {
return nil, err
}
}
if n.isLeaf() {
output, err := n.output()
if err != nil {
return nil, err
}
err = updater.AddOutputs([]psetv2.OutputArgs{*output})
if err != nil {
return nil, err
}
return pset, nil
}
outputLeft, err := n.left.output()
if err != nil {
return nil, err
}
outputRight, err := n.right.output()
if err != nil {
return nil, err
}
err = updater.AddOutputs([]psetv2.OutputArgs{*outputLeft, *outputRight})
if err != nil {
return nil, err
}
return pset, nil
}
type psetWithLevel struct {
pset *psetv2.Pset
level int
leaf bool
}
// create the node pset and all the psets of its children recursively, updating the input arg at each step
// the function stops when it reaches a leaf node
func (n *node) psets(inputArgs *psetArgs, level int) ([]psetWithLevel, error) {
if inputArgs == nil && level != 0 {
return nil, fmt.Errorf("only the first level must be pluggable")
}
pset, err := n.pset(inputArgs)
if err != nil {
return nil, err
}
nodeResult := []psetWithLevel{
{pset, level, n.isLeaf()},
}
if n.isLeaf() {
return nodeResult, nil
}
unsignedTx, err := pset.UnsignedTx()
if err != nil {
return nil, err
}
txID := unsignedTx.TxHash().String()
_, taprootTree, err := n.taprootKey()
if err != nil {
return nil, err
}
psetsLeft, err := n.left.psets(&psetArgs{
input: psetv2.InputArgs{
Txid: txID,
TxIndex: 0,
},
taprootTree: taprootTree,
}, level+1)
if err != nil {
return nil, err
}
psetsRight, err := n.right.psets(&psetArgs{
input: psetv2.InputArgs{
Txid: txID,
TxIndex: 1,
},
taprootTree: taprootTree,
}, level+1)
if err != nil {
return nil, err
}
return append(nodeResult, append(psetsLeft, psetsRight...)...), nil
}

View File

@@ -7,7 +7,9 @@ import (
"github.com/ark-network/ark/internal/core/domain" "github.com/ark-network/ark/internal/core/domain"
"github.com/ark-network/ark/internal/core/ports" "github.com/ark-network/ark/internal/core/ports"
"github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/vulpemventures/go-elements/address"
"github.com/vulpemventures/go-elements/network" "github.com/vulpemventures/go-elements/network"
"github.com/vulpemventures/go-elements/payment"
"github.com/vulpemventures/go-elements/psetv2" "github.com/vulpemventures/go-elements/psetv2"
"github.com/vulpemventures/go-elements/transaction" "github.com/vulpemventures/go-elements/transaction"
) )
@@ -24,25 +26,6 @@ func NewTxBuilder(net network.Network) ports.TxBuilder {
return &txBuilder{net} return &txBuilder{net}
} }
// BuildCongestionTree implements ports.TxBuilder.
func (b *txBuilder) BuildCongestionTree(
aspPubkey *secp256k1.PublicKey, poolTx string, payments []domain.Payment,
) (congestionTree domain.CongestionTree, err error) {
poolTxID, err := getTxid(poolTx)
if err != nil {
return nil, err
}
receivers := receiversFromPayments(payments)
return buildCongestionTree(
newOutputScriptFactory(aspPubkey, b.net),
b.net,
poolTxID,
receivers,
)
}
// BuildForfeitTxs implements ports.TxBuilder. // BuildForfeitTxs implements ports.TxBuilder.
func (b *txBuilder) BuildForfeitTxs( func (b *txBuilder) BuildForfeitTxs(
aspPubkey *secp256k1.PublicKey, poolTx string, payments []domain.Payment, aspPubkey *secp256k1.PublicKey, poolTx string, payments []domain.Payment,
@@ -108,10 +91,10 @@ func (b *txBuilder) BuildForfeitTxs(
// BuildPoolTx implements ports.TxBuilder. // BuildPoolTx implements ports.TxBuilder.
func (b *txBuilder) BuildPoolTx( func (b *txBuilder) BuildPoolTx(
aspPubkey *secp256k1.PublicKey, wallet ports.WalletService, payments []domain.Payment, aspPubkey *secp256k1.PublicKey, wallet ports.WalletService, payments []domain.Payment,
) (poolTx string, err error) { ) (poolTx string, congestionTree domain.CongestionTree, err error) {
aspScriptBytes, err := p2wpkhScript(aspPubkey, b.net) aspScriptBytes, err := p2wpkhScript(aspPubkey, b.net)
if err != nil { if err != nil {
return "", err return "", nil, err
} }
aspScript := hex.EncodeToString(aspScriptBytes) aspScript := hex.EncodeToString(aspScriptBytes)
@@ -124,10 +107,33 @@ func (b *txBuilder) BuildPoolTx(
ctx := context.Background() ctx := context.Background()
return wallet.Transfer(ctx, []ports.TxOutput{ poolTx, err = wallet.Transfer(ctx, []ports.TxOutput{
newOutput(aspScript, sharedOutputAmount, b.net.AssetID), newOutput(aspScript, sharedOutputAmount, b.net.AssetID),
newOutput(aspScript, connectorOutputAmount, b.net.AssetID), newOutput(aspScript, connectorOutputAmount, b.net.AssetID),
}) })
if err != nil {
return "", nil, err
}
poolTxID, err := getTxid(poolTx)
if err != nil {
return "", nil, err
}
congestionTree, err = buildCongestionTree(
newOutputScriptFactory(aspPubkey, b.net),
b.net,
poolTxID,
receivers,
)
return poolTx, congestionTree, err
}
func (b *txBuilder) GetLeafOutputScript(userPubkey, _ *secp256k1.PublicKey) ([]byte, error) {
p2wpkh := payment.FromPublicKey(userPubkey, &b.net, nil)
addr, _ := p2wpkh.WitnessPubKeyHash()
return address.ToOutputScript(addr)
} }
func connectorsToInputArgs(connectors []string) ([]psetv2.InputArgs, error) { func connectorsToInputArgs(connectors []string) ([]psetv2.InputArgs, error) {

View File

@@ -1,6 +1,7 @@
package txbuilder_test package txbuilder_test
import ( import (
"context"
"testing" "testing"
"github.com/ark-network/ark/common" "github.com/ark-network/ark/common"
@@ -8,6 +9,7 @@ import (
"github.com/ark-network/ark/internal/core/ports" "github.com/ark-network/ark/internal/core/ports"
txbuilder "github.com/ark-network/ark/internal/infrastructure/tx-builder/dummy" txbuilder "github.com/ark-network/ark/internal/infrastructure/tx-builder/dummy"
"github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/chaincfg/chainhash"
secp256k1 "github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/vulpemventures/go-elements/network" "github.com/vulpemventures/go-elements/network"
"github.com/vulpemventures/go-elements/payment" "github.com/vulpemventures/go-elements/payment"
@@ -18,10 +20,6 @@ const (
testingKey = "apub1qgvdtj5ttpuhkldavhq8thtm5auyk0ec4dcmrfdgu0u5hgp9we22v3hrs4x" testingKey = "apub1qgvdtj5ttpuhkldavhq8thtm5auyk0ec4dcmrfdgu0u5hgp9we22v3hrs4x"
) )
func createTestTxBuilder() (ports.TxBuilder, error) {
return txbuilder.NewTxBuilder(network.Liquid), nil
}
func createTestPoolTx(sharedOutputAmount, numberOfInputs uint64) (string, error) { func createTestPoolTx(sharedOutputAmount, numberOfInputs uint64) (string, error) {
_, key, err := common.DecodePubKey(testingKey) _, key, err := common.DecodePubKey(testingKey)
if err != nil { if err != nil {
@@ -76,20 +74,45 @@ func createTestPoolTx(sharedOutputAmount, numberOfInputs uint64) (string, error)
return pset.ToBase64() return pset.ToBase64()
} }
type mockedWalletService struct{}
// BroadcastTransaction implements ports.WalletService.
func (*mockedWalletService) BroadcastTransaction(ctx context.Context, txHex string) (string, error) {
panic("unimplemented")
}
// Close implements ports.WalletService.
func (*mockedWalletService) Close() {
panic("unimplemented")
}
// DeriveAddresses implements ports.WalletService.
func (*mockedWalletService) DeriveAddresses(ctx context.Context, num int) ([]string, error) {
panic("unimplemented")
}
// GetPubkey implements ports.WalletService.
func (*mockedWalletService) GetPubkey(ctx context.Context) (*secp256k1.PublicKey, error) {
panic("unimplemented")
}
// SignPset implements ports.WalletService.
func (*mockedWalletService) SignPset(ctx context.Context, pset string, extractRawTx bool) (string, error) {
panic("unimplemented")
}
// Status implements ports.WalletService.
func (*mockedWalletService) Status(ctx context.Context) (ports.WalletStatus, error) {
panic("unimplemented")
}
// Transfer implements ports.WalletService.
func (*mockedWalletService) Transfer(ctx context.Context, outs []ports.TxOutput) (string, error) {
return createTestPoolTx(1000, (450+500)*1)
}
func TestBuildCongestionTree(t *testing.T) { func TestBuildCongestionTree(t *testing.T) {
builder, err := createTestTxBuilder() builder := txbuilder.NewTxBuilder(network.Liquid)
require.NoError(t, err)
poolTx, err := createTestPoolTx(1000, (450+500)*1)
require.NoError(t, err)
poolPset, err := psetv2.NewPsetFromBase64(poolTx)
require.NoError(t, err)
poolTxUnsigned, err := poolPset.UnsignedTx()
require.NoError(t, err)
poolTxID := poolTxUnsigned.TxHash().String()
fixtures := []struct { fixtures := []struct {
payments []domain.Payment payments []domain.Payment
@@ -215,11 +238,19 @@ func TestBuildCongestionTree(t *testing.T) {
require.NotNil(t, key) require.NotNil(t, key)
for _, f := range fixtures { for _, f := range fixtures {
tree, err := builder.BuildCongestionTree(key, poolTx, f.payments) poolTx, tree, err := builder.BuildPoolTx(key, &mockedWalletService{}, f.payments)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, f.expectedNodesNum, tree.NumberOfNodes()) require.Equal(t, f.expectedNodesNum, tree.NumberOfNodes())
require.Len(t, tree.Leaves(), f.expectedLeavesNum) require.Len(t, tree.Leaves(), f.expectedLeavesNum)
poolPset, err := psetv2.NewPsetFromBase64(poolTx)
require.NoError(t, err)
poolTxUnsigned, err := poolPset.UnsignedTx()
require.NoError(t, err)
poolTxID := poolTxUnsigned.TxHash().String()
// check the root // check the root
require.Len(t, tree[0], 1) require.Len(t, tree[0], 1)
require.Equal(t, poolTxID, tree[0][0].ParentTxid) require.Equal(t, poolTxID, tree[0][0].ParentTxid)
@@ -237,7 +268,7 @@ func TestBuildCongestionTree(t *testing.T) {
} }
// check the nodes // check the nodes
for i, level := range tree[:len(tree)-2] { for _, level := range tree[:len(tree)-2] {
for _, node := range level { for _, node := range level {
pset, err := psetv2.NewPsetFromBase64(node.Tx) pset, err := psetv2.NewPsetFromBase64(node.Tx)
require.NoError(t, err) require.NoError(t, err)
@@ -248,22 +279,15 @@ func TestBuildCongestionTree(t *testing.T) {
inputTxID := chainhash.Hash(pset.Inputs[0].PreviousTxid).String() inputTxID := chainhash.Hash(pset.Inputs[0].PreviousTxid).String()
require.Equal(t, node.ParentTxid, inputTxID) require.Equal(t, node.ParentTxid, inputTxID)
nextLevel := tree[i+1] children := tree.Children(node.Txid)
childs := 0 require.Len(t, children, 2)
for _, n := range nextLevel {
if n.ParentTxid == node.Txid {
childs++
}
}
require.Equal(t, 2, childs)
} }
} }
} }
} }
func TestBuildForfeitTxs(t *testing.T) { func TestBuildForfeitTxs(t *testing.T) {
builder, err := createTestTxBuilder() builder := txbuilder.NewTxBuilder(network.Liquid)
require.NoError(t, err)
poolTx, err := createTestPoolTx(1000, 450*2) poolTx, err := createTestPoolTx(1000, 450*2)
require.NoError(t, err) require.NoError(t, err)