mirror of
https://github.com/aljazceru/ark.git
synced 2025-12-17 12:14:21 +01:00
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:
4
.github/workflows/ark.artifacts.yaml
vendored
4
.github/workflows/ark.artifacts.yaml
vendored
@@ -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
|
||||
|
||||
12
.github/workflows/ark.integration.yaml
vendored
12
.github/workflows/ark.integration.yaml
vendored
@@ -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
|
||||
|
||||
4
.github/workflows/ark.release.yaml
vendored
4
.github/workflows/ark.release.yaml
vendored
@@ -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
|
||||
|
||||
8
.github/workflows/ark.unit.yaml
vendored
8
.github/workflows/ark.unit.yaml
vendored
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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{}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user