Skip to content
Open
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
4 changes: 2 additions & 2 deletions universalClient/chains/chains.go
Original file line number Diff line number Diff line change
Expand Up @@ -278,9 +278,9 @@ func (c *Chains) addChain(ctx context.Context, cfg *uregistrytypes.ChainConfig)
var client common.ChainClient
switch cfg.VmType {
case uregistrytypes.VmType_EVM:
client, err = evm.NewClient(cfg, chainDB, chainConfig, c.pushSigner, c.logger)
client, err = evm.NewClient(cfg, chainDB, chainConfig, c.pushSigner, c.config.AllowsZeroConfirmations(), c.logger)
case uregistrytypes.VmType_SVM:
client, err = svm.NewClient(cfg, chainDB, chainConfig, c.pushSigner, c.config.NodeHome, c.logger)
client, err = svm.NewClient(cfg, chainDB, chainConfig, c.pushSigner, c.config.NodeHome, c.config.AllowsZeroConfirmations(), c.logger)
default:
return fmt.Errorf("unsupported VM type: %v", cfg.VmType)
}
Expand Down
19 changes: 19 additions & 0 deletions universalClient/chains/common/confirmation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package common

// Safe fallback confirmation depths used when the registry configures 0 and
// instant routes are not enabled.
const (
DefaultFastConfirmations uint64 = 5
DefaultStandardConfirmations uint64 = 12
)

// ConfirmationDepth returns latestHeight - txHeight + 1, the confirmation count
// with the inclusion block counted as one. ok is false when latestHeight <
// txHeight (a cross-RPC height skew); callers must defer rather than trust the
// depth, since the unchecked subtraction would underflow.
func ConfirmationDepth(latestHeight, txHeight uint64) (depth uint64, ok bool) {
if latestHeight < txHeight {
return 0, false
}
return latestHeight - txHeight + 1, true
}
42 changes: 42 additions & 0 deletions universalClient/chains/common/confirmation_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package common

import (
"math"
"testing"

"github.com/stretchr/testify/assert"
)

func TestConfirmationDepth(t *testing.T) {
tests := []struct {
name string
latest uint64
tx uint64
wantDepth uint64
wantOK bool
}{
{"latest greater than tx", 110, 100, 11, true},
{"latest equals tx (inclusion block)", 100, 100, 1, true},
{"latest one below tx (skew)", 99, 100, 0, false},
{"latest far below tx (skew)", 1, math.MaxUint64, 0, false},
{"no underflow to near-2^64", 0, 1, 0, false},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
depth, ok := ConfirmationDepth(tc.latest, tc.tx)
assert.Equal(t, tc.wantOK, ok)
assert.Equal(t, tc.wantDepth, depth)
})
}
}

