mirror of
https://github.com/aljazceru/ark.git
synced 2025-12-19 05:04:21 +01:00
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:
@@ -9,7 +9,8 @@ import (
|
||||
"github.com/ark-network/ark/internal/core/ports"
|
||||
"github.com/ark-network/ark/internal/infrastructure/db"
|
||||
oceanwallet "github.com/ark-network/ark/internal/infrastructure/ocean-wallet"
|
||||
txbuilder "github.com/ark-network/ark/internal/infrastructure/tx-builder/dummy"
|
||||
txbuilder "github.com/ark-network/ark/internal/infrastructure/tx-builder/covenant"
|
||||
txbuilderdummy "github.com/ark-network/ark/internal/infrastructure/tx-builder/dummy"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/vulpemventures/go-elements/network"
|
||||
)
|
||||
@@ -23,6 +24,7 @@ var (
|
||||
}
|
||||
supportedTxBuilders = supportedType{
|
||||
"dummy": {},
|
||||
"covenant": {},
|
||||
}
|
||||
)
|
||||
|
||||
@@ -121,9 +123,11 @@ func (c *Config) txBuilderService() error {
|
||||
net := c.mainChain()
|
||||
switch c.TxBuilderType {
|
||||
case "dummy":
|
||||
svc = txbuilderdummy.NewTxBuilder(net)
|
||||
case "covenant":
|
||||
svc = txbuilder.NewTxBuilder(net)
|
||||
default:
|
||||
err = fmt.Errorf("unknown db type")
|
||||
err = fmt.Errorf("unknown tx builder type")
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -40,7 +40,7 @@ var (
|
||||
defaultPort = 6000
|
||||
defaultDbType = "badger"
|
||||
defaultSchedulerType = "gocron"
|
||||
defaultTxBuilderType = "dummy"
|
||||
defaultTxBuilderType = "covenant"
|
||||
defaultInsecure = true
|
||||
defaultNetwork = "testnet"
|
||||
defaultLogLevel = 5
|
||||
|
||||
@@ -12,9 +12,7 @@ import (
|
||||
"github.com/ark-network/ark/internal/core/ports"
|
||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/vulpemventures/go-elements/address"
|
||||
"github.com/vulpemventures/go-elements/network"
|
||||
"github.com/vulpemventures/go-elements/payment"
|
||||
"github.com/vulpemventures/go-elements/psetv2"
|
||||
)
|
||||
|
||||
@@ -267,26 +265,20 @@ func (s *service) startFinalization() {
|
||||
return
|
||||
}
|
||||
|
||||
signedPoolTx, err := s.builder.BuildPoolTx(s.pubkey, s.wallet, payments)
|
||||
signedPoolTx, tree, err := s.builder.BuildPoolTx(s.pubkey, s.wallet, payments)
|
||||
if err != nil {
|
||||
changes = round.Fail(fmt.Errorf("failed to create pool tx: %s", err))
|
||||
log.WithError(err).Warn("failed to create pool tx")
|
||||
return
|
||||
}
|
||||
|
||||
tree, err := s.builder.BuildCongestionTree(s.pubkey, signedPoolTx, payments)
|
||||
if err != nil {
|
||||
changes = round.Fail(fmt.Errorf("failed to create congestion tree: %s", err))
|
||||
log.WithError(err).Warn("failed to create congestion tree")
|
||||
return
|
||||
}
|
||||
|
||||
connectors, forfeitTxs, err := s.builder.BuildForfeitTxs(s.pubkey, signedPoolTx, payments)
|
||||
if err != nil {
|
||||
changes = round.Fail(fmt.Errorf("failed to create connectors and forfeit txs: %s", err))
|
||||
log.WithError(err).Warn("failed to create connectors and forfeit txs")
|
||||
return
|
||||
}
|
||||
|
||||
events, _ := round.StartFinalization(connectors, tree, signedPoolTx)
|
||||
changes = append(changes, events...)
|
||||
|
||||
@@ -354,7 +346,7 @@ func (s *service) updateProjectionStore(round *domain.Round) {
|
||||
}
|
||||
}
|
||||
|
||||
newVtxos := getNewVtxos(s.onchainNework, round)
|
||||
newVtxos := s.getNewVtxos(round)
|
||||
for {
|
||||
if err := repo.AddVtxos(ctx, newVtxos); err != nil {
|
||||
log.WithError(err).Warn("failed to add new vtxos, retrying soon")
|
||||
@@ -393,7 +385,7 @@ func (s *service) propagateEvents(round *domain.Round) {
|
||||
}
|
||||
}
|
||||
|
||||
func getNewVtxos(net network.Network, round *domain.Round) []domain.Vtxo {
|
||||
func (s *service) getNewVtxos(round *domain.Round) []domain.Vtxo {
|
||||
leaves := round.CongestionTree.Leaves()
|
||||
vtxos := make([]domain.Vtxo, 0)
|
||||
for _, node := range leaves {
|
||||
@@ -405,9 +397,7 @@ func getNewVtxos(net network.Network, round *domain.Round) []domain.Vtxo {
|
||||
for _, r := range p.Receivers {
|
||||
buf, _ := hex.DecodeString(r.Pubkey)
|
||||
pk, _ := secp256k1.ParsePubKey(buf)
|
||||
p2wpkh := payment.FromPublicKey(pk, &net, nil)
|
||||
addr, _ := p2wpkh.WitnessPubKeyHash()
|
||||
script, _ := address.ToOutputScript(addr)
|
||||
script, _ := s.builder.GetLeafOutputScript(pk, s.pubkey)
|
||||
if bytes.Equal(script, out.Script) {
|
||||
found = true
|
||||
pubkey = r.Pubkey
|
||||
|
||||
@@ -22,6 +22,19 @@ func (c CongestionTree) Leaves() []Node {
|
||||
return leaves
|
||||
}
|
||||
|
||||
func (c CongestionTree) Children(nodeTxid string) []Node {
|
||||
var children []Node
|
||||
for _, level := range c {
|
||||
for _, node := range level {
|
||||
if node.ParentTxid == nodeTxid {
|
||||
children = append(children, node)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return children
|
||||
}
|
||||
|
||||
func (c CongestionTree) NumberOfNodes() int {
|
||||
var count int
|
||||
for _, level := range c {
|
||||
|
||||
@@ -8,11 +8,9 @@ import (
|
||||
type TxBuilder interface {
|
||||
BuildPoolTx(
|
||||
aspPubkey *secp256k1.PublicKey, wallet WalletService, payments []domain.Payment,
|
||||
) (poolTx string, err error)
|
||||
BuildCongestionTree(
|
||||
aspPubkey *secp256k1.PublicKey, poolTx string, payments []domain.Payment,
|
||||
) (congestionTree domain.CongestionTree, err error)
|
||||
) (poolTx string, congestionTree domain.CongestionTree, err error)
|
||||
BuildForfeitTxs(
|
||||
aspPubkey *secp256k1.PublicKey, poolTx string, payments []domain.Payment,
|
||||
) (connectors []string, forfeitTxs []string, err error)
|
||||
GetLeafOutputScript(userPubkey, aspPubkey *secp256k1.PublicKey) ([]byte, error)
|
||||
}
|
||||
|
||||
291
asp/internal/infrastructure/tx-builder/covenant/builder.go
Normal file
291
asp/internal/infrastructure/tx-builder/covenant/builder.go
Normal 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
|
||||
}
|
||||
406
asp/internal/infrastructure/tx-builder/covenant/builder_test.go
Normal file
406
asp/internal/infrastructure/tx-builder/covenant/builder_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
103
asp/internal/infrastructure/tx-builder/covenant/connectors.go
Normal file
103
asp/internal/infrastructure/tx-builder/covenant/connectors.go
Normal 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
|
||||
}
|
||||
42
asp/internal/infrastructure/tx-builder/covenant/forfeit.go
Normal file
42
asp/internal/infrastructure/tx-builder/covenant/forfeit.go
Normal 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()
|
||||
}
|
||||
106
asp/internal/infrastructure/tx-builder/covenant/scriptnum.go
Normal file
106
asp/internal/infrastructure/tx-builder/covenant/scriptnum.go
Normal 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)
|
||||
}
|
||||
565
asp/internal/infrastructure/tx-builder/covenant/tree.go
Normal file
565
asp/internal/infrastructure/tx-builder/covenant/tree.go
Normal 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
|
||||
}
|
||||
@@ -7,7 +7,9 @@ import (
|
||||
"github.com/ark-network/ark/internal/core/domain"
|
||||
"github.com/ark-network/ark/internal/core/ports"
|
||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||
"github.com/vulpemventures/go-elements/address"
|
||||
"github.com/vulpemventures/go-elements/network"
|
||||
"github.com/vulpemventures/go-elements/payment"
|
||||
"github.com/vulpemventures/go-elements/psetv2"
|
||||
"github.com/vulpemventures/go-elements/transaction"
|
||||
)
|
||||
@@ -24,25 +26,6 @@ func NewTxBuilder(net network.Network) ports.TxBuilder {
|
||||
return &txBuilder{net}
|
||||
}
|
||||
|
||||
// BuildCongestionTree implements ports.TxBuilder.
|
||||
func (b *txBuilder) BuildCongestionTree(
|
||||
aspPubkey *secp256k1.PublicKey, poolTx string, payments []domain.Payment,
|
||||
) (congestionTree domain.CongestionTree, err error) {
|
||||
poolTxID, err := getTxid(poolTx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
receivers := receiversFromPayments(payments)
|
||||
|
||||
return buildCongestionTree(
|
||||
newOutputScriptFactory(aspPubkey, b.net),
|
||||
b.net,
|
||||
poolTxID,
|
||||
receivers,
|
||||
)
|
||||
}
|
||||
|
||||
// BuildForfeitTxs implements ports.TxBuilder.
|
||||
func (b *txBuilder) BuildForfeitTxs(
|
||||
aspPubkey *secp256k1.PublicKey, poolTx string, payments []domain.Payment,
|
||||
@@ -108,10 +91,10 @@ func (b *txBuilder) BuildForfeitTxs(
|
||||
// BuildPoolTx implements ports.TxBuilder.
|
||||
func (b *txBuilder) BuildPoolTx(
|
||||
aspPubkey *secp256k1.PublicKey, wallet ports.WalletService, payments []domain.Payment,
|
||||
) (poolTx string, err error) {
|
||||
) (poolTx string, congestionTree domain.CongestionTree, err error) {
|
||||
aspScriptBytes, err := p2wpkhScript(aspPubkey, b.net)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
aspScript := hex.EncodeToString(aspScriptBytes)
|
||||
@@ -124,10 +107,33 @@ func (b *txBuilder) BuildPoolTx(
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
return wallet.Transfer(ctx, []ports.TxOutput{
|
||||
poolTx, err = wallet.Transfer(ctx, []ports.TxOutput{
|
||||
newOutput(aspScript, sharedOutputAmount, b.net.AssetID),
|
||||
newOutput(aspScript, connectorOutputAmount, b.net.AssetID),
|
||||
})
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
poolTxID, err := getTxid(poolTx)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
congestionTree, err = buildCongestionTree(
|
||||
newOutputScriptFactory(aspPubkey, b.net),
|
||||
b.net,
|
||||
poolTxID,
|
||||
receivers,
|
||||
)
|
||||
|
||||
return poolTx, congestionTree, err
|
||||
}
|
||||
|
||||
func (b *txBuilder) GetLeafOutputScript(userPubkey, _ *secp256k1.PublicKey) ([]byte, error) {
|
||||
p2wpkh := payment.FromPublicKey(userPubkey, &b.net, nil)
|
||||
addr, _ := p2wpkh.WitnessPubKeyHash()
|
||||
return address.ToOutputScript(addr)
|
||||
}
|
||||
|
||||
func connectorsToInputArgs(connectors []string) ([]psetv2.InputArgs, error) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package txbuilder_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/ark-network/ark/common"
|
||||
@@ -8,6 +9,7 @@ import (
|
||||
"github.com/ark-network/ark/internal/core/ports"
|
||||
txbuilder "github.com/ark-network/ark/internal/infrastructure/tx-builder/dummy"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
secp256k1 "github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/vulpemventures/go-elements/network"
|
||||
"github.com/vulpemventures/go-elements/payment"
|
||||
@@ -18,10 +20,6 @@ const (
|
||||
testingKey = "apub1qgvdtj5ttpuhkldavhq8thtm5auyk0ec4dcmrfdgu0u5hgp9we22v3hrs4x"
|
||||
)
|
||||
|
||||
func createTestTxBuilder() (ports.TxBuilder, error) {
|
||||
return txbuilder.NewTxBuilder(network.Liquid), nil
|
||||
}
|
||||
|
||||
func createTestPoolTx(sharedOutputAmount, numberOfInputs uint64) (string, error) {
|
||||
_, key, err := common.DecodePubKey(testingKey)
|
||||
if err != nil {
|
||||
@@ -76,20 +74,45 @@ func createTestPoolTx(sharedOutputAmount, numberOfInputs uint64) (string, error)
|
||||
return pset.ToBase64()
|
||||
}
|
||||
|
||||
type mockedWalletService struct{}
|
||||
|
||||
// BroadcastTransaction implements ports.WalletService.
|
||||
func (*mockedWalletService) BroadcastTransaction(ctx context.Context, txHex string) (string, error) {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// Close implements ports.WalletService.
|
||||
func (*mockedWalletService) Close() {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// DeriveAddresses implements ports.WalletService.
|
||||
func (*mockedWalletService) DeriveAddresses(ctx context.Context, num int) ([]string, error) {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// GetPubkey implements ports.WalletService.
|
||||
func (*mockedWalletService) GetPubkey(ctx context.Context) (*secp256k1.PublicKey, error) {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// SignPset implements ports.WalletService.
|
||||
func (*mockedWalletService) SignPset(ctx context.Context, pset string, extractRawTx bool) (string, error) {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// Status implements ports.WalletService.
|
||||
func (*mockedWalletService) Status(ctx context.Context) (ports.WalletStatus, error) {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// Transfer implements ports.WalletService.
|
||||
func (*mockedWalletService) Transfer(ctx context.Context, outs []ports.TxOutput) (string, error) {
|
||||
return createTestPoolTx(1000, (450+500)*1)
|
||||
}
|
||||
|
||||
func TestBuildCongestionTree(t *testing.T) {
|
||||
builder, err := createTestTxBuilder()
|
||||
require.NoError(t, err)
|
||||
|
||||
poolTx, err := createTestPoolTx(1000, (450+500)*1)
|
||||
require.NoError(t, err)
|
||||
|
||||
poolPset, err := psetv2.NewPsetFromBase64(poolTx)
|
||||
require.NoError(t, err)
|
||||
|
||||
poolTxUnsigned, err := poolPset.UnsignedTx()
|
||||
require.NoError(t, err)
|
||||
|
||||
poolTxID := poolTxUnsigned.TxHash().String()
|
||||
builder := txbuilder.NewTxBuilder(network.Liquid)
|
||||
|
||||
fixtures := []struct {
|
||||
payments []domain.Payment
|
||||
@@ -215,11 +238,19 @@ func TestBuildCongestionTree(t *testing.T) {
|
||||
require.NotNil(t, key)
|
||||
|
||||
for _, f := range fixtures {
|
||||
tree, err := builder.BuildCongestionTree(key, poolTx, f.payments)
|
||||
poolTx, tree, err := builder.BuildPoolTx(key, &mockedWalletService{}, f.payments)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, f.expectedNodesNum, tree.NumberOfNodes())
|
||||
require.Len(t, tree.Leaves(), f.expectedLeavesNum)
|
||||
|
||||
poolPset, err := psetv2.NewPsetFromBase64(poolTx)
|
||||
require.NoError(t, err)
|
||||
|
||||
poolTxUnsigned, err := poolPset.UnsignedTx()
|
||||
require.NoError(t, err)
|
||||
|
||||
poolTxID := poolTxUnsigned.TxHash().String()
|
||||
|
||||
// check the root
|
||||
require.Len(t, tree[0], 1)
|
||||
require.Equal(t, poolTxID, tree[0][0].ParentTxid)
|
||||
@@ -237,7 +268,7 @@ func TestBuildCongestionTree(t *testing.T) {
|
||||
}
|
||||
|
||||
// check the nodes
|
||||
for i, level := range tree[:len(tree)-2] {
|
||||
for _, level := range tree[:len(tree)-2] {
|
||||
for _, node := range level {
|
||||
pset, err := psetv2.NewPsetFromBase64(node.Tx)
|
||||
require.NoError(t, err)
|
||||
@@ -248,22 +279,15 @@ func TestBuildCongestionTree(t *testing.T) {
|
||||
inputTxID := chainhash.Hash(pset.Inputs[0].PreviousTxid).String()
|
||||
require.Equal(t, node.ParentTxid, inputTxID)
|
||||
|
||||
nextLevel := tree[i+1]
|
||||
childs := 0
|
||||
for _, n := range nextLevel {
|
||||
if n.ParentTxid == node.Txid {
|
||||
childs++
|
||||
}
|
||||
}
|
||||
require.Equal(t, 2, childs)
|
||||
children := tree.Children(node.Txid)
|
||||
require.Len(t, children, 2)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildForfeitTxs(t *testing.T) {
|
||||
builder, err := createTestTxBuilder()
|
||||
require.NoError(t, err)
|
||||
builder := txbuilder.NewTxBuilder(network.Liquid)
|
||||
|
||||
poolTx, err := createTestPoolTx(1000, 450*2)
|
||||
require.NoError(t, err)
|
||||
|
||||
Reference in New Issue
Block a user