Add tests for adversarial scenarios (#300)

* fix and test cheating scenario (malicious double spend)

* test and fix async vtxo cheating cases

* add replace statement in go.mod

* Update server/internal/core/application/covenantless.go

Co-authored-by: Pietralberto Mazza <18440657+altafan@users.noreply.github.com>
Signed-off-by: Louis Singer <41042567+louisinger@users.noreply.github.com>

* Update server/internal/infrastructure/wallet/btc-embedded/psbt.go

Co-authored-by: Pietralberto Mazza <18440657+altafan@users.noreply.github.com>
Signed-off-by: Louis Singer <41042567+louisinger@users.noreply.github.com>

* Update server/test/e2e/covenant/e2e_test.go

Co-authored-by: Pietralberto Mazza <18440657+altafan@users.noreply.github.com>
Signed-off-by: Louis Singer <41042567+louisinger@users.noreply.github.com>

* Update server/test/e2e/covenantless/e2e_test.go

Co-authored-by: Pietralberto Mazza <18440657+altafan@users.noreply.github.com>
Signed-off-by: Louis Singer <41042567+louisinger@users.noreply.github.com>

* Update server/test/e2e/covenantless/e2e_test.go

Co-authored-by: Pietralberto Mazza <18440657+altafan@users.noreply.github.com>
Signed-off-by: Louis Singer <41042567+louisinger@users.noreply.github.com>

* remove unused

* [btc-embedded] fix GetNotificationChannel

* [tx-builder] fix redeem transaction fee estimator

* close grpc client in tests

* [application] rework listentoscannerNotification

* [application][covenant] fix getConnectorAmount

* [tx-builder][covenant] get connector amount from wallet

* e2e test sleep time

* [liquid-standalone] ListConnectorUtxos: filter by script client side

* fix Makefile integrationtest

* do not use cache in integration tests

* use VtxoKey as argument of findForfeitTxBitcoin

* wrap adversarial test in t.Run

* increaste test timeout

* CI: setup go 1.23.1

* CI: revert go version

* add replace in server/go.mod

* Update server/internal/core/application/covenant.go

Co-authored-by: Pietralberto Mazza <18440657+altafan@users.noreply.github.com>
Signed-off-by: Louis Singer <41042567+louisinger@users.noreply.github.com>

* remove replace

* readd replace statement

* fixes

* go work sync

* fix CI

---------

Signed-off-by: Louis Singer <41042567+louisinger@users.noreply.github.com>
Co-authored-by: Pietralberto Mazza <18440657+altafan@users.noreply.github.com>
This commit is contained in:
Louis Singer
2024-09-16 17:03:43 +02:00
committed by GitHub
parent 4c8c5c06ed
commit 3782793431
31 changed files with 709 additions and 275 deletions

View File

@@ -12,9 +12,9 @@ jobs:
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v2 uses: actions/setup-go@v4
with: with:
go-version: 1.21.0 go-version: 1.23.1
- name: Build binaries - name: Build binaries
run: make build-all run: make build-all

View File

@@ -2,7 +2,8 @@ name: ci_integration
on: on:
push: push:
branches: [master] branches:
- master
pull_request: pull_request:
branches: branches:
- master - master
@@ -15,11 +16,12 @@ jobs:
run: run:
working-directory: ./server working-directory: ./server
steps: steps:
- uses: actions/setup-go@v3
with:
go-version: ">1.17.2"
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- run: go get -v -t -d ./... - uses: actions/setup-go@v4
with:
go-version: '>=1.23.1'
- name: Run go work sync
run: go work sync
- name: Run Nigiri - name: Run Nigiri
uses: vulpemventures/nigiri-github-action@v1 uses: vulpemventures/nigiri-github-action@v1

View File

@@ -16,9 +16,9 @@ jobs:
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v2 uses: actions/setup-go@v4
with: with:
go-version: 1.21.0 go-version: 1.23.1
# Build binaries for all architectures # Build binaries for all architectures
- name: Build binaries - name: Build binaries

View File

@@ -2,10 +2,11 @@ name: ci_unit
on: on:
push: push:
branches:
- master
paths: paths:
- 'server/**' - 'server/**'
- 'pkg/client-sdk/**' - 'pkg/client-sdk/**'
branches: [master]
pull_request: pull_request:
branches: branches:
- master - master
@@ -56,7 +57,8 @@ jobs:
uses: securego/gosec@master uses: securego/gosec@master
with: with:
args: '-severity high -quiet -exclude=G115 ./...' args: '-severity high -quiet -exclude=G115 ./...'
- run: go get -v -t -d ./... - name: Run go work sync
run: go work sync
- name: unit testing - name: unit testing
run: make test run: make test
@@ -83,4 +85,4 @@ jobs:
args: '-severity high -quiet -exclude=G115 ./...' args: '-severity high -quiet -exclude=G115 ./...'
- run: go get -v -t -d ./... - run: go get -v -t -d ./...
- name: unit testing - name: unit testing
run: make test run: make test

View File

@@ -11,7 +11,6 @@ services:
- ARK_LOG_LEVEL=5 - ARK_LOG_LEVEL=5
- ARK_ROUND_LIFETIME=512 - ARK_ROUND_LIFETIME=512
- ARK_TX_BUILDER_TYPE=covenantless - ARK_TX_BUILDER_TYPE=covenantless
- ARK_MIN_RELAY_FEE=200
- ARK_ESPLORA_URL=http://chopsticks:3000 - ARK_ESPLORA_URL=http://chopsticks:3000
- ARK_BITCOIND_RPC_USER=admin1 - ARK_BITCOIND_RPC_USER=admin1
- ARK_BITCOIND_RPC_PASS=123 - ARK_BITCOIND_RPC_PASS=123

View File

@@ -15,7 +15,7 @@ type ArkClient interface {
Unlock(ctx context.Context, password string) error Unlock(ctx context.Context, password string) error
Lock(ctx context.Context, password string) error Lock(ctx context.Context, password string) error
Balance(ctx context.Context, computeExpiryDetails bool) (*Balance, error) Balance(ctx context.Context, computeExpiryDetails bool) (*Balance, error)
Receive(ctx context.Context) (string, string, error) Receive(ctx context.Context) (offchainAddr, boardingAddr string, err error)
SendOnChain(ctx context.Context, receivers []Receiver) (string, error) SendOnChain(ctx context.Context, receivers []Receiver) (string, error)
SendOffChain( SendOffChain(
ctx context.Context, withExpiryCoinselect bool, receivers []Receiver, ctx context.Context, withExpiryCoinselect bool, receivers []Receiver,
@@ -26,7 +26,7 @@ type ArkClient interface {
) (string, error) ) (string, error)
SendAsync(ctx context.Context, withExpiryCoinselect bool, receivers []Receiver) (string, error) SendAsync(ctx context.Context, withExpiryCoinselect bool, receivers []Receiver) (string, error)
Claim(ctx context.Context) (string, error) Claim(ctx context.Context) (string, error)
ListVtxos(ctx context.Context) ([]client.Vtxo, []client.Vtxo, error) ListVtxos(ctx context.Context) (spendable, spent []client.Vtxo, err error)
GetTransactionHistory(ctx context.Context) ([]Transaction, error) GetTransactionHistory(ctx context.Context) ([]Transaction, error)
Dump(ctx context.Context) (seed string, err error) Dump(ctx context.Context) (seed string, err error)
} }

View File

@@ -17,7 +17,7 @@ import (
"github.com/ark-network/ark/pkg/client-sdk/client" "github.com/ark-network/ark/pkg/client-sdk/client"
"github.com/ark-network/ark/pkg/client-sdk/explorer" "github.com/ark-network/ark/pkg/client-sdk/explorer"
"github.com/ark-network/ark/pkg/client-sdk/internal/utils" "github.com/ark-network/ark/pkg/client-sdk/internal/utils"
"github.com/ark-network/ark/pkg/client-sdk/internal/utils/redemption" "github.com/ark-network/ark/pkg/client-sdk/redemption"
"github.com/ark-network/ark/pkg/client-sdk/store" "github.com/ark-network/ark/pkg/client-sdk/store"
"github.com/ark-network/ark/pkg/client-sdk/wallet" "github.com/ark-network/ark/pkg/client-sdk/wallet"
"github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/btcsuite/btcd/btcec/v2/schnorr"

View File

@@ -18,7 +18,7 @@ import (
"github.com/ark-network/ark/pkg/client-sdk/client" "github.com/ark-network/ark/pkg/client-sdk/client"
"github.com/ark-network/ark/pkg/client-sdk/explorer" "github.com/ark-network/ark/pkg/client-sdk/explorer"
"github.com/ark-network/ark/pkg/client-sdk/internal/utils" "github.com/ark-network/ark/pkg/client-sdk/internal/utils"
"github.com/ark-network/ark/pkg/client-sdk/internal/utils/redemption" "github.com/ark-network/ark/pkg/client-sdk/redemption"
"github.com/ark-network/ark/pkg/client-sdk/store" "github.com/ark-network/ark/pkg/client-sdk/store"
"github.com/ark-network/ark/pkg/client-sdk/wallet" "github.com/ark-network/ark/pkg/client-sdk/wallet"
"github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/btcsuite/btcd/btcec/v2/schnorr"
@@ -975,6 +975,15 @@ func (a *covenantlessArkClient) handleRoundStream(
var signerSession bitcointree.SignerSession var signerSession bitcointree.SignerSession
const (
start = iota
roundSigningStarted
roundSigningNoncesGenerated
roundFinalization
)
step := start
for { for {
select { select {
case <-ctx.Done(): case <-ctx.Done():
@@ -991,6 +1000,9 @@ func (a *covenantlessArkClient) handleRoundStream(
return "", fmt.Errorf("round failed: %s", event.(client.RoundFailedEvent).Reason) return "", fmt.Errorf("round failed: %s", event.(client.RoundFailedEvent).Reason)
case client.RoundSigningStartedEvent: case client.RoundSigningStartedEvent:
pingStop() pingStop()
if step != start {
continue
}
log.Info("a round signing started") log.Info("a round signing started")
signerSession, err = a.handleRoundSigningStarted( signerSession, err = a.handleRoundSigningStarted(
ctx, roundEphemeralKey, event.(client.RoundSigningStartedEvent), ctx, roundEphemeralKey, event.(client.RoundSigningStartedEvent),
@@ -998,8 +1010,12 @@ func (a *covenantlessArkClient) handleRoundStream(
if err != nil { if err != nil {
return "", err return "", err
} }
step++
continue continue
case client.RoundSigningNoncesGeneratedEvent: case client.RoundSigningNoncesGeneratedEvent:
if step != roundSigningStarted {
continue
}
pingStop() pingStop()
log.Info("round combined nonces generated") log.Info("round combined nonces generated")
if err := a.handleRoundSigningNoncesGenerated( if err := a.handleRoundSigningNoncesGenerated(
@@ -1007,8 +1023,12 @@ func (a *covenantlessArkClient) handleRoundStream(
); err != nil { ); err != nil {
return "", err return "", err
} }
step++
continue continue
case client.RoundFinalizationEvent: case client.RoundFinalizationEvent:
if step != roundSigningNoncesGenerated {
continue
}
pingStop() pingStop()
log.Info("a round finalization started") log.Info("a round finalization started")
@@ -1031,6 +1051,8 @@ func (a *covenantlessArkClient) handleRoundStream(
log.Info("done.") log.Info("done.")
log.Info("waiting for round finalization...") log.Info("waiting for round finalization...")
step++
continue
} }
} }
} }

View File

@@ -8,6 +8,7 @@ import (
"fmt" "fmt"
"runtime/debug" "runtime/debug"
"sort" "sort"
"sync"
"github.com/ark-network/ark/common" "github.com/ark-network/ark/common"
"github.com/ark-network/ark/pkg/client-sdk/client" "github.com/ark-network/ark/pkg/client-sdk/client"
@@ -265,8 +266,13 @@ func DecryptAES128(encrypted, password []byte) ([]byte, error) {
return plaintext, nil return plaintext, nil
} }
var lock = &sync.Mutex{}
// deriveKey derives a 32 byte array key from a custom passhprase // deriveKey derives a 32 byte array key from a custom passhprase
func deriveKey(password, salt []byte) ([]byte, []byte, error) { func deriveKey(password, salt []byte) ([]byte, []byte, error) {
lock.Lock()
defer lock.Unlock()
if salt == nil { if salt == nil {
salt = make([]byte, 32) salt = make([]byte, 32)
if _, err := rand.Read(salt); err != nil { if _, err := rand.Read(salt); err != nil {

View File

@@ -181,14 +181,16 @@ func (s *bitcoinWallet) SignTransaction(
return "", fmt.Errorf("signature verification failed") return "", fmt.Errorf("signature verification failed")
} }
updater.Upsbt.Inputs[i].TaprootScriptSpendSig = []*psbt.TaprootScriptSpendSig{ if len(updater.Upsbt.Inputs[i].TaprootScriptSpendSig) == 0 {
{ updater.Upsbt.Inputs[i].TaprootScriptSpendSig = make([]*psbt.TaprootScriptSpendSig, 0)
XOnlyPubKey: schnorr.SerializePubKey(pubkey),
LeafHash: hash.CloneBytes(),
Signature: sig.Serialize(),
SigHash: txscript.SigHashDefault,
},
} }
updater.Upsbt.Inputs[i].TaprootScriptSpendSig = append(updater.Upsbt.Inputs[i].TaprootScriptSpendSig, &psbt.TaprootScriptSpendSig{
XOnlyPubKey: schnorr.SerializePubKey(pubkey),
LeafHash: hash.CloneBytes(),
Signature: sig.Serialize(),
SigHash: txscript.SigHashDefault,
})
} }
} }
} }

View File

@@ -23,7 +23,8 @@ help:
## intergrationtest: runs integration tests ## intergrationtest: runs integration tests
integrationtest: integrationtest:
@echo "Running integration tests..." @echo "Running integration tests..."
@go test -v -count=1 -race -timeout 200s github.com/ark-network/ark/server/test/e2e/... @go test -v -count 1 -timeout 300s github.com/ark-network/ark/server/test/e2e/covenant
@go test -v -count 1 -timeout 300s github.com/ark-network/ark/server/test/e2e/covenantless
## lint: lint codebase ## lint: lint codebase
lint: lint:

View File

@@ -5,7 +5,6 @@ import (
"context" "context"
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"strings"
"sync" "sync"
"time" "time"
@@ -15,7 +14,6 @@ import (
"github.com/ark-network/ark/server/internal/core/domain" "github.com/ark-network/ark/server/internal/core/domain"
"github.com/ark-network/ark/server/internal/core/ports" "github.com/ark-network/ark/server/internal/core/ports"
"github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/btcutil/psbt"
"github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/decred/dcrd/dcrec/secp256k1/v4"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
@@ -613,87 +611,35 @@ func (s *covenantService) listenToScannerNotifications() {
mutx := &sync.Mutex{} mutx := &sync.Mutex{}
for vtxoKeys := range chVtxos { for vtxoKeys := range chVtxos {
go func(vtxoKeys map[string]ports.VtxoWithValue) { go func(vtxoKeys map[string][]ports.VtxoWithValue) {
vtxosRepo := s.repoManager.Vtxos() vtxosRepo := s.repoManager.Vtxos()
roundRepo := s.repoManager.Rounds()
for _, v := range vtxoKeys { for _, keys := range vtxoKeys {
// redeem for _, v := range keys {
vtxos, err := vtxosRepo.GetVtxos(ctx, []domain.VtxoKey{v.VtxoKey}) vtxos, err := vtxosRepo.GetVtxos(ctx, []domain.VtxoKey{v.VtxoKey})
if err != nil { if err != nil {
log.WithError(err).Warn("failed to retrieve vtxos, skipping...") log.WithError(err).Warn("failed to retrieve vtxos, skipping...")
continue continue
}
vtxo := vtxos[0]
if !vtxo.Redeemed {
go func() {
if err := s.markAsRedeemed(ctx, vtxo); err != nil {
log.WithError(err).Warnf("failed to mark vtxo %s:%d as redeemed", vtxo.Txid, vtxo.VOut)
}
}()
}
if vtxo.Spent {
log.Infof("fraud detected on vtxo %s:%d", vtxo.Txid, vtxo.VOut)
go func() {
if err := s.reactToFraud(ctx, vtxo, mutx); err != nil {
log.WithError(err).Warnf("failed to prevent fraud for vtxo %s:%d", vtxo.Txid, vtxo.VOut)
}
}()
}
} }
vtxo := vtxos[0]
if vtxo.Redeemed {
continue
}
if err := s.repoManager.Vtxos().RedeemVtxos(ctx, []domain.VtxoKey{vtxo.VtxoKey}); err != nil {
log.WithError(err).Warn("failed to redeem vtxos, retrying...")
continue
}
log.Debugf("vtxo %s redeemed", vtxo.Txid)
if !vtxo.Spent {
continue
}
log.Debugf("fraud detected on vtxo %s", vtxo.Txid)
round, err := roundRepo.GetRoundWithTxid(ctx, vtxo.SpentBy)
if err != nil {
log.WithError(err).Warn("failed to retrieve round")
continue
}
mutx.Lock()
defer mutx.Unlock()
connectorTxid, connectorVout, err := s.getNextConnector(ctx, *round)
if err != nil {
log.WithError(err).Warn("failed to retrieve next connector")
continue
}
forfeitTx, err := findForfeitTxLiquid(round.ForfeitTxs, connectorTxid, connectorVout, vtxo.Txid)
if err != nil {
log.WithError(err).Warn("failed to retrieve forfeit tx")
continue
}
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.SignTransaction(ctx, forfeitTx, false)
if err != nil {
log.WithError(err).Warn("failed to sign connector input in forfeit tx")
continue
}
signedForfeitTx, err = s.wallet.SignTransactionTapscript(ctx, signedForfeitTx, []int{1})
if err != nil {
log.WithError(err).Warn("failed to sign vtxo input in forfeit tx")
continue
}
forfeitTxHex, err := s.builder.FinalizeAndExtractForfeit(signedForfeitTx)
if err != nil {
log.WithError(err).Warn("failed to finalize forfeit tx")
continue
}
forfeitTxid, err := s.wallet.BroadcastTransaction(ctx, forfeitTxHex)
if err != nil {
log.WithError(err).Warn("failed to broadcast forfeit tx")
continue
}
log.Debugf("broadcasted forfeit tx %s", forfeitTxid)
} }
}(vtxoKeys) }(vtxoKeys)
} }
@@ -703,20 +649,27 @@ func (s *covenantService) getNextConnector(
ctx context.Context, ctx context.Context,
round domain.Round, round domain.Round,
) (string, uint32, error) { ) (string, uint32, error) {
connectorTx, err := psetv2.NewPsetFromBase64(round.Connectors[0]) lastConnectorPtx, err := psetv2.NewPsetFromBase64(round.Connectors[len(round.Connectors)-1])
if err != nil { if err != nil {
return "", 0, err return "", 0, err
} }
prevout := connectorTx.Inputs[0].WitnessUtxo var connectorAmount uint64
if prevout == nil { for i := len(lastConnectorPtx.Outputs) - 1; i >= 0; i-- {
return "", 0, fmt.Errorf("connector prevout not found") o := lastConnectorPtx.Outputs[i]
if len(o.Script) <= 0 {
continue // skip the fee output
}
connectorAmount = o.Value
break
} }
utxos, err := s.wallet.ListConnectorUtxos(ctx, round.ConnectorAddress) utxos, err := s.wallet.ListConnectorUtxos(ctx, round.ConnectorAddress)
if err != nil { if err != nil {
return "", 0, err return "", 0, err
} }
log.Debugf("found %d connector utxos, dust amount is %d", len(utxos), connectorAmount)
// if we do not find any utxos, we make sure to wait for the connector outpoint to be confirmed then we retry // if we do not find any utxos, we make sure to wait for the connector outpoint to be confirmed then we retry
if len(utxos) <= 0 { if len(utxos) <= 0 {
@@ -732,21 +685,26 @@ func (s *covenantService) getNextConnector(
// search for an already existing connector // search for an already existing connector
for _, u := range utxos { for _, u := range utxos {
if u.GetValue() == 450 { if u.GetValue() == connectorAmount {
return u.GetTxid(), u.GetIndex(), nil return u.GetTxid(), u.GetIndex(), nil
} }
} }
for _, u := range utxos { for _, u := range utxos {
if u.GetValue() > 450 { if u.GetValue() > connectorAmount {
for _, b64 := range round.Connectors { for _, b64 := range round.Connectors {
partial, err := psbt.NewFromRawBytes(strings.NewReader(b64), true) partial, err := psetv2.NewPsetFromBase64(b64)
if err != nil { if err != nil {
return "", 0, err return "", 0, err
} }
for _, i := range partial.UnsignedTx.TxIn { for _, i := range partial.Inputs {
if i.PreviousOutPoint.Hash.String() == u.GetTxid() && i.PreviousOutPoint.Index == u.GetIndex() { txhash, err := chainhash.NewHash(i.PreviousTxid)
if err != nil {
return "", 0, err
}
if txhash.String() == u.GetTxid() && i.PreviousTxIndex == u.GetIndex() {
connectorOutpoint := txOutpoint{u.GetTxid(), u.GetIndex()} connectorOutpoint := txOutpoint{u.GetTxid(), u.GetIndex()}
if err := s.wallet.LockConnectorUtxos(ctx, []ports.TxOutpoint{connectorOutpoint}); err != nil { if err := s.wallet.LockConnectorUtxos(ctx, []ports.TxOutpoint{connectorOutpoint}); err != nil {
@@ -780,6 +738,61 @@ func (s *covenantService) getNextConnector(
return "", 0, fmt.Errorf("no connector utxos found") return "", 0, fmt.Errorf("no connector utxos found")
} }
func (s *covenantService) reactToFraud(ctx context.Context, vtxo domain.Vtxo, mutx *sync.Mutex) error {
round, err := s.repoManager.Rounds().GetRoundWithTxid(ctx, vtxo.SpentBy)
if err != nil {
return fmt.Errorf("failed to retrieve round: %s", err)
}
mutx.Lock()
defer mutx.Unlock()
connectorTxid, connectorVout, err := s.getNextConnector(ctx, *round)
if err != nil {
return fmt.Errorf("failed to retrieve next connector: %s", err)
}
forfeitTx, err := findForfeitTxLiquid(round.ForfeitTxs, connectorTxid, connectorVout, vtxo.Txid)
if err != nil {
return fmt.Errorf("failed to find forfeit tx: %s", err)
}
if err := s.wallet.LockConnectorUtxos(ctx, []ports.TxOutpoint{txOutpoint{connectorTxid, connectorVout}}); err != nil {
return fmt.Errorf("failed to lock connector utxos: %s", err)
}
signedForfeitTx, err := s.wallet.SignTransaction(ctx, forfeitTx, false)
if err != nil {
return fmt.Errorf("failed to sign connector input in forfeit tx: %s", err)
}
signedForfeitTx, err = s.wallet.SignTransactionTapscript(ctx, signedForfeitTx, []int{1})
if err != nil {
return fmt.Errorf("failed to sign vtxo input in forfeit tx: %s", err)
}
forfeitTxHex, err := s.builder.FinalizeAndExtract(signedForfeitTx)
if err != nil {
return fmt.Errorf("failed to finalize forfeit tx: %s", err)
}
forfeitTxid, err := s.wallet.BroadcastTransaction(ctx, forfeitTxHex)
if err != nil {
return fmt.Errorf("failed to broadcast forfeit tx: %s", err)
}
log.Debugf("broadcasted forfeit tx %s", forfeitTxid)
return nil
}
func (s *covenantService) markAsRedeemed(ctx context.Context, vtxo domain.Vtxo) error {
if err := s.repoManager.Vtxos().RedeemVtxos(ctx, []domain.VtxoKey{vtxo.VtxoKey}); err != nil {
return err
}
log.Debugf("vtxo %s redeemed", vtxo.Txid)
return nil
}
func (s *covenantService) updateVtxoSet(round *domain.Round) { func (s *covenantService) updateVtxoSet(round *domain.Round) {
// Update the vtxo set only after a round is finalized. // Update the vtxo set only after a round is finalized.
if !round.IsEnded() { if !round.IsEnded() {

View File

@@ -259,13 +259,17 @@ func (s *covenantlessService) CompleteAsyncPayment(
} }
vtxos := make([]domain.Vtxo, 0, len(asyncPayData.receivers)) vtxos := make([]domain.Vtxo, 0, len(asyncPayData.receivers))
for i, receiver := range asyncPayData.receivers {
for outIndex, out := range redeemPtx.UnsignedTx.TxOut {
vtxos = append(vtxos, domain.Vtxo{ vtxos = append(vtxos, domain.Vtxo{
VtxoKey: domain.VtxoKey{ VtxoKey: domain.VtxoKey{
Txid: redeemTxid, Txid: redeemTxid,
VOut: uint32(i), VOut: uint32(outIndex),
},
Receiver: domain.Receiver{
Pubkey: asyncPayData.receivers[outIndex].Pubkey,
Amount: uint64(out.Value),
}, },
Receiver: receiver,
ExpireAt: asyncPayData.expireAt, ExpireAt: asyncPayData.expireAt,
AsyncPayment: &domain.AsyncPaymentTxs{ AsyncPayment: &domain.AsyncPaymentTxs{
RedeemTx: redeemTx, RedeemTx: redeemTx,
@@ -278,6 +282,12 @@ func (s *covenantlessService) CompleteAsyncPayment(
return fmt.Errorf("failed to add vtxos: %s", err) return fmt.Errorf("failed to add vtxos: %s", err)
} }
log.Infof("added %d vtxos", len(vtxos)) log.Infof("added %d vtxos", len(vtxos))
if err := s.startWatchingVtxos(vtxos); err != nil {
log.WithError(err).Warn(
"failed to start watching vtxos",
)
}
log.Debugf("started watching %d vtxos", len(vtxos))
if err := s.repoManager.Vtxos().SpendVtxos(ctx, spentVtxos, redeemTxid); err != nil { if err := s.repoManager.Vtxos().SpendVtxos(ctx, spentVtxos, redeemTxid); err != nil {
return fmt.Errorf("failed to spend vtxo: %s", err) return fmt.Errorf("failed to spend vtxo: %s", err)
@@ -546,8 +556,8 @@ func (s *covenantlessService) GetEventsChannel(ctx context.Context) <-chan domai
return s.eventsCh return s.eventsCh
} }
func (s *covenantlessService) GetRoundByTxid(ctx context.Context, poolTxid string) (*domain.Round, error) { func (s *covenantlessService) GetRoundByTxid(ctx context.Context, roundTxid string) (*domain.Round, error) {
return s.repoManager.Rounds().GetRoundWithTxid(ctx, poolTxid) return s.repoManager.Rounds().GetRoundWithTxid(ctx, roundTxid)
} }
func (s *covenantlessService) GetRoundById(ctx context.Context, id string) (*domain.Round, error) { func (s *covenantlessService) GetRoundById(ctx context.Context, id string) (*domain.Round, error) {
@@ -1070,89 +1080,33 @@ func (s *covenantlessService) listenToScannerNotifications() {
mutx := &sync.Mutex{} mutx := &sync.Mutex{}
for vtxoKeys := range chVtxos { for vtxoKeys := range chVtxos {
go func(vtxoKeys map[string]ports.VtxoWithValue) { go func(vtxoKeys map[string][]ports.VtxoWithValue) {
vtxosRepo := s.repoManager.Vtxos() for _, keys := range vtxoKeys {
roundRepo := s.repoManager.Rounds() for _, v := range keys {
vtxos, err := s.repoManager.Vtxos().GetVtxos(ctx, []domain.VtxoKey{v.VtxoKey})
if err != nil {
log.WithError(err).Warn("failed to retrieve vtxos, skipping...")
return
}
vtxo := vtxos[0]
for _, v := range vtxoKeys { if !vtxo.Redeemed {
// redeem go func() {
vtxos, err := vtxosRepo.GetVtxos(ctx, []domain.VtxoKey{v.VtxoKey}) if err := s.markAsRedeemed(ctx, vtxo); err != nil {
if err != nil { log.WithError(err).Warnf("failed to mark vtxo %s:%d as redeemed", vtxo.Txid, vtxo.VOut)
log.WithError(err).Warn("failed to retrieve vtxos, skipping...") }
continue }()
}
if vtxo.Spent {
log.Infof("fraud detected on vtxo %s:%d", vtxo.Txid, vtxo.VOut)
go func() {
if err := s.reactToFraud(ctx, vtxo, mutx); err != nil {
log.WithError(err).Warnf("failed to prevent fraud for vtxo %s:%d", vtxo.Txid, vtxo.VOut)
}
}()
}
} }
vtxo := vtxos[0]
if vtxo.Redeemed {
continue
}
if err := s.repoManager.Vtxos().RedeemVtxos(
ctx, []domain.VtxoKey{vtxo.VtxoKey},
); err != nil {
log.WithError(err).Warn("failed to redeem vtxos, retrying...")
continue
}
log.Debugf("vtxo %s redeemed", vtxo.Txid)
if !vtxo.Spent {
continue
}
log.Debugf("fraud detected on vtxo %s", vtxo.Txid)
round, err := roundRepo.GetRoundWithTxid(ctx, vtxo.SpentBy)
if err != nil {
log.WithError(err).Warn("failed to retrieve round")
continue
}
mutx.Lock()
defer mutx.Unlock()
connectorTxid, connectorVout, err := s.getNextConnector(ctx, *round)
if err != nil {
log.WithError(err).Warn("failed to retrieve next connector")
continue
}
forfeitTx, err := findForfeitTxBitcoin(round.ForfeitTxs, connectorTxid, connectorVout, vtxo.Txid)
if err != nil {
log.WithError(err).Warn("failed to retrieve forfeit tx")
continue
}
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.SignTransaction(ctx, forfeitTx, false)
if err != nil {
log.WithError(err).Warn("failed to sign connector input in forfeit tx")
continue
}
signedForfeitTx, err = s.wallet.SignTransactionTapscript(ctx, signedForfeitTx, []int{1})
if err != nil {
log.WithError(err).Warn("failed to sign vtxo input in forfeit tx")
continue
}
forfeitTxHex, err := s.builder.FinalizeAndExtractForfeit(signedForfeitTx)
if err != nil {
log.WithError(err).Warn("failed to finalize forfeit tx")
continue
}
forfeitTxid, err := s.wallet.BroadcastTransaction(ctx, forfeitTxHex)
if err != nil {
log.WithError(err).Warn("failed to broadcast forfeit tx")
continue
}
log.Debugf("broadcasted forfeit tx %s", forfeitTxid)
} }
}(vtxoKeys) }(vtxoKeys)
} }
@@ -1162,10 +1116,19 @@ func (s *covenantlessService) getNextConnector(
ctx context.Context, ctx context.Context,
round domain.Round, round domain.Round,
) (string, uint32, error) { ) (string, uint32, error) {
lastConnectorPtx, err := psbt.NewFromRawBytes(strings.NewReader(round.Connectors[len(round.Connectors)-1]), true)
if err != nil {
return "", 0, err
}
lastOutput := lastConnectorPtx.UnsignedTx.TxOut[len(lastConnectorPtx.UnsignedTx.TxOut)-1]
connectorAmount := lastOutput.Value
utxos, err := s.wallet.ListConnectorUtxos(ctx, round.ConnectorAddress) utxos, err := s.wallet.ListConnectorUtxos(ctx, round.ConnectorAddress)
if err != nil { if err != nil {
return "", 0, err return "", 0, err
} }
log.Debugf("found %d connector utxos, dust amount is %d", len(utxos), connectorAmount)
// if we do not find any utxos, we make sure to wait for the connector outpoint to be confirmed then we retry // if we do not find any utxos, we make sure to wait for the connector outpoint to be confirmed then we retry
if len(utxos) <= 0 { if len(utxos) <= 0 {
@@ -1181,13 +1144,13 @@ func (s *covenantlessService) getNextConnector(
// search for an already existing connector // search for an already existing connector
for _, u := range utxos { for _, u := range utxos {
if u.GetValue() == 450 { if u.GetValue() == uint64(connectorAmount) {
return u.GetTxid(), u.GetIndex(), nil return u.GetTxid(), u.GetIndex(), nil
} }
} }
for _, u := range utxos { for _, u := range utxos {
if u.GetValue() > 450 { if u.GetValue() > uint64(connectorAmount) {
for _, b64 := range round.Connectors { for _, b64 := range round.Connectors {
ptx, err := psbt.NewFromRawBytes(strings.NewReader(b64), true) ptx, err := psbt.NewFromRawBytes(strings.NewReader(b64), true)
if err != nil { if err != nil {
@@ -1210,7 +1173,7 @@ func (s *covenantlessService) getNextConnector(
connectorTxid, err := s.wallet.BroadcastTransaction(ctx, signedConnectorTx) connectorTxid, err := s.wallet.BroadcastTransaction(ctx, signedConnectorTx)
if err != nil { if err != nil {
return "", 0, err return "", 0, fmt.Errorf("failed to broadcast connector tx: %s", err)
} }
log.Debugf("broadcasted connector tx %s", connectorTxid) log.Debugf("broadcasted connector tx %s", connectorTxid)
@@ -1464,8 +1427,90 @@ func (s *covenantlessService) saveEvents(
return s.repoManager.Rounds().AddOrUpdateRound(ctx, *round) return s.repoManager.Rounds().AddOrUpdateRound(ctx, *round)
} }
func (s *covenantlessService) reactToFraud(ctx context.Context, vtxo domain.Vtxo, mutx *sync.Mutex) error {
mutx.Lock()
defer mutx.Unlock()
roundRepo := s.repoManager.Rounds()
round, err := roundRepo.GetRoundWithTxid(ctx, vtxo.SpentBy)
if err != nil {
vtxosRepo := s.repoManager.Vtxos()
// if the round is not found, the utxo may be spent by an async payment redeem tx
vtxos, err := vtxosRepo.GetVtxos(ctx, []domain.VtxoKey{
{Txid: vtxo.SpentBy, VOut: 0},
})
if err != nil || len(vtxos) <= 0 {
return fmt.Errorf("failed to retrieve round: %s", err)
}
asyncPayVtxo := vtxos[0]
if asyncPayVtxo.Redeemed { // redeem tx is already onchain
return nil
}
log.Debugf("vtxo %s:%d has been spent by async payment", vtxo.Txid, vtxo.VOut)
redeemTxHex, err := s.builder.FinalizeAndExtract(asyncPayVtxo.AsyncPayment.RedeemTx)
if err != nil {
return fmt.Errorf("failed to finalize redeem tx: %s", err)
}
redeemTxid, err := s.wallet.BroadcastTransaction(ctx, redeemTxHex)
if err != nil {
return fmt.Errorf("failed to broadcast redeem tx: %s", err)
}
log.Debugf("broadcasted redeem tx %s", redeemTxid)
return nil
}
connectorTxid, connectorVout, err := s.getNextConnector(ctx, *round)
if err != nil {
return fmt.Errorf("failed to get next connector: %s", err)
}
log.Debugf("found next connector %s:%d", connectorTxid, connectorVout)
forfeitTx, err := findForfeitTxBitcoin(round.ForfeitTxs, connectorTxid, connectorVout, vtxo.VtxoKey)
if err != nil {
return fmt.Errorf("failed to find forfeit tx: %s", err)
}
if err := s.wallet.LockConnectorUtxos(ctx, []ports.TxOutpoint{txOutpoint{connectorTxid, connectorVout}}); err != nil {
return fmt.Errorf("failed to lock connector utxos: %s", err)
}
signedForfeitTx, err := s.wallet.SignTransactionTapscript(ctx, forfeitTx, nil)
if err != nil {
return fmt.Errorf("failed to sign forfeit tx: %s", err)
}
forfeitTxHex, err := s.builder.FinalizeAndExtract(signedForfeitTx)
if err != nil {
return fmt.Errorf("failed to finalize forfeit tx: %s", err)
}
forfeitTxid, err := s.wallet.BroadcastTransaction(ctx, forfeitTxHex)
if err != nil {
return fmt.Errorf("failed to broadcast forfeit tx: %s", err)
}
log.Debugf("broadcasted forfeit tx %s", forfeitTxid)
return nil
}
func (s *covenantlessService) markAsRedeemed(ctx context.Context, vtxo domain.Vtxo) error {
if err := s.repoManager.Vtxos().RedeemVtxos(ctx, []domain.VtxoKey{vtxo.VtxoKey}); err != nil {
return err
}
log.Debugf("vtxo %s:%d redeemed", vtxo.Txid, vtxo.VOut)
return nil
}
func findForfeitTxBitcoin( func findForfeitTxBitcoin(
forfeits []string, connectorTxid string, connectorVout uint32, vtxoTxid string, forfeits []string, connectorTxid string, connectorVout uint32, vtxo domain.VtxoKey,
) (string, error) { ) (string, error) {
for _, forfeit := range forfeits { for _, forfeit := range forfeits {
forfeitTx, err := psbt.NewFromRawBytes(strings.NewReader(forfeit), true) forfeitTx, err := psbt.NewFromRawBytes(strings.NewReader(forfeit), true)
@@ -1476,9 +1521,10 @@ func findForfeitTxBitcoin(
connector := forfeitTx.UnsignedTx.TxIn[0] connector := forfeitTx.UnsignedTx.TxIn[0]
vtxoInput := forfeitTx.UnsignedTx.TxIn[1] vtxoInput := forfeitTx.UnsignedTx.TxIn[1]
if connector.PreviousOutPoint.String() == connectorTxid && if connector.PreviousOutPoint.Hash.String() == connectorTxid &&
connector.PreviousOutPoint.Index == connectorVout && connector.PreviousOutPoint.Index == connectorVout &&
vtxoInput.PreviousOutPoint.String() == vtxoTxid { vtxoInput.PreviousOutPoint.Hash.String() == vtxo.Txid &&
vtxoInput.PreviousOutPoint.Index == vtxo.VOut {
return forfeit, nil return forfeit, nil
} }
} }

View File

@@ -13,6 +13,6 @@ type VtxoWithValue struct {
type BlockchainScanner interface { type BlockchainScanner interface {
WatchScripts(ctx context.Context, scripts []string) error WatchScripts(ctx context.Context, scripts []string) error
UnwatchScripts(ctx context.Context, scripts []string) error UnwatchScripts(ctx context.Context, scripts []string) error
GetNotificationChannel(ctx context.Context) <-chan map[string]VtxoWithValue GetNotificationChannel(ctx context.Context) <-chan map[string][]VtxoWithValue
IsTransactionConfirmed(ctx context.Context, txid string) (isConfirmed bool, blocktime int64, err error) IsTransactionConfirmed(ctx context.Context, txid string) (isConfirmed bool, blocktime int64, err error)
} }

View File

@@ -32,8 +32,8 @@ type TxBuilder interface {
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)
GetSweepInput(parentblocktime int64, node tree.Node) (expirationtime int64, sweepInput SweepInput, err error) GetSweepInput(parentblocktime int64, node tree.Node) (expirationtime int64, sweepInput SweepInput, err error)
FinalizeAndExtract(tx string) (txhex string, err error)
VerifyTapscriptPartialSigs(tx string) (valid bool, txid string, err error) VerifyTapscriptPartialSigs(tx string) (valid bool, txid string, err error)
FinalizeAndExtractForfeit(tx string) (txhex string, err error)
// FindLeaves returns all the leaves txs that are reachable from the given outpoint // FindLeaves returns all the leaves txs that are reachable from the given outpoint
FindLeaves(congestionTree tree.CongestionTree, fromtxid string, vout uint32) (leaves []tree.Node, err error) FindLeaves(congestionTree tree.CongestionTree, fromtxid string, vout uint32) (leaves []tree.Node, err error)
BuildAsyncPaymentTransactions( BuildAsyncPaymentTransactions(

View File

@@ -23,10 +23,6 @@ import (
"github.com/vulpemventures/go-elements/transaction" "github.com/vulpemventures/go-elements/transaction"
) )
const (
connectorAmount = uint64(450)
)
type txBuilder struct { type txBuilder struct {
wallet ports.WalletService wallet ports.WalletService
net common.Network net common.Network
@@ -112,12 +108,17 @@ func (b *txBuilder) BuildForfeitTxs(
return nil, nil, err return nil, nil, err
} }
connectorTxs, err := b.createConnectors(poolTx, payments, connectorAddress, connectorFeeAmount) connectorAmount, err := b.wallet.GetDustAmount(context.Background())
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
forfeitTxs, err = b.createForfeitTxs(aspPubkey, payments, connectorTxs) connectorTxs, err := b.createConnectors(poolTx, payments, connectorAddress, connectorAmount, connectorFeeAmount)
if err != nil {
return nil, nil, err
}
forfeitTxs, err = b.createForfeitTxs(aspPubkey, payments, connectorTxs, connectorAmount)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@@ -309,7 +310,7 @@ func (b *txBuilder) VerifyTapscriptPartialSigs(tx string) (bool, string, error)
return true, txid, nil return true, txid, nil
} }
func (b *txBuilder) FinalizeAndExtractForfeit(tx string) (string, error) { func (b *txBuilder) FinalizeAndExtract(tx string) (string, error) {
p, err := psetv2.NewPsetFromBase64(tx) p, err := psetv2.NewPsetFromBase64(tx)
if err != nil { if err != nil {
return "", err return "", err
@@ -437,9 +438,14 @@ func (b *txBuilder) createPoolTx(
return nil, err return nil, err
} }
dustAmount, err := b.wallet.GetDustAmount(context.Background())
if err != nil {
return nil, err
}
receivers := getOnchainReceivers(payments) receivers := getOnchainReceivers(payments)
nbOfInputs := countSpentVtxos(payments) nbOfInputs := countSpentVtxos(payments)
connectorsAmount := (connectorAmount + connectorMinRelayFee) * nbOfInputs connectorsAmount := (dustAmount + connectorMinRelayFee) * nbOfInputs
if nbOfInputs > 1 { if nbOfInputs > 1 {
connectorsAmount -= connectorMinRelayFee connectorsAmount -= connectorMinRelayFee
} }
@@ -744,7 +750,9 @@ func (b *txBuilder) VerifyAndCombinePartialTx(dest string, src string) (string,
} }
func (b *txBuilder) createConnectors( func (b *txBuilder) createConnectors(
poolTx string, payments []domain.Payment, connectorAddress string, feeAmount uint64, poolTx string, payments []domain.Payment,
connectorAddress string,
connectorAmount, feeAmount uint64,
) ([]*psetv2.Pset, error) { ) ([]*psetv2.Pset, error) {
txid, _ := getTxid(poolTx) txid, _ := getTxid(poolTx)
@@ -798,7 +806,10 @@ func (b *txBuilder) createConnectors(
return nil, err return nil, err
} }
txid, _ := getPsetId(connectorTx) txid, err := getPsetId(connectorTx)
if err != nil {
return nil, err
}
previousInput = psetv2.InputArgs{ previousInput = psetv2.InputArgs{
Txid: txid, Txid: txid,
@@ -812,7 +823,7 @@ func (b *txBuilder) createConnectors(
} }
func (b *txBuilder) createForfeitTxs( func (b *txBuilder) createForfeitTxs(
aspPubkey *secp256k1.PublicKey, payments []domain.Payment, connectors []*psetv2.Pset, aspPubkey *secp256k1.PublicKey, payments []domain.Payment, connectors []*psetv2.Pset, connectorAmount uint64,
) ([]string, error) { ) ([]string, error) {
aspScript, err := p2wpkhScript(aspPubkey, b.onchainNetwork()) aspScript, err := p2wpkhScript(aspPubkey, b.onchainNetwork())
if err != nil { if err != nil {
@@ -855,7 +866,7 @@ func (b *txBuilder) createForfeitTxs(
for _, connector := range connectors { for _, connector := range connectors {
txs, err := b.craftForfeitTxs( txs, err := b.craftForfeitTxs(
connector, vtxo, *forfeitProof, vtxoScript, aspScript, connector, connectorAmount, vtxo, *forfeitProof, vtxoScript, aspScript,
) )
if err != nil { if err != nil {
return nil, err return nil, err

View File

@@ -52,7 +52,7 @@ func craftConnectorTx(
return ptx, nil return ptx, nil
} }
func getConnectorInputs(pset *psetv2.Pset) ([]psetv2.InputArgs, []*transaction.TxOutput) { func getConnectorInputs(pset *psetv2.Pset, connectorAmount uint64) ([]psetv2.InputArgs, []*transaction.TxOutput) {
txID, _ := getPsetId(pset) txID, _ := getPsetId(pset)
inputs := make([]psetv2.InputArgs, 0, len(pset.Outputs)) inputs := make([]psetv2.InputArgs, 0, len(pset.Outputs))

View File

@@ -16,11 +16,12 @@ import (
func (b *txBuilder) craftForfeitTxs( func (b *txBuilder) craftForfeitTxs(
connectorTx *psetv2.Pset, connectorTx *psetv2.Pset,
connectorAmount uint64,
vtxo domain.Vtxo, vtxo domain.Vtxo,
vtxoForfeitTapleaf taproot.TapscriptElementsProof, vtxoForfeitTapleaf taproot.TapscriptElementsProof,
vtxoScript, aspScript []byte, vtxoScript, aspScript []byte,
) (forfeitTxs []string, err error) { ) (forfeitTxs []string, err error) {
connectors, prevouts := getConnectorInputs(connectorTx) connectors, prevouts := getConnectorInputs(connectorTx, connectorAmount)
for i, connectorInput := range connectors { for i, connectorInput := range connectors {
weightEstimator := &input.TxWeightEstimator{} weightEstimator := &input.TxWeightEstimator{}

View File

@@ -190,12 +190,12 @@ func (m *mockedWallet) UnwatchScripts(
return args.Error(0) return args.Error(0)
} }
func (m *mockedWallet) GetNotificationChannel(ctx context.Context) <-chan map[string]ports.VtxoWithValue { func (m *mockedWallet) GetNotificationChannel(ctx context.Context) <-chan map[string][]ports.VtxoWithValue {
args := m.Called(ctx) args := m.Called(ctx)
var res <-chan map[string]ports.VtxoWithValue var res <-chan map[string][]ports.VtxoWithValue
if a := args.Get(0); a != nil { if a := args.Get(0); a != nil {
res = a.(<-chan map[string]ports.VtxoWithValue) res = a.(<-chan map[string][]ports.VtxoWithValue)
} }
return res return res
} }

View File

@@ -102,12 +102,60 @@ func (b *txBuilder) VerifyTapscriptPartialSigs(tx string) (bool, string, error)
return true, txid, nil return true, txid, nil
} }
func (b *txBuilder) FinalizeAndExtractForfeit(tx string) (string, error) { func (b *txBuilder) FinalizeAndExtract(tx string) (string, error) {
ptx, _ := psbt.NewFromRawBytes(strings.NewReader(tx), true) ptx, err := psbt.NewFromRawBytes(strings.NewReader(tx), true)
if err != nil {
return "", err
}
for i, in := range ptx.Inputs {
isTaproot := txscript.IsPayToTaproot(in.WitnessUtxo.PkScript)
if isTaproot && len(in.TaprootLeafScript) > 0 {
closure, err := bitcointree.DecodeClosure(in.TaprootLeafScript[0].Script)
if err != nil {
return "", err
}
witness := make(wire.TxWitness, 4)
castClosure, isTaprootMultisig := closure.(*bitcointree.MultisigClosure)
if isTaprootMultisig {
ownerPubkey := schnorr.SerializePubKey(castClosure.Pubkey)
aspKey := schnorr.SerializePubKey(castClosure.AspPubkey)
for _, sig := range in.TaprootScriptSpendSig {
if bytes.Equal(sig.XOnlyPubKey, ownerPubkey) {
witness[0] = sig.Signature
}
if bytes.Equal(sig.XOnlyPubKey, aspKey) {
witness[1] = sig.Signature
}
}
witness[2] = in.TaprootLeafScript[0].Script
witness[3] = in.TaprootLeafScript[0].ControlBlock
for idw, w := range witness {
if w == nil {
return "", fmt.Errorf("missing witness element %d, cannot finalize taproot mutlisig input %d", idw, i)
}
}
var witnessBuf bytes.Buffer
if err := psbt.WriteTxWitness(&witnessBuf, witness); err != nil {
return "", err
}
ptx.Inputs[i].FinalScriptWitness = witnessBuf.Bytes()
continue
}
}
for i := range ptx.Inputs {
if err := psbt.Finalize(ptx, i); err != nil { if err := psbt.Finalize(ptx, i); err != nil {
return "", err return "", fmt.Errorf("failed to finalize input %d: %w", i, err)
} }
} }
@@ -477,7 +525,7 @@ func (b *txBuilder) BuildAsyncPaymentTransactions(
unconditionalForfeitTxs = append(unconditionalForfeitTxs, forfeitTx) unconditionalForfeitTxs = append(unconditionalForfeitTxs, forfeitTx)
ins = append(ins, vtxoOutpoint) ins = append(ins, vtxoOutpoint)
redeemTxWeightEstimator.AddTapscriptInput(64, tapscript) redeemTxWeightEstimator.AddTapscriptInput(64*2, tapscript)
} }
for range receivers { for range receivers {
@@ -1199,7 +1247,7 @@ func (b *txBuilder) getConnectorPkScript(poolTx string) ([]byte, error) {
return nil, fmt.Errorf("connector output not found in pool tx") return nil, fmt.Errorf("connector output not found in pool tx")
} }
return partialTx.UnsignedTx.TxOut[0].PkScript, nil return partialTx.UnsignedTx.TxOut[1].PkScript, nil
} }
func (b *txBuilder) selectUtxos(ctx context.Context, sweptRounds []domain.Round, amount uint64) ([]ports.TxInput, uint64, error) { func (b *txBuilder) selectUtxos(ctx context.Context, sweptRounds []domain.Round, amount uint64) ([]ports.TxInput, uint64, error) {

View File

@@ -190,12 +190,12 @@ func (m *mockedWallet) UnwatchScripts(
return args.Error(0) return args.Error(0)
} }
func (m *mockedWallet) GetNotificationChannel(ctx context.Context) <-chan map[string]ports.VtxoWithValue { func (m *mockedWallet) GetNotificationChannel(ctx context.Context) <-chan map[string][]ports.VtxoWithValue {
args := m.Called(ctx) args := m.Called(ctx)
var res <-chan map[string]ports.VtxoWithValue var res <-chan map[string][]ports.VtxoWithValue
if a := args.Get(0); a != nil { if a := args.Get(0); a != nil {
res = a.(<-chan map[string]ports.VtxoWithValue) res = a.(<-chan map[string][]ports.VtxoWithValue)
} }
return res return res
} }

View File

@@ -1,6 +1,7 @@
package btcwallet package btcwallet
import ( import (
"encoding/hex"
"fmt" "fmt"
"github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/btcsuite/btcd/btcec/v2/schnorr"
@@ -41,6 +42,7 @@ func (s *service) signPsbt(packet *psbt.Packet, inputsToSign []int) ([]uint32, e
} }
tx := packet.UnsignedTx tx := packet.UnsignedTx
signedInputs := make([]uint32, 0)
for idx := range tx.TxIn { for idx := range tx.TxIn {
in := &packet.Inputs[idx] in := &packet.Inputs[idx]
@@ -76,13 +78,19 @@ func (s *service) signPsbt(packet *psbt.Packet, inputsToSign []int) ([]uint32, e
managedAddress = s.aspTaprootAddr managedAddress = s.aspTaprootAddr
} else { } else {
// segwit v0 // segwit v0
var err error
managedAddress, _, _, err = s.wallet.ScriptForOutput(in.WitnessUtxo) managedAddress, _, _, err = s.wallet.ScriptForOutput(in.WitnessUtxo)
if err != nil { if err != nil {
log.Debugf("SignPsbt: Skipping input %d, error "+ log.WithError(err).Debugf(
"fetching script for output: %v", idx, err) "failed to fetch address for input %d with script %s",
idx, hex.EncodeToString(in.WitnessUtxo.PkScript),
)
continue continue
} }
} }
signedInputs = append(signedInputs, uint32(idx))
bip32Infos := derivationPathForAddress(managedAddress) bip32Infos := derivationPathForAddress(managedAddress)
packet.Inputs[idx].Bip32Derivation = []*psbt.Bip32Derivation{bip32Infos} packet.Inputs[idx].Bip32Derivation = []*psbt.Bip32Derivation{bip32Infos}
@@ -106,25 +114,18 @@ func (s *service) signPsbt(packet *psbt.Packet, inputsToSign []int) ([]uint32, e
} }
} }
// TODO (@louisinger): shall we delete this code? ins, err := s.wallet.SignPsbt(packet)
// prevOutputFetcher := wallet.PsbtPrevOutputFetcher(packet) if err != nil {
// sigHashes := txscript.NewTxSigHashes(tx, prevOutputFetcher) return nil, err
}
// in := packet.Inputs[0] // delete derivation paths to avoid duplicate keys error
for idx := range signedInputs {
packet.Inputs[idx].Bip32Derivation = nil
packet.Inputs[idx].TaprootBip32Derivation = nil
}
// preimage, err := txscript.CalcTapscriptSignaturehash( return ins, nil
// sigHashes,
// txscript.SigHashType(in.SighashType),
// tx,
// 0,
// txscript.NewCannedPrevOutputFetcher(in.WitnessUtxo.PkScript, in.WitnessUtxo.Value),
// txscript.NewBaseTapLeaf(in.TaprootLeafScript[0].Script),
// )
// if err != nil {
// return nil, err
// }
return s.wallet.SignPsbt(packet)
} }
func derivationPathForAddress(addr waddrmgr.ManagedPubKeyAddress) *psbt.Bip32Derivation { func derivationPathForAddress(addr waddrmgr.ManagedPubKeyAddress) *psbt.Bip32Derivation {

View File

@@ -70,6 +70,10 @@ const (
mainAccount accountName = "main" mainAccount accountName = "main"
connectorAccount accountName = "connector" connectorAccount accountName = "connector"
aspKeyAccount accountName = "aspkey" aspKeyAccount accountName = "aspkey"
// https://github.com/bitcoin/bitcoin/blob/439e58c4d8194ca37f70346727d31f52e69592ec/src/policy/policy.cpp#L23C8-L23C11
// biggest input size to compute the maximum dust amount
biggestInputSize = 148 + 182 // = 330 vbytes
) )
var ( var (
@@ -852,18 +856,37 @@ func (s *service) UnwatchScripts(ctx context.Context, scripts []string) error {
func (s *service) GetNotificationChannel( func (s *service) GetNotificationChannel(
ctx context.Context, ctx context.Context,
) <-chan map[string]ports.VtxoWithValue { ) <-chan map[string][]ports.VtxoWithValue {
ch := make(chan map[string]ports.VtxoWithValue) ch := make(chan map[string][]ports.VtxoWithValue)
go func() { go func() {
const maxCacheSize = 100
sentTxs := make(map[chainhash.Hash]struct{})
cache := func(hash chainhash.Hash) {
if len(sentTxs) > maxCacheSize {
sentTxs = make(map[chainhash.Hash]struct{})
}
sentTxs[hash] = struct{}{}
}
for n := range s.scanner.Notifications() { for n := range s.scanner.Notifications() {
switch m := n.(type) { switch m := n.(type) {
case chain.RelevantTx: case chain.RelevantTx:
if _, sent := sentTxs[m.TxRecord.Hash]; sent {
continue
}
notification := s.castNotification(m.TxRecord) notification := s.castNotification(m.TxRecord)
cache(m.TxRecord.Hash)
ch <- notification ch <- notification
case chain.FilteredBlockConnected: case chain.FilteredBlockConnected:
for _, tx := range m.RelevantTxs { for _, tx := range m.RelevantTxs {
if _, sent := sentTxs[tx.Hash]; sent {
continue
}
notification := s.castNotification(tx) notification := s.castNotification(tx)
cache(tx.Hash)
ch <- notification ch <- notification
} }
} }
@@ -879,11 +902,10 @@ func (s *service) IsTransactionConfirmed(
return s.extraAPI.getTxStatus(txid) return s.extraAPI.getTxStatus(txid)
} }
// https://github.com/bitcoin/bitcoin/blob/439e58c4d8194ca37f70346727d31f52e69592ec/src/policy/policy.cpp#L23C8-L23C11
func (s *service) GetDustAmount( func (s *service) GetDustAmount(
ctx context.Context, ctx context.Context,
) (uint64, error) { ) (uint64, error) {
return s.MinRelayFee(ctx, 182) // non-segwit 1-in-1-out tx return s.MinRelayFee(ctx, biggestInputSize)
} }
func (s *service) GetTransaction(ctx context.Context, txid string) (string, error) { func (s *service) GetTransaction(ctx context.Context, txid string) (string, error) {
@@ -904,8 +926,8 @@ func (s *service) GetTransaction(ctx context.Context, txid string) (string, erro
return hex.EncodeToString(buf.Bytes()), nil return hex.EncodeToString(buf.Bytes()), nil
} }
func (s *service) castNotification(tx *wtxmgr.TxRecord) map[string]ports.VtxoWithValue { func (s *service) castNotification(tx *wtxmgr.TxRecord) map[string][]ports.VtxoWithValue {
vtxos := make(map[string]ports.VtxoWithValue) vtxos := make(map[string][]ports.VtxoWithValue)
s.watchedScriptsLock.RLock() s.watchedScriptsLock.RLock()
defer s.watchedScriptsLock.RUnlock() defer s.watchedScriptsLock.RUnlock()
@@ -916,13 +938,17 @@ func (s *service) castNotification(tx *wtxmgr.TxRecord) map[string]ports.VtxoWit
continue continue
} }
vtxos[script] = ports.VtxoWithValue{ if len(vtxos[script]) <= 0 {
vtxos[script] = make([]ports.VtxoWithValue, 0)
}
vtxos[script] = append(vtxos[script], ports.VtxoWithValue{
VtxoKey: domain.VtxoKey{ VtxoKey: domain.VtxoKey{
Txid: tx.Hash.String(), Txid: tx.Hash.String(),
VOut: uint32(outputIndex), VOut: uint32(outputIndex),
}, },
Value: uint64(txout.Value), Value: uint64(txout.Value),
} })
} }
return vtxos return vtxos

View File

@@ -2,6 +2,7 @@ package oceanwallet
import ( import (
"context" "context"
"encoding/hex"
pb "github.com/ark-network/ark/api-spec/protobuf/gen/ocean/v1" pb "github.com/ark-network/ark/api-spec/protobuf/gen/ocean/v1"
"github.com/ark-network/ark/server/internal/core/ports" "github.com/ark-network/ark/server/internal/core/ports"
@@ -27,20 +28,26 @@ func (s *service) DeriveConnectorAddress(ctx context.Context) (string, error) {
func (s *service) ListConnectorUtxos( func (s *service) ListConnectorUtxos(
ctx context.Context, connectorAddress string, ctx context.Context, connectorAddress string,
) ([]ports.TxInput, error) { ) ([]ports.TxInput, error) {
addresses := make([]string, 0) connectorScript, err := address.ToOutputScript(connectorAddress)
if len(connectorAddress) > 0 { if err != nil {
addresses = append(addresses, connectorAddress) return nil, err
} }
res, err := s.accountClient.ListUtxos(ctx, &pb.ListUtxosRequest{ res, err := s.accountClient.ListUtxos(ctx, &pb.ListUtxosRequest{
AccountName: connectorAccount, AccountName: connectorAccount,
Addresses: addresses,
}) })
if err != nil { if err != nil {
return nil, err return nil, err
} }
utxos := make([]ports.TxInput, 0) utxos := make([]ports.TxInput, 0)
connectorScriptHex := hex.EncodeToString(connectorScript)
for _, utxo := range res.GetSpendableUtxos().GetUtxos() { for _, utxo := range res.GetSpendableUtxos().GetUtxos() {
if utxo.Script != connectorScriptHex {
continue
}
utxos = append(utxos, utxo) utxos = append(utxos, utxo)
} }

View File

@@ -35,7 +35,7 @@ func (s *service) UnwatchScripts(ctx context.Context, scripts []string) error {
return nil return nil
} }
func (s *service) GetNotificationChannel(ctx context.Context) <-chan map[string]ports.VtxoWithValue { func (s *service) GetNotificationChannel(ctx context.Context) <-chan map[string][]ports.VtxoWithValue {
return s.chVtxos return s.chVtxos
} }

View File

@@ -22,7 +22,7 @@ type service struct {
accountClient pb.AccountServiceClient accountClient pb.AccountServiceClient
txClient pb.TransactionServiceClient txClient pb.TransactionServiceClient
notifyClient pb.NotificationServiceClient notifyClient pb.NotificationServiceClient
chVtxos chan map[string]ports.VtxoWithValue chVtxos chan map[string][]ports.VtxoWithValue
isListening bool isListening bool
} }
@@ -35,7 +35,7 @@ func NewService(addr string) (ports.WalletService, error) {
accountClient := pb.NewAccountServiceClient(conn) accountClient := pb.NewAccountServiceClient(conn)
txClient := pb.NewTransactionServiceClient(conn) txClient := pb.NewTransactionServiceClient(conn)
notifyClient := pb.NewNotificationServiceClient(conn) notifyClient := pb.NewNotificationServiceClient(conn)
chVtxos := make(chan map[string]ports.VtxoWithValue) chVtxos := make(chan map[string][]ports.VtxoWithValue)
svc := &service{ svc := &service{
addr: addr, addr: addr,
conn: conn, conn: conn,
@@ -190,8 +190,8 @@ func (s *service) listenToNotifications() {
} }
} }
func toVtxos(utxos []*pb.Utxo) map[string]ports.VtxoWithValue { func toVtxos(utxos []*pb.Utxo) map[string][]ports.VtxoWithValue {
vtxos := make(map[string]ports.VtxoWithValue, len(utxos)) vtxos := make(map[string][]ports.VtxoWithValue, len(utxos))
for _, utxo := range utxos { for _, utxo := range utxos {
// We want to notify for activity related to vtxos owner, therefore we skip // We want to notify for activity related to vtxos owner, therefore we skip
// returning anything related to the internal accounts of the wallet, like // returning anything related to the internal accounts of the wallet, like
@@ -200,11 +200,13 @@ func toVtxos(utxos []*pb.Utxo) map[string]ports.VtxoWithValue {
continue continue
} }
vtxos[utxo.Script] = ports.VtxoWithValue{ vtxos[utxo.Script] = []ports.VtxoWithValue{
VtxoKey: domain.VtxoKey{Txid: utxo.GetTxid(), {
VOut: utxo.GetIndex(), VtxoKey: domain.VtxoKey{Txid: utxo.GetTxid(),
VOut: utxo.GetIndex(),
},
Value: utxo.GetValue(),
}, },
Value: utxo.GetValue(),
} }
} }
return vtxos return vtxos

View File

@@ -275,7 +275,7 @@ func (s *service) LockConnectorUtxos(ctx context.Context, utxos []ports.TxOutpoi
} }
func (s *service) MinRelayFee(ctx context.Context, vbytes uint64) (uint64, error) { func (s *service) MinRelayFee(ctx context.Context, vbytes uint64) (uint64, error) {
feeRate := 0.11 feeRate := 0.2
fee := uint64(float64(vbytes) * feeRate) fee := uint64(float64(vbytes) * feeRate)
return fee, nil return fee, nil
} }

View File

@@ -2,6 +2,7 @@ package e2e_test
import ( import (
"bytes" "bytes"
"context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/http" "net/http"
@@ -10,6 +11,12 @@ import (
"time" "time"
"github.com/ark-network/ark/common" "github.com/ark-network/ark/common"
arksdk "github.com/ark-network/ark/pkg/client-sdk"
"github.com/ark-network/ark/pkg/client-sdk/client"
grpcclient "github.com/ark-network/ark/pkg/client-sdk/client/grpc"
"github.com/ark-network/ark/pkg/client-sdk/explorer"
"github.com/ark-network/ark/pkg/client-sdk/redemption"
inmemorystore "github.com/ark-network/ark/pkg/client-sdk/store/inmemory"
utils "github.com/ark-network/ark/server/test/e2e" utils "github.com/ark-network/ark/server/test/e2e"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@@ -143,6 +150,58 @@ func TestCollaborativeExit(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
} }
func TestReactToSpentVtxosRedemption(t *testing.T) {
ctx := context.Background()
client, grpcClient := setupArkSDK(t)
defer grpcClient.Close()
offchainAddress, boardingAddress, err := client.Receive(ctx)
require.NoError(t, err)
_, err = utils.RunCommand("nigiri", "faucet", "--liquid", boardingAddress)
require.NoError(t, err)
time.Sleep(5 * time.Second)
_, err = client.Claim(ctx)
require.NoError(t, err)
time.Sleep(3 * time.Second)
spendable, _, err := client.ListVtxos(ctx)
require.NoError(t, err)
require.NotEmpty(t, spendable)
vtxo := spendable[0]
_, err = client.SendOffChain(ctx, false, []arksdk.Receiver{arksdk.NewLiquidReceiver(offchainAddress, 1000)})
require.NoError(t, err)
round, err := grpcClient.GetRound(ctx, vtxo.RoundTxid)
require.NoError(t, err)
expl := explorer.NewExplorer("http://localhost:3001", common.LiquidRegTest)
branch, err := redemption.NewCovenantRedeemBranch(expl, round.Tree, vtxo)
require.NoError(t, err)
txs, err := branch.RedeemPath()
require.NoError(t, err)
for _, tx := range txs {
_, err := expl.Broadcast(tx)
require.NoError(t, err)
}
// give time for the ASP to detect and process the fraud
time.Sleep(18 * time.Second)
balance, err := client.Balance(ctx, true)
require.NoError(t, err)
require.Empty(t, balance.OnchainBalance.LockedAmount)
}
func runArkCommand(arg ...string) (string, error) { func runArkCommand(arg ...string) (string, error) {
args := append([]string{"exec", "-t", "arkd", "ark"}, arg...) args := append([]string{"exec", "-t", "arkd", "ark"}, arg...)
return utils.RunCommand("docker", args...) return utils.RunCommand("docker", args...)
@@ -237,3 +296,27 @@ func setupAspWallet() error {
return nil return nil
} }
func setupArkSDK(t *testing.T) (arksdk.ArkClient, client.ASPClient) {
storeSvc, err := inmemorystore.NewConfigStore()
require.NoError(t, err)
client, err := arksdk.NewCovenantClient(storeSvc)
require.NoError(t, err)
err = client.Init(context.Background(), arksdk.InitArgs{
WalletType: arksdk.SingleKeyWallet,
ClientType: arksdk.GrpcClient,
AspUrl: "localhost:6060",
Password: utils.Password,
})
require.NoError(t, err)
err = client.Unlock(context.Background(), utils.Password)
require.NoError(t, err)
grpcClient, err := grpcclient.NewClient("localhost:6060")
require.NoError(t, err)
return client, grpcClient
}

View File

@@ -2,6 +2,7 @@ package e2e_test
import ( import (
"bytes" "bytes"
"context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/http" "net/http"
@@ -9,6 +10,13 @@ import (
"testing" "testing"
"time" "time"
"github.com/ark-network/ark/common"
arksdk "github.com/ark-network/ark/pkg/client-sdk"
"github.com/ark-network/ark/pkg/client-sdk/client"
grpcclient "github.com/ark-network/ark/pkg/client-sdk/client/grpc"
"github.com/ark-network/ark/pkg/client-sdk/explorer"
"github.com/ark-network/ark/pkg/client-sdk/redemption"
inmemorystore "github.com/ark-network/ark/pkg/client-sdk/store/inmemory"
utils "github.com/ark-network/ark/server/test/e2e" utils "github.com/ark-network/ark/server/test/e2e"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@@ -155,6 +163,126 @@ func TestCollaborativeExit(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
} }
func TestReactToSpentVtxosRedemption(t *testing.T) {
ctx := context.Background()
client, grpcClient := setupArkSDK(t)
defer grpcClient.Close()
offchainAddress, boardingAddress, err := client.Receive(ctx)
require.NoError(t, err)
_, err = utils.RunCommand("nigiri", "faucet", boardingAddress)
require.NoError(t, err)
time.Sleep(5 * time.Second)
_, err = client.Claim(ctx)
require.NoError(t, err)
_, err = client.SendOffChain(ctx, false, []arksdk.Receiver{arksdk.NewBitcoinReceiver(offchainAddress, 1000)})
require.NoError(t, err)
time.Sleep(2 * time.Second)
_, spentVtxos, err := client.ListVtxos(ctx)
require.NoError(t, err)
require.NotEmpty(t, spentVtxos)
vtxo := spentVtxos[0]
round, err := grpcClient.GetRound(ctx, vtxo.RoundTxid)
require.NoError(t, err)
expl := explorer.NewExplorer("http://localhost:3000", common.BitcoinRegTest)
branch, err := redemption.NewCovenantlessRedeemBranch(expl, round.Tree, vtxo)
require.NoError(t, err)
txs, err := branch.RedeemPath()
require.NoError(t, err)
for _, tx := range txs {
_, err := expl.Broadcast(tx)
require.NoError(t, err)
}
// give time for the ASP to detect and process the fraud
time.Sleep(20 * time.Second)
balance, err := client.Balance(ctx, true)
require.NoError(t, err)
require.Empty(t, balance.OnchainBalance.LockedAmount)
}
func TestReactToAsyncSpentVtxosRedemption(t *testing.T) {
t.Run("receveir claimed funds", func(t *testing.T) {
ctx := context.Background()
sdkClient, grpcClient := setupArkSDK(t)
defer grpcClient.Close()
offchainAddress, boardingAddress, err := sdkClient.Receive(ctx)
require.NoError(t, err)
_, err = utils.RunCommand("nigiri", "faucet", boardingAddress)
require.NoError(t, err)
time.Sleep(5 * time.Second)
roundId, err := sdkClient.Claim(ctx)
require.NoError(t, err)
err = utils.GenerateBlock()
require.NoError(t, err)
_, err = sdkClient.SendAsync(ctx, false, []arksdk.Receiver{arksdk.NewBitcoinReceiver(offchainAddress, 1000)})
require.NoError(t, err)
_, err = sdkClient.Claim(ctx)
require.NoError(t, err)
time.Sleep(5 * time.Second)
_, spentVtxos, err := sdkClient.ListVtxos(ctx)
require.NoError(t, err)
require.NotEmpty(t, spentVtxos)
var vtxo client.Vtxo
for _, v := range spentVtxos {
if v.RoundTxid == roundId {
vtxo = v
break
}
}
require.NotEmpty(t, vtxo)
round, err := grpcClient.GetRound(ctx, vtxo.RoundTxid)
require.NoError(t, err)
expl := explorer.NewExplorer("http://localhost:3000", common.BitcoinRegTest)
branch, err := redemption.NewCovenantlessRedeemBranch(expl, round.Tree, vtxo)
require.NoError(t, err)
txs, err := branch.RedeemPath()
require.NoError(t, err)
for _, tx := range txs {
_, err := expl.Broadcast(tx)
require.NoError(t, err)
}
// give time for the ASP to detect and process the fraud
time.Sleep(50 * time.Second)
balance, err := sdkClient.Balance(ctx, true)
require.NoError(t, err)
require.Empty(t, balance.OnchainBalance.LockedAmount)
})
}
func runClarkCommand(arg ...string) (string, error) { func runClarkCommand(arg ...string) (string, error) {
args := append([]string{"exec", "-t", "clarkd", "ark"}, arg...) args := append([]string{"exec", "-t", "clarkd", "ark"}, arg...)
return utils.RunCommand("docker", args...) return utils.RunCommand("docker", args...)
@@ -254,7 +382,41 @@ func setupAspWallet() error {
return fmt.Errorf("failed to fund wallet: %s", err) return fmt.Errorf("failed to fund wallet: %s", err)
} }
_, err = utils.RunCommand("nigiri", "faucet", addr.Address)
if err != nil {
return fmt.Errorf("failed to fund wallet: %s", err)
}
_, err = utils.RunCommand("nigiri", "faucet", addr.Address)
if err != nil {
return fmt.Errorf("failed to fund wallet: %s", err)
}
time.Sleep(5 * time.Second) time.Sleep(5 * time.Second)
return nil return nil
} }
func setupArkSDK(t *testing.T) (arksdk.ArkClient, client.ASPClient) {
storeSvc, err := inmemorystore.NewConfigStore()
require.NoError(t, err)
client, err := arksdk.NewCovenantlessClient(storeSvc)
require.NoError(t, err)
err = client.Init(context.Background(), arksdk.InitArgs{
WalletType: arksdk.SingleKeyWallet,
ClientType: arksdk.GrpcClient,
AspUrl: "localhost:7070",
Password: utils.Password,
})
require.NoError(t, err)
err = client.Unlock(context.Background(), utils.Password)
require.NoError(t, err)
grpcClient, err := grpcclient.NewClient("localhost:7070")
require.NoError(t, err)
return client, grpcClient
}