-
Notifications
You must be signed in to change notification settings - Fork 1.7k
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
Add Transaction Simulation to the TXM #12503
Changes from 19 commits
5a87d87
bdb9fef
46b13be
141dfea
4835554
27922a3
befeaf0
fbdd110
2f7078f
fc70e7f
389e18f
37b783e
cdc58b2
e126805
0c22a6f
c7eae50
53efe22
8ad2510
3f53867
ce8a3c7
d7605cc
f301d43
eb34bc7
68de594
1a25270
6798e4d
87c8eac
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"chainlink": minor | ||
--- | ||
|
||
Added a tx simulation feature to the chain client to enable testing for zk out-of-counter (OOC) errors |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -38,7 +38,8 @@ type chainClient struct { | |
RPCClient, | ||
rpc.BatchElem, | ||
] | ||
logger logger.SugaredLogger | ||
logger logger.SugaredLogger | ||
chainType config.ChainType | ||
} | ||
|
||
func NewChainClient( | ||
|
@@ -269,3 +270,12 @@ func (c *chainClient) TransactionReceipt(ctx context.Context, txHash common.Hash | |
func (c *chainClient) LatestFinalizedBlock(ctx context.Context) (*evmtypes.Head, error) { | ||
return c.multiNode.LatestFinalizedBlock(ctx) | ||
} | ||
|
||
func (c *chainClient) CheckTxValidity(ctx context.Context, from common.Address, to common.Address, data []byte) error { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How should the product understand if the received error is OOC type? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since we standardize the error we send back, they could use ErrOutOfCounters to determine if the error is an OOC error. I think to make it simpler for them though we'd maybe need to return a second output either another error or a flag. But I took this approach in efforts to keep the return a single field. |
||
msg := ethereum.CallMsg{ | ||
From: from, | ||
To: &to, | ||
Data: data, | ||
} | ||
return SimulateTransaction(ctx, c, c.logger, c.chainType, msg) | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why create a separate simulatorClient instead of adding the logic directly to the existing client? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Before when we were using the custom zkEvm method, it was so we could maintain chain specific code without crowding the client. It's possible now that it's simplified but I think there's a realistic possibility that we may need to support a custom method for a chain that's still in-progress. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I might regret this in the future, but I still prefer to have this inside There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would maybe argue that chain client is more so EVM specific code. So if something requires chain specific code where different EVM chains have different implementation, my opinion is we'd want to separate that from the chain client. That being said, we skipped on using the zkevm chain specific code here so I'm not against moving this to the chain client. We could always separate this out again later when there's a need. But before I do, let me get agreement from others. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would be ok with anything here. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd prefer to wait to avoid another back and forth while product teams are waiting. But I agree we'll have more changes come to this code in the near future when we can make this decision. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,134 @@ | ||
package client | ||
|
||
import ( | ||
"context" | ||
"errors" | ||
"fmt" | ||
|
||
"github.com/ethereum/go-ethereum" | ||
"github.com/ethereum/go-ethereum/common/hexutil" | ||
"github.com/ethereum/go-ethereum/core/types" | ||
|
||
"github.com/smartcontractkit/chainlink-common/pkg/logger" | ||
"github.com/smartcontractkit/chainlink/v2/common/config" | ||
|
||
commonclient "github.com/smartcontractkit/chainlink/v2/common/client" | ||
) | ||
|
||
const ErrOutOfCounters = "not enough counters to continue the execution" | ||
|
||
type simulatorClient interface { | ||
CallContext(ctx context.Context, result interface{}, method string, args ...interface{}) error | ||
} | ||
|
||
// ZK chains can return an out-of-counters error | ||
// This method allows a caller to determine if a tx would fail due to OOC error by simulating the transaction | ||
// Used as an entry point for custom simulation across different chains | ||
func SimulateTransaction(ctx context.Context, client simulatorClient, lggr logger.SugaredLogger, chainType config.ChainType, msg ethereum.CallMsg) error { | ||
var err error | ||
switch chainType { | ||
case config.ChainZkEvm: | ||
err = simulateTransactionZkEvm(ctx, client, lggr, msg) | ||
default: | ||
err = simulateTransactionDefault(ctx, client, msg) | ||
} | ||
// ClassifySendError will not have the proper fields for logging within the method due to the empty Transaction passed | ||
code := ClassifySendError(err, lggr, &types.Transaction{}, msg.From, chainType.IsL2()) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ClassifySendError is used only for Errors that are a response to eth_sendTransaction/eth_sendRawTransaction. Any reason why you want to use that for thus simulate call? I would think we just need to extract reason from the json response like you did for simulateTransactionDefault. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it was a suggestion from Dimitris so we could take advantage of configurable errors after Nick's changes unless I misunderstood. You bring up a good point though that this would be mixing different types of errors though. I could setup a different parser for tx simulation errors but would that be overkill? |
||
// Only return error if ZK OOC error is identified | ||
if code == commonclient.OutOfCounters { | ||
return errors.New(ErrOutOfCounters) | ||
} | ||
return nil | ||
} | ||
|
||
// eth_estimateGas returns out-of-counters (OOC) error if the transaction would result in an overflow | ||
func simulateTransactionDefault(ctx context.Context, client simulatorClient, msg ethereum.CallMsg) error { | ||
var result hexutil.Big | ||
errCall := client.CallContext(ctx, &result, "eth_estimateGas", toCallArg(msg), "pending") | ||
jsonErr, _ := ExtractRPCError(errCall) | ||
if jsonErr != nil && len(jsonErr.Message) > 0 { | ||
return errors.New(jsonErr.Message) | ||
} | ||
return nil | ||
} | ||
|
||
type zkEvmEstimateCountResponse struct { | ||
CountersUsed struct { | ||
GasUsed string | ||
UsedKeccakHashes string | ||
UsedPoseidonHashes string | ||
UsedPoseidonPaddings string | ||
UsedMemAligns string | ||
UsedArithmetics string | ||
UsedBinaries string | ||
UsedSteps string | ||
UsedSHA256Hashes string | ||
} | ||
CountersLimit struct { | ||
MaxGasUsed string | ||
MaxKeccakHashes string | ||
MaxPoseidonHashes string | ||
MaxPoseidonPaddings string | ||
MaxMemAligns string | ||
MaxArithmetics string | ||
MaxBinaries string | ||
MaxSteps string | ||
MaxSHA256Hashes string | ||
} | ||
OocError string | ||
} | ||
|
||
// zkEVM implemented a custom zkevm_estimateCounters method to detect if a transaction would result in an out-of-counters (OOC) error | ||
func simulateTransactionZkEvm(ctx context.Context, client simulatorClient, lggr logger.SugaredLogger, msg ethereum.CallMsg) error { | ||
var result zkEvmEstimateCountResponse | ||
err := client.CallContext(ctx, &result, "zkevm_estimateCounters", toCallArg(msg), "pending") | ||
if err != nil { | ||
return fmt.Errorf("failed to simulate tx: %w", err) | ||
} | ||
if detectZkEvmCounterOverflow(result) && len(result.OocError) > 0 { | ||
friedemannf marked this conversation as resolved.
Show resolved
Hide resolved
|
||
lggr.Debugw("zkevm_estimateCounters returned", "result", result) | ||
return errors.New(result.OocError) | ||
} | ||
return nil | ||
} | ||
|
||
// Helper method for zkEvm to determine if response indicates an overflow | ||
func detectZkEvmCounterOverflow(result zkEvmEstimateCountResponse) bool { | ||
if result.CountersUsed.UsedKeccakHashes > result.CountersLimit.MaxKeccakHashes || | ||
result.CountersUsed.UsedPoseidonHashes > result.CountersLimit.MaxPoseidonHashes || | ||
result.CountersUsed.UsedPoseidonPaddings > result.CountersLimit.MaxPoseidonPaddings || | ||
result.CountersUsed.UsedMemAligns > result.CountersLimit.MaxMemAligns || | ||
result.CountersUsed.UsedArithmetics > result.CountersLimit.MaxArithmetics || | ||
result.CountersUsed.UsedBinaries > result.CountersLimit.MaxBinaries || | ||
result.CountersUsed.UsedSteps > result.CountersLimit.MaxSteps || | ||
result.CountersUsed.UsedSHA256Hashes > result.CountersLimit.MaxSHA256Hashes { | ||
friedemannf marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return true | ||
} | ||
return false | ||
} | ||
|
||
func toCallArg(msg ethereum.CallMsg) interface{} { | ||
arg := map[string]interface{}{ | ||
"from": msg.From, | ||
"to": msg.To, | ||
} | ||
if len(msg.Data) > 0 { | ||
arg["input"] = hexutil.Bytes(msg.Data) | ||
} | ||
if msg.Value != nil { | ||
arg["value"] = (*hexutil.Big)(msg.Value) | ||
} | ||
if msg.Gas != 0 { | ||
arg["gas"] = hexutil.Uint64(msg.Gas) | ||
} | ||
if msg.GasPrice != nil { | ||
arg["gasPrice"] = (*hexutil.Big)(msg.GasPrice) | ||
} | ||
if msg.GasFeeCap != nil { | ||
arg["maxFeePerGas"] = (*hexutil.Big)(msg.GasFeeCap) | ||
} | ||
if msg.GasTipCap != nil { | ||
arg["maxPriorityFeePerGas"] = (*hexutil.Big)(msg.GasTipCap) | ||
} | ||
return arg | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this needed?
Didn't see any behavior depending on this type.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh good call, missed cleaning this out after removing the zkevm specific method