// TestConfirmationDepth_SkewNeverSatisfiesThreshold guards the exact finding:
// a transaction one block ahead of the observed tip must not produce a depth
// that clears a realistic confirmation threshold.
func TestConfirmationDepth_SkewNeverSatisfiesThreshold(t *testing.T) {
const threshold = uint64(12)
depth, ok := ConfirmationDepth(500, 501)
assert.False(t, ok, "skewed read must be flagged not-ok")
assert.False(t, depth >= threshold, "skewed depth must not satisfy threshold")
}
38 changes: 26 additions & 12 deletions universalClient/chains/evm/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,11 @@ import (
// Client implements the ChainClient interface for EVM chains
type Client struct {
// Core configuration
logger zerolog.Logger
chainIDStr string
registryConfig *uregistrytypes.ChainConfig
chainConfig *config.ChainSpecificConfig
logger zerolog.Logger
chainIDStr string
registryConfig *uregistrytypes.ChainConfig
chainConfig *config.ChainSpecificConfig
allowZeroConfirmations bool

// Infrastructure
rpcClient *RPCClient
Expand All @@ -49,6 +50,7 @@ func NewClient(
database *db.DB,
chainConfig *config.ChainSpecificConfig,
pushSigner *pushsigner.Signer,
allowZeroConfirmations bool,
logger zerolog.Logger,
) (*Client, error) {
if config == nil {
Expand All @@ -68,12 +70,13 @@ func NewClient(
}

client := &Client{
logger: log,
chainIDStr: chainIDStr,
registryConfig: config,
chainConfig: chainConfig,
database: database,
pushSigner: pushSigner,
logger: log,
chainIDStr: chainIDStr,
registryConfig: config,
chainConfig: chainConfig,
allowZeroConfirmations: allowZeroConfirmations,
database: database,
pushSigner: pushSigner,
}

client.eventCleaner = common.NewEventCleaner(
Expand Down Expand Up @@ -356,8 +359,8 @@ func (c *Client) applyDefaults() componentConfig {
config := componentConfig{
eventPollingInterval: 5, // default
gasPriceInterval: 30, // default
fastConfirmations: 2,
standardConfirmations: 12,
fastConfirmations: common.DefaultFastConfirmations,
standardConfirmations: common.DefaultStandardConfirmations,
}

// Apply event polling interval
Expand All @@ -381,6 +384,17 @@ func (c *Client) applyDefaults() componentConfig {
config.standardConfirmations = uint64(c.registryConfig.BlockConfirmation.StandardInbound)
}

// A registry-configured 0 disables the reorg-safety depth. Honor it only
// when instant routes are enabled; otherwise fall back to a safe default.
if !c.allowZeroConfirmations {
if config.fastConfirmations == 0 {
config.fastConfirmations = common.DefaultFastConfirmations
}
if config.standardConfirmations == 0 {
config.standardConfirmations = common.DefaultStandardConfirmations
}
}

return config
}

Expand Down
87 changes: 72 additions & 15 deletions universalClient/chains/evm/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,15 @@ func TestClientInitialization(t *testing.T) {
}

chainSpecificConfig := testChainConfig([]string{"https://eth-mainnet.example.com"})
client, err := NewClient(chainConfig, nil, chainSpecificConfig, nil, logger)
client, err := NewClient(chainConfig, nil, chainSpecificConfig, nil, false, logger)
require.NoError(t, err)
assert.NotNil(t, client)
assert.Equal(t, chainConfig, client.GetConfig())
assert.Equal(t, "eip155:1", client.ChainID())
})

t.Run("Nil config", func(t *testing.T) {
client, err := NewClient(nil, nil, nil, nil, logger)
client, err := NewClient(nil, nil, nil, nil, false, logger)
assert.Error(t, err)
assert.Nil(t, client)
assert.Contains(t, err.Error(), "config is nil")
Expand All @@ -57,7 +57,7 @@ func TestClientInitialization(t *testing.T) {
}

chainSpecificConfig := testChainConfig([]string{})
client, err := NewClient(chainConfig, nil, chainSpecificConfig, nil, logger)
client, err := NewClient(chainConfig, nil, chainSpecificConfig, nil, false, logger)
assert.Error(t, err)
assert.Nil(t, client)
assert.Contains(t, err.Error(), "no RPC URLs configured")
Expand All @@ -69,7 +69,7 @@ func TestClientInitialization(t *testing.T) {
VmType: uregistrytypes.VmType_SVM, // Wrong VM type
}

client, err := NewClient(chainConfig, nil, nil, nil, logger)
client, err := NewClient(chainConfig, nil, nil, nil, false, logger)
assert.Error(t, err)
assert.Nil(t, client)
assert.Contains(t, err.Error(), "invalid VM type for EVM client")
Expand Down Expand Up @@ -177,7 +177,7 @@ func TestClientStartStop(t *testing.T) {
}

chainSpecificConfig := testChainConfig([]string{server.URL})
client, err := NewClient(chainConfig, nil, chainSpecificConfig, nil, logger)
client, err := NewClient(chainConfig, nil, chainSpecificConfig, nil, false, logger)
require.NoError(t, err)

ctx := context.Background()
Expand All @@ -199,7 +199,7 @@ func TestClientStartStop(t *testing.T) {

chainSpecificConfig := testChainConfig([]string{"http://invalid.localhost:99999"})

client, err := NewClient(chainConfig, nil, chainSpecificConfig, nil, logger)
client, err := NewClient(chainConfig, nil, chainSpecificConfig, nil, false, logger)
require.NoError(t, err)

// Use context with timeout to ensure fast failure
Expand Down Expand Up @@ -238,7 +238,7 @@ func TestClientStartStop(t *testing.T) {

// Use valid URL but cancel context immediately
chainSpecificConfig := testChainConfig([]string{server.URL})
client, err := NewClient(chainConfig, nil, chainSpecificConfig, nil, logger)
client, err := NewClient(chainConfig, nil, chainSpecificConfig, nil, false, logger)
require.NoError(t, err)

ctx, cancel := context.WithCancel(context.Background())
Expand Down Expand Up @@ -295,7 +295,7 @@ func TestClientIsHealthy(t *testing.T) {
}

chainSpecificConfig := testChainConfig([]string{server.URL})
client, err := NewClient(chainConfig, nil, chainSpecificConfig, nil, logger)
client, err := NewClient(chainConfig, nil, chainSpecificConfig, nil, false, logger)
require.NoError(t, err)

// Start the client
Expand All @@ -320,7 +320,7 @@ func TestClientIsHealthy(t *testing.T) {
// Provide valid RPC URLs for NewClient to succeed
// But don't start the client
chainSpecificConfig := testChainConfig([]string{"https://eth-mainnet.example.com"})
client, err := NewClient(chainConfig, nil, chainSpecificConfig, nil, logger)
client, err := NewClient(chainConfig, nil, chainSpecificConfig, nil, false, logger)
require.NoError(t, err)

healthy := client.IsHealthy()
Expand All @@ -342,7 +342,7 @@ func TestApplyDefaults(t *testing.T) {
assert.Equal(t, 5, cfg.eventPollingInterval)
assert.Equal(t, 30, cfg.gasPriceInterval)
assert.Equal(t, 0, cfg.gasPriceMarkupPercent)
assert.Equal(t, uint64(2), cfg.fastConfirmations)
assert.Equal(t, uint64(5), cfg.fastConfirmations)
assert.Equal(t, uint64(12), cfg.standardConfirmations)
})

Expand Down Expand Up @@ -412,7 +412,7 @@ func TestApplyDefaults(t *testing.T) {
}

cfg := client.applyDefaults()
assert.Equal(t, uint64(2), cfg.fastConfirmations)
assert.Equal(t, uint64(5), cfg.fastConfirmations)
assert.Equal(t, uint64(12), cfg.standardConfirmations)
})

Expand All @@ -426,11 +426,68 @@ func TestApplyDefaults(t *testing.T) {
}

cfg := client.applyDefaults()
assert.Equal(t, uint64(2), cfg.fastConfirmations)
assert.Equal(t, uint64(5), cfg.fastConfirmations)
assert.Equal(t, uint64(12), cfg.standardConfirmations)
})
}

// A registry-configured 0 falls back to a safe depth unless instant routes are
// enabled, in which case it is honored.
func TestApplyDefaults_ZeroConfirmations(t *testing.T) {
logger := zerolog.New(zerolog.NewTestWriter(t))

zeroRegistry := &uregistrytypes.ChainConfig{
BlockConfirmation: &uregistrytypes.BlockConfirmation{
FastInbound: 0,
StandardInbound: 0,
},
}

t.Run("mainnet falls back to safe depth", func(t *testing.T) {
client := &Client{
logger: logger,
chainIDStr: "eip155:1",
registryConfig: zeroRegistry,
allowZeroConfirmations: false,
}

cfg := client.applyDefaults()
assert.Equal(t, uint64(5), cfg.fastConfirmations, "zero fast must not disable depth on mainnet")
assert.Equal(t, uint64(12), cfg.standardConfirmations, "zero standard must not disable depth on mainnet")
})

t.Run("testnet honors zero as instant", func(t *testing.T) {
client := &Client{
logger: logger,
chainIDStr: "eip155:1",
registryConfig: zeroRegistry,
allowZeroConfirmations: true,
}

cfg := client.applyDefaults()
assert.Equal(t, uint64(0), cfg.fastConfirmations, "testnet instant route keeps zero")
assert.Equal(t, uint64(0), cfg.standardConfirmations, "testnet instant route keeps zero")
})

t.Run("nonzero registry values unaffected by flag", func(t *testing.T) {
client := &Client{
logger: logger,
chainIDStr: "eip155:1",
registryConfig: &uregistrytypes.ChainConfig{
BlockConfirmation: &uregistrytypes.BlockConfirmation{
FastInbound: 3,
StandardInbound: 9,
},
},
allowZeroConfirmations: false,
}

cfg := client.applyDefaults()
assert.Equal(t, uint64(3), cfg.fastConfirmations)
assert.Equal(t, uint64(9), cfg.standardConfirmations)
})
}

// TestGetTxBuilderNil tests GetTxBuilder when txBuilder is not initialized
func TestGetTxBuilderNil(t *testing.T) {
logger := zerolog.New(zerolog.NewTestWriter(t))
Expand All @@ -441,7 +498,7 @@ func TestGetTxBuilderNil(t *testing.T) {
}

chainSpecificConfig := testChainConfig([]string{"https://eth-mainnet.example.com"})
client, err := NewClient(chainConfig, nil, chainSpecificConfig, nil, logger)
client, err := NewClient(chainConfig, nil, chainSpecificConfig, nil, false, logger)
require.NoError(t, err)

// txBuilder is nil because gateway is not configured / Start not called
Expand All @@ -464,7 +521,7 @@ func TestClientGetMethods(t *testing.T) {
}

chainSpecificConfig := testChainConfig([]string{"https://eth-sepolia.example.com"})
client, err := NewClient(chainConfig, nil, chainSpecificConfig, nil, logger)
client, err := NewClient(chainConfig, nil, chainSpecificConfig, nil, false, logger)
require.NoError(t, err)

t.Run("ChainID", func(t *testing.T) {
Expand Down Expand Up @@ -496,7 +553,7 @@ func TestClientConcurrency(t *testing.T) {
}

chainSpecificConfig := testChainConfig([]string{server.URL})
client, err := NewClient(chainConfig, nil, chainSpecificConfig, nil, logger)
client, err := NewClient(chainConfig, nil, chainSpecificConfig, nil, false, logger)
require.NoError(t, err)

ctx := context.Background()
Expand Down
31 changes: 15 additions & 16 deletions universalClient/chains/evm/event_confirmer.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,17 @@ func (ec *EventConfirmer) processPendingEvents(ctx context.Context) error {

// Check if transaction is confirmed based on confirmation type
requiredConfirmations := ec.getRequiredConfirmations(event.ConfirmationType)
confirmations := latestBlock - receipt.BlockNumber.Uint64() + 1
txBlock := receipt.BlockNumber.Uint64()
confirmations, ok := chaincommon.ConfirmationDepth(latestBlock, txBlock)
if !ok {
// RPC height skew: latest block is behind the tx block. Defer.
ec.logger.Debug().
Str("event_id", event.EventID).
Uint64("latest_block", latestBlock).
Uint64("tx_block", txBlock).
Msg("latest block behind tx block (RPC height skew); deferring confirmation")
continue
}

if confirmations >= requiredConfirmations {
var rowsAffected int64
Expand Down Expand Up @@ -245,24 +255,13 @@ func (ec *EventConfirmer) getTxHashFromEventID(eventID string) string {
return parts[0]
}

// getRequiredConfirmations returns the required number of confirmations based on confirmation type
// getRequiredConfirmations returns the depth for a confirmation type. Values are
// resolved by applyDefaults, so a 0 here is an intentional instant route.
func (ec *EventConfirmer) getRequiredConfirmations(confirmationType string) uint64 {
switch confirmationType {
case store.ConfirmationFast:
if ec.fastConfirmations >= 0 {
return ec.fastConfirmations
}
return 5
case store.ConfirmationStandard:
if ec.standardConfirmations >= 0 {
return ec.standardConfirmations
}
return 12
return ec.fastConfirmations
default:
// Default to standard if unknown
if ec.standardConfirmations >= 0 {
return ec.standardConfirmations
}
return 12
return ec.standardConfirmations
}
}
Loading
Loading