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/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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
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/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) {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user