Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 36 additions & 2 deletions pkg/chain/ethereum/bitcoin_difficulty.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"fmt"
"math/big"
"time"

"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
Expand All @@ -23,6 +24,12 @@ const (
LightRelayMaintainerProxyContractName = "LightRelayMaintainerProxy"
)

// waitDeployBackendTransactionMinedTimeout bounds the synchronous post-submit
// wait for retarget transactions. The wait covers both receipt polling and the
// follow-up confirmation-depth wait. Without a bound, a stalled RPC or a chain
// that stops producing blocks would hang the maintainer indefinitely.
const waitDeployBackendTransactionMinedTimeout = 10 * time.Minute

// BitcoinDifficultyChain represents a Bitcoin difficulty-specific chain handle.
type BitcoinDifficultyChain struct {
*baseChain
Expand Down Expand Up @@ -138,7 +145,13 @@ func (bdc *BitcoinDifficultyChain) waitDeployBackendTransactionMined(
if tx == nil {
return fmt.Errorf("nil transaction waiting for [%s]", method)
}
receipt, err := bind.WaitMined(context.Background(), bdc.client, tx)
ctx, cancel := context.WithTimeout(
context.Background(),
waitDeployBackendTransactionMinedTimeout,
)
defer cancel()

receipt, err := bind.WaitMined(ctx, bdc.client, tx)
if err != nil {
return fmt.Errorf("waiting for transaction [%s] [%s]: [%w]", method, tx.Hash().Hex(), err)
}
Expand All @@ -155,7 +168,9 @@ func (bdc *BitcoinDifficultyChain) waitDeployBackendTransactionMined(
if receipt.BlockNumber != nil && bdc.blockCounter != nil {
includedAt := receipt.BlockNumber.Uint64()
const confirmDepth = uint64(3)
if err := bdc.blockCounter.WaitForBlockHeight(includedAt + confirmDepth); err != nil {
if err := waitForBlockHeightCtx(
ctx, bdc.blockCounter, includedAt+confirmDepth,
); err != nil {
return fmt.Errorf(
"waiting confirmation depth after [%s] [%s]: [%w]",
method,
Expand All @@ -167,6 +182,25 @@ func (bdc *BitcoinDifficultyChain) waitDeployBackendTransactionMined(
return nil
}

// waitForBlockHeightCtx wraps the context-less chain.BlockCounter.WaitForBlockHeight
// so the caller can enforce a deadline. The underlying interface does not accept
// a context; if ctx fires before the height is reached, the wait goroutine stays
// parked until the next block tick eventually unblocks it.
func waitForBlockHeightCtx(
ctx context.Context,
bc chain.BlockCounter,
blockNumber uint64,
) error {
done := make(chan error, 1)
go func() { done <- bc.WaitForBlockHeight(blockNumber) }()
select {
case err := <-done:
return err
case <-ctx.Done():
return ctx.Err()
}
}

// Ready checks whether the relay is active (i.e. genesis has been performed).
// Note that if the relay is used by querying the current and previous epoch
// difficulty, at least one retarget needs to be provided after genesis;
Expand Down
63 changes: 63 additions & 0 deletions pkg/chain/ethereum/bitcoin_difficulty_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package ethereum

import (
"context"
"errors"
"testing"
"time"
)

// blockingBlockCounter blocks WaitForBlockHeight until ch is closed, simulating
// a chain that has stopped producing blocks (or an RPC stuck behind a stale
// load balancer).
type blockingBlockCounter struct {
ch chan struct{}
}

func (b *blockingBlockCounter) CurrentBlock() (uint64, error) { return 0, nil }
func (b *blockingBlockCounter) WatchBlocks(_ context.Context) <-chan uint64 { return nil }
func (b *blockingBlockCounter) BlockHeightWaiter(_ uint64) (<-chan uint64, error) {
return nil, nil
}
func (b *blockingBlockCounter) WaitForBlockHeight(_ uint64) error {
<-b.ch
return nil
}

// TestWaitForBlockHeightCtx_DeadlineExceeded asserts that the context shim
// honors the parent context's deadline when WaitForBlockHeight blocks forever.
// Regression: the original implementation called WaitForBlockHeight without any
// cancellation path, which could hang the maintainer indefinitely if the chain
// stalled mid-retarget. See waitDeployBackendTransactionMinedTimeout.
func TestWaitForBlockHeightCtx_DeadlineExceeded(t *testing.T) {
bc := &blockingBlockCounter{ch: make(chan struct{})}
defer close(bc.ch) // release the parked goroutine

ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
defer cancel()

start := time.Now()
err := waitForBlockHeightCtx(ctx, bc, 100)
elapsed := time.Since(start)

if !errors.Is(err, context.DeadlineExceeded) {
t.Fatalf("expected context.DeadlineExceeded, got %v", err)
}
if elapsed > 500*time.Millisecond {
t.Fatalf("wait took too long: %v", elapsed)
}
}

// TestWaitForBlockHeightCtx_ReturnsImmediatelyOnSuccess asserts the shim does
// not introduce extra latency when the underlying counter returns promptly.
func TestWaitForBlockHeightCtx_ReturnsImmediatelyOnSuccess(t *testing.T) {
bc := &blockingBlockCounter{ch: make(chan struct{})}
close(bc.ch) // pre-close: WaitForBlockHeight returns nil immediately

ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()

if err := waitForBlockHeightCtx(ctx, bc, 1); err != nil {
t.Fatalf("expected nil error, got %v", err)
}
}
6 changes: 5 additions & 1 deletion pkg/chain/ethereum/tbtc.go
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,11 @@ func newTbtcChain(
case err == nil:
ecdsaDkgValidatorAddress = validatorAddr
case errors.Is(err, ethereum.ErrAddressNotConfigured):
// Optional: without it TBTC falls back to defaultGroupParameters(network).
logger.Warnf(
"%s contract address is not configured; TBTC group parameters "+
"will fall back to network defaults instead of on-chain values",
EcdsaDkgValidatorContractName,
)
default:
return nil, fmt.Errorf(
"failed to resolve %s contract address: [%w]",
Expand Down
8 changes: 6 additions & 2 deletions pkg/tbtc/tbtc.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,12 @@ func (gp *GroupParameters) DishonestThreshold() int {
func defaultGroupParameters(n ethereum.Network) *GroupParameters {
switch n {
case ethereum.Sepolia, ethereum.Developer:
logger.Infof(
"TBTC group parameters: testnet/small group (size=3, quorum=3, honest=2) for %s",
logger.Warnf(
"TBTC group parameters: testnet/small group (size=3, quorum=3, "+
"honest=2) for %s; quorum equals size so all three operators "+
"must remain online for DKG to progress. Configure an "+
"EcdsaDkgValidator contract address to override these defaults "+
"with on-chain values.",
n,
)
return &GroupParameters{
Expand Down
Loading