mirror of
https://github.com/aljazceru/ark.git
synced 2025-12-17 20:24: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
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v2
|
uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
go-version: 1.21.0
|
go-version: 1.23.1
|
||||||
|
|
||||||
- name: Build binaries
|
- name: Build binaries
|
||||||
run: make build-all
|
run: make build-all
|
||||||
|
|||||||
12
.github/workflows/ark.integration.yaml
vendored
12
.github/workflows/ark.integration.yaml
vendored
@@ -2,7 +2,8 @@ name: ci_integration
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [master]
|
branches:
|
||||||
|
- master
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
@@ -15,11 +16,12 @@ jobs:
|
|||||||
run:
|
run:
|
||||||
working-directory: ./server
|
working-directory: ./server
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/setup-go@v3
|
|
||||||
with:
|
|
||||||
go-version: ">1.17.2"
|
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- run: go get -v -t -d ./...
|
- uses: actions/setup-go@v4
|
||||||
|
with:
|
||||||
|
go-version: '>=1.23.1'
|
||||||
|
- name: Run go work sync
|
||||||
|
run: go work sync
|
||||||
|
|
||||||
- name: Run Nigiri
|
- name: Run Nigiri
|
||||||
uses: vulpemventures/nigiri-github-action@v1
|
uses: vulpemventures/nigiri-github-action@v1
|
||||||
|
|||||||
4
.github/workflows/ark.release.yaml
vendored
4
.github/workflows/ark.release.yaml
vendored
@@ -16,9 +16,9 @@ jobs:
|
|||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v2
|
uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
go-version: 1.21.0
|
go-version: 1.23.1
|
||||||
|
|
||||||
# Build binaries for all architectures
|
# Build binaries for all architectures
|
||||||
- name: Build binaries
|
- name: Build binaries
|
||||||
|
|||||||
8
.github/workflows/ark.unit.yaml
vendored
8
.github/workflows/ark.unit.yaml
vendored
@@ -2,10 +2,11 @@ name: ci_unit
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
paths:
|
paths:
|
||||||
- 'server/**'
|
- 'server/**'
|
||||||
- 'pkg/client-sdk/**'
|
- 'pkg/client-sdk/**'
|
||||||
branches: [master]
|
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
@@ -56,7 +57,8 @@ jobs:
|
|||||||
uses: securego/gosec@master
|
uses: securego/gosec@master
|
||||||
with:
|
with:
|
||||||
args: '-severity high -quiet -exclude=G115 ./...'
|
args: '-severity high -quiet -exclude=G115 ./...'
|
||||||
- run: go get -v -t -d ./...
|
- name: Run go work sync
|
||||||
|
run: go work sync
|
||||||
- name: unit testing
|
- name: unit testing
|
||||||
run: make test
|
run: make test
|
||||||
|
|
||||||
@@ -83,4 +85,4 @@ jobs:
|
|||||||
args: '-severity high -quiet -exclude=G115 ./...'
|
args: '-severity high -quiet -exclude=G115 ./...'
|
||||||
- run: go get -v -t -d ./...
|
- run: go get -v -t -d ./...
|
||||||
- name: unit testing
|
- name: unit testing
|
||||||
run: make test
|
run: make test
|
||||||
@@ -11,7 +11,6 @@ services:
|
|||||||
- ARK_LOG_LEVEL=5
|
- ARK_LOG_LEVEL=5
|
||||||
- ARK_ROUND_LIFETIME=512
|
- ARK_ROUND_LIFETIME=512
|
||||||
- ARK_TX_BUILDER_TYPE=covenantless
|
- ARK_TX_BUILDER_TYPE=covenantless
|
||||||
- ARK_MIN_RELAY_FEE=200
|
|
||||||
- ARK_ESPLORA_URL=http://chopsticks:3000
|
- ARK_ESPLORA_URL=http://chopsticks:3000
|
||||||
- ARK_BITCOIND_RPC_USER=admin1
|
- ARK_BITCOIND_RPC_USER=admin1
|
||||||
- ARK_BITCOIND_RPC_PASS=123
|
- ARK_BITCOIND_RPC_PASS=123
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ type ArkClient interface {
|
|||||||
Unlock(ctx context.Context, password string) error
|
Unlock(ctx context.Context, password string) error
|
||||||
Lock(ctx context.Context, password string) error
|
Lock(ctx context.Context, password string) error
|
||||||
Balance(ctx context.Context, computeExpiryDetails bool) (*Balance, error)
|
Balance(ctx context.Context, computeExpiryDetails bool) (*Balance, error)
|
||||||
Receive(ctx context.Context) (string, string, error)
|
Receive(ctx context.Context) (offchainAddr, boardingAddr string, err error)
|
||||||
SendOnChain(ctx context.Context, receivers []Receiver) (string, error)
|
SendOnChain(ctx context.Context, receivers []Receiver) (string, error)
|
||||||
SendOffChain(
|
SendOffChain(
|
||||||
ctx context.Context, withExpiryCoinselect bool, receivers []Receiver,
|
ctx context.Context, withExpiryCoinselect bool, receivers []Receiver,
|
||||||
@@ -26,7 +26,7 @@ type ArkClient interface {
|
|||||||
) (string, error)
|
) (string, error)
|
||||||
SendAsync(ctx context.Context, withExpiryCoinselect bool, receivers []Receiver) (string, error)
|
SendAsync(ctx context.Context, withExpiryCoinselect bool, receivers []Receiver) (string, error)
|
||||||
Claim(ctx context.Context) (string, error)
|
Claim(ctx context.Context) (string, error)
|
||||||
ListVtxos(ctx context.Context) ([]client.Vtxo, []client.Vtxo, error)
|
ListVtxos(ctx context.Context) (spendable, spent []client.Vtxo, err error)
|
||||||
GetTransactionHistory(ctx context.Context) ([]Transaction, error)
|
GetTransactionHistory(ctx context.Context) ([]Transaction, error)
|
||||||
Dump(ctx context.Context) (seed string, err error)
|
Dump(ctx context.Context) (seed string, err error)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import (
|
|||||||
"github.com/ark-network/ark/pkg/client-sdk/client"
|
"github.com/ark-network/ark/pkg/client-sdk/client"
|
||||||
"github.com/ark-network/ark/pkg/client-sdk/explorer"
|
"github.com/ark-network/ark/pkg/client-sdk/explorer"
|
||||||
"github.com/ark-network/ark/pkg/client-sdk/internal/utils"
|
"github.com/ark-network/ark/pkg/client-sdk/internal/utils"
|
||||||
"github.com/ark-network/ark/pkg/client-sdk/internal/utils/redemption"
|
"github.com/ark-network/ark/pkg/client-sdk/redemption"
|
||||||
"github.com/ark-network/ark/pkg/client-sdk/store"
|
"github.com/ark-network/ark/pkg/client-sdk/store"
|
||||||
"github.com/ark-network/ark/pkg/client-sdk/wallet"
|
"github.com/ark-network/ark/pkg/client-sdk/wallet"
|
||||||
"github.com/btcsuite/btcd/btcec/v2/schnorr"
|
"github.com/btcsuite/btcd/btcec/v2/schnorr"
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import (
|
|||||||
"github.com/ark-network/ark/pkg/client-sdk/client"
|
"github.com/ark-network/ark/pkg/client-sdk/client"
|
||||||
"github.com/ark-network/ark/pkg/client-sdk/explorer"
|
"github.com/ark-network/ark/pkg/client-sdk/explorer"
|
||||||
"github.com/ark-network/ark/pkg/client-sdk/internal/utils"
|
"github.com/ark-network/ark/pkg/client-sdk/internal/utils"
|
||||||
"github.com/ark-network/ark/pkg/client-sdk/internal/utils/redemption"
|
"github.com/ark-network/ark/pkg/client-sdk/redemption"
|
||||||
"github.com/ark-network/ark/pkg/client-sdk/store"
|
"github.com/ark-network/ark/pkg/client-sdk/store"
|
||||||
"github.com/ark-network/ark/pkg/client-sdk/wallet"
|
"github.com/ark-network/ark/pkg/client-sdk/wallet"
|
||||||
"github.com/btcsuite/btcd/btcec/v2/schnorr"
|
"github.com/btcsuite/btcd/btcec/v2/schnorr"
|
||||||
@@ -975,6 +975,15 @@ func (a *covenantlessArkClient) handleRoundStream(
|
|||||||
|
|
||||||
var signerSession bitcointree.SignerSession
|
var signerSession bitcointree.SignerSession
|
||||||
|
|
||||||
|
const (
|
||||||
|
start = iota
|
||||||
|
roundSigningStarted
|
||||||
|
roundSigningNoncesGenerated
|
||||||
|
roundFinalization
|
||||||
|
)
|
||||||
|
|
||||||
|
step := start
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
@@ -991,6 +1000,9 @@ func (a *covenantlessArkClient) handleRoundStream(
|
|||||||
return "", fmt.Errorf("round failed: %s", event.(client.RoundFailedEvent).Reason)
|
return "", fmt.Errorf("round failed: %s", event.(client.RoundFailedEvent).Reason)
|
||||||
case client.RoundSigningStartedEvent:
|
case client.RoundSigningStartedEvent:
|
||||||
pingStop()
|
pingStop()
|
||||||
|
if step != start {
|
||||||
|
continue
|
||||||
|
}
|
||||||
log.Info("a round signing started")
|
log.Info("a round signing started")
|
||||||
signerSession, err = a.handleRoundSigningStarted(
|
signerSession, err = a.handleRoundSigningStarted(
|
||||||
ctx, roundEphemeralKey, event.(client.RoundSigningStartedEvent),
|
ctx, roundEphemeralKey, event.(client.RoundSigningStartedEvent),
|
||||||
@@ -998,8 +1010,12 @@ func (a *covenantlessArkClient) handleRoundStream(
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
step++
|
||||||
continue
|
continue
|
||||||
case client.RoundSigningNoncesGeneratedEvent:
|
case client.RoundSigningNoncesGeneratedEvent:
|
||||||
|
if step != roundSigningStarted {
|
||||||
|
continue
|
||||||
|
}
|
||||||
pingStop()
|
pingStop()
|
||||||
log.Info("round combined nonces generated")
|
log.Info("round combined nonces generated")
|
||||||
if err := a.handleRoundSigningNoncesGenerated(
|
if err := a.handleRoundSigningNoncesGenerated(
|
||||||
@@ -1007,8 +1023,12 @@ func (a *covenantlessArkClient) handleRoundStream(
|
|||||||
); err != nil {
|
); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
step++
|
||||||
continue
|
continue
|
||||||
case client.RoundFinalizationEvent:
|
case client.RoundFinalizationEvent:
|
||||||
|
if step != roundSigningNoncesGenerated {
|
||||||
|
continue
|
||||||
|
}
|
||||||
pingStop()
|
pingStop()
|
||||||
log.Info("a round finalization started")
|
log.Info("a round finalization started")
|
||||||
|
|
||||||
@@ -1031,6 +1051,8 @@ func (a *covenantlessArkClient) handleRoundStream(
|
|||||||
|
|
||||||
log.Info("done.")
|
log.Info("done.")
|
||||||
log.Info("waiting for round finalization...")
|
log.Info("waiting for round finalization...")
|
||||||
|
step++
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
"sort"
|
"sort"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/ark-network/ark/common"
|
"github.com/ark-network/ark/common"
|
||||||
"github.com/ark-network/ark/pkg/client-sdk/client"
|
"github.com/ark-network/ark/pkg/client-sdk/client"
|
||||||
@@ -265,8 +266,13 @@ func DecryptAES128(encrypted, password []byte) ([]byte, error) {
|
|||||||
return plaintext, nil
|
return plaintext, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var lock = &sync.Mutex{}
|
||||||
|
|
||||||
// deriveKey derives a 32 byte array key from a custom passhprase
|
// deriveKey derives a 32 byte array key from a custom passhprase
|
||||||
func deriveKey(password, salt []byte) ([]byte, []byte, error) {
|
func deriveKey(password, salt []byte) ([]byte, []byte, error) {
|
||||||
|
lock.Lock()
|
||||||
|
defer lock.Unlock()
|
||||||
|
|
||||||
if salt == nil {
|
if salt == nil {
|
||||||
salt = make([]byte, 32)
|
salt = make([]byte, 32)
|
||||||
if _, err := rand.Read(salt); err != nil {
|
if _, err := rand.Read(salt); err != nil {
|
||||||
|
|||||||
@@ -181,14 +181,16 @@ func (s *bitcoinWallet) SignTransaction(
|
|||||||
return "", fmt.Errorf("signature verification failed")
|
return "", fmt.Errorf("signature verification failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
updater.Upsbt.Inputs[i].TaprootScriptSpendSig = []*psbt.TaprootScriptSpendSig{
|
if len(updater.Upsbt.Inputs[i].TaprootScriptSpendSig) == 0 {
|
||||||
{
|
updater.Upsbt.Inputs[i].TaprootScriptSpendSig = make([]*psbt.TaprootScriptSpendSig, 0)
|
||||||
XOnlyPubKey: schnorr.SerializePubKey(pubkey),
|
|
||||||
LeafHash: hash.CloneBytes(),
|
|
||||||
Signature: sig.Serialize(),
|
|
||||||
SigHash: txscript.SigHashDefault,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updater.Upsbt.Inputs[i].TaprootScriptSpendSig = append(updater.Upsbt.Inputs[i].TaprootScriptSpendSig, &psbt.TaprootScriptSpendSig{
|
||||||
|
XOnlyPubKey: schnorr.SerializePubKey(pubkey),
|
||||||
|
LeafHash: hash.CloneBytes(),
|
||||||
|
Signature: sig.Serialize(),
|
||||||
|
SigHash: txscript.SigHashDefault,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ help:
|
|||||||
## intergrationtest: runs integration tests
|
## intergrationtest: runs integration tests
|
||||||
integrationtest:
|
integrationtest:
|
||||||
@echo "Running integration tests..."
|
@echo "Running integration tests..."
|
||||||
@go test -v -count=1 -race -timeout 200s github.com/ark-network/ark/server/test/e2e/...
|
@go test -v -count 1 -timeout 300s github.com/ark-network/ark/server/test/e2e/covenant
|
||||||
|
@go test -v -count 1 -timeout 300s github.com/ark-network/ark/server/test/e2e/covenantless
|
||||||
|
|
||||||
## lint: lint codebase
|
## lint: lint codebase
|
||||||
lint:
|
lint:
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -15,7 +14,6 @@ import (
|
|||||||
"github.com/ark-network/ark/server/internal/core/domain"
|
"github.com/ark-network/ark/server/internal/core/domain"
|
||||||
"github.com/ark-network/ark/server/internal/core/ports"
|
"github.com/ark-network/ark/server/internal/core/ports"
|
||||||
"github.com/btcsuite/btcd/btcec/v2/schnorr"
|
"github.com/btcsuite/btcd/btcec/v2/schnorr"
|
||||||
"github.com/btcsuite/btcd/btcutil/psbt"
|
|
||||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
@@ -613,87 +611,35 @@ func (s *covenantService) listenToScannerNotifications() {
|
|||||||
|
|
||||||
mutx := &sync.Mutex{}
|
mutx := &sync.Mutex{}
|
||||||
for vtxoKeys := range chVtxos {
|
for vtxoKeys := range chVtxos {
|
||||||
go func(vtxoKeys map[string]ports.VtxoWithValue) {
|
go func(vtxoKeys map[string][]ports.VtxoWithValue) {
|
||||||
vtxosRepo := s.repoManager.Vtxos()
|
vtxosRepo := s.repoManager.Vtxos()
|
||||||
roundRepo := s.repoManager.Rounds()
|
|
||||||
|
|
||||||
for _, v := range vtxoKeys {
|
for _, keys := range vtxoKeys {
|
||||||
// redeem
|
for _, v := range keys {
|
||||||
vtxos, err := vtxosRepo.GetVtxos(ctx, []domain.VtxoKey{v.VtxoKey})
|
vtxos, err := vtxosRepo.GetVtxos(ctx, []domain.VtxoKey{v.VtxoKey})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithError(err).Warn("failed to retrieve vtxos, skipping...")
|
log.WithError(err).Warn("failed to retrieve vtxos, skipping...")
|
||||||
continue
|
continue
|
||||||
|
}
|
||||||
|
vtxo := vtxos[0]
|
||||||
|
|
||||||
|
if !vtxo.Redeemed {
|
||||||
|
go func() {
|
||||||
|
if err := s.markAsRedeemed(ctx, vtxo); err != nil {
|
||||||
|
log.WithError(err).Warnf("failed to mark vtxo %s:%d as redeemed", vtxo.Txid, vtxo.VOut)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
if vtxo.Spent {
|
||||||
|
log.Infof("fraud detected on vtxo %s:%d", vtxo.Txid, vtxo.VOut)
|
||||||
|
go func() {
|
||||||
|
if err := s.reactToFraud(ctx, vtxo, mutx); err != nil {
|
||||||
|
log.WithError(err).Warnf("failed to prevent fraud for vtxo %s:%d", vtxo.Txid, vtxo.VOut)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
vtxo := vtxos[0]
|
|
||||||
|
|
||||||
if vtxo.Redeemed {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.repoManager.Vtxos().RedeemVtxos(ctx, []domain.VtxoKey{vtxo.VtxoKey}); err != nil {
|
|
||||||
log.WithError(err).Warn("failed to redeem vtxos, retrying...")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
log.Debugf("vtxo %s redeemed", vtxo.Txid)
|
|
||||||
|
|
||||||
if !vtxo.Spent {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debugf("fraud detected on vtxo %s", vtxo.Txid)
|
|
||||||
|
|
||||||
round, err := roundRepo.GetRoundWithTxid(ctx, vtxo.SpentBy)
|
|
||||||
if err != nil {
|
|
||||||
log.WithError(err).Warn("failed to retrieve round")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
mutx.Lock()
|
|
||||||
defer mutx.Unlock()
|
|
||||||
|
|
||||||
connectorTxid, connectorVout, err := s.getNextConnector(ctx, *round)
|
|
||||||
if err != nil {
|
|
||||||
log.WithError(err).Warn("failed to retrieve next connector")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
forfeitTx, err := findForfeitTxLiquid(round.ForfeitTxs, connectorTxid, connectorVout, vtxo.Txid)
|
|
||||||
if err != nil {
|
|
||||||
log.WithError(err).Warn("failed to retrieve forfeit tx")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.wallet.LockConnectorUtxos(ctx, []ports.TxOutpoint{txOutpoint{connectorTxid, connectorVout}}); err != nil {
|
|
||||||
log.WithError(err).Warn("failed to lock connector utxos")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
signedForfeitTx, err := s.wallet.SignTransaction(ctx, forfeitTx, false)
|
|
||||||
if err != nil {
|
|
||||||
log.WithError(err).Warn("failed to sign connector input in forfeit tx")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
signedForfeitTx, err = s.wallet.SignTransactionTapscript(ctx, signedForfeitTx, []int{1})
|
|
||||||
if err != nil {
|
|
||||||
log.WithError(err).Warn("failed to sign vtxo input in forfeit tx")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
forfeitTxHex, err := s.builder.FinalizeAndExtractForfeit(signedForfeitTx)
|
|
||||||
if err != nil {
|
|
||||||
log.WithError(err).Warn("failed to finalize forfeit tx")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
forfeitTxid, err := s.wallet.BroadcastTransaction(ctx, forfeitTxHex)
|
|
||||||
if err != nil {
|
|
||||||
log.WithError(err).Warn("failed to broadcast forfeit tx")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debugf("broadcasted forfeit tx %s", forfeitTxid)
|
|
||||||
}
|
}
|
||||||
}(vtxoKeys)
|
}(vtxoKeys)
|
||||||
}
|
}
|
||||||
@@ -703,20 +649,27 @@ func (s *covenantService) getNextConnector(
|
|||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
round domain.Round,
|
round domain.Round,
|
||||||
) (string, uint32, error) {
|
) (string, uint32, error) {
|
||||||
connectorTx, err := psetv2.NewPsetFromBase64(round.Connectors[0])
|
lastConnectorPtx, err := psetv2.NewPsetFromBase64(round.Connectors[len(round.Connectors)-1])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", 0, err
|
return "", 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
prevout := connectorTx.Inputs[0].WitnessUtxo
|
var connectorAmount uint64
|
||||||
if prevout == nil {
|
for i := len(lastConnectorPtx.Outputs) - 1; i >= 0; i-- {
|
||||||
return "", 0, fmt.Errorf("connector prevout not found")
|
o := lastConnectorPtx.Outputs[i]
|
||||||
|
if len(o.Script) <= 0 {
|
||||||
|
continue // skip the fee output
|
||||||
|
}
|
||||||
|
|
||||||
|
connectorAmount = o.Value
|
||||||
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
utxos, err := s.wallet.ListConnectorUtxos(ctx, round.ConnectorAddress)
|
utxos, err := s.wallet.ListConnectorUtxos(ctx, round.ConnectorAddress)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", 0, err
|
return "", 0, err
|
||||||
}
|
}
|
||||||
|
log.Debugf("found %d connector utxos, dust amount is %d", len(utxos), connectorAmount)
|
||||||
|
|
||||||
// if we do not find any utxos, we make sure to wait for the connector outpoint to be confirmed then we retry
|
// if we do not find any utxos, we make sure to wait for the connector outpoint to be confirmed then we retry
|
||||||
if len(utxos) <= 0 {
|
if len(utxos) <= 0 {
|
||||||
@@ -732,21 +685,26 @@ func (s *covenantService) getNextConnector(
|
|||||||
|
|
||||||
// search for an already existing connector
|
// search for an already existing connector
|
||||||
for _, u := range utxos {
|
for _, u := range utxos {
|
||||||
if u.GetValue() == 450 {
|
if u.GetValue() == connectorAmount {
|
||||||
return u.GetTxid(), u.GetIndex(), nil
|
return u.GetTxid(), u.GetIndex(), nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, u := range utxos {
|
for _, u := range utxos {
|
||||||
if u.GetValue() > 450 {
|
if u.GetValue() > connectorAmount {
|
||||||
for _, b64 := range round.Connectors {
|
for _, b64 := range round.Connectors {
|
||||||
partial, err := psbt.NewFromRawBytes(strings.NewReader(b64), true)
|
partial, err := psetv2.NewPsetFromBase64(b64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", 0, err
|
return "", 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, i := range partial.UnsignedTx.TxIn {
|
for _, i := range partial.Inputs {
|
||||||
if i.PreviousOutPoint.Hash.String() == u.GetTxid() && i.PreviousOutPoint.Index == u.GetIndex() {
|
txhash, err := chainhash.NewHash(i.PreviousTxid)
|
||||||
|
if err != nil {
|
||||||
|
return "", 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if txhash.String() == u.GetTxid() && i.PreviousTxIndex == u.GetIndex() {
|
||||||
connectorOutpoint := txOutpoint{u.GetTxid(), u.GetIndex()}
|
connectorOutpoint := txOutpoint{u.GetTxid(), u.GetIndex()}
|
||||||
|
|
||||||
if err := s.wallet.LockConnectorUtxos(ctx, []ports.TxOutpoint{connectorOutpoint}); err != nil {
|
if err := s.wallet.LockConnectorUtxos(ctx, []ports.TxOutpoint{connectorOutpoint}); err != nil {
|
||||||
@@ -780,6 +738,61 @@ func (s *covenantService) getNextConnector(
|
|||||||
return "", 0, fmt.Errorf("no connector utxos found")
|
return "", 0, fmt.Errorf("no connector utxos found")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *covenantService) reactToFraud(ctx context.Context, vtxo domain.Vtxo, mutx *sync.Mutex) error {
|
||||||
|
round, err := s.repoManager.Rounds().GetRoundWithTxid(ctx, vtxo.SpentBy)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to retrieve round: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
mutx.Lock()
|
||||||
|
defer mutx.Unlock()
|
||||||
|
|
||||||
|
connectorTxid, connectorVout, err := s.getNextConnector(ctx, *round)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to retrieve next connector: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
forfeitTx, err := findForfeitTxLiquid(round.ForfeitTxs, connectorTxid, connectorVout, vtxo.Txid)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to find forfeit tx: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.wallet.LockConnectorUtxos(ctx, []ports.TxOutpoint{txOutpoint{connectorTxid, connectorVout}}); err != nil {
|
||||||
|
return fmt.Errorf("failed to lock connector utxos: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
signedForfeitTx, err := s.wallet.SignTransaction(ctx, forfeitTx, false)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to sign connector input in forfeit tx: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
signedForfeitTx, err = s.wallet.SignTransactionTapscript(ctx, signedForfeitTx, []int{1})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to sign vtxo input in forfeit tx: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
forfeitTxHex, err := s.builder.FinalizeAndExtract(signedForfeitTx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to finalize forfeit tx: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
forfeitTxid, err := s.wallet.BroadcastTransaction(ctx, forfeitTxHex)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to broadcast forfeit tx: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("broadcasted forfeit tx %s", forfeitTxid)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *covenantService) markAsRedeemed(ctx context.Context, vtxo domain.Vtxo) error {
|
||||||
|
if err := s.repoManager.Vtxos().RedeemVtxos(ctx, []domain.VtxoKey{vtxo.VtxoKey}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Debugf("vtxo %s redeemed", vtxo.Txid)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *covenantService) updateVtxoSet(round *domain.Round) {
|
func (s *covenantService) updateVtxoSet(round *domain.Round) {
|
||||||
// Update the vtxo set only after a round is finalized.
|
// Update the vtxo set only after a round is finalized.
|
||||||
if !round.IsEnded() {
|
if !round.IsEnded() {
|
||||||
|
|||||||
@@ -259,13 +259,17 @@ func (s *covenantlessService) CompleteAsyncPayment(
|
|||||||
}
|
}
|
||||||
|
|
||||||
vtxos := make([]domain.Vtxo, 0, len(asyncPayData.receivers))
|
vtxos := make([]domain.Vtxo, 0, len(asyncPayData.receivers))
|
||||||
for i, receiver := range asyncPayData.receivers {
|
|
||||||
|
for outIndex, out := range redeemPtx.UnsignedTx.TxOut {
|
||||||
vtxos = append(vtxos, domain.Vtxo{
|
vtxos = append(vtxos, domain.Vtxo{
|
||||||
VtxoKey: domain.VtxoKey{
|
VtxoKey: domain.VtxoKey{
|
||||||
Txid: redeemTxid,
|
Txid: redeemTxid,
|
||||||
VOut: uint32(i),
|
VOut: uint32(outIndex),
|
||||||
|
},
|
||||||
|
Receiver: domain.Receiver{
|
||||||
|
Pubkey: asyncPayData.receivers[outIndex].Pubkey,
|
||||||
|
Amount: uint64(out.Value),
|
||||||
},
|
},
|
||||||
Receiver: receiver,
|
|
||||||
ExpireAt: asyncPayData.expireAt,
|
ExpireAt: asyncPayData.expireAt,
|
||||||
AsyncPayment: &domain.AsyncPaymentTxs{
|
AsyncPayment: &domain.AsyncPaymentTxs{
|
||||||
RedeemTx: redeemTx,
|
RedeemTx: redeemTx,
|
||||||
@@ -278,6 +282,12 @@ func (s *covenantlessService) CompleteAsyncPayment(
|
|||||||
return fmt.Errorf("failed to add vtxos: %s", err)
|
return fmt.Errorf("failed to add vtxos: %s", err)
|
||||||
}
|
}
|
||||||
log.Infof("added %d vtxos", len(vtxos))
|
log.Infof("added %d vtxos", len(vtxos))
|
||||||
|
if err := s.startWatchingVtxos(vtxos); err != nil {
|
||||||
|
log.WithError(err).Warn(
|
||||||
|
"failed to start watching vtxos",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
log.Debugf("started watching %d vtxos", len(vtxos))
|
||||||
|
|
||||||
if err := s.repoManager.Vtxos().SpendVtxos(ctx, spentVtxos, redeemTxid); err != nil {
|
if err := s.repoManager.Vtxos().SpendVtxos(ctx, spentVtxos, redeemTxid); err != nil {
|
||||||
return fmt.Errorf("failed to spend vtxo: %s", err)
|
return fmt.Errorf("failed to spend vtxo: %s", err)
|
||||||
@@ -546,8 +556,8 @@ func (s *covenantlessService) GetEventsChannel(ctx context.Context) <-chan domai
|
|||||||
return s.eventsCh
|
return s.eventsCh
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *covenantlessService) GetRoundByTxid(ctx context.Context, poolTxid string) (*domain.Round, error) {
|
func (s *covenantlessService) GetRoundByTxid(ctx context.Context, roundTxid string) (*domain.Round, error) {
|
||||||
return s.repoManager.Rounds().GetRoundWithTxid(ctx, poolTxid)
|
return s.repoManager.Rounds().GetRoundWithTxid(ctx, roundTxid)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *covenantlessService) GetRoundById(ctx context.Context, id string) (*domain.Round, error) {
|
func (s *covenantlessService) GetRoundById(ctx context.Context, id string) (*domain.Round, error) {
|
||||||
@@ -1070,89 +1080,33 @@ func (s *covenantlessService) listenToScannerNotifications() {
|
|||||||
|
|
||||||
mutx := &sync.Mutex{}
|
mutx := &sync.Mutex{}
|
||||||
for vtxoKeys := range chVtxos {
|
for vtxoKeys := range chVtxos {
|
||||||
go func(vtxoKeys map[string]ports.VtxoWithValue) {
|
go func(vtxoKeys map[string][]ports.VtxoWithValue) {
|
||||||
vtxosRepo := s.repoManager.Vtxos()
|
for _, keys := range vtxoKeys {
|
||||||
roundRepo := s.repoManager.Rounds()
|
for _, v := range keys {
|
||||||
|
vtxos, err := s.repoManager.Vtxos().GetVtxos(ctx, []domain.VtxoKey{v.VtxoKey})
|
||||||
|
if err != nil {
|
||||||
|
log.WithError(err).Warn("failed to retrieve vtxos, skipping...")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
vtxo := vtxos[0]
|
||||||
|
|
||||||
for _, v := range vtxoKeys {
|
if !vtxo.Redeemed {
|
||||||
// redeem
|
go func() {
|
||||||
vtxos, err := vtxosRepo.GetVtxos(ctx, []domain.VtxoKey{v.VtxoKey})
|
if err := s.markAsRedeemed(ctx, vtxo); err != nil {
|
||||||
if err != nil {
|
log.WithError(err).Warnf("failed to mark vtxo %s:%d as redeemed", vtxo.Txid, vtxo.VOut)
|
||||||
log.WithError(err).Warn("failed to retrieve vtxos, skipping...")
|
}
|
||||||
continue
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
if vtxo.Spent {
|
||||||
|
log.Infof("fraud detected on vtxo %s:%d", vtxo.Txid, vtxo.VOut)
|
||||||
|
go func() {
|
||||||
|
if err := s.reactToFraud(ctx, vtxo, mutx); err != nil {
|
||||||
|
log.WithError(err).Warnf("failed to prevent fraud for vtxo %s:%d", vtxo.Txid, vtxo.VOut)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
vtxo := vtxos[0]
|
|
||||||
|
|
||||||
if vtxo.Redeemed {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.repoManager.Vtxos().RedeemVtxos(
|
|
||||||
ctx, []domain.VtxoKey{vtxo.VtxoKey},
|
|
||||||
); err != nil {
|
|
||||||
log.WithError(err).Warn("failed to redeem vtxos, retrying...")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
log.Debugf("vtxo %s redeemed", vtxo.Txid)
|
|
||||||
|
|
||||||
if !vtxo.Spent {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debugf("fraud detected on vtxo %s", vtxo.Txid)
|
|
||||||
|
|
||||||
round, err := roundRepo.GetRoundWithTxid(ctx, vtxo.SpentBy)
|
|
||||||
if err != nil {
|
|
||||||
log.WithError(err).Warn("failed to retrieve round")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
mutx.Lock()
|
|
||||||
defer mutx.Unlock()
|
|
||||||
|
|
||||||
connectorTxid, connectorVout, err := s.getNextConnector(ctx, *round)
|
|
||||||
if err != nil {
|
|
||||||
log.WithError(err).Warn("failed to retrieve next connector")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
forfeitTx, err := findForfeitTxBitcoin(round.ForfeitTxs, connectorTxid, connectorVout, vtxo.Txid)
|
|
||||||
if err != nil {
|
|
||||||
log.WithError(err).Warn("failed to retrieve forfeit tx")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.wallet.LockConnectorUtxos(ctx, []ports.TxOutpoint{txOutpoint{connectorTxid, connectorVout}}); err != nil {
|
|
||||||
log.WithError(err).Warn("failed to lock connector utxos")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
signedForfeitTx, err := s.wallet.SignTransaction(ctx, forfeitTx, false)
|
|
||||||
if err != nil {
|
|
||||||
log.WithError(err).Warn("failed to sign connector input in forfeit tx")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
signedForfeitTx, err = s.wallet.SignTransactionTapscript(ctx, signedForfeitTx, []int{1})
|
|
||||||
if err != nil {
|
|
||||||
log.WithError(err).Warn("failed to sign vtxo input in forfeit tx")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
forfeitTxHex, err := s.builder.FinalizeAndExtractForfeit(signedForfeitTx)
|
|
||||||
if err != nil {
|
|
||||||
log.WithError(err).Warn("failed to finalize forfeit tx")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
forfeitTxid, err := s.wallet.BroadcastTransaction(ctx, forfeitTxHex)
|
|
||||||
if err != nil {
|
|
||||||
log.WithError(err).Warn("failed to broadcast forfeit tx")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debugf("broadcasted forfeit tx %s", forfeitTxid)
|
|
||||||
}
|
}
|
||||||
}(vtxoKeys)
|
}(vtxoKeys)
|
||||||
}
|
}
|
||||||
@@ -1162,10 +1116,19 @@ func (s *covenantlessService) getNextConnector(
|
|||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
round domain.Round,
|
round domain.Round,
|
||||||
) (string, uint32, error) {
|
) (string, uint32, error) {
|
||||||
|
lastConnectorPtx, err := psbt.NewFromRawBytes(strings.NewReader(round.Connectors[len(round.Connectors)-1]), true)
|
||||||
|
if err != nil {
|
||||||
|
return "", 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
lastOutput := lastConnectorPtx.UnsignedTx.TxOut[len(lastConnectorPtx.UnsignedTx.TxOut)-1]
|
||||||
|
connectorAmount := lastOutput.Value
|
||||||
|
|
||||||
utxos, err := s.wallet.ListConnectorUtxos(ctx, round.ConnectorAddress)
|
utxos, err := s.wallet.ListConnectorUtxos(ctx, round.ConnectorAddress)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", 0, err
|
return "", 0, err
|
||||||
}
|
}
|
||||||
|
log.Debugf("found %d connector utxos, dust amount is %d", len(utxos), connectorAmount)
|
||||||
|
|
||||||
// if we do not find any utxos, we make sure to wait for the connector outpoint to be confirmed then we retry
|
// if we do not find any utxos, we make sure to wait for the connector outpoint to be confirmed then we retry
|
||||||
if len(utxos) <= 0 {
|
if len(utxos) <= 0 {
|
||||||
@@ -1181,13 +1144,13 @@ func (s *covenantlessService) getNextConnector(
|
|||||||
|
|
||||||
// search for an already existing connector
|
// search for an already existing connector
|
||||||
for _, u := range utxos {
|
for _, u := range utxos {
|
||||||
if u.GetValue() == 450 {
|
if u.GetValue() == uint64(connectorAmount) {
|
||||||
return u.GetTxid(), u.GetIndex(), nil
|
return u.GetTxid(), u.GetIndex(), nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, u := range utxos {
|
for _, u := range utxos {
|
||||||
if u.GetValue() > 450 {
|
if u.GetValue() > uint64(connectorAmount) {
|
||||||
for _, b64 := range round.Connectors {
|
for _, b64 := range round.Connectors {
|
||||||
ptx, err := psbt.NewFromRawBytes(strings.NewReader(b64), true)
|
ptx, err := psbt.NewFromRawBytes(strings.NewReader(b64), true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1210,7 +1173,7 @@ func (s *covenantlessService) getNextConnector(
|
|||||||
|
|
||||||
connectorTxid, err := s.wallet.BroadcastTransaction(ctx, signedConnectorTx)
|
connectorTxid, err := s.wallet.BroadcastTransaction(ctx, signedConnectorTx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", 0, err
|
return "", 0, fmt.Errorf("failed to broadcast connector tx: %s", err)
|
||||||
}
|
}
|
||||||
log.Debugf("broadcasted connector tx %s", connectorTxid)
|
log.Debugf("broadcasted connector tx %s", connectorTxid)
|
||||||
|
|
||||||
@@ -1464,8 +1427,90 @@ func (s *covenantlessService) saveEvents(
|
|||||||
return s.repoManager.Rounds().AddOrUpdateRound(ctx, *round)
|
return s.repoManager.Rounds().AddOrUpdateRound(ctx, *round)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *covenantlessService) reactToFraud(ctx context.Context, vtxo domain.Vtxo, mutx *sync.Mutex) error {
|
||||||
|
mutx.Lock()
|
||||||
|
defer mutx.Unlock()
|
||||||
|
roundRepo := s.repoManager.Rounds()
|
||||||
|
|
||||||
|
round, err := roundRepo.GetRoundWithTxid(ctx, vtxo.SpentBy)
|
||||||
|
if err != nil {
|
||||||
|
vtxosRepo := s.repoManager.Vtxos()
|
||||||
|
|
||||||
|
// if the round is not found, the utxo may be spent by an async payment redeem tx
|
||||||
|
vtxos, err := vtxosRepo.GetVtxos(ctx, []domain.VtxoKey{
|
||||||
|
{Txid: vtxo.SpentBy, VOut: 0},
|
||||||
|
})
|
||||||
|
if err != nil || len(vtxos) <= 0 {
|
||||||
|
return fmt.Errorf("failed to retrieve round: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
asyncPayVtxo := vtxos[0]
|
||||||
|
if asyncPayVtxo.Redeemed { // redeem tx is already onchain
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("vtxo %s:%d has been spent by async payment", vtxo.Txid, vtxo.VOut)
|
||||||
|
|
||||||
|
redeemTxHex, err := s.builder.FinalizeAndExtract(asyncPayVtxo.AsyncPayment.RedeemTx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to finalize redeem tx: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
redeemTxid, err := s.wallet.BroadcastTransaction(ctx, redeemTxHex)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to broadcast redeem tx: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("broadcasted redeem tx %s", redeemTxid)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
connectorTxid, connectorVout, err := s.getNextConnector(ctx, *round)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get next connector: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("found next connector %s:%d", connectorTxid, connectorVout)
|
||||||
|
|
||||||
|
forfeitTx, err := findForfeitTxBitcoin(round.ForfeitTxs, connectorTxid, connectorVout, vtxo.VtxoKey)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to find forfeit tx: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.wallet.LockConnectorUtxos(ctx, []ports.TxOutpoint{txOutpoint{connectorTxid, connectorVout}}); err != nil {
|
||||||
|
return fmt.Errorf("failed to lock connector utxos: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
signedForfeitTx, err := s.wallet.SignTransactionTapscript(ctx, forfeitTx, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to sign forfeit tx: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
forfeitTxHex, err := s.builder.FinalizeAndExtract(signedForfeitTx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to finalize forfeit tx: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
forfeitTxid, err := s.wallet.BroadcastTransaction(ctx, forfeitTxHex)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to broadcast forfeit tx: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("broadcasted forfeit tx %s", forfeitTxid)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *covenantlessService) markAsRedeemed(ctx context.Context, vtxo domain.Vtxo) error {
|
||||||
|
if err := s.repoManager.Vtxos().RedeemVtxos(ctx, []domain.VtxoKey{vtxo.VtxoKey}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debugf("vtxo %s:%d redeemed", vtxo.Txid, vtxo.VOut)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func findForfeitTxBitcoin(
|
func findForfeitTxBitcoin(
|
||||||
forfeits []string, connectorTxid string, connectorVout uint32, vtxoTxid string,
|
forfeits []string, connectorTxid string, connectorVout uint32, vtxo domain.VtxoKey,
|
||||||
) (string, error) {
|
) (string, error) {
|
||||||
for _, forfeit := range forfeits {
|
for _, forfeit := range forfeits {
|
||||||
forfeitTx, err := psbt.NewFromRawBytes(strings.NewReader(forfeit), true)
|
forfeitTx, err := psbt.NewFromRawBytes(strings.NewReader(forfeit), true)
|
||||||
@@ -1476,9 +1521,10 @@ func findForfeitTxBitcoin(
|
|||||||
connector := forfeitTx.UnsignedTx.TxIn[0]
|
connector := forfeitTx.UnsignedTx.TxIn[0]
|
||||||
vtxoInput := forfeitTx.UnsignedTx.TxIn[1]
|
vtxoInput := forfeitTx.UnsignedTx.TxIn[1]
|
||||||
|
|
||||||
if connector.PreviousOutPoint.String() == connectorTxid &&
|
if connector.PreviousOutPoint.Hash.String() == connectorTxid &&
|
||||||
connector.PreviousOutPoint.Index == connectorVout &&
|
connector.PreviousOutPoint.Index == connectorVout &&
|
||||||
vtxoInput.PreviousOutPoint.String() == vtxoTxid {
|
vtxoInput.PreviousOutPoint.Hash.String() == vtxo.Txid &&
|
||||||
|
vtxoInput.PreviousOutPoint.Index == vtxo.VOut {
|
||||||
return forfeit, nil
|
return forfeit, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,6 @@ type VtxoWithValue struct {
|
|||||||
type BlockchainScanner interface {
|
type BlockchainScanner interface {
|
||||||
WatchScripts(ctx context.Context, scripts []string) error
|
WatchScripts(ctx context.Context, scripts []string) error
|
||||||
UnwatchScripts(ctx context.Context, scripts []string) error
|
UnwatchScripts(ctx context.Context, scripts []string) error
|
||||||
GetNotificationChannel(ctx context.Context) <-chan map[string]VtxoWithValue
|
GetNotificationChannel(ctx context.Context) <-chan map[string][]VtxoWithValue
|
||||||
IsTransactionConfirmed(ctx context.Context, txid string) (isConfirmed bool, blocktime int64, err error)
|
IsTransactionConfirmed(ctx context.Context, txid string) (isConfirmed bool, blocktime int64, err error)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,8 +32,8 @@ type TxBuilder interface {
|
|||||||
BuildSweepTx(inputs []SweepInput) (signedSweepTx string, err error)
|
BuildSweepTx(inputs []SweepInput) (signedSweepTx string, err error)
|
||||||
GetVtxoScript(userPubkey, aspPubkey *secp256k1.PublicKey) ([]byte, error)
|
GetVtxoScript(userPubkey, aspPubkey *secp256k1.PublicKey) ([]byte, error)
|
||||||
GetSweepInput(parentblocktime int64, node tree.Node) (expirationtime int64, sweepInput SweepInput, err error)
|
GetSweepInput(parentblocktime int64, node tree.Node) (expirationtime int64, sweepInput SweepInput, err error)
|
||||||
|
FinalizeAndExtract(tx string) (txhex string, err error)
|
||||||
VerifyTapscriptPartialSigs(tx string) (valid bool, txid string, err error)
|
VerifyTapscriptPartialSigs(tx string) (valid bool, txid string, err error)
|
||||||
FinalizeAndExtractForfeit(tx string) (txhex string, err error)
|
|
||||||
// FindLeaves returns all the leaves txs that are reachable from the given outpoint
|
// FindLeaves returns all the leaves txs that are reachable from the given outpoint
|
||||||
FindLeaves(congestionTree tree.CongestionTree, fromtxid string, vout uint32) (leaves []tree.Node, err error)
|
FindLeaves(congestionTree tree.CongestionTree, fromtxid string, vout uint32) (leaves []tree.Node, err error)
|
||||||
BuildAsyncPaymentTransactions(
|
BuildAsyncPaymentTransactions(
|
||||||
|
|||||||
@@ -23,10 +23,6 @@ import (
|
|||||||
"github.com/vulpemventures/go-elements/transaction"
|
"github.com/vulpemventures/go-elements/transaction"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
connectorAmount = uint64(450)
|
|
||||||
)
|
|
||||||
|
|
||||||
type txBuilder struct {
|
type txBuilder struct {
|
||||||
wallet ports.WalletService
|
wallet ports.WalletService
|
||||||
net common.Network
|
net common.Network
|
||||||
@@ -112,12 +108,17 @@ func (b *txBuilder) BuildForfeitTxs(
|
|||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
connectorTxs, err := b.createConnectors(poolTx, payments, connectorAddress, connectorFeeAmount)
|
connectorAmount, err := b.wallet.GetDustAmount(context.Background())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
forfeitTxs, err = b.createForfeitTxs(aspPubkey, payments, connectorTxs)
|
connectorTxs, err := b.createConnectors(poolTx, payments, connectorAddress, connectorAmount, connectorFeeAmount)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
forfeitTxs, err = b.createForfeitTxs(aspPubkey, payments, connectorTxs, connectorAmount)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
@@ -309,7 +310,7 @@ func (b *txBuilder) VerifyTapscriptPartialSigs(tx string) (bool, string, error)
|
|||||||
return true, txid, nil
|
return true, txid, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *txBuilder) FinalizeAndExtractForfeit(tx string) (string, error) {
|
func (b *txBuilder) FinalizeAndExtract(tx string) (string, error) {
|
||||||
p, err := psetv2.NewPsetFromBase64(tx)
|
p, err := psetv2.NewPsetFromBase64(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
@@ -437,9 +438,14 @@ func (b *txBuilder) createPoolTx(
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dustAmount, err := b.wallet.GetDustAmount(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
receivers := getOnchainReceivers(payments)
|
receivers := getOnchainReceivers(payments)
|
||||||
nbOfInputs := countSpentVtxos(payments)
|
nbOfInputs := countSpentVtxos(payments)
|
||||||
connectorsAmount := (connectorAmount + connectorMinRelayFee) * nbOfInputs
|
connectorsAmount := (dustAmount + connectorMinRelayFee) * nbOfInputs
|
||||||
if nbOfInputs > 1 {
|
if nbOfInputs > 1 {
|
||||||
connectorsAmount -= connectorMinRelayFee
|
connectorsAmount -= connectorMinRelayFee
|
||||||
}
|
}
|
||||||
@@ -744,7 +750,9 @@ func (b *txBuilder) VerifyAndCombinePartialTx(dest string, src string) (string,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (b *txBuilder) createConnectors(
|
func (b *txBuilder) createConnectors(
|
||||||
poolTx string, payments []domain.Payment, connectorAddress string, feeAmount uint64,
|
poolTx string, payments []domain.Payment,
|
||||||
|
connectorAddress string,
|
||||||
|
connectorAmount, feeAmount uint64,
|
||||||
) ([]*psetv2.Pset, error) {
|
) ([]*psetv2.Pset, error) {
|
||||||
txid, _ := getTxid(poolTx)
|
txid, _ := getTxid(poolTx)
|
||||||
|
|
||||||
@@ -798,7 +806,10 @@ func (b *txBuilder) createConnectors(
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
txid, _ := getPsetId(connectorTx)
|
txid, err := getPsetId(connectorTx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
previousInput = psetv2.InputArgs{
|
previousInput = psetv2.InputArgs{
|
||||||
Txid: txid,
|
Txid: txid,
|
||||||
@@ -812,7 +823,7 @@ func (b *txBuilder) createConnectors(
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (b *txBuilder) createForfeitTxs(
|
func (b *txBuilder) createForfeitTxs(
|
||||||
aspPubkey *secp256k1.PublicKey, payments []domain.Payment, connectors []*psetv2.Pset,
|
aspPubkey *secp256k1.PublicKey, payments []domain.Payment, connectors []*psetv2.Pset, connectorAmount uint64,
|
||||||
) ([]string, error) {
|
) ([]string, error) {
|
||||||
aspScript, err := p2wpkhScript(aspPubkey, b.onchainNetwork())
|
aspScript, err := p2wpkhScript(aspPubkey, b.onchainNetwork())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -855,7 +866,7 @@ func (b *txBuilder) createForfeitTxs(
|
|||||||
|
|
||||||
for _, connector := range connectors {
|
for _, connector := range connectors {
|
||||||
txs, err := b.craftForfeitTxs(
|
txs, err := b.craftForfeitTxs(
|
||||||
connector, vtxo, *forfeitProof, vtxoScript, aspScript,
|
connector, connectorAmount, vtxo, *forfeitProof, vtxoScript, aspScript,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ func craftConnectorTx(
|
|||||||
return ptx, nil
|
return ptx, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getConnectorInputs(pset *psetv2.Pset) ([]psetv2.InputArgs, []*transaction.TxOutput) {
|
func getConnectorInputs(pset *psetv2.Pset, connectorAmount uint64) ([]psetv2.InputArgs, []*transaction.TxOutput) {
|
||||||
txID, _ := getPsetId(pset)
|
txID, _ := getPsetId(pset)
|
||||||
|
|
||||||
inputs := make([]psetv2.InputArgs, 0, len(pset.Outputs))
|
inputs := make([]psetv2.InputArgs, 0, len(pset.Outputs))
|
||||||
|
|||||||
@@ -16,11 +16,12 @@ import (
|
|||||||
|
|
||||||
func (b *txBuilder) craftForfeitTxs(
|
func (b *txBuilder) craftForfeitTxs(
|
||||||
connectorTx *psetv2.Pset,
|
connectorTx *psetv2.Pset,
|
||||||
|
connectorAmount uint64,
|
||||||
vtxo domain.Vtxo,
|
vtxo domain.Vtxo,
|
||||||
vtxoForfeitTapleaf taproot.TapscriptElementsProof,
|
vtxoForfeitTapleaf taproot.TapscriptElementsProof,
|
||||||
vtxoScript, aspScript []byte,
|
vtxoScript, aspScript []byte,
|
||||||
) (forfeitTxs []string, err error) {
|
) (forfeitTxs []string, err error) {
|
||||||
connectors, prevouts := getConnectorInputs(connectorTx)
|
connectors, prevouts := getConnectorInputs(connectorTx, connectorAmount)
|
||||||
|
|
||||||
for i, connectorInput := range connectors {
|
for i, connectorInput := range connectors {
|
||||||
weightEstimator := &input.TxWeightEstimator{}
|
weightEstimator := &input.TxWeightEstimator{}
|
||||||
|
|||||||
@@ -190,12 +190,12 @@ func (m *mockedWallet) UnwatchScripts(
|
|||||||
return args.Error(0)
|
return args.Error(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockedWallet) GetNotificationChannel(ctx context.Context) <-chan map[string]ports.VtxoWithValue {
|
func (m *mockedWallet) GetNotificationChannel(ctx context.Context) <-chan map[string][]ports.VtxoWithValue {
|
||||||
args := m.Called(ctx)
|
args := m.Called(ctx)
|
||||||
|
|
||||||
var res <-chan map[string]ports.VtxoWithValue
|
var res <-chan map[string][]ports.VtxoWithValue
|
||||||
if a := args.Get(0); a != nil {
|
if a := args.Get(0); a != nil {
|
||||||
res = a.(<-chan map[string]ports.VtxoWithValue)
|
res = a.(<-chan map[string][]ports.VtxoWithValue)
|
||||||
}
|
}
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -102,12 +102,60 @@ func (b *txBuilder) VerifyTapscriptPartialSigs(tx string) (bool, string, error)
|
|||||||
return true, txid, nil
|
return true, txid, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *txBuilder) FinalizeAndExtractForfeit(tx string) (string, error) {
|
func (b *txBuilder) FinalizeAndExtract(tx string) (string, error) {
|
||||||
ptx, _ := psbt.NewFromRawBytes(strings.NewReader(tx), true)
|
ptx, err := psbt.NewFromRawBytes(strings.NewReader(tx), true)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, in := range ptx.Inputs {
|
||||||
|
isTaproot := txscript.IsPayToTaproot(in.WitnessUtxo.PkScript)
|
||||||
|
if isTaproot && len(in.TaprootLeafScript) > 0 {
|
||||||
|
closure, err := bitcointree.DecodeClosure(in.TaprootLeafScript[0].Script)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
witness := make(wire.TxWitness, 4)
|
||||||
|
|
||||||
|
castClosure, isTaprootMultisig := closure.(*bitcointree.MultisigClosure)
|
||||||
|
if isTaprootMultisig {
|
||||||
|
ownerPubkey := schnorr.SerializePubKey(castClosure.Pubkey)
|
||||||
|
aspKey := schnorr.SerializePubKey(castClosure.AspPubkey)
|
||||||
|
|
||||||
|
for _, sig := range in.TaprootScriptSpendSig {
|
||||||
|
if bytes.Equal(sig.XOnlyPubKey, ownerPubkey) {
|
||||||
|
witness[0] = sig.Signature
|
||||||
|
}
|
||||||
|
|
||||||
|
if bytes.Equal(sig.XOnlyPubKey, aspKey) {
|
||||||
|
witness[1] = sig.Signature
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
witness[2] = in.TaprootLeafScript[0].Script
|
||||||
|
witness[3] = in.TaprootLeafScript[0].ControlBlock
|
||||||
|
|
||||||
|
for idw, w := range witness {
|
||||||
|
if w == nil {
|
||||||
|
return "", fmt.Errorf("missing witness element %d, cannot finalize taproot mutlisig input %d", idw, i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var witnessBuf bytes.Buffer
|
||||||
|
|
||||||
|
if err := psbt.WriteTxWitness(&witnessBuf, witness); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
ptx.Inputs[i].FinalScriptWitness = witnessBuf.Bytes()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
for i := range ptx.Inputs {
|
|
||||||
if err := psbt.Finalize(ptx, i); err != nil {
|
if err := psbt.Finalize(ptx, i); err != nil {
|
||||||
return "", err
|
return "", fmt.Errorf("failed to finalize input %d: %w", i, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -477,7 +525,7 @@ func (b *txBuilder) BuildAsyncPaymentTransactions(
|
|||||||
|
|
||||||
unconditionalForfeitTxs = append(unconditionalForfeitTxs, forfeitTx)
|
unconditionalForfeitTxs = append(unconditionalForfeitTxs, forfeitTx)
|
||||||
ins = append(ins, vtxoOutpoint)
|
ins = append(ins, vtxoOutpoint)
|
||||||
redeemTxWeightEstimator.AddTapscriptInput(64, tapscript)
|
redeemTxWeightEstimator.AddTapscriptInput(64*2, tapscript)
|
||||||
}
|
}
|
||||||
|
|
||||||
for range receivers {
|
for range receivers {
|
||||||
@@ -1199,7 +1247,7 @@ func (b *txBuilder) getConnectorPkScript(poolTx string) ([]byte, error) {
|
|||||||
return nil, fmt.Errorf("connector output not found in pool tx")
|
return nil, fmt.Errorf("connector output not found in pool tx")
|
||||||
}
|
}
|
||||||
|
|
||||||
return partialTx.UnsignedTx.TxOut[0].PkScript, nil
|
return partialTx.UnsignedTx.TxOut[1].PkScript, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *txBuilder) selectUtxos(ctx context.Context, sweptRounds []domain.Round, amount uint64) ([]ports.TxInput, uint64, error) {
|
func (b *txBuilder) selectUtxos(ctx context.Context, sweptRounds []domain.Round, amount uint64) ([]ports.TxInput, uint64, error) {
|
||||||
|
|||||||
@@ -190,12 +190,12 @@ func (m *mockedWallet) UnwatchScripts(
|
|||||||
return args.Error(0)
|
return args.Error(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockedWallet) GetNotificationChannel(ctx context.Context) <-chan map[string]ports.VtxoWithValue {
|
func (m *mockedWallet) GetNotificationChannel(ctx context.Context) <-chan map[string][]ports.VtxoWithValue {
|
||||||
args := m.Called(ctx)
|
args := m.Called(ctx)
|
||||||
|
|
||||||
var res <-chan map[string]ports.VtxoWithValue
|
var res <-chan map[string][]ports.VtxoWithValue
|
||||||
if a := args.Get(0); a != nil {
|
if a := args.Get(0); a != nil {
|
||||||
res = a.(<-chan map[string]ports.VtxoWithValue)
|
res = a.(<-chan map[string][]ports.VtxoWithValue)
|
||||||
}
|
}
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package btcwallet
|
package btcwallet
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/btcsuite/btcd/btcec/v2/schnorr"
|
"github.com/btcsuite/btcd/btcec/v2/schnorr"
|
||||||
@@ -41,6 +42,7 @@ func (s *service) signPsbt(packet *psbt.Packet, inputsToSign []int) ([]uint32, e
|
|||||||
}
|
}
|
||||||
|
|
||||||
tx := packet.UnsignedTx
|
tx := packet.UnsignedTx
|
||||||
|
signedInputs := make([]uint32, 0)
|
||||||
for idx := range tx.TxIn {
|
for idx := range tx.TxIn {
|
||||||
in := &packet.Inputs[idx]
|
in := &packet.Inputs[idx]
|
||||||
|
|
||||||
@@ -76,13 +78,19 @@ func (s *service) signPsbt(packet *psbt.Packet, inputsToSign []int) ([]uint32, e
|
|||||||
managedAddress = s.aspTaprootAddr
|
managedAddress = s.aspTaprootAddr
|
||||||
} else {
|
} else {
|
||||||
// segwit v0
|
// segwit v0
|
||||||
|
var err error
|
||||||
managedAddress, _, _, err = s.wallet.ScriptForOutput(in.WitnessUtxo)
|
managedAddress, _, _, err = s.wallet.ScriptForOutput(in.WitnessUtxo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debugf("SignPsbt: Skipping input %d, error "+
|
log.WithError(err).Debugf(
|
||||||
"fetching script for output: %v", idx, err)
|
"failed to fetch address for input %d with script %s",
|
||||||
|
idx, hex.EncodeToString(in.WitnessUtxo.PkScript),
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
signedInputs = append(signedInputs, uint32(idx))
|
||||||
|
|
||||||
bip32Infos := derivationPathForAddress(managedAddress)
|
bip32Infos := derivationPathForAddress(managedAddress)
|
||||||
packet.Inputs[idx].Bip32Derivation = []*psbt.Bip32Derivation{bip32Infos}
|
packet.Inputs[idx].Bip32Derivation = []*psbt.Bip32Derivation{bip32Infos}
|
||||||
|
|
||||||
@@ -106,25 +114,18 @@ func (s *service) signPsbt(packet *psbt.Packet, inputsToSign []int) ([]uint32, e
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO (@louisinger): shall we delete this code?
|
ins, err := s.wallet.SignPsbt(packet)
|
||||||
// prevOutputFetcher := wallet.PsbtPrevOutputFetcher(packet)
|
if err != nil {
|
||||||
// sigHashes := txscript.NewTxSigHashes(tx, prevOutputFetcher)
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
// in := packet.Inputs[0]
|
// delete derivation paths to avoid duplicate keys error
|
||||||
|
for idx := range signedInputs {
|
||||||
|
packet.Inputs[idx].Bip32Derivation = nil
|
||||||
|
packet.Inputs[idx].TaprootBip32Derivation = nil
|
||||||
|
}
|
||||||
|
|
||||||
// preimage, err := txscript.CalcTapscriptSignaturehash(
|
return ins, nil
|
||||||
// sigHashes,
|
|
||||||
// txscript.SigHashType(in.SighashType),
|
|
||||||
// tx,
|
|
||||||
// 0,
|
|
||||||
// txscript.NewCannedPrevOutputFetcher(in.WitnessUtxo.PkScript, in.WitnessUtxo.Value),
|
|
||||||
// txscript.NewBaseTapLeaf(in.TaprootLeafScript[0].Script),
|
|
||||||
// )
|
|
||||||
// if err != nil {
|
|
||||||
// return nil, err
|
|
||||||
// }
|
|
||||||
|
|
||||||
return s.wallet.SignPsbt(packet)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func derivationPathForAddress(addr waddrmgr.ManagedPubKeyAddress) *psbt.Bip32Derivation {
|
func derivationPathForAddress(addr waddrmgr.ManagedPubKeyAddress) *psbt.Bip32Derivation {
|
||||||
|
|||||||
@@ -70,6 +70,10 @@ const (
|
|||||||
mainAccount accountName = "main"
|
mainAccount accountName = "main"
|
||||||
connectorAccount accountName = "connector"
|
connectorAccount accountName = "connector"
|
||||||
aspKeyAccount accountName = "aspkey"
|
aspKeyAccount accountName = "aspkey"
|
||||||
|
|
||||||
|
// https://github.com/bitcoin/bitcoin/blob/439e58c4d8194ca37f70346727d31f52e69592ec/src/policy/policy.cpp#L23C8-L23C11
|
||||||
|
// biggest input size to compute the maximum dust amount
|
||||||
|
biggestInputSize = 148 + 182 // = 330 vbytes
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -852,18 +856,37 @@ func (s *service) UnwatchScripts(ctx context.Context, scripts []string) error {
|
|||||||
|
|
||||||
func (s *service) GetNotificationChannel(
|
func (s *service) GetNotificationChannel(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
) <-chan map[string]ports.VtxoWithValue {
|
) <-chan map[string][]ports.VtxoWithValue {
|
||||||
ch := make(chan map[string]ports.VtxoWithValue)
|
ch := make(chan map[string][]ports.VtxoWithValue)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
|
const maxCacheSize = 100
|
||||||
|
sentTxs := make(map[chainhash.Hash]struct{})
|
||||||
|
|
||||||
|
cache := func(hash chainhash.Hash) {
|
||||||
|
if len(sentTxs) > maxCacheSize {
|
||||||
|
sentTxs = make(map[chainhash.Hash]struct{})
|
||||||
|
}
|
||||||
|
|
||||||
|
sentTxs[hash] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
for n := range s.scanner.Notifications() {
|
for n := range s.scanner.Notifications() {
|
||||||
switch m := n.(type) {
|
switch m := n.(type) {
|
||||||
case chain.RelevantTx:
|
case chain.RelevantTx:
|
||||||
|
if _, sent := sentTxs[m.TxRecord.Hash]; sent {
|
||||||
|
continue
|
||||||
|
}
|
||||||
notification := s.castNotification(m.TxRecord)
|
notification := s.castNotification(m.TxRecord)
|
||||||
|
cache(m.TxRecord.Hash)
|
||||||
ch <- notification
|
ch <- notification
|
||||||
case chain.FilteredBlockConnected:
|
case chain.FilteredBlockConnected:
|
||||||
for _, tx := range m.RelevantTxs {
|
for _, tx := range m.RelevantTxs {
|
||||||
|
if _, sent := sentTxs[tx.Hash]; sent {
|
||||||
|
continue
|
||||||
|
}
|
||||||
notification := s.castNotification(tx)
|
notification := s.castNotification(tx)
|
||||||
|
cache(tx.Hash)
|
||||||
ch <- notification
|
ch <- notification
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -879,11 +902,10 @@ func (s *service) IsTransactionConfirmed(
|
|||||||
return s.extraAPI.getTxStatus(txid)
|
return s.extraAPI.getTxStatus(txid)
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://github.com/bitcoin/bitcoin/blob/439e58c4d8194ca37f70346727d31f52e69592ec/src/policy/policy.cpp#L23C8-L23C11
|
|
||||||
func (s *service) GetDustAmount(
|
func (s *service) GetDustAmount(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
) (uint64, error) {
|
) (uint64, error) {
|
||||||
return s.MinRelayFee(ctx, 182) // non-segwit 1-in-1-out tx
|
return s.MinRelayFee(ctx, biggestInputSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *service) GetTransaction(ctx context.Context, txid string) (string, error) {
|
func (s *service) GetTransaction(ctx context.Context, txid string) (string, error) {
|
||||||
@@ -904,8 +926,8 @@ func (s *service) GetTransaction(ctx context.Context, txid string) (string, erro
|
|||||||
return hex.EncodeToString(buf.Bytes()), nil
|
return hex.EncodeToString(buf.Bytes()), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *service) castNotification(tx *wtxmgr.TxRecord) map[string]ports.VtxoWithValue {
|
func (s *service) castNotification(tx *wtxmgr.TxRecord) map[string][]ports.VtxoWithValue {
|
||||||
vtxos := make(map[string]ports.VtxoWithValue)
|
vtxos := make(map[string][]ports.VtxoWithValue)
|
||||||
|
|
||||||
s.watchedScriptsLock.RLock()
|
s.watchedScriptsLock.RLock()
|
||||||
defer s.watchedScriptsLock.RUnlock()
|
defer s.watchedScriptsLock.RUnlock()
|
||||||
@@ -916,13 +938,17 @@ func (s *service) castNotification(tx *wtxmgr.TxRecord) map[string]ports.VtxoWit
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
vtxos[script] = ports.VtxoWithValue{
|
if len(vtxos[script]) <= 0 {
|
||||||
|
vtxos[script] = make([]ports.VtxoWithValue, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
vtxos[script] = append(vtxos[script], ports.VtxoWithValue{
|
||||||
VtxoKey: domain.VtxoKey{
|
VtxoKey: domain.VtxoKey{
|
||||||
Txid: tx.Hash.String(),
|
Txid: tx.Hash.String(),
|
||||||
VOut: uint32(outputIndex),
|
VOut: uint32(outputIndex),
|
||||||
},
|
},
|
||||||
Value: uint64(txout.Value),
|
Value: uint64(txout.Value),
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return vtxos
|
return vtxos
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package oceanwallet
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/hex"
|
||||||
|
|
||||||
pb "github.com/ark-network/ark/api-spec/protobuf/gen/ocean/v1"
|
pb "github.com/ark-network/ark/api-spec/protobuf/gen/ocean/v1"
|
||||||
"github.com/ark-network/ark/server/internal/core/ports"
|
"github.com/ark-network/ark/server/internal/core/ports"
|
||||||
@@ -27,20 +28,26 @@ func (s *service) DeriveConnectorAddress(ctx context.Context) (string, error) {
|
|||||||
func (s *service) ListConnectorUtxos(
|
func (s *service) ListConnectorUtxos(
|
||||||
ctx context.Context, connectorAddress string,
|
ctx context.Context, connectorAddress string,
|
||||||
) ([]ports.TxInput, error) {
|
) ([]ports.TxInput, error) {
|
||||||
addresses := make([]string, 0)
|
connectorScript, err := address.ToOutputScript(connectorAddress)
|
||||||
if len(connectorAddress) > 0 {
|
if err != nil {
|
||||||
addresses = append(addresses, connectorAddress)
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
res, err := s.accountClient.ListUtxos(ctx, &pb.ListUtxosRequest{
|
res, err := s.accountClient.ListUtxos(ctx, &pb.ListUtxosRequest{
|
||||||
AccountName: connectorAccount,
|
AccountName: connectorAccount,
|
||||||
Addresses: addresses,
|
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
utxos := make([]ports.TxInput, 0)
|
utxos := make([]ports.TxInput, 0)
|
||||||
|
connectorScriptHex := hex.EncodeToString(connectorScript)
|
||||||
|
|
||||||
for _, utxo := range res.GetSpendableUtxos().GetUtxos() {
|
for _, utxo := range res.GetSpendableUtxos().GetUtxos() {
|
||||||
|
if utxo.Script != connectorScriptHex {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
utxos = append(utxos, utxo)
|
utxos = append(utxos, utxo)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ func (s *service) UnwatchScripts(ctx context.Context, scripts []string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *service) GetNotificationChannel(ctx context.Context) <-chan map[string]ports.VtxoWithValue {
|
func (s *service) GetNotificationChannel(ctx context.Context) <-chan map[string][]ports.VtxoWithValue {
|
||||||
return s.chVtxos
|
return s.chVtxos
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ type service struct {
|
|||||||
accountClient pb.AccountServiceClient
|
accountClient pb.AccountServiceClient
|
||||||
txClient pb.TransactionServiceClient
|
txClient pb.TransactionServiceClient
|
||||||
notifyClient pb.NotificationServiceClient
|
notifyClient pb.NotificationServiceClient
|
||||||
chVtxos chan map[string]ports.VtxoWithValue
|
chVtxos chan map[string][]ports.VtxoWithValue
|
||||||
isListening bool
|
isListening bool
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ func NewService(addr string) (ports.WalletService, error) {
|
|||||||
accountClient := pb.NewAccountServiceClient(conn)
|
accountClient := pb.NewAccountServiceClient(conn)
|
||||||
txClient := pb.NewTransactionServiceClient(conn)
|
txClient := pb.NewTransactionServiceClient(conn)
|
||||||
notifyClient := pb.NewNotificationServiceClient(conn)
|
notifyClient := pb.NewNotificationServiceClient(conn)
|
||||||
chVtxos := make(chan map[string]ports.VtxoWithValue)
|
chVtxos := make(chan map[string][]ports.VtxoWithValue)
|
||||||
svc := &service{
|
svc := &service{
|
||||||
addr: addr,
|
addr: addr,
|
||||||
conn: conn,
|
conn: conn,
|
||||||
@@ -190,8 +190,8 @@ func (s *service) listenToNotifications() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func toVtxos(utxos []*pb.Utxo) map[string]ports.VtxoWithValue {
|
func toVtxos(utxos []*pb.Utxo) map[string][]ports.VtxoWithValue {
|
||||||
vtxos := make(map[string]ports.VtxoWithValue, len(utxos))
|
vtxos := make(map[string][]ports.VtxoWithValue, len(utxos))
|
||||||
for _, utxo := range utxos {
|
for _, utxo := range utxos {
|
||||||
// We want to notify for activity related to vtxos owner, therefore we skip
|
// We want to notify for activity related to vtxos owner, therefore we skip
|
||||||
// returning anything related to the internal accounts of the wallet, like
|
// returning anything related to the internal accounts of the wallet, like
|
||||||
@@ -200,11 +200,13 @@ func toVtxos(utxos []*pb.Utxo) map[string]ports.VtxoWithValue {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
vtxos[utxo.Script] = ports.VtxoWithValue{
|
vtxos[utxo.Script] = []ports.VtxoWithValue{
|
||||||
VtxoKey: domain.VtxoKey{Txid: utxo.GetTxid(),
|
{
|
||||||
VOut: utxo.GetIndex(),
|
VtxoKey: domain.VtxoKey{Txid: utxo.GetTxid(),
|
||||||
|
VOut: utxo.GetIndex(),
|
||||||
|
},
|
||||||
|
Value: utxo.GetValue(),
|
||||||
},
|
},
|
||||||
Value: utxo.GetValue(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return vtxos
|
return vtxos
|
||||||
|
|||||||
@@ -275,7 +275,7 @@ func (s *service) LockConnectorUtxos(ctx context.Context, utxos []ports.TxOutpoi
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *service) MinRelayFee(ctx context.Context, vbytes uint64) (uint64, error) {
|
func (s *service) MinRelayFee(ctx context.Context, vbytes uint64) (uint64, error) {
|
||||||
feeRate := 0.11
|
feeRate := 0.2
|
||||||
fee := uint64(float64(vbytes) * feeRate)
|
fee := uint64(float64(vbytes) * feeRate)
|
||||||
return fee, nil
|
return fee, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package e2e_test
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -10,6 +11,12 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/ark-network/ark/common"
|
"github.com/ark-network/ark/common"
|
||||||
|
arksdk "github.com/ark-network/ark/pkg/client-sdk"
|
||||||
|
"github.com/ark-network/ark/pkg/client-sdk/client"
|
||||||
|
grpcclient "github.com/ark-network/ark/pkg/client-sdk/client/grpc"
|
||||||
|
"github.com/ark-network/ark/pkg/client-sdk/explorer"
|
||||||
|
"github.com/ark-network/ark/pkg/client-sdk/redemption"
|
||||||
|
inmemorystore "github.com/ark-network/ark/pkg/client-sdk/store/inmemory"
|
||||||
utils "github.com/ark-network/ark/server/test/e2e"
|
utils "github.com/ark-network/ark/server/test/e2e"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
@@ -143,6 +150,58 @@ func TestCollaborativeExit(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestReactToSpentVtxosRedemption(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
client, grpcClient := setupArkSDK(t)
|
||||||
|
defer grpcClient.Close()
|
||||||
|
|
||||||
|
offchainAddress, boardingAddress, err := client.Receive(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = utils.RunCommand("nigiri", "faucet", "--liquid", boardingAddress)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
time.Sleep(5 * time.Second)
|
||||||
|
|
||||||
|
_, err = client.Claim(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
time.Sleep(3 * time.Second)
|
||||||
|
|
||||||
|
spendable, _, err := client.ListVtxos(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotEmpty(t, spendable)
|
||||||
|
|
||||||
|
vtxo := spendable[0]
|
||||||
|
|
||||||
|
_, err = client.SendOffChain(ctx, false, []arksdk.Receiver{arksdk.NewLiquidReceiver(offchainAddress, 1000)})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
round, err := grpcClient.GetRound(ctx, vtxo.RoundTxid)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
expl := explorer.NewExplorer("http://localhost:3001", common.LiquidRegTest)
|
||||||
|
|
||||||
|
branch, err := redemption.NewCovenantRedeemBranch(expl, round.Tree, vtxo)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
txs, err := branch.RedeemPath()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
for _, tx := range txs {
|
||||||
|
_, err := expl.Broadcast(tx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// give time for the ASP to detect and process the fraud
|
||||||
|
time.Sleep(18 * time.Second)
|
||||||
|
|
||||||
|
balance, err := client.Balance(ctx, true)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Empty(t, balance.OnchainBalance.LockedAmount)
|
||||||
|
}
|
||||||
|
|
||||||
func runArkCommand(arg ...string) (string, error) {
|
func runArkCommand(arg ...string) (string, error) {
|
||||||
args := append([]string{"exec", "-t", "arkd", "ark"}, arg...)
|
args := append([]string{"exec", "-t", "arkd", "ark"}, arg...)
|
||||||
return utils.RunCommand("docker", args...)
|
return utils.RunCommand("docker", args...)
|
||||||
@@ -237,3 +296,27 @@ func setupAspWallet() error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setupArkSDK(t *testing.T) (arksdk.ArkClient, client.ASPClient) {
|
||||||
|
storeSvc, err := inmemorystore.NewConfigStore()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
client, err := arksdk.NewCovenantClient(storeSvc)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = client.Init(context.Background(), arksdk.InitArgs{
|
||||||
|
WalletType: arksdk.SingleKeyWallet,
|
||||||
|
ClientType: arksdk.GrpcClient,
|
||||||
|
AspUrl: "localhost:6060",
|
||||||
|
Password: utils.Password,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = client.Unlock(context.Background(), utils.Password)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
grpcClient, err := grpcclient.NewClient("localhost:6060")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
return client, grpcClient
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package e2e_test
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -9,6 +10,13 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/ark-network/ark/common"
|
||||||
|
arksdk "github.com/ark-network/ark/pkg/client-sdk"
|
||||||
|
"github.com/ark-network/ark/pkg/client-sdk/client"
|
||||||
|
grpcclient "github.com/ark-network/ark/pkg/client-sdk/client/grpc"
|
||||||
|
"github.com/ark-network/ark/pkg/client-sdk/explorer"
|
||||||
|
"github.com/ark-network/ark/pkg/client-sdk/redemption"
|
||||||
|
inmemorystore "github.com/ark-network/ark/pkg/client-sdk/store/inmemory"
|
||||||
utils "github.com/ark-network/ark/server/test/e2e"
|
utils "github.com/ark-network/ark/server/test/e2e"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
@@ -155,6 +163,126 @@ func TestCollaborativeExit(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestReactToSpentVtxosRedemption(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
client, grpcClient := setupArkSDK(t)
|
||||||
|
defer grpcClient.Close()
|
||||||
|
|
||||||
|
offchainAddress, boardingAddress, err := client.Receive(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = utils.RunCommand("nigiri", "faucet", boardingAddress)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
time.Sleep(5 * time.Second)
|
||||||
|
|
||||||
|
_, err = client.Claim(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = client.SendOffChain(ctx, false, []arksdk.Receiver{arksdk.NewBitcoinReceiver(offchainAddress, 1000)})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
time.Sleep(2 * time.Second)
|
||||||
|
|
||||||
|
_, spentVtxos, err := client.ListVtxos(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotEmpty(t, spentVtxos)
|
||||||
|
|
||||||
|
vtxo := spentVtxos[0]
|
||||||
|
|
||||||
|
round, err := grpcClient.GetRound(ctx, vtxo.RoundTxid)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
expl := explorer.NewExplorer("http://localhost:3000", common.BitcoinRegTest)
|
||||||
|
|
||||||
|
branch, err := redemption.NewCovenantlessRedeemBranch(expl, round.Tree, vtxo)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
txs, err := branch.RedeemPath()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
for _, tx := range txs {
|
||||||
|
_, err := expl.Broadcast(tx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// give time for the ASP to detect and process the fraud
|
||||||
|
time.Sleep(20 * time.Second)
|
||||||
|
|
||||||
|
balance, err := client.Balance(ctx, true)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Empty(t, balance.OnchainBalance.LockedAmount)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReactToAsyncSpentVtxosRedemption(t *testing.T) {
|
||||||
|
t.Run("receveir claimed funds", func(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
sdkClient, grpcClient := setupArkSDK(t)
|
||||||
|
defer grpcClient.Close()
|
||||||
|
|
||||||
|
offchainAddress, boardingAddress, err := sdkClient.Receive(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = utils.RunCommand("nigiri", "faucet", boardingAddress)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
time.Sleep(5 * time.Second)
|
||||||
|
|
||||||
|
roundId, err := sdkClient.Claim(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = utils.GenerateBlock()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = sdkClient.SendAsync(ctx, false, []arksdk.Receiver{arksdk.NewBitcoinReceiver(offchainAddress, 1000)})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = sdkClient.Claim(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
time.Sleep(5 * time.Second)
|
||||||
|
|
||||||
|
_, spentVtxos, err := sdkClient.ListVtxos(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotEmpty(t, spentVtxos)
|
||||||
|
|
||||||
|
var vtxo client.Vtxo
|
||||||
|
|
||||||
|
for _, v := range spentVtxos {
|
||||||
|
if v.RoundTxid == roundId {
|
||||||
|
vtxo = v
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
require.NotEmpty(t, vtxo)
|
||||||
|
|
||||||
|
round, err := grpcClient.GetRound(ctx, vtxo.RoundTxid)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
expl := explorer.NewExplorer("http://localhost:3000", common.BitcoinRegTest)
|
||||||
|
|
||||||
|
branch, err := redemption.NewCovenantlessRedeemBranch(expl, round.Tree, vtxo)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
txs, err := branch.RedeemPath()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
for _, tx := range txs {
|
||||||
|
_, err := expl.Broadcast(tx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// give time for the ASP to detect and process the fraud
|
||||||
|
time.Sleep(50 * time.Second)
|
||||||
|
|
||||||
|
balance, err := sdkClient.Balance(ctx, true)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Empty(t, balance.OnchainBalance.LockedAmount)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func runClarkCommand(arg ...string) (string, error) {
|
func runClarkCommand(arg ...string) (string, error) {
|
||||||
args := append([]string{"exec", "-t", "clarkd", "ark"}, arg...)
|
args := append([]string{"exec", "-t", "clarkd", "ark"}, arg...)
|
||||||
return utils.RunCommand("docker", args...)
|
return utils.RunCommand("docker", args...)
|
||||||
@@ -254,7 +382,41 @@ func setupAspWallet() error {
|
|||||||
return fmt.Errorf("failed to fund wallet: %s", err)
|
return fmt.Errorf("failed to fund wallet: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_, err = utils.RunCommand("nigiri", "faucet", addr.Address)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to fund wallet: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = utils.RunCommand("nigiri", "faucet", addr.Address)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to fund wallet: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
time.Sleep(5 * time.Second)
|
time.Sleep(5 * time.Second)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setupArkSDK(t *testing.T) (arksdk.ArkClient, client.ASPClient) {
|
||||||
|
storeSvc, err := inmemorystore.NewConfigStore()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
client, err := arksdk.NewCovenantlessClient(storeSvc)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = client.Init(context.Background(), arksdk.InitArgs{
|
||||||
|
WalletType: arksdk.SingleKeyWallet,
|
||||||
|
ClientType: arksdk.GrpcClient,
|
||||||
|
AspUrl: "localhost:7070",
|
||||||
|
Password: utils.Password,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = client.Unlock(context.Background(), utils.Password)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
grpcClient, err := grpcclient.NewClient("localhost:7070")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
return client, grpcClient
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user