Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(rfq-relayer): wait for finality before proving #3062

Merged
merged 18 commits into from
Aug 30, 2024
Merged
Show file tree
Hide file tree
Changes from 7 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
2 changes: 1 addition & 1 deletion services/rfq/guard/guardconfig/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ func NewGuardConfigFromRelayer(relayerCfg relconfig.Config) Config {
for chainID, chainCfg := range relayerCfg.GetChains() {
cfg.Chains[chainID] = ChainConfig{
RFQAddress: chainCfg.RFQAddress,
Confirmations: chainCfg.Confirmations,
Confirmations: chainCfg.FinalityConfirmations,
}
}
return cfg
Expand Down
45 changes: 44 additions & 1 deletion services/rfq/guard/service/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,17 @@
metrics.EndSpanWithErr(span, err)
}()

// first, get the corresponding bridge request
// first, verify that the prove tx is finalized
finalized, err := g.isFinalized(ctx, proven.TxHash, int(proven.Origin))
if err != nil {
return fmt.Errorf("could not check if tx is finalized: %w", err)
}
span.SetAttributes(attribute.Bool("finalized", finalized))
if !finalized {
return nil
}

// get the corresponding bridge request
bridgeRequest, err := g.db.GetBridgeRequestByID(ctx, proven.TransactionID)
if err != nil {
return fmt.Errorf("could not get bridge request for txid %s: %w", hexutil.Encode(proven.TransactionID[:]), err)
Expand Down Expand Up @@ -242,3 +252,36 @@
}
return true
}

