mirror of
https://github.com/aljazceru/ark.git
synced 2025-12-17 20:24:21 +01:00
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:
1
client/.gitignore
vendored
1
client/.gitignore
vendored
@@ -1 +1,2 @@
|
||||
/build/
|
||||
.vscode/
|
||||
@@ -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
1
common/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.vscode/
|
||||
@@ -29,6 +29,7 @@ services:
|
||||
- ARK_ROUND_INTERVAL=10
|
||||
- ARK_NETWORK=regtest
|
||||
- ARK_LOG_LEVEL=5
|
||||
- ARK_ROUND_LIFETIME=512
|
||||
ports:
|
||||
- "6000:6000"
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user