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

3
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)
expireAt = &t
}
if v.Swept {
continue
}
vtxos = append(vtxos, vtxo{
amount: v.Receiver.Amount,
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_NETWORK=regtest
- ARK_LOG_LEVEL=5
- ARK_ROUND_LIFETIME=512
ports:
- "6000:6000"

View File

@@ -382,7 +382,14 @@ func (s *service) startFinalization() {
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 {
changes = round.Fail(fmt.Errorf("failed to create pool tx: %s", err))
log.WithError(err).Warn("failed to create pool tx")
@@ -619,7 +626,12 @@ func (s *service) listenToScannerNotifications() {
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 {
log.WithError(err).Warn("failed to sign connector input in forfeit tx")
continue
@@ -697,8 +709,14 @@ func (s *service) getNextConnector(
for _, i := range pset.Inputs {
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
signedConnectorTx, err := s.wallet.SignConnectorInput(ctx, b64, []int{0}, true)
signedConnectorTx, err := s.wallet.SignPset(ctx, b64, true)
if err != nil {
return "", 0, err
}
@@ -1015,3 +1033,23 @@ func findForfeitTx(
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 (
"context"
"fmt"
"strings"
"time"
"github.com/ark-network/ark/common/tree"
@@ -227,7 +228,7 @@ func (s *sweeper) createTask(
err = nil
txid := ""
// 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 {
log.Debugln("sweep tx not BIP68 final, retrying in 5 seconds")
time.Sleep(5 * time.Second)

View File

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

View File

@@ -14,7 +14,7 @@ type SweepInput struct {
}
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)
BuildSweepTx(inputs []SweepInput) (signedSweepTx string, err 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)
WaitForSync(ctx context.Context, txid string) 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)
LockConnectorUtxos(ctx context.Context, utxos []TxOutpoint) error
Close()
}
@@ -39,3 +39,8 @@ type TxInput interface {
GetAsset() string
GetValue() uint64
}
type TxOutpoint interface {
GetTxid() string
GetIndex() uint32
}

View File

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

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) {
// TODO: select coins from the connector account IF the round is swept
res, err := s.txClient.SelectUtxos(ctx, &pb.SelectUtxosRequest{
AccountName: arkAccount,
TargetAsset: asset,
@@ -82,24 +81,6 @@ func (s *service) SelectUtxos(ctx context.Context, asset string, amount uint64)
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(
ctx context.Context, txHex string,
) (string, error) {
@@ -221,36 +202,20 @@ func (s *service) SignPsetWithKey(ctx context.Context, b64 string, indexes []int
return signedPset.GetSignedTx(), nil
}
func (s *service) SignConnectorInput(ctx context.Context, pset string, inputIndexes []int, extract bool) (string, error) {
decodedTx, err := psetv2.NewPsetFromBase64(pset)
if err != nil {
return "", err
}
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,
func (s *service) LockConnectorUtxos(ctx context.Context, utxos []ports.TxOutpoint) error {
pbUtxos := make([]*pb.Input, 0, len(utxos))
for _, utxo := range utxos {
pbUtxos = append(pbUtxos, &pb.Input{
Txid: utxo.GetTxid(),
Index: utxo.GetIndex(),
})
}
_, err = s.txClient.LockUtxos(ctx, &pb.LockUtxosRequest{
_, err := s.txClient.LockUtxos(ctx, &pb.LockUtxosRequest{
AccountName: connectorAccount,
Utxos: utxos,
Utxos: pbUtxos,
})
if err != nil {
return "", err
}
return s.SignPset(ctx, pset, extract)
return err
}
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
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(
wallet ports.WalletService, net network.Network, roundLifetime int64, exitDelay int64,
wallet ports.WalletService,
net network.Network,
roundLifetime int64,
exitDelay int64,
) ports.TxBuilder {
return &txBuilder{wallet, &net, roundLifetime, exitDelay}
}
@@ -106,7 +109,7 @@ func (b *txBuilder) BuildForfeitTxs(
}
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) {
// The creation of the tree and the pool tx are tightly coupled:
// - building the tree requires knowing the shared outpoint (txid:vout)
@@ -139,7 +142,7 @@ func (b *txBuilder) BuildPoolTx(
}
ptx, err := b.createPoolTx(
sharedOutputAmount, sharedOutputScript, payments, aspPubkey, connectorAddress, minRelayFee,
sharedOutputAmount, sharedOutputScript, payments, aspPubkey, connectorAddress, minRelayFee, sweptRounds,
)
if err != nil {
return
@@ -210,6 +213,7 @@ func (b *txBuilder) getLeafScriptAndTree(
func (b *txBuilder) createPoolTx(
sharedOutputAmount uint64, sharedOutputScript []byte,
payments []domain.Payment, aspPubKey *secp256k1.PublicKey, connectorAddress string, minRelayFee uint64,
sweptRounds []domain.Round,
) (*psetv2.Pset, error) {
aspScript, err := p2wpkhScript(aspPubKey, b.net)
if err != nil {
@@ -263,7 +267,7 @@ func (b *txBuilder) createPoolTx(
}
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 {
return nil, err
}
@@ -325,7 +329,7 @@ func (b *txBuilder) createPoolTx(
// remove change output if present
ptx.Outputs = ptx.Outputs[:len(ptx.Outputs)-1]
}
newUtxos, change, err := b.wallet.SelectUtxos(ctx, b.net.AssetID, feeAmount-change)
newUtxos, change, err := b.selectUtxos(ctx, sweptRounds, feeAmount-change)
if err != nil {
return nil, err
}
@@ -347,7 +351,7 @@ func (b *txBuilder) createPoolTx(
}
}
} 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 {
return nil, err
}

View File

@@ -60,7 +60,7 @@ func TestBuildPoolTx(t *testing.T) {
t.Run("valid", func(t *testing.T) {
for _, f := range fixtures.Valid {
poolTx, congestionTree, connAddr, err := builder.BuildPoolTx(
pubkey, f.Payments, minRelayFee,
pubkey, f.Payments, minRelayFee, []domain.Round{},
)
require.NoError(t, err)
require.NotEmpty(t, poolTx)
@@ -81,7 +81,7 @@ func TestBuildPoolTx(t *testing.T) {
t.Run("invalid", func(t *testing.T) {
for _, f := range fixtures.Invalid {
poolTx, congestionTree, connAddr, err := builder.BuildPoolTx(
pubkey, f.Payments, minRelayFee,
pubkey, f.Payments, minRelayFee, []domain.Round{},
)
require.EqualError(t, err, f.ExpectedErr)
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)
}
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(
ctx context.Context, scripts []string,
) error {
@@ -179,6 +168,11 @@ func (m *mockedWallet) ListConnectorUtxos(ctx context.Context, addr string) ([]p
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 {
args := m.Called(ctx, txid)
return args.Error(0)

View File

@@ -204,7 +204,7 @@ func TestUnilateralExit(t *testing.T) {
require.NoError(t, err)
require.NoError(t, json.Unmarshal([]byte(balanceStr), &balance))
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
require.NotZero(t, lockedBalance)