// isFinalized checks if a transaction is finalized versus the configured confirmations threshold.
func (g *Guard) isFinalized(ctx context.Context, txHash common.Hash, chainID int) (bool, error) {
span := trace.SpanFromContext(ctx)

client, err := g.client.GetChainClient(ctx, chainID)
if err != nil {
return false, fmt.Errorf("could not get chain client: %w", err)
}
receipt, err := client.TransactionReceipt(ctx, txHash)
if err != nil {
return false, fmt.Errorf("could not get receipt: %w", err)
}
currentBlockNumber, err := client.BlockNumber(ctx)
if err != nil {
return false, fmt.Errorf("could not get block number: %w", err)
}

chainCfg, ok := g.cfg.Chains[chainID]
if !ok {
return false, fmt.Errorf("could not get chain config for chain %d", chainID)
}
threshBlockNumber := uint64(receipt.BlockNumber.Int64()) + chainCfg.Confirmations

Check failure on line 277 in services/rfq/guard/service/handlers.go

View workflow job for this annotation

GitHub Actions / Lint (services/rfq)

SA5011: possible nil pointer dereference (staticcheck)
span.SetAttributes(
attribute.String("tx_hash", txHash.Hex()),
attribute.Int("chain_id", chainID),
attribute.Int64("current_block_number", int64(currentBlockNumber)),
attribute.Int64("receipt_block_number", receipt.BlockNumber.Int64()),

Check failure on line 282 in services/rfq/guard/service/handlers.go

View workflow job for this annotation

GitHub Actions / Lint (services/rfq)

SA5011: possible nil pointer dereference (staticcheck)
attribute.Int("confirmations", int(chainCfg.Confirmations)),
)

return receipt != nil && currentBlockNumber >= threshBlockNumber, nil

Check failure on line 286 in services/rfq/guard/service/handlers.go

View workflow job for this annotation

GitHub Actions / Lint (services/rfq)

SA5011(related information): this check suggests that the pointer can be nil (staticcheck)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fix potential nil pointer dereference.

The function isFinalized has a potential issue with nil pointer dereference. Ensure that the receipt is checked for nil before accessing its fields.

Apply this diff to fix the potential nil pointer dereference:

 func (g *Guard) isFinalized(ctx context.Context, txHash common.Hash, chainID int) (bool, error) {
	span := trace.SpanFromContext(ctx)

	client, err := g.client.GetChainClient(ctx, chainID)
	if err != nil {
		return false, fmt.Errorf("could not get chain client: %w", err)
	}
	receipt, err := client.TransactionReceipt(ctx, txHash)
	if err != nil {
		return false, fmt.Errorf("could not get receipt: %w", err)
	}
+	if receipt == nil {
+		return false, fmt.Errorf("receipt is nil")
+	}
	currentBlockNumber, err := client.BlockNumber(ctx)
	if err != nil {
		return false, fmt.Errorf("could not get block number: %w", err)
	}

	chainCfg, ok := g.cfg.Chains[chainID]
	if !ok {
		return false, fmt.Errorf("could not get chain config for chain %d", chainID)
	}
	threshBlockNumber := uint64(receipt.BlockNumber.Int64()) + chainCfg.Confirmations
	span.SetAttributes(
		attribute.String("tx_hash", txHash.Hex()),
		attribute.Int("chain_id", chainID),
		attribute.Int64("current_block_number", int64(currentBlockNumber)),
		attribute.Int64("receipt_block_number", receipt.BlockNumber.Int64()),
		attribute.Int("confirmations", int(chainCfg.Confirmations)),
	)

	return receipt != nil && currentBlockNumber >= threshBlockNumber, nil
}
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// isFinalized checks if a transaction is finalized versus the configured confirmations threshold.
func (g *Guard) isFinalized(ctx context.Context, txHash common.Hash, chainID int) (bool, error) {
span := trace.SpanFromContext(ctx)
client, err := g.client.GetChainClient(ctx, chainID)
if err != nil {
return false, fmt.Errorf("could not get chain client: %w", err)
}
receipt, err := client.TransactionReceipt(ctx, txHash)
if err != nil {
return false, fmt.Errorf("could not get receipt: %w", err)
}
currentBlockNumber, err := client.BlockNumber(ctx)
if err != nil {
return false, fmt.Errorf("could not get block number: %w", err)
}
chainCfg, ok := g.cfg.Chains[chainID]
if !ok {
return false, fmt.Errorf("could not get chain config for chain %d", chainID)
}
threshBlockNumber := uint64(receipt.BlockNumber.Int64()) + chainCfg.Confirmations
span.SetAttributes(
attribute.String("tx_hash", txHash.Hex()),
attribute.Int("chain_id", chainID),
attribute.Int64("current_block_number", int64(currentBlockNumber)),
attribute.Int64("receipt_block_number", receipt.BlockNumber.Int64()),
attribute.Int("confirmations", int(chainCfg.Confirmations)),
)
return receipt != nil && currentBlockNumber >= threshBlockNumber, nil
}
func (g *Guard) isFinalized(ctx context.Context, txHash common.Hash, chainID int) (bool, error) {
span := trace.SpanFromContext(ctx)
client, err := g.client.GetChainClient(ctx, chainID)
if err != nil {
return false, fmt.Errorf("could not get chain client: %w", err)
}
receipt, err := client.TransactionReceipt(ctx, txHash)
if err != nil {
return false, fmt.Errorf("could not get receipt: %w", err)
}
if receipt == nil {
return false, fmt.Errorf("receipt is nil")
}
currentBlockNumber, err := client.BlockNumber(ctx)
if err != nil {
return false, fmt.Errorf("could not get block number: %w", err)
}
chainCfg, ok := g.cfg.Chains[chainID]
if !ok {
return false, fmt.Errorf("could not get chain config for chain %d", chainID)
}
threshBlockNumber := uint64(receipt.BlockNumber.Int64()) + chainCfg.Confirmations
span.SetAttributes(
attribute.String("tx_hash", txHash.Hex()),
attribute.Int("chain_id", chainID),
attribute.Int64("current_block_number", int64(currentBlockNumber)),
attribute.Int64("receipt_block_number", receipt.BlockNumber.Int64()),
attribute.Int("confirmations", int(chainCfg.Confirmations)),
)
return receipt != nil && currentBlockNumber >= threshBlockNumber, nil
}
Tools
GitHub Check: Lint (services/rfq)

[failure] 277-277:
SA5011: possible nil pointer dereference (staticcheck)


[failure] 286-286:
SA5011(related information): this check suggests that the pointer can be nil (staticcheck)


[failure] 282-282:
SA5011: possible nil pointer dereference (staticcheck)

2 changes: 2 additions & 0 deletions services/rfq/relayer/relconfig/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ type ChainConfig struct {
RFQAddress string `yaml:"rfq_address"`
// Confirmations is the number of required confirmations.
Confirmations uint64 `yaml:"confirmations"`
// FinalityConfirmations is the number of required confirmations before proving.
FinalityConfirmations uint64 `yaml:"prove_confirmations"`
// Tokens is a map of token name -> token config.
Tokens map[string]TokenConfig `yaml:"tokens"`
// NativeToken is the native token of the chain (pays gas).
Expand Down
14 changes: 14 additions & 0 deletions services/rfq/relayer/relconfig/getters.go
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,20 @@
return value, nil
}

// GetFinalityConfirmations returns the FinalityConfirmations for the given chainID.
func (c Config) GetFinalityConfirmations(chainID int) (value uint64, err error) {
rawValue, err := c.getChainConfigValue(chainID, "FinalityConfirmations")
if err != nil {
return value, err
}

Check warning on line 223 in services/rfq/relayer/relconfig/getters.go

View check run for this annotation

Codecov / codecov/patch

services/rfq/relayer/relconfig/getters.go#L221-L223

Added lines #L221 - L223 were not covered by tests

value, ok := rawValue.(uint64)
if !ok {

Check warning on line 226 in services/rfq/relayer/relconfig/getters.go

View check run for this annotation

Codecov / codecov/patch

services/rfq/relayer/relconfig/getters.go#L225-L226

Added lines #L225 - L226 were not covered by tests
return value, fmt.Errorf("failed to cast FinalityConfirmations to int")
}
return value, nil

Check warning on line 229 in services/rfq/relayer/relconfig/getters.go

View check run for this annotation

Codecov / codecov/patch

services/rfq/relayer/relconfig/getters.go#L228-L229

Added lines #L228 - L229 were not covered by tests
}
Comment on lines +219 to +230
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add unit tests for the new function.

The function should include unit tests to ensure its correctness.

Do you want me to generate the unit testing code or open a GitHub issue to track this task?

Tools
GitHub Check: codecov/patch

[warning] 221-223: services/rfq/relayer/relconfig/getters.go#L221-L223
Added lines #L221 - L223 were not covered by tests


[warning] 225-226: services/rfq/relayer/relconfig/getters.go#L225-L226
Added lines #L225 - L226 were not covered by tests


[warning] 228-229: services/rfq/relayer/relconfig/getters.go#L228-L229
Added lines #L228 - L229 were not covered by tests


Correct the error message in the type assertion failure.

The error message should mention uint64 instead of int.

- return value, fmt.Errorf("failed to cast FinalityConfirmations to int")
+ return value, fmt.Errorf("failed to cast FinalityConfirmations to uint64")
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func (c Config) GetFinalityConfirmations(chainID int) (value uint64, err error) {
rawValue, err := c.getChainConfigValue(chainID, "FinalityConfirmations")
if err != nil {
return value, err
}
value, ok := rawValue.(uint64)
if !ok {
return value, fmt.Errorf("failed to cast FinalityConfirmations to int")
}
return value, nil
}
func (c Config) GetFinalityConfirmations(chainID int) (value uint64, err error) {
rawValue, err := c.getChainConfigValue(chainID, "FinalityConfirmations")
if err != nil {
return value, err
}
value, ok := rawValue.(uint64)
if !ok {
return value, fmt.Errorf("failed to cast FinalityConfirmations to uint64")
}
return value, nil
}
Tools
GitHub Check: codecov/patch

[warning] 221-223: services/rfq/relayer/relconfig/getters.go#L221-L223
Added lines #L221 - L223 were not covered by tests


[warning] 225-226: services/rfq/relayer/relconfig/getters.go#L225-L226
Added lines #L225 - L226 were not covered by tests


[warning] 228-229: services/rfq/relayer/relconfig/getters.go#L228-L229
Added lines #L228 - L229 were not covered by tests


// GetNativeToken returns the NativeToken for the given chainID.
func (c Config) GetNativeToken(chainID int) (value string, err error) {
rawValue, err := c.getChainConfigValue(chainID, "NativeToken")
Expand Down
24 changes: 22 additions & 2 deletions services/rfq/relayer/service/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -378,8 +378,28 @@
// Step 6: ProvePosting
//
// This is the sixth step in the bridge process. Here we submit the claim transaction to the origin chain.
func (q *QuoteRequestHandler) handleRelayCompleted(ctx context.Context, _ trace.Span, request reldb.QuoteRequest) (err error) {
// relays been completed, it's time to go back to the origin chain and try to prove
func (q *QuoteRequestHandler) handleRelayCompleted(ctx context.Context, span trace.Span, request reldb.QuoteRequest) (err error) {
// first, ensure that enough confirmations have passed before proving
receipt, err := q.Dest.Client.TransactionReceipt(ctx, request.DestTxHash)
if err != nil {
return fmt.Errorf("could not get transaction receipt: %w", err)
}
currentBlockNumber := q.Origin.LatestBlock()

Check warning on line 387 in services/rfq/relayer/service/handlers.go

View check run for this annotation

Codecov / codecov/patch

services/rfq/relayer/service/handlers.go#L381-L387

Added lines #L381 - L387 were not covered by tests
proveConfirmations, err := q.cfg.GetFinalityConfirmations(int(q.Dest.ChainID))
if err != nil {
return fmt.Errorf("could not get prove confirmations: %w", err)
}
span.SetAttributes(
attribute.Int("current_block_number", int(currentBlockNumber)),
attribute.Int("dest_block_number", int(receipt.BlockNumber.Int64())),
attribute.Int("prove_confirmations", int(proveConfirmations)),
)
if currentBlockNumber < request.BlockNumber+proveConfirmations {
span.AddEvent("not enough confirmations")
return nil
}

Check warning on line 400 in services/rfq/relayer/service/handlers.go

View check run for this annotation

Codecov / codecov/patch

services/rfq/relayer/service/handlers.go#L389-L400

Added lines #L389 - L400 were not covered by tests
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reminder: Add tests.

The added lines are not covered by tests. Ensure that the new logic is adequately tested.

Do you want me to generate the unit testing code or open a GitHub issue to track this task?

Tools
GitHub Check: codecov/patch

[warning] 381-387: services/rfq/relayer/service/handlers.go#L381-L387
Added lines #L381 - L387 were not covered by tests


[warning] 389-400: services/rfq/relayer/service/handlers.go#L389-L400
Added lines #L389 - L400 were not covered by tests


// relay has been finalized, it's time to go back to the origin chain and try to prove
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dwasse we need to do a check similar to Guard's isProveValid here to handle the case where our relay tx was reorged.

In the event where we submitted a tx, witnessed a BridgeRelayed event, and later on this tx was reorged and ended up being reverted, we need a way for the Relayer to mark the tx as RelayRaceLost here (I'm not sure if we can safely assume that we will index the correct event with another relayer before that).

The current code will still get a valid receipt for DestTxHash, it's just the receipt will not have a corresponding BridgeRelayed event there.

_, err = q.Origin.SubmitTransaction(ctx, func(transactor *bind.TransactOpts) (tx *types.Transaction, err error) {
tx, err = q.Origin.Bridge.Prove(transactor, request.RawRequest, request.DestTxHash)
if err != nil {
Expand Down
4 changes: 4 additions & 0 deletions services/rfq/relayer/service/statushandler.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"github.com/synapsecns/sanguine/services/rfq/relayer/chain"
"github.com/synapsecns/sanguine/services/rfq/relayer/inventory"
"github.com/synapsecns/sanguine/services/rfq/relayer/quoter"
"github.com/synapsecns/sanguine/services/rfq/relayer/relconfig"
"github.com/synapsecns/sanguine/services/rfq/relayer/reldb"
"github.com/synapsecns/sanguine/services/rfq/util"
"go.opentelemetry.io/otel/attribute"
Expand Down Expand Up @@ -54,6 +55,8 @@
handlerMtx mapmutex.StringMapMutex
// balanceMtx is the mutex for balances.
balanceMtx mapmutex.StringMapMutex
// cfg is the relayer config.
cfg relconfig.Config
}

func getBalanceMtxKey(chainID uint32, token common.Address) string {
Expand Down Expand Up @@ -88,6 +91,7 @@
mutexMiddlewareFunc: r.mutexMiddleware,
handlerMtx: r.handlerMtx,
balanceMtx: r.balanceMtx,
cfg: r.cfg,

Check warning on line 94 in services/rfq/relayer/service/statushandler.go

View check run for this annotation

Codecov / codecov/patch

services/rfq/relayer/service/statushandler.go#L94

Added line #L94 was not covered by tests
}

// wrap in deadline middleware since the relay has not yet happened
Expand Down
Loading