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
- name: Set up Go
uses: actions/setup-go@v2
uses: actions/setup-go@v4
with:
go-version: 1.21.0
go-version: 1.23.1
- name: Build binaries
run: make build-all

View File

@@ -2,7 +2,8 @@ name: ci_integration
on:
push:
branches: [master]
branches:
- master
pull_request:
branches:
- master
@@ -15,11 +16,12 @@ jobs:
run:
working-directory: ./server
steps:
- uses: actions/setup-go@v3
with:
go-version: ">1.17.2"
- 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
uses: vulpemventures/nigiri-github-action@v1

View File

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

View File

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

View File

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

View File

@@ -15,7 +15,7 @@ type ArkClient interface {
Unlock(ctx context.Context, password string) error
Lock(ctx context.Context, password string) 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)
SendOffChain(
ctx context.Context, withExpiryCoinselect bool, receivers []Receiver,
@@ -26,7 +26,7 @@ type ArkClient interface {
) (string, error)
SendAsync(ctx context.Context, withExpiryCoinselect bool, receivers []Receiver) (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)
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/explorer"
"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/wallet"
"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/explorer"
"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/wallet"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
@@ -975,6 +975,15 @@ func (a *covenantlessArkClient) handleRoundStream(
var signerSession bitcointree.SignerSession
const (
start = iota
roundSigningStarted
roundSigningNoncesGenerated
roundFinalization
)
step := start
for {
select {
case <-ctx.Done():
@@ -991,6 +1000,9 @@ func (a *covenantlessArkClient) handleRoundStream(
return "", fmt.Errorf("round failed: %s", event.(client.RoundFailedEvent).Reason)
case client.RoundSigningStartedEvent:
pingStop()
if step != start {
continue
}
log.Info("a round signing started")
signerSession, err = a.handleRoundSigningStarted(
ctx, roundEphemeralKey, event.(client.RoundSigningStartedEvent),
@@ -998,8 +1010,12 @@ func (a *covenantlessArkClient) handleRoundStream(
if err != nil {
return "", err
}
step++
continue
case client.RoundSigningNoncesGeneratedEvent:
if step != roundSigningStarted {
continue
}
pingStop()
log.Info("round combined nonces generated")
if err := a.handleRoundSigningNoncesGenerated(
@@ -1007,8 +1023,12 @@ func (a *covenantlessArkClient) handleRoundStream(
); err != nil {
return "", err
}
step++
continue
case client.RoundFinalizationEvent:
if step != roundSigningNoncesGenerated {
continue
}
pingStop()
log.Info("a round finalization started")
@@ -1031,6 +1051,8 @@ func (a *covenantlessArkClient) handleRoundStream(
log.Info("done.")
log.Info("waiting for round finalization...")
step++
continue
}
}
}

View File

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

View File

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

View File

@@ -5,7 +5,6 @@ import (
"context"
"encoding/hex"
"fmt"
"strings"
"sync"
"time"
@@ -15,7 +14,6 @@ import (
"github.com/ark-network/ark/server/internal/core/domain"
"github.com/ark-network/ark/server/internal/core/ports"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/btcutil/psbt"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
log "github.com/sirupsen/logrus"
@@ -613,87 +611,35 @@ func (s *covenantService) listenToScannerNotifications() {
mutx := &sync.Mutex{}
for vtxoKeys := range chVtxos {
go func(vtxoKeys map[string]ports.VtxoWithValue) {
go func(vtxoKeys map[string][]ports.VtxoWithValue) {
vtxosRepo := s.repoManager.Vtxos()
roundRepo := s.repoManager.Rounds()
for _, v := range vtxoKeys {
// redeem
vtxos, err := vtxosRepo.GetVtxos(ctx, []domain.VtxoKey{v.VtxoKey})
if err != nil {
log.WithError(err).Warn("failed to retrieve vtxos, skipping...")
continue
for _, keys := range vtxoKeys {
for _, v := range keys {
vtxos, err := vtxosRepo.GetVtxos(ctx, []domain.VtxoKey{v.VtxoKey})
if err != nil {
log.WithError(err).Warn("failed to retrieve vtxos, skipping...")
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)
}
@@ -703,20 +649,27 @@ func (s *covenantService) getNextConnector(
ctx context.Context,
round domain.Round,
) (string, uint32, error) {
connectorTx, err := psetv2.NewPsetFromBase64(round.Connectors[0])
lastConnectorPtx, err := psetv2.NewPsetFromBase64(round.Connectors[len(round.Connectors)-1])
if err != nil {
return "", 0, err
}
prevout := connectorTx.Inputs[0].WitnessUtxo
if prevout == nil {
return "", 0, fmt.Errorf("connector prevout not found")
var connectorAmount uint64
for i := len(lastConnectorPtx.Outputs) - 1; i >= 0; i-- {
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)
if err != nil {
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 len(utxos) <= 0 {
@@ -732,21 +685,26 @@ func (s *covenantService) getNextConnector(
// search for an already existing connector
for _, u := range utxos {
if u.GetValue() == 450 {
if u.GetValue() == connectorAmount {
return u.GetTxid(), u.GetIndex(), nil
}
}
for _, u := range utxos {
if u.GetValue() > 450 {
if u.GetValue() > connectorAmount {
for _, b64 := range round.Connectors {
partial, err := psbt.NewFromRawBytes(strings.NewReader(b64), true)
partial, err := psetv2.NewPsetFromBase64(b64)
if err != nil {
return "", 0, err
}
for _, i := range partial.UnsignedTx.TxIn {
if i.PreviousOutPoint.Hash.String() == u.GetTxid() && i.PreviousOutPoint.Index == u.GetIndex() {
for _, i := range partial.Inputs {
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()}
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")
}
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) {
// Update the vtxo set only after a round is finalized.
if !round.IsEnded() {

View File

@@ -259,13 +259,17 @@ func (s *covenantlessService) CompleteAsyncPayment(
}
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{
VtxoKey: domain.VtxoKey{
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,
AsyncPayment: &domain.AsyncPaymentTxs{
RedeemTx: redeemTx,
@@ -278,6 +282,12 @@ func (s *covenantlessService) CompleteAsyncPayment(
return fmt.Errorf("failed to add vtxos: %s", err)
}
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 {
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
}
func (s *covenantlessService) GetRoundByTxid(ctx context.Context, poolTxid string) (*domain.Round, error) {
return s.repoManager.Rounds().GetRoundWithTxid(ctx, poolTxid)
func (s *covenantlessService) GetRoundByTxid(ctx context.Context, roundTxid string) (*domain.Round, error) {
return s.repoManager.Rounds().GetRoundWithTxid(ctx, roundTxid)
}
func (s *covenantlessService) GetRoundById(ctx context.Context, id string) (*domain.Round, error) {
@@ -1070,89 +1080,33 @@ func (s *covenantlessService) listenToScannerNotifications() {
mutx := &sync.Mutex{}
for vtxoKeys := range chVtxos {
go func(vtxoKeys map[string]ports.VtxoWithValue) {
vtxosRepo := s.repoManager.Vtxos()
roundRepo := s.repoManager.Rounds()
go func(vtxoKeys map[string][]ports.VtxoWithValue) {
for _, keys := range vtxoKeys {
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 {
// redeem
vtxos, err := vtxosRepo.GetVtxos(ctx, []domain.VtxoKey{v.VtxoKey})
if err != nil {
log.WithError(err).Warn("failed to retrieve vtxos, skipping...")
continue
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 := 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)
}
@@ -1162,10 +1116,19 @@ func (s *covenantlessService) getNextConnector(
ctx context.Context,
round domain.Round,
) (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)
if err != nil {
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 len(utxos) <= 0 {
@@ -1181,13 +1144,13 @@ func (s *covenantlessService) getNextConnector(
// search for an already existing connector
for _, u := range utxos {
if u.GetValue() == 450 {
if u.GetValue() == uint64(connectorAmount) {
return u.GetTxid(), u.GetIndex(), nil
}
}
for _, u := range utxos {
if u.GetValue() > 450 {
if u.GetValue() > uint64(connectorAmount) {
for _, b64 := range round.Connectors {
ptx, err := psbt.NewFromRawBytes(strings.NewReader(b64), true)
if err != nil {
@@ -1210,7 +1173,7 @@ func (s *covenantlessService) getNextConnector(
connectorTxid, err := s.wallet.BroadcastTransaction(ctx, signedConnectorTx)
if err != nil {
return "", 0, err
return "", 0, fmt.Errorf("failed to broadcast connector tx: %s", err)
}
log.Debugf("broadcasted connector tx %s", connectorTxid)
@@ -1464,8 +1427,90 @@ func (s *covenantlessService) saveEvents(
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(
forfeits []string, connectorTxid string, connectorVout uint32, vtxoTxid string,
forfeits []string, connectorTxid string, connectorVout uint32, vtxo domain.VtxoKey,
) (string, error) {
for _, forfeit := range forfeits {
forfeitTx, err := psbt.NewFromRawBytes(strings.NewReader(forfeit), true)
@@ -1476,9 +1521,10 @@ func findForfeitTxBitcoin(
connector := forfeitTx.UnsignedTx.TxIn[0]
vtxoInput := forfeitTx.UnsignedTx.TxIn[1]
if connector.PreviousOutPoint.String() == connectorTxid &&
if connector.PreviousOutPoint.Hash.String() == connectorTxid &&
connector.PreviousOutPoint.Index == connectorVout &&
vtxoInput.PreviousOutPoint.String() == vtxoTxid {
vtxoInput.PreviousOutPoint.Hash.String() == vtxo.Txid &&
vtxoInput.PreviousOutPoint.Index == vtxo.VOut {
return forfeit, nil
}
}

View File

@@ -13,6 +13,6 @@ type VtxoWithValue struct {
type BlockchainScanner interface {
WatchScripts(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)
}

View File

@@ -32,8 +32,8 @@ type TxBuilder interface {
BuildSweepTx(inputs []SweepInput) (signedSweepTx string, err error)
GetVtxoScript(userPubkey, aspPubkey *secp256k1.PublicKey) ([]byte, 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)
FinalizeAndExtractForfeit(tx string) (txhex string, err error)
// 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)
BuildAsyncPaymentTransactions(

View File

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

View File

@@ -52,7 +52,7 @@ func craftConnectorTx(
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)
inputs := make([]psetv2.InputArgs, 0, len(pset.Outputs))

View File

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

View File

@@ -190,12 +190,12 @@ func (m *mockedWallet) UnwatchScripts(
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)
var res <-chan map[string]ports.VtxoWithValue
var res <-chan map[string][]ports.VtxoWithValue
if a := args.Get(0); a != nil {
res = a.(<-chan map[string]ports.VtxoWithValue)
res = a.(<-chan map[string][]ports.VtxoWithValue)
}
return res
}

View File

@@ -102,12 +102,60 @@ func (b *txBuilder) VerifyTapscriptPartialSigs(tx string) (bool, string, error)
return true, txid, nil
}
func (b *txBuilder) FinalizeAndExtractForfeit(tx string) (string, error) {
ptx, _ := psbt.NewFromRawBytes(strings.NewReader(tx), true)
func (b *txBuilder) FinalizeAndExtract(tx string) (string, error) {
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 {
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)
ins = append(ins, vtxoOutpoint)
redeemTxWeightEstimator.AddTapscriptInput(64, tapscript)
redeemTxWeightEstimator.AddTapscriptInput(64*2, tapscript)
}
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 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) {

View File

@@ -190,12 +190,12 @@ func (m *mockedWallet) UnwatchScripts(
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)
var res <-chan map[string]ports.VtxoWithValue
var res <-chan map[string][]ports.VtxoWithValue
if a := args.Get(0); a != nil {
res = a.(<-chan map[string]ports.VtxoWithValue)
res = a.(<-chan map[string][]ports.VtxoWithValue)
}
return res
}

View File

@@ -1,6 +1,7 @@
package btcwallet
import (
"encoding/hex"
"fmt"
"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
signedInputs := make([]uint32, 0)
for idx := range tx.TxIn {
in := &packet.Inputs[idx]
@@ -76,13 +78,19 @@ func (s *service) signPsbt(packet *psbt.Packet, inputsToSign []int) ([]uint32, e
managedAddress = s.aspTaprootAddr
} else {
// segwit v0
var err error
managedAddress, _, _, err = s.wallet.ScriptForOutput(in.WitnessUtxo)
if err != nil {
log.Debugf("SignPsbt: Skipping input %d, error "+
"fetching script for output: %v", idx, err)
log.WithError(err).Debugf(
"failed to fetch address for input %d with script %s",
idx, hex.EncodeToString(in.WitnessUtxo.PkScript),
)
continue
}
}
signedInputs = append(signedInputs, uint32(idx))
bip32Infos := derivationPathForAddress(managedAddress)
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?
// prevOutputFetcher := wallet.PsbtPrevOutputFetcher(packet)
// sigHashes := txscript.NewTxSigHashes(tx, prevOutputFetcher)
ins, err := s.wallet.SignPsbt(packet)
if err != nil {
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(
// 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)
return ins, nil
}
func derivationPathForAddress(addr waddrmgr.ManagedPubKeyAddress) *psbt.Bip32Derivation {

View File

@@ -70,6 +70,10 @@ const (
mainAccount accountName = "main"
connectorAccount accountName = "connector"
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 (
@@ -852,18 +856,37 @@ func (s *service) UnwatchScripts(ctx context.Context, scripts []string) error {
func (s *service) GetNotificationChannel(
ctx context.Context,
) <-chan map[string]ports.VtxoWithValue {
ch := make(chan map[string]ports.VtxoWithValue)
) <-chan map[string][]ports.VtxoWithValue {
ch := make(chan map[string][]ports.VtxoWithValue)
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() {
switch m := n.(type) {
case chain.RelevantTx:
if _, sent := sentTxs[m.TxRecord.Hash]; sent {
continue
}
notification := s.castNotification(m.TxRecord)
cache(m.TxRecord.Hash)
ch <- notification
case chain.FilteredBlockConnected:
for _, tx := range m.RelevantTxs {
if _, sent := sentTxs[tx.Hash]; sent {
continue
}
notification := s.castNotification(tx)
cache(tx.Hash)
ch <- notification
}
}
@@ -879,11 +902,10 @@ func (s *service) IsTransactionConfirmed(
return s.extraAPI.getTxStatus(txid)
}
// https://github.com/bitcoin/bitcoin/blob/439e58c4d8194ca37f70346727d31f52e69592ec/src/policy/policy.cpp#L23C8-L23C11
func (s *service) GetDustAmount(
ctx context.Context,
) (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) {
@@ -904,8 +926,8 @@ func (s *service) GetTransaction(ctx context.Context, txid string) (string, erro
return hex.EncodeToString(buf.Bytes()), nil
}
func (s *service) castNotification(tx *wtxmgr.TxRecord) map[string]ports.VtxoWithValue {
vtxos := make(map[string]ports.VtxoWithValue)
func (s *service) castNotification(tx *wtxmgr.TxRecord) map[string][]ports.VtxoWithValue {
vtxos := make(map[string][]ports.VtxoWithValue)
s.watchedScriptsLock.RLock()
defer s.watchedScriptsLock.RUnlock()
@@ -916,13 +938,17 @@ func (s *service) castNotification(tx *wtxmgr.TxRecord) map[string]ports.VtxoWit
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{
Txid: tx.Hash.String(),
VOut: uint32(outputIndex),
},
Value: uint64(txout.Value),
}
})
}
return vtxos

View File

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

View File

@@ -35,7 +35,7 @@ func (s *service) UnwatchScripts(ctx context.Context, scripts []string) error {
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
}

View File

@@ -22,7 +22,7 @@ type service struct {
accountClient pb.AccountServiceClient
txClient pb.TransactionServiceClient
notifyClient pb.NotificationServiceClient
chVtxos chan map[string]ports.VtxoWithValue
chVtxos chan map[string][]ports.VtxoWithValue
isListening bool
}
@@ -35,7 +35,7 @@ func NewService(addr string) (ports.WalletService, error) {
accountClient := pb.NewAccountServiceClient(conn)
txClient := pb.NewTransactionServiceClient(conn)
notifyClient := pb.NewNotificationServiceClient(conn)
chVtxos := make(chan map[string]ports.VtxoWithValue)
chVtxos := make(chan map[string][]ports.VtxoWithValue)
svc := &service{
addr: addr,
conn: conn,
@@ -190,8 +190,8 @@ func (s *service) listenToNotifications() {
}
}
func toVtxos(utxos []*pb.Utxo) map[string]ports.VtxoWithValue {
vtxos := make(map[string]ports.VtxoWithValue, len(utxos))
func toVtxos(utxos []*pb.Utxo) map[string][]ports.VtxoWithValue {
vtxos := make(map[string][]ports.VtxoWithValue, len(utxos))
for _, utxo := range utxos {
// We want to notify for activity related to vtxos owner, therefore we skip
// 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
}
vtxos[utxo.Script] = ports.VtxoWithValue{
VtxoKey: domain.VtxoKey{Txid: utxo.GetTxid(),
VOut: utxo.GetIndex(),
vtxos[utxo.Script] = []ports.VtxoWithValue{
{
VtxoKey: domain.VtxoKey{Txid: utxo.GetTxid(),
VOut: utxo.GetIndex(),
},
Value: utxo.GetValue(),
},
Value: utxo.GetValue(),
}
}
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) {
feeRate := 0.11
feeRate := 0.2
fee := uint64(float64(vbytes) * feeRate)
return fee, nil
}

View File

@@ -2,6 +2,7 @@ package e2e_test
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
@@ -10,6 +11,12 @@ import (
"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"
"github.com/stretchr/testify/require"
)
@@ -143,6 +150,58 @@ func TestCollaborativeExit(t *testing.T) {
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) {
args := append([]string{"exec", "-t", "arkd", "ark"}, arg...)
return utils.RunCommand("docker", args...)
@@ -237,3 +296,27 @@ func setupAspWallet() error {
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 (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
@@ -9,6 +10,13 @@ import (
"testing"
"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"
"github.com/stretchr/testify/require"
)
@@ -155,6 +163,126 @@ func TestCollaborativeExit(t *testing.T) {
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) {
args := append([]string{"exec", "-t", "clarkd", "ark"}, arg...)
return utils.RunCommand("docker", args...)
@@ -254,7 +382,41 @@ func setupAspWallet() error {
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)
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
}