feat: Amsterdam hard fork — EIP-7825 tx gas cap and EIP-7702 authorization gas repricing#317
feat: Amsterdam hard fork — EIP-7825 tx gas cap and EIP-7702 authorization gas repricing#317SegueII wants to merge 8 commits into
Conversation
Consensus change — off by default on all chain configs.
Motivation
----------
EIP-7702 per-auth intrinsic pricing in morph (and current upstream
go-ethereum) follows the official spec: charge `CallNewAccountGas`
(25000) per authorization up front, then refund
`CallNewAccountGas - TxAuthTupleGas` (12500) via `StateDB.refund` when
the authority account already exists in state. That refund shares the
single StateDB refund counter with SSTORE refunds and is capped at
`gasUsed/5` by `refundGas`. A transaction that declares many
authorizations all pointing at existing accounts can therefore have its
refund truncated, leading users to pay materially more gas than the EIP
intent. See .vscode/Eezo_Shunt_Final.md and .vscode/Eezo_Shunt_Spec.md
for the full write-up and worked example.
Design
------
The fix is a consensus-layer change and is strictly gated behind a new
time-based fork tentatively named Amsterdam. All existing chain configs
(Mainnet / Hoodi / Holesky / AllEthashProtocolChanges / TestChainConfig
/ …) leave `AmsterdamTime` nil, so this commit ships *without* altering
live behavior — the fork must be activated explicitly in a later chain
config update.
Pre-Amsterdam path (unchanged):
- `IntrinsicGas` charges `CallNewAccountGas` per authorization.
- `applyAuthorization` calls `AddRefund(12500)` when the authority
already exists. Historical blocks remain reproducible.
Post-Amsterdam path:
- `IntrinsicGas` charges only `TxAuthTupleGas` per authorization.
- `applyAuthorization` debits `CallNewAccountGas - TxAuthTupleGas`
directly from `st.gas` when the authority does not exist. This
bypasses the StateDB refund counter entirely and is therefore
immune to the `gasUsed/5` cap.
- When remaining `st.gas` is below the delta, the authorization is
skipped (no nonce bump, no delegation install) rather than
triggering an OutOfGas; this mirrors the existing "validation
errors silently skip the auth" contract in the TransitionDb loop
("errors are ignored, we simply skip invalid authorizations here")
and prevents a partial debit from corrupting the subsequent EVM
call's gas budget. A dedicated sentinel error
`ErrInsufficientAuthorizationGas` makes the decision observable in
tests.
Fork infrastructure
-------------------
- `params/forks/forks.go`: add `JadeFork` and `Amsterdam` to the
enum (JadeFork was previously missing — fill while at it).
- `params/config.go`:
* new `ChainConfig.AmsterdamTime` field, `IsAmsterdam(time)`
predicate, and `Rules.IsAmsterdam`.
* `CheckConfigForkOrder` / `CheckCompatible` entries.
* `LatestFork` and `Timestamp` updated; JadeFork backfilled.
* `String()` prints the new field.
Call-site propagation
---------------------
- `IntrinsicGas` gains a trailing `isAmsterdam bool` argument. All
six call sites updated:
core/state_transition.go (TransitionDb),
core/tx_pool.go (validateTx, new `amsterdam` fork indicator),
light/txpool.go (new `amsterdam` fork indicator),
tests/transaction_test_util.go,
cmd/evm/internal/t8ntool/transaction.go (t8n has block-number
context only and keeps the flag false, matching upstream t8n
semantics),
core/bench_test.go.
- `applyAuthorization` gains `isAmsterdam bool`; the single caller
inside `TransitionDb` passes `rules.IsAmsterdam`.
Tests
-----
`core/state_transition_test.go` (new):
- `TestIntrinsicGas` — table-driven, covers zero/one/many auth
scenarios both pre- and post-Amsterdam, plus AccessList interaction.
- `TestIntrinsicGasAuthListScalingPreVsPost` — asserts the exact
pre-vs-post-fork delta for N authorizations.
- `TestApplyAuthorizationPreFork{Refund,NoRefund}` — refund-counter
behavior for the legacy path.
- `TestApplyAuthorizationPostFork{Charge,NoCharge,Skip,Boundary}` —
st.gas debit behavior, including the exact-boundary and
insufficient-gas edge cases.
- `TestApplyAuthorizationInvalidSignatureSkipped` — verifies the
"validation error → skip" contract still wins over the new gas
accounting under both forks.
`params/config_test.go` (extended):
- `TestIsAmsterdam` — nil / future / at-activation / past / genesis.
- `TestLatestForkIncludesJadeForkAndAmsterdam` — asserts the
correct ordering including the JadeFork backfill.
- `TestTimestampForkLookup` — round-trips every time-based fork.
- `TestCheckCompatibleAmsterdamTimestamp` — rejects an incompatible
reschedule when the head is past the stored activation.
- `TestAmsterdamForkOrdering` — enforces the Amsterdam-after-JadeFork
ordering in `CheckConfigForkOrder`.
Made-with: Cursor
Consensus change — off by default on all chain configs.
Motivation
----------
EIP-7825 introduces a protocol-level cap of 2^24 (16_777_216) gas on
any single transaction, as an additional DoS mitigation that keeps
individual transactions well within the zkEVM circuit budget. morph
does not yet implement this limit; this commit adds the constant and
the preCheck enforcement, strictly gated on the Amsterdam fork added
in the preceding P1-3 commit.
Design
------
`params.MaxTxGas` is unconditionally defined so tooling (gas
estimators, RPC, block builders) can reference it. The enforcement
lives in `core/state_transition.go::preCheck` inside the existing
`!st.msg.IsFake()` block:
- L1MessageTx transactions already take the early-return branch at
the top of preCheck and are therefore naturally exempt (matches
the spec's "L1 message transactions are excluded").
- Simulation / eth_call transactions (`IsFake() == true`) skip the
enforcement block entirely, matching upstream's
`SkipTransactionChecks` semantics.
- All other transactions, once Amsterdam is active, must have
`msg.Gas() <= params.MaxTxGas`. The check precedes the nonce
and EOA lookups and the balance check, so a malformed oversized
transaction is rejected cheaply.
All existing chain configs leave `AmsterdamTime` nil, so this check
is a no-op on Holesky / Hoodi / Mainnet until the fork is activated
in chain configuration.
A new sentinel `core.ErrGasLimitTooHigh` makes the rejection
observable and matches the upstream go-ethereum error name.
Tests
-----
`core/state_transition_test.go` adds six direct preCheck tests,
covering every scenario called out in the Eezo Shunt spec
acceptance criteria:
- `TestPreCheckMaxTxGasPreAmsterdamIgnoresCap` — confirms the
check is fork-gated (pre-Amsterdam must not return
ErrGasLimitTooHigh even for gas limits far above MaxTxGas).
- `TestPreCheckMaxTxGasPostAmsterdamRejectsAboveCap` —
`gas = MaxTxGas + 1` is rejected.
- `TestPreCheckMaxTxGasPostAmsterdamAcceptsAtCap` —
`gas = MaxTxGas` is accepted (predicate is strict `>`).
- `TestPreCheckMaxTxGasPostAmsterdamAcceptsBelowCap` —
`gas = MaxTxGas - 1` is accepted.
- `TestPreCheckMaxTxGasFakeExempt` — `IsFake()` transactions
bypass the cap.
- `TestPreCheckMaxTxGasL1Exempt` — a real `L1MessageTx` built via
`types.NewTx(&types.L1MessageTx{...}).AsMessage(...)` hits the
early-return branch and is unaffected.
Made-with: Cursor
📝 WalkthroughWalkthroughAdds Amsterdam fork activation, a per-transaction gas cap, and fork-aware intrinsic-gas and authorization accounting across core execution, tx pools, RPC, CLI, and tests. ChangesAmsterdam fork rollout
Estimated code review effort: 4 (Complex) | ~70 minutes Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…tion validation - Introduced `--input.env` flag in the transaction command to allow specifying an environment file. - Updated the transaction processing logic to read and validate the environment data, particularly for Amsterdam fork scenarios. - Enhanced test cases to cover new input environment scenarios, ensuring proper handling of oversized gas transactions pre- and post-Amsterdam activation. - Added new test data files for environment configurations and expected outputs for various transaction scenarios.
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
cmd/evm/internal/t8ntool/transaction.go (2)
163-180:⚠️ Potential issue | 🟡 MinorApply the Amsterdam gas-cap check before intrinsic-gas validation.
Production txpool validation rejects
gas > params.MaxTxGasbefore callingIntrinsicGas. Here, an oversized Amsterdam transaction can instead reportErrIntrinsicGaswhen its intrinsic requirement is also above the supplied gas limit, producing inconsistent t9n results.🐛 Proposed fix
+ if isAmsterdam && !tx.IsL1MessageTx() && tx.Gas() > params.MaxTxGas { + r.Error = fmt.Errorf("%w (cap: %d, tx: %d)", core.ErrGasLimitTooHigh, params.MaxTxGas, tx.Gas()) + results = append(results, r) + continue + } if gas, err := core.IntrinsicGas(tx.Data(), tx.AccessList(), tx.SetCodeAuthorizations(), tx.To() == nil, chainConfig.IsHomestead(new(big.Int)), chainConfig.IsIstanbul(new(big.Int)), chainConfig.IsShanghai(new(big.Int)), isAmsterdam); err != nil { r.Error = err @@ - if isAmsterdam && !tx.IsL1MessageTx() && tx.Gas() > params.MaxTxGas { - r.Error = fmt.Errorf("%w (cap: %d, tx: %d)", core.ErrGasLimitTooHigh, params.MaxTxGas, tx.Gas()) - results = append(results, r) - continue - }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@cmd/evm/internal/t8ntool/transaction.go` around lines 163 - 180, Move the Amsterdam gas-cap check so it runs before calling core.IntrinsicGas: before invoking core.IntrinsicGas(...) in the transaction loop, check if isAmsterdam && !tx.IsL1MessageTx() && tx.Gas() > params.MaxTxGas and, if true, set r.Error to fmt.Errorf("%w (cap: %d, tx: %d)", core.ErrGasLimitTooHigh, params.MaxTxGas, tx.Gas()), append r to results and continue; this ensures the params.MaxTxGas cap is enforced consistently (matching txpool) and prevents oversized txs from incorrectly surfacing core.ErrIntrinsicGas.
97-120:⚠️ Potential issue | 🟠 MajorStill load tx RLP when only
--input.envis stdin.With
envStr == stdinSelectorandtxStrpointing to a file, this branch decodes stdin but never opens the tx file, sobodystays empty and the command validates no transactions.🐛 Proposed fix
- if txStr == stdinSelector || envStr == stdinSelector { + if txStr == stdinSelector || envStr == stdinSelector { decoder := json.NewDecoder(os.Stdin) if err := decoder.Decode(inputData); err != nil { return NewError(ErrorJson, fmt.Errorf("failed unmarshaling stdin: %v", err)) } - if txStr == stdinSelector { - // Decode the body of already signed transactions - body = common.FromHex(inputData.TxRlp) - } - } else { + } + if txStr == stdinSelector { + // Decode the body of already signed transactions + body = common.FromHex(inputData.TxRlp) + } else { // Read input from file inFile, err := os.Open(txStr) if err != nil { return NewError(ErrorIO, fmt.Errorf("failed reading txs file: %v", err))🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@cmd/evm/internal/t8ntool/transaction.go` around lines 97 - 120, When envStr == stdinSelector the code decodes inputData from stdin but currently omits opening/decoding txStr when txStr points to a file, leaving body empty; update the branch that handles stdin (where decoder.Decode(inputData) occurs) so that after decoding inputData you check if txStr != stdinSelector and then open txStr (os.Open), create a json.Decoder for that file and decode the RLP body into the variable body (or return a NewError/ErrorIO on failure), and still keep the existing handling of txStr == stdinSelector where body = common.FromHex(inputData.TxRlp); ensure you reference txStr, envStr, stdinSelector, inputData, body and use os.Open/json.NewDecoder as in the file-reading branch.core/tx_pool.go (1)
1497-1587:⚠️ Potential issue | 🟠 MajorRefresh fork indicators before reinjecting reorged transactions.
pool.addTxsLocked(reinject, false)still validates with the previouspool.amsterdamvalue. On a reorg from an Amsterdam head back to a pre-Amsterdam head, transactions withgas > params.MaxTxGasare valid again for the new pending context but will be rejected during reinjection.🐛 Proposed fix
// Initialize the internal state to the current head if newHead == nil { newHead = pool.chain.CurrentBlock().Header() // Special case during testing } @@ pool.currentState = statedb pool.pendingNonces = newTxNoncer(statedb) pool.currentMaxGas = newHead.GasLimit + // Update all fork indicators by next pending block number before + // validating reinjected transactions against the new head context. + next := new(big.Int).Add(newHead.Number, big.NewInt(1)) + pool.istanbul = pool.chainconfig.IsIstanbul(next) + pool.eip2718 = pool.chainconfig.IsCurie(next) + pool.eip1559 = pool.chainconfig.IsCurie(next) + pool.shanghai = pool.chainconfig.IsShanghai(next) + pool.eip7702 = pool.chainconfig.IsViridian(next, newHead.Time) + pool.jade = pool.chainconfig.IsJadeFork(newHead.Time) + pool.amsterdam = pool.chainconfig.IsAmsterdam(newHead.Time) + pool.currentHead = next + // Inject any transactions discarded due to reorgs log.Debug("Reinjecting stale transactions", "count", len(reinject)) senderCacher.recover(pool.signer, reinject) pool.addTxsLocked(reinject, false) - // Update all fork indicator by next pending block number. - next := new(big.Int).Add(newHead.Number, big.NewInt(1)) - pool.istanbul = pool.chainconfig.IsIstanbul(next) - - pool.eip2718 = pool.chainconfig.IsCurie(next) - pool.eip1559 = pool.chainconfig.IsCurie(next) - pool.shanghai = pool.chainconfig.IsShanghai(next) - pool.eip7702 = pool.chainconfig.IsViridian(next, newHead.Time) - pool.jade = pool.chainconfig.IsJadeFork(newHead.Time) - pool.amsterdam = pool.chainconfig.IsAmsterdam(newHead.Time) - // Remove MorphTx V1 transactions if jade fork is not active (e.g. after reorg) if !pool.jade { pool.removeMorphTxV1() } @@ - - // Update current head - pool.currentHead = next }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@core/tx_pool.go` around lines 1497 - 1587, Reinjecting stale transactions uses the old fork flags (pool.amsterdam, pool.eip1559, eip2718, shanghai, eip7702, jade) so addTxsLocked(reinject, false) may reject txs that are valid under the new head; move the block that updates fork indicators (the pool.istanbul / pool.eip2718 / pool.eip1559 / pool.shanghai / pool.eip7702 / pool.jade / pool.amsterdam assignments that call pool.chainconfig.Is* with next/newHead.Time) to occur before calling senderCacher.recover(...) and pool.addTxsLocked(reinject, false) so reinjection validates against the correct fork rules for newHead while keeping pool.currentState and pendingNonces initialization as-is.
🧹 Nitpick comments (1)
params/config_test.go (1)
262-266: Consider asserting the Amsterdam rewind target too.This compatibility error drives rollback behavior, so checking
RewindToTimewould make the Amsterdam-specific test match the existing timestamp compatibility coverage.🧪 Proposed test assertion
require.Equal(t, "AmsterdamTime fork timestamp", err.What) require.Equal(t, NewUint64(100), err.StoredTime) require.Equal(t, NewUint64(200), err.NewTime) + require.Equal(t, NewUint64(99), err.RewindToTime)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@params/config_test.go` around lines 262 - 266, The test currently asserts the compatibility error's What, StoredTime and NewTime after calling stored.CheckCompatible(newer, 0, 150) but does not assert the Amsterdam rewind target; add an assertion that err.RewindToTime equals NewUint64(150) so the Amsterdam-specific behavior is covered (referencing the CheckCompatible call and the err.RewindToTime field).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@core/tx_pool_test.go`:
- Around line 247-258: The helper txPoolNextHead can panic because it
dereferences parent.Hash() and parent.GasLimit even when parent is nil; update
txPoolNextHead to first handle a nil parent by providing safe defaults (e.g.,
nil ParentHash, zero GasLimit and a Number of 1) and only call parent.Hash(),
parent.GasLimit and parent.Number when parent != nil; ensure the returned Header
uses these safe defaults and preserves the existing increment logic for
parent.Number when parent is non-nil.
In `@light/trie.go`:
- Around line 92-94: odrDatabase.TrieDB() was changed to always return a fresh
*trie.Database (via trie.NewDatabase(db.backend.Database())) instead of nil, so
callers that rely on a nil sentinel (e.g., the nil check around the trie
returned in the tracer/state access paths) must be updated: remove or adapt the
nil-checks and use the returned trie.Database directly (or, if you prefer
preserving old semantics, change odrDatabase.TrieDB to return nil when
db.backend.Database() is nil). Locate odrDatabase.TrieDB and the callers that
check for nil (e.g., tracer/state accessor code that checks the trie result) and
either delete the dead nil-branch and proceed with the returned trie.Database,
or implement the conditional nil-return in odrDatabase.TrieDB to restore
previous behavior.
In `@light/txpool.go`:
- Around line 515-530: The dropOversizedAmsterdamTxs function currently deletes
entries from pool.pending and the DB but leaves per-sender nonce state stale in
pool.nonce/nonce-tracking, causing GetNonce to return removedNonce+1; update
dropOversizedAmsterdamTxs to collect the senders (tx.From or tx.Sender) of all
removed txs, then for each affected sender recompute their pending nonce by
scanning remaining pool.pending for the highest nonce (or clear the sender entry
if none remain), and if you detect remaining pending txs with nonces higher than
the recomputed lowest missing nonce, also remove those higher nonces to preserve
the “no queued transactions” invariant; finally still call batch.Write() and
pool.relay.Discard(hashes) as before. Ensure you reference
dropOversizedAmsterdamTxs, pool.pending, pool.nonce (or the nonce map used), and
pool.relay.Discard when making changes.
In `@tests/init.go`:
- Around line 235-254: The Amsterdam test config (map key "Amsterdam") sets
AmsterdamTime but omits predecessor timestamp forks, causing inconsistent fork
activation; update the Amsterdam entry to populate the missing timestamp fields
(Morph203Time, ViridianTime, EmeraldTime, JadeForkTime) using the same pattern
as other test configs (e.g., params.NewUint64(<value>)) so all predecessor
timestamp forks are activated before AmsterdamTime; edit the struct where
AmsterdamTime is set to add these fields with appropriate NewUint64 values
consistent with other test definitions.
---
Outside diff comments:
In `@cmd/evm/internal/t8ntool/transaction.go`:
- Around line 163-180: Move the Amsterdam gas-cap check so it runs before
calling core.IntrinsicGas: before invoking core.IntrinsicGas(...) in the
transaction loop, check if isAmsterdam && !tx.IsL1MessageTx() && tx.Gas() >
params.MaxTxGas and, if true, set r.Error to fmt.Errorf("%w (cap: %d, tx: %d)",
core.ErrGasLimitTooHigh, params.MaxTxGas, tx.Gas()), append r to results and
continue; this ensures the params.MaxTxGas cap is enforced consistently
(matching txpool) and prevents oversized txs from incorrectly surfacing
core.ErrIntrinsicGas.
- Around line 97-120: When envStr == stdinSelector the code decodes inputData
from stdin but currently omits opening/decoding txStr when txStr points to a
file, leaving body empty; update the branch that handles stdin (where
decoder.Decode(inputData) occurs) so that after decoding inputData you check if
txStr != stdinSelector and then open txStr (os.Open), create a json.Decoder for
that file and decode the RLP body into the variable body (or return a
NewError/ErrorIO on failure), and still keep the existing handling of txStr ==
stdinSelector where body = common.FromHex(inputData.TxRlp); ensure you reference
txStr, envStr, stdinSelector, inputData, body and use os.Open/json.NewDecoder as
in the file-reading branch.
In `@core/tx_pool.go`:
- Around line 1497-1587: Reinjecting stale transactions uses the old fork flags
(pool.amsterdam, pool.eip1559, eip2718, shanghai, eip7702, jade) so
addTxsLocked(reinject, false) may reject txs that are valid under the new head;
move the block that updates fork indicators (the pool.istanbul / pool.eip2718 /
pool.eip1559 / pool.shanghai / pool.eip7702 / pool.jade / pool.amsterdam
assignments that call pool.chainconfig.Is* with next/newHead.Time) to occur
before calling senderCacher.recover(...) and pool.addTxsLocked(reinject, false)
so reinjection validates against the correct fork rules for newHead while
keeping pool.currentState and pendingNonces initialization as-is.
---
Nitpick comments:
In `@params/config_test.go`:
- Around line 262-266: The test currently asserts the compatibility error's
What, StoredTime and NewTime after calling stored.CheckCompatible(newer, 0, 150)
but does not assert the Amsterdam rewind target; add an assertion that
err.RewindToTime equals NewUint64(150) so the Amsterdam-specific behavior is
covered (referencing the CheckCompatible call and the err.RewindToTime field).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 0a08e4f3-bc32-4fd6-ab78-c739331aaf58
📒 Files selected for processing (26)
cmd/evm/internal/t8ntool/transaction.gocmd/evm/main.gocmd/evm/t8n_test.gocmd/evm/testdata/15/exp3.jsoncmd/evm/testdata/24/env.post.jsoncmd/evm/testdata/24/env.pre.jsoncmd/evm/testdata/24/exp.post.jsoncmd/evm/testdata/24/exp.pre.jsoncmd/evm/testdata/24/signed_txs.rlpcore/bench_test.gocore/error.gocore/state_transition.gocore/state_transition_test.gocore/tx_pool.gocore/tx_pool_test.gointernal/ethapi/api.gointernal/ethapi/api_morph_test.golight/trie.golight/txpool.golight/txpool_test.goparams/config.goparams/config_test.goparams/forks/forks.goparams/protocol_params.gotests/init.gotests/transaction_test_util.go
- Introduced a new test, `TestAccessListAuthorizationLimitUsesAmsterdamPricing`, to validate the behavior of access list authorization limits under pre- and post-Amsterdam conditions. - Updated the `mockEstimateGasBackend` to include an `RPCGasCap` method for better gas estimation handling. - Adjusted the `AccessList` function to align with Amsterdam pricing semantics, ensuring correct gas calculations based on the active fork. - Enhanced error handling for insufficient gas scenarios in access list creation, providing clearer feedback during testing.
9694d76 to
85478c3
Compare
Co-authored-by: Cursor <cursoragent@cursor.com>
…nsus Co-authored-by: Cursor <cursoragent@cursor.com> # Conflicts: # internal/ethapi/api_morph_test.go # params/config.go
- core/tx_pool.go: refresh fork indicators before reinjecting reorged transactions so they are validated against the new head's fork rules - light/txpool.go: reset affected sender nonces and drop now-gapped transactions when purging oversized txs on Amsterdam activation - cmd/evm/t8ntool: enforce Amsterdam gas cap before intrinsic gas check (matching txpool ordering) and load tx RLP file when only --input.env comes from stdin - tests/init.go: activate predecessor timestamp forks in Amsterdam config - core/tx_pool_test.go: handle nil parent in txPoolNextHead helper - params/config_test.go: assert Amsterdam rewind target timestamp Co-authored-by: Cursor <cursoragent@cursor.com>
|
Addressed all CodeRabbit review findings in e023b95, and merged the latest Outside-diff comments:
Nitpick:
Merge notes (0d61c47):
Verification: |
Summary by CodeRabbit
Summary by CodeRabbit
New Features
--input.env.Behavior Changes
MaxTxGas) across transaction validation/pooling and execution.Bug Fixes
nil.Tests
Close: #341