Use connectors utxos from swept rounds (#167)

* use connectors utxos from swept rounds

* revert docker-compose.regtest.yml

* add gitignore

* fix integration tests
This commit is contained in:
Louis Singer
2024-05-29 14:34:35 +02:00
committed by GitHub
parent dca302df69
commit b5bac540ef
16 changed files with 167 additions and 76 deletions

1
client/.gitignore vendored
View File

@@ -1 +1,2 @@
/build/ /build/
.vscode/

View File

@@ -38,6 +38,9 @@ func getVtxos(
t := time.Unix(v.ExpireAt, 0) t := time.Unix(v.ExpireAt, 0)
expireAt = &t expireAt = &t
} }
if v.Swept {
continue
}
vtxos = append(vtxos, vtxo{ vtxos = append(vtxos, vtxo{
amount: v.Receiver.Amount, amount: v.Receiver.Amount,
txid: v.Outpoint.Txid, txid: v.Outpoint.Txid,

1
common/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
.vscode/

View File

@@ -29,6 +29,7 @@ services:
- ARK_ROUND_INTERVAL=10 - ARK_ROUND_INTERVAL=10
- ARK_NETWORK=regtest - ARK_NETWORK=regtest
- ARK_LOG_LEVEL=5 - ARK_LOG_LEVEL=5
- ARK_ROUND_LIFETIME=512
ports: ports:
- "6000:6000" - "6000:6000"

View File

@@ -382,7 +382,14 @@ func (s *service) startFinalization() {
return return
} }
unsignedPoolTx, tree, connectorAddress, err := s.builder.BuildPoolTx(s.pubkey, payments, s.minRelayFee) sweptRounds, err := s.repoManager.Rounds().GetSweptRounds(ctx)
if err != nil {
changes = round.Fail(fmt.Errorf("failed to retrieve swept rounds: %s", err))
log.WithError(err).Warn("failed to retrieve swept rounds")
return
}
unsignedPoolTx, tree, connectorAddress, err := s.builder.BuildPoolTx(s.pubkey, payments, s.minRelayFee, sweptRounds)
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")
@@ -619,7 +626,12 @@ func (s *service) listenToScannerNotifications() {
continue continue
} }
signedForfeitTx, err := s.wallet.SignConnectorInput(ctx, forfeitTx, []int{0}, false) if err := s.wallet.LockConnectorUtxos(ctx, []ports.TxOutpoint{txOutpoint{connectorTxid, connectorVout}}); err != nil {
log.WithError(err).Warn("failed to lock connector utxos")
continue
}
signedForfeitTx, err := s.wallet.SignPset(ctx, forfeitTx, false)
if err != nil { if err != nil {
log.WithError(err).Warn("failed to sign connector input in forfeit tx") log.WithError(err).Warn("failed to sign connector input in forfeit tx")
continue continue
@@ -697,8 +709,14 @@ func (s *service) getNextConnector(
for _, i := range pset.Inputs { for _, i := range pset.Inputs {
if chainhash.Hash(i.PreviousTxid).String() == u.GetTxid() && i.PreviousTxIndex == u.GetIndex() { if chainhash.Hash(i.PreviousTxid).String() == u.GetTxid() && i.PreviousTxIndex == u.GetIndex() {
connectorOutpoint := newOutpointFromPsetInput(pset.Inputs[0])
if err := s.wallet.LockConnectorUtxos(ctx, []ports.TxOutpoint{connectorOutpoint}); err != nil {
return "", 0, err
}
// sign & broadcast the connector tx // sign & broadcast the connector tx
signedConnectorTx, err := s.wallet.SignConnectorInput(ctx, b64, []int{0}, true) signedConnectorTx, err := s.wallet.SignPset(ctx, b64, true)
if err != nil { if err != nil {
return "", 0, err return "", 0, err
} }
@@ -1015,3 +1033,23 @@ func findForfeitTx(
return "", fmt.Errorf("forfeit tx not found") return "", fmt.Errorf("forfeit tx not found")
} }
type txOutpoint struct {
txid string
vout uint32
}
func newOutpointFromPsetInput(input psetv2.Input) txOutpoint {
return txOutpoint{
txid: chainhash.Hash(input.PreviousTxid).String(),
vout: input.PreviousTxIndex,
}
}
func (outpoint txOutpoint) GetTxid() string {
return outpoint.txid
}
func (outpoint txOutpoint) GetIndex() uint32 {
return outpoint.vout
}

View File

@@ -3,6 +3,7 @@ package application
import ( import (
"context" "context"
"fmt" "fmt"
"strings"
"time" "time"
"github.com/ark-network/ark/common/tree" "github.com/ark-network/ark/common/tree"
@@ -227,7 +228,7 @@ func (s *sweeper) createTask(
err = nil err = nil
txid := "" txid := ""
// retry until the tx is broadcasted or the error is not BIP68 final // retry until the tx is broadcasted or the error is not BIP68 final
for len(txid) == 0 && (err == nil || err == fmt.Errorf("non-BIP68-final")) { for len(txid) == 0 && (err == nil || strings.Contains(err.Error(), "non-BIP68-final")) {
if err != nil { if err != nil {
log.Debugln("sweep tx not BIP68 final, retrying in 5 seconds") log.Debugln("sweep tx not BIP68 final, retrying in 5 seconds")
time.Sleep(5 * time.Second) time.Sleep(5 * time.Second)

View File

@@ -15,6 +15,7 @@ type RoundRepository interface {
GetRoundWithId(ctx context.Context, id string) (*Round, error) GetRoundWithId(ctx context.Context, id string) (*Round, error)
GetRoundWithTxid(ctx context.Context, txid string) (*Round, error) GetRoundWithTxid(ctx context.Context, txid string) (*Round, error)
GetSweepableRounds(ctx context.Context) ([]Round, error) GetSweepableRounds(ctx context.Context) ([]Round, error)
GetSweptRounds(ctx context.Context) ([]Round, error)
} }
type VtxoRepository interface { type VtxoRepository interface {

View File

@@ -14,7 +14,7 @@ type SweepInput struct {
} }
type TxBuilder interface { type TxBuilder interface {
BuildPoolTx(aspPubkey *secp256k1.PublicKey, payments []domain.Payment, minRelayFee uint64) (poolTx string, congestionTree tree.CongestionTree, connectorAddress string, err error) BuildPoolTx(aspPubkey *secp256k1.PublicKey, payments []domain.Payment, minRelayFee uint64, sweptRounds []domain.Round) (poolTx string, congestionTree tree.CongestionTree, connectorAddress string, err error)
BuildForfeitTxs(aspPubkey *secp256k1.PublicKey, poolTx string, payments []domain.Payment, minRelayFee uint64) (connectors []string, forfeitTxs []string, err error) BuildForfeitTxs(aspPubkey *secp256k1.PublicKey, poolTx string, payments []domain.Payment, minRelayFee uint64) (connectors []string, forfeitTxs []string, err error)
BuildSweepTx(inputs []SweepInput) (signedSweepTx string, err error) BuildSweepTx(inputs []SweepInput) (signedSweepTx string, err error)
GetVtxoScript(userPubkey, aspPubkey *secp256k1.PublicKey) ([]byte, error) GetVtxoScript(userPubkey, aspPubkey *secp256k1.PublicKey) ([]byte, error)

View File

@@ -21,8 +21,8 @@ type WalletService interface {
IsTransactionConfirmed(ctx context.Context, txid string) (isConfirmed bool, blocktime int64, err error) IsTransactionConfirmed(ctx context.Context, txid string) (isConfirmed bool, blocktime int64, err error)
WaitForSync(ctx context.Context, txid string) error WaitForSync(ctx context.Context, txid string) error
EstimateFees(ctx context.Context, pset string) (uint64, error) EstimateFees(ctx context.Context, pset string) (uint64, error)
SignConnectorInput(ctx context.Context, pset string, inputIndexes []int, extract bool) (string, error)
ListConnectorUtxos(ctx context.Context, connectorAddress string) ([]TxInput, error) ListConnectorUtxos(ctx context.Context, connectorAddress string) ([]TxInput, error)
LockConnectorUtxos(ctx context.Context, utxos []TxOutpoint) error
Close() Close()
} }
@@ -39,3 +39,8 @@ type TxInput interface {
GetAsset() string GetAsset() string
GetValue() uint64 GetValue() uint64
} }
type TxOutpoint interface {
GetTxid() string
GetIndex() uint32
}

View File

@@ -100,11 +100,13 @@ func (r *roundRepository) GetSweepableRounds(
) ([]domain.Round, error) { ) ([]domain.Round, error) {
query := badgerhold.Where("Stage.Code").Eq(domain.FinalizationStage). query := badgerhold.Where("Stage.Code").Eq(domain.FinalizationStage).
And("Stage.Ended").Eq(true).And("Swept").Eq(false) And("Stage.Ended").Eq(true).And("Swept").Eq(false)
rounds, err := r.findRound(ctx, query) return r.findRound(ctx, query)
if err != nil { }
return nil, err
} func (r *roundRepository) GetSweptRounds(ctx context.Context) ([]domain.Round, error) {
return rounds, nil query := badgerhold.Where("Stage.Code").Eq(domain.FinalizationStage).
And("Stage.Ended").Eq(true).And("Swept").Eq(true).And("ConnectorAddress").Ne("")
return r.findRound(ctx, query)
} }
func (r *roundRepository) Close() { func (r *roundRepository) Close() {

View File

@@ -59,7 +59,6 @@ func (s *service) SignPset(
} }
func (s *service) SelectUtxos(ctx context.Context, asset string, amount uint64) ([]ports.TxInput, uint64, error) { func (s *service) SelectUtxos(ctx context.Context, asset string, amount uint64) ([]ports.TxInput, uint64, error) {
// TODO: select coins from the connector account IF the round is swept
res, err := s.txClient.SelectUtxos(ctx, &pb.SelectUtxosRequest{ res, err := s.txClient.SelectUtxos(ctx, &pb.SelectUtxosRequest{
AccountName: arkAccount, AccountName: arkAccount,
TargetAsset: asset, TargetAsset: asset,
@@ -82,24 +81,6 @@ func (s *service) SelectUtxos(ctx context.Context, asset string, amount uint64)
return inputs, res.GetChange(), nil return inputs, res.GetChange(), nil
} }
func (s *service) getTransaction(
ctx context.Context, txid string,
) (string, bool, int64, error) {
res, err := s.txClient.GetTransaction(ctx, &pb.GetTransactionRequest{
Txid: txid,
})
if err != nil {
return "", false, 0, err
}
if res.GetBlockDetails().GetTimestamp() > 0 {
return res.GetTxHex(), true, res.BlockDetails.GetTimestamp(), nil
}
// if not confirmed, we return now + 1 min to estimate the next blocktime
return res.GetTxHex(), false, time.Now().Add(time.Minute).Unix(), nil
}
func (s *service) BroadcastTransaction( func (s *service) BroadcastTransaction(
ctx context.Context, txHex string, ctx context.Context, txHex string,
) (string, error) { ) (string, error) {
@@ -221,36 +202,20 @@ func (s *service) SignPsetWithKey(ctx context.Context, b64 string, indexes []int
return signedPset.GetSignedTx(), nil return signedPset.GetSignedTx(), nil
} }
func (s *service) SignConnectorInput(ctx context.Context, pset string, inputIndexes []int, extract bool) (string, error) { func (s *service) LockConnectorUtxos(ctx context.Context, utxos []ports.TxOutpoint) error {
decodedTx, err := psetv2.NewPsetFromBase64(pset) pbUtxos := make([]*pb.Input, 0, len(utxos))
if err != nil { for _, utxo := range utxos {
return "", err pbUtxos = append(pbUtxos, &pb.Input{
} Txid: utxo.GetTxid(),
Index: utxo.GetIndex(),
utxos := make([]*pb.Input, 0, len(decodedTx.Inputs))
for i := range inputIndexes {
if i >= len(decodedTx.Inputs) {
return "", fmt.Errorf("input index %d out of range", i)
}
input := decodedTx.Inputs[i]
utxos = append(utxos, &pb.Input{
Txid: chainhash.Hash(input.PreviousTxid).String(),
Index: input.PreviousTxIndex,
}) })
} }
_, err = s.txClient.LockUtxos(ctx, &pb.LockUtxosRequest{ _, err := s.txClient.LockUtxos(ctx, &pb.LockUtxosRequest{
AccountName: connectorAccount, AccountName: connectorAccount,
Utxos: utxos, Utxos: pbUtxos,
}) })
if err != nil { return err
return "", err
}
return s.SignPset(ctx, pset, extract)
} }
func (s *service) EstimateFees( func (s *service) EstimateFees(
@@ -315,3 +280,21 @@ func (s *service) EstimateFees(
// we add 5 sats in order to avoid min-relay-fee not met errors // we add 5 sats in order to avoid min-relay-fee not met errors
return fee.GetFeeAmount() + 5, nil return fee.GetFeeAmount() + 5, nil
} }
func (s *service) getTransaction(
ctx context.Context, txid string,
) (string, bool, int64, error) {
res, err := s.txClient.GetTransaction(ctx, &pb.GetTransactionRequest{
Txid: txid,
})
if err != nil {
return "", false, 0, err
}
if res.GetBlockDetails().GetTimestamp() > 0 {
return res.GetTxHex(), true, res.BlockDetails.GetTimestamp(), nil
}
// if not confirmed, we return now + 1 min to estimate the next blocktime
return res.GetTxHex(), false, time.Now().Add(time.Minute).Unix(), nil
}

View File

@@ -29,7 +29,10 @@ type txBuilder struct {
} }
func NewTxBuilder( func NewTxBuilder(
wallet ports.WalletService, net network.Network, roundLifetime int64, exitDelay int64, wallet ports.WalletService,
net network.Network,
roundLifetime int64,
exitDelay int64,
) ports.TxBuilder { ) ports.TxBuilder {
return &txBuilder{wallet, &net, roundLifetime, exitDelay} return &txBuilder{wallet, &net, roundLifetime, exitDelay}
} }
@@ -106,7 +109,7 @@ func (b *txBuilder) BuildForfeitTxs(
} }
func (b *txBuilder) BuildPoolTx( func (b *txBuilder) BuildPoolTx(
aspPubkey *secp256k1.PublicKey, payments []domain.Payment, minRelayFee uint64, aspPubkey *secp256k1.PublicKey, payments []domain.Payment, minRelayFee uint64, sweptRounds []domain.Round,
) (poolTx string, congestionTree tree.CongestionTree, connectorAddress string, err error) { ) (poolTx string, congestionTree tree.CongestionTree, connectorAddress string, err error) {
// The creation of the tree and the pool tx are tightly coupled: // The creation of the tree and the pool tx are tightly coupled:
// - building the tree requires knowing the shared outpoint (txid:vout) // - building the tree requires knowing the shared outpoint (txid:vout)
@@ -139,7 +142,7 @@ func (b *txBuilder) BuildPoolTx(
} }
ptx, err := b.createPoolTx( ptx, err := b.createPoolTx(
sharedOutputAmount, sharedOutputScript, payments, aspPubkey, connectorAddress, minRelayFee, sharedOutputAmount, sharedOutputScript, payments, aspPubkey, connectorAddress, minRelayFee, sweptRounds,
) )
if err != nil { if err != nil {
return return
@@ -210,6 +213,7 @@ func (b *txBuilder) getLeafScriptAndTree(
func (b *txBuilder) createPoolTx( func (b *txBuilder) createPoolTx(
sharedOutputAmount uint64, sharedOutputScript []byte, sharedOutputAmount uint64, sharedOutputScript []byte,
payments []domain.Payment, aspPubKey *secp256k1.PublicKey, connectorAddress string, minRelayFee uint64, payments []domain.Payment, aspPubKey *secp256k1.PublicKey, connectorAddress string, minRelayFee uint64,
sweptRounds []domain.Round,
) (*psetv2.Pset, error) { ) (*psetv2.Pset, error) {
aspScript, err := p2wpkhScript(aspPubKey, b.net) aspScript, err := p2wpkhScript(aspPubKey, b.net)
if err != nil { if err != nil {
@@ -263,7 +267,7 @@ func (b *txBuilder) createPoolTx(
} }
ctx := context.Background() ctx := context.Background()
utxos, change, err := b.wallet.SelectUtxos(ctx, b.net.AssetID, targetAmount) utxos, change, err := b.selectUtxos(ctx, sweptRounds, targetAmount)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -325,7 +329,7 @@ func (b *txBuilder) createPoolTx(
// remove change output if present // remove change output if present
ptx.Outputs = ptx.Outputs[:len(ptx.Outputs)-1] ptx.Outputs = ptx.Outputs[:len(ptx.Outputs)-1]
} }
newUtxos, change, err := b.wallet.SelectUtxos(ctx, b.net.AssetID, feeAmount-change) newUtxos, change, err := b.selectUtxos(ctx, sweptRounds, feeAmount-change)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -347,7 +351,7 @@ func (b *txBuilder) createPoolTx(
} }
} }
} else if feeAmount-dust > 0 { } else if feeAmount-dust > 0 {
newUtxos, change, err := b.wallet.SelectUtxos(ctx, b.net.AssetID, feeAmount-dust) newUtxos, change, err := b.selectUtxos(ctx, sweptRounds, feeAmount-dust)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -60,7 +60,7 @@ func TestBuildPoolTx(t *testing.T) {
t.Run("valid", func(t *testing.T) { t.Run("valid", func(t *testing.T) {
for _, f := range fixtures.Valid { for _, f := range fixtures.Valid {
poolTx, congestionTree, connAddr, err := builder.BuildPoolTx( poolTx, congestionTree, connAddr, err := builder.BuildPoolTx(
pubkey, f.Payments, minRelayFee, pubkey, f.Payments, minRelayFee, []domain.Round{},
) )
require.NoError(t, err) require.NoError(t, err)
require.NotEmpty(t, poolTx) require.NotEmpty(t, poolTx)
@@ -81,7 +81,7 @@ func TestBuildPoolTx(t *testing.T) {
t.Run("invalid", func(t *testing.T) { t.Run("invalid", func(t *testing.T) {
for _, f := range fixtures.Invalid { for _, f := range fixtures.Invalid {
poolTx, congestionTree, connAddr, err := builder.BuildPoolTx( poolTx, congestionTree, connAddr, err := builder.BuildPoolTx(
pubkey, f.Payments, minRelayFee, pubkey, f.Payments, minRelayFee, []domain.Round{},
) )
require.EqualError(t, err, f.ExpectedErr) require.EqualError(t, err, f.ExpectedErr)
require.Empty(t, poolTx) require.Empty(t, poolTx)

View File

@@ -0,0 +1,57 @@
package txbuilder
import (
"context"
"github.com/ark-network/ark/internal/core/domain"
"github.com/ark-network/ark/internal/core/ports"
)
func (b *txBuilder) selectUtxos(ctx context.Context, sweptRounds []domain.Round, amount uint64) ([]ports.TxInput, uint64, error) {
selectedConnectorsUtxos := make([]ports.TxInput, 0)
selectedConnectorsAmount := uint64(0)
for _, round := range sweptRounds {
if selectedConnectorsAmount >= amount {
break
}
connectors, err := b.wallet.ListConnectorUtxos(ctx, round.ConnectorAddress)
if err != nil {
return nil, 0, err
}
for _, connector := range connectors {
if selectedConnectorsAmount >= amount {
break
}
selectedConnectorsUtxos = append(selectedConnectorsUtxos, connector)
selectedConnectorsAmount += connector.GetValue()
}
}
if len(selectedConnectorsUtxos) > 0 {
if err := b.wallet.LockConnectorUtxos(ctx, castToOutpoints(selectedConnectorsUtxos)); err != nil {
return nil, 0, err
}
}
if selectedConnectorsAmount >= amount {
return selectedConnectorsUtxos, selectedConnectorsAmount - amount, nil
}
utxos, change, err := b.wallet.SelectUtxos(ctx, b.net.AssetID, amount-selectedConnectorsAmount)
if err != nil {
return nil, 0, err
}
return append(selectedConnectorsUtxos, utxos...), change, nil
}
func castToOutpoints(inputs []ports.TxInput) []ports.TxOutpoint {
outpoints := make([]ports.TxOutpoint, 0, len(inputs))
for _, input := range inputs {
outpoints = append(outpoints, input)
}
return outpoints
}

View File

@@ -133,17 +133,6 @@ func (m *mockedWallet) SignPsetWithKey(ctx context.Context, pset string, inputIn
return res, args.Error(1) return res, args.Error(1)
} }
func (m *mockedWallet) SignConnectorInput(ctx context.Context, pset string, inputIndexes []int, extract bool) (string, error) {
args := m.Called(ctx, pset, inputIndexes, extract)
var res string
if a := args.Get(0); a != nil {
res = a.(string)
}
return res, args.Error(1)
}
func (m *mockedWallet) WatchScripts( func (m *mockedWallet) WatchScripts(
ctx context.Context, scripts []string, ctx context.Context, scripts []string,
) error { ) error {
@@ -179,6 +168,11 @@ func (m *mockedWallet) ListConnectorUtxos(ctx context.Context, addr string) ([]p
return res, args.Error(1) return res, args.Error(1)
} }
func (m *mockedWallet) LockConnectorUtxos(ctx context.Context, utxos []ports.TxOutpoint) error {
args := m.Called(ctx, utxos)
return args.Error(0)
}
func (m *mockedWallet) WaitForSync(ctx context.Context, txid string) error { func (m *mockedWallet) WaitForSync(ctx context.Context, txid string) error {
args := m.Called(ctx, txid) args := m.Called(ctx, txid)
return args.Error(0) return args.Error(0)

View File

@@ -204,7 +204,7 @@ func TestUnilateralExit(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.NoError(t, json.Unmarshal([]byte(balanceStr), &balance)) require.NoError(t, json.Unmarshal([]byte(balanceStr), &balance))
require.Zero(t, balance.Offchain.Total) require.Zero(t, balance.Offchain.Total)
require.Len(t, balance.Onchain.Locked, 1) require.Greater(t, len(balance.Onchain.Locked), 0)
lockedBalance := balance.Onchain.Locked[0].Amount lockedBalance := balance.Onchain.Locked[0].Amount
require.NotZero(t, lockedBalance) require.NotZero(t, lockedBalance)