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
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@subsquid/evm-normalization",
"comment": "Treat normalized transaction `nonce` as optional so OP-stack deposit txs (type 0x7e) that omit it no longer fail mapping",
"type": "patch"
}
],
"packageName": "@subsquid/evm-normalization"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@subsquid/evm-rpc",
"comment": "Treat transaction `nonce` as optional so OP-stack deposit txs (type 0x7e) that omit it (e.g. from Alchemy) no longer fail validation",
"type": "patch"
}
],
"packageName": "@subsquid/evm-rpc"
}
3 changes: 2 additions & 1 deletion evm/evm-normalization/src/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,8 @@ export interface TempoFeePayerSignature {
export interface Transaction {
transactionIndex: number,
hash: Bytes32,
nonce: number,
// Optional for OP-stack deposit (0x7e) transactions, which some providers omit `nonce` for
nonce?: number,
from: Bytes20,
to?: Bytes20,
// Optional for Tempo 0x76 transactions which use batched `calls` instead of `input`
Expand Down
2 changes: 1 addition & 1 deletion evm/evm-normalization/src/mapping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -555,7 +555,7 @@ function mapTransaction(src: rpc.Transaction, receipt?: rpc.Receipt): Transactio
return {
transactionIndex: qty2Int(src.transactionIndex),
hash: src.hash,
nonce: qty2Int(src.nonce),
nonce: src.nonce != null ? qty2Int(src.nonce) : undefined,
from: src.from.toLowerCase(),
to: src.to ? src.to.toLowerCase() : undefined,
input: src.input ?? undefined,
Expand Down
42 changes: 42 additions & 0 deletions evm/evm-rpc/src/rpc-data.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import {describe, it, expect} from 'vitest'
import {cast} from '@subsquid/util-internal-validation'
import {Transaction} from './rpc-data'

// Real optimism-mainnet block 0x93570f5 tx[0] as returned by Alchemy: an
// OP-stack deposit transaction (type 0x7e). Alchemy omits the `nonce` field
// for these system transactions (Dwellir/geth include it), so the validator
// must treat `nonce` as optional or it crashes the dumper on every OP block.
const ALCHEMY_DEPOSIT_TX = {
type: '0x7e',
sourceHash: '0x74e8e903225d29359d3e2b53682e28da2e415872533659255f28951e4e1c3af1',
from: '0xdeaddeaddeaddeaddeaddeaddeaddeaddead0001',
to: '0x4200000000000000000000000000000000000015',
mint: '0x0',
value: '0x0',
gas: '0xf4240',
input: '0x3db6be2b0000146b000f79c50000000000000003000000006a5ebb53000000000186480000000000000000000000000000000000000000000000000000000000045488f6000000000000000000000000000000000000000000000000000000000039ef561076949313403c81113fe7b7de83f0e1686e0df1afbfd2112469530562d0bb8e0000000000000000000000006887246668a3b87f54deb3b94ba47a6f63f329850000000000000000000000000190',
hash: '0x0ba6943635ddd8ea164cd7945dd35f69a2ca6dab7b58bdf23ca1512c30d950ad',
r: '0x0',
s: '0x0',
yParity: '0x0',
v: '0x0',
blockHash: '0x0607985081d5cd537cd4ab5fb7159cf6d1e92d5009c6cb03a617d4240ee03b4d',
blockNumber: '0x93570f5',
transactionIndex: '0x0',
blockTimestamp: '0x6a5ebba3',
gasPrice: '0x0',
// note: no `nonce`
}

describe('Transaction validation', () => {
it('accepts an OP-stack deposit tx (0x7e) that omits nonce', () => {
const tx = cast(Transaction, ALCHEMY_DEPOSIT_TX)
expect(tx.type).toBe(0x7e)
expect(tx.nonce).toBeUndefined()
})

it('still parses nonce when present', () => {
const tx = cast(Transaction, {...ALCHEMY_DEPOSIT_TX, nonce: '0x2efae9e'})
expect(tx.nonce).toBe(0x2efae9e)
})
})
4 changes: 3 additions & 1 deletion evm/evm-rpc/src/rpc-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,9 @@ export const Transaction = object({
input: option(BYTES),
maxFeePerGas: option(QTY),
maxPriorityFeePerGas: option(QTY),
nonce: SMALL_QTY,
// Optional: OP-stack deposit transactions (type 0x7e) may omit `nonce`
// entirely (e.g. Alchemy), so it can't be required for every tx type.
nonce: option(SMALL_QTY),
v: option(QTY),
r: option(BYTES),
s: option(BYTES),
Expand Down
4 changes: 2 additions & 2 deletions evm/evm-rpc/src/rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -528,7 +528,7 @@ export class Rpc {
// Group all txs by (sender, nonce) so we can detect multi-tx-per-nonce cases.
let txsBySenderNonce = new Map<string, Transaction[]>()
for (let tx of transactions) {
let key = `${tx.from}:${qty2Int(tx.nonce)}`
let key = `${tx.from}:${qty2Int(assertNotNull(tx.nonce, 'tx.nonce is missing'))}`
let list = txsBySenderNonce.get(key)
if (list == null) {
list = []
Expand All @@ -540,7 +540,7 @@ export class Rpc {
let phantomHashes = new Set<string>()
for (let candidate of candidates) {
let nonceAfter = nonceAfterBySender.get(candidate.from)!
let txNonce = qty2Int(candidate.nonce)
let txNonce = qty2Int(assertNotNull(candidate.nonce, 'tx.nonce is missing'))

if (txNonce >= nonceAfter) {
// Nonce not consumed at this block — definitely phantom.
Expand Down
32 changes: 16 additions & 16 deletions evm/evm-rpc/src/verification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ function encodeTransaction(tx: Transaction): Buffer {
if (tx.type == '0x0') {
return Buffer.from(
RLP.encode([
BigInt(tx.nonce),
BigInt(assertNotNull(tx.nonce, 'tx.nonce is missing')),
BigInt(assertNotNull(tx.gasPrice, 'tx.gasPrice is missing')),
BigInt(tx.gas),
tx.to ? decodeHex(tx.to) : Buffer.alloc(0),
Expand All @@ -183,7 +183,7 @@ function encodeTransaction(tx: Transaction): Buffer {
} else if (tx.type == '0x1') {
let payload = RLP.encode([
BigInt(assertNotNull(tx.chainId, 'tx.chainId is missing')),
BigInt(tx.nonce),
BigInt(assertNotNull(tx.nonce, 'tx.nonce is missing')),
BigInt(assertNotNull(tx.gasPrice, 'tx.gasPrice is missing')),
BigInt(tx.gas),
tx.to ? decodeHex(tx.to) : Buffer.alloc(0),
Expand All @@ -198,7 +198,7 @@ function encodeTransaction(tx: Transaction): Buffer {
} else if (tx.type == '0x2') {
let payload = RLP.encode([
BigInt(assertNotNull(tx.chainId, 'tx.chainId is missing')),
BigInt(tx.nonce),
BigInt(assertNotNull(tx.nonce, 'tx.nonce is missing')),
BigInt(assertNotNull(tx.maxPriorityFeePerGas, 'tx.maxPriorityFeePerGas is missing')),
BigInt(assertNotNull(tx.maxFeePerGas, 'tx.maxFeePerGas is missing')),
BigInt(tx.gas),
Expand All @@ -215,7 +215,7 @@ function encodeTransaction(tx: Transaction): Buffer {
// https://eips.ethereum.org/EIPS/eip-4844
let payload = RLP.encode([
BigInt(assertNotNull(tx.chainId, 'tx.chainId is missing')),
BigInt(tx.nonce),
BigInt(assertNotNull(tx.nonce, 'tx.nonce is missing')),
BigInt(assertNotNull(tx.maxPriorityFeePerGas, 'tx.maxPriorityFeePerGas is missing')),
BigInt(assertNotNull(tx.maxFeePerGas, 'tx.maxFeePerGas is missing')),
BigInt(tx.gas),
Expand All @@ -234,7 +234,7 @@ function encodeTransaction(tx: Transaction): Buffer {
// https://eips.ethereum.org/EIPS/eip-7702
let payload = RLP.encode([
BigInt(assertNotNull(tx.chainId, 'tx.chainId is missing')),
BigInt(tx.nonce),
BigInt(assertNotNull(tx.nonce, 'tx.nonce is missing')),
BigInt(assertNotNull(tx.maxPriorityFeePerGas, 'tx.maxPriorityFeePerGas is missing')),
BigInt(assertNotNull(tx.maxFeePerGas, 'tx.maxFeePerGas is missing')),
BigInt(tx.gas),
Expand Down Expand Up @@ -263,7 +263,7 @@ function encodeTransaction(tx: Transaction): Buffer {
let payload = RLP.encode([
BigInt(assertNotNull(tx.chainId, 'tx.chainId is missing')),
decodeHex(tx.from),
BigInt(tx.nonce),
BigInt(assertNotNull(tx.nonce, 'tx.nonce is missing')),
BigInt(tx.gasPrice ?? 0),
BigInt(tx.gas),
tx.to ? decodeHex(tx.to) : Buffer.alloc(0),
Expand All @@ -288,7 +288,7 @@ function encodeTransaction(tx: Transaction): Buffer {
// https://github.com/OffchainLabs/go-ethereum/blob/7503143fd13f73e46a966ea2c42a058af96f7fcf/core/types/arb_types.go#L161
let payload = RLP.encode([
BigInt(assertNotNull(tx.chainId, 'tx.chainId is missing')),
BigInt(tx.nonce),
BigInt(assertNotNull(tx.nonce, 'tx.nonce is missing')),
decodeHex(tx.from),
BigInt(tx.gasPrice ?? 0),
BigInt(tx.gas),
Expand Down Expand Up @@ -331,7 +331,7 @@ function encodeTransaction(tx: Transaction): Buffer {
// EIP-1559 base fields + nonceKey and timeoutTimestamp appended after the signature
let payload = RLP.encode([
BigInt(assertNotNull(tx.chainId, 'tx.chainId is missing')),
BigInt(tx.nonce),
BigInt(assertNotNull(tx.nonce, 'tx.nonce is missing')),
BigInt(assertNotNull(tx.maxPriorityFeePerGas, 'tx.maxPriorityFeePerGas is missing')),
BigInt(assertNotNull(tx.maxFeePerGas, 'tx.maxFeePerGas is missing')),
BigInt(tx.gas),
Expand Down Expand Up @@ -584,7 +584,7 @@ function encodeTempoTransactionFields(tx: Transaction): any[] {
assertNotNull(tx.calls, 'tx.calls is missing for 0x76 tx').map(encodeTempoCall),
decodeAccessList(tx.accessList ?? []),
BigInt(assertNotNull(tx.nonceKey, 'tx.nonceKey is missing for 0x76 tx')),
BigInt(tx.nonce),
BigInt(assertNotNull(tx.nonce, 'tx.nonce is missing')),
// valid_before: u64 when present, 0x80 (empty string) when null
tx.validBefore != null ? BigInt(tx.validBefore) : Buffer.alloc(0),
// valid_after: u64 when present, 0x80 (empty string) when null
Expand Down Expand Up @@ -636,7 +636,7 @@ function encodeTempoTransactionFieldsForSigning(tx: Transaction): any[] {
assertNotNull(tx.calls, 'tx.calls is missing for 0x76 tx').map(encodeTempoCall),
decodeAccessList(tx.accessList ?? []),
BigInt(assertNotNull(tx.nonceKey, 'tx.nonceKey is missing for 0x76 tx')),
BigInt(tx.nonce),
BigInt(assertNotNull(tx.nonce, 'tx.nonce is missing')),
tx.validBefore != null ? BigInt(tx.validBefore) : Buffer.alloc(0),
tx.validAfter != null ? BigInt(tx.validAfter) : Buffer.alloc(0),
// fee_token: skipped when fee_payer_signature is present
Expand Down Expand Up @@ -803,7 +803,7 @@ export function isBloomSuperset(superset: string, subset: string): boolean {
function serializeTransaction(tx: Transaction): Uint8Array | undefined {
if (tx.type == '0x0') {
let fields = [
BigInt(tx.nonce),
BigInt(assertNotNull(tx.nonce, 'tx.nonce is missing')),
BigInt(tx.gasPrice ?? 0),
BigInt(tx.gas),
tx.to ? decodeHex(tx.to) : Buffer.alloc(0),
Expand All @@ -820,7 +820,7 @@ function serializeTransaction(tx: Transaction): Uint8Array | undefined {
} else if (tx.type == '0x1') {
let payload = RLP.encode([
BigInt(assertNotNull(tx.chainId, 'tx.chainId is missing')),
BigInt(tx.nonce),
BigInt(assertNotNull(tx.nonce, 'tx.nonce is missing')),
BigInt(tx.gasPrice ?? 0),
BigInt(tx.gas),
tx.to ? decodeHex(tx.to) : Buffer.alloc(0),
Expand All @@ -832,7 +832,7 @@ function serializeTransaction(tx: Transaction): Uint8Array | undefined {
} else if (tx.type == '0x2') {
let payload = RLP.encode([
BigInt(assertNotNull(tx.chainId, 'tx.chainId is missing')),
BigInt(tx.nonce),
BigInt(assertNotNull(tx.nonce, 'tx.nonce is missing')),
BigInt(assertNotNull(tx.maxPriorityFeePerGas, 'tx.maxPriorityFeePerGas is missing')),
BigInt(assertNotNull(tx.maxFeePerGas, 'tx.maxFeePerGas is missing')),
BigInt(tx.gas),
Expand All @@ -845,7 +845,7 @@ function serializeTransaction(tx: Transaction): Uint8Array | undefined {
} else if (tx.type == '0x3') {
let payload = RLP.encode([
BigInt(assertNotNull(tx.chainId, 'tx.chainId is missing')),
BigInt(tx.nonce),
BigInt(assertNotNull(tx.nonce, 'tx.nonce is missing')),
BigInt(assertNotNull(tx.maxPriorityFeePerGas, 'tx.maxPriorityFeePerGas is missing')),
BigInt(assertNotNull(tx.maxFeePerGas, 'tx.maxFeePerGas is missing')),
BigInt(tx.gas),
Expand All @@ -860,7 +860,7 @@ function serializeTransaction(tx: Transaction): Uint8Array | undefined {
} else if (tx.type == '0x4') {
let payload = RLP.encode([
BigInt(assertNotNull(tx.chainId, 'tx.chainId is missing')),
BigInt(tx.nonce),
BigInt(assertNotNull(tx.nonce, 'tx.nonce is missing')),
BigInt(assertNotNull(tx.maxPriorityFeePerGas, 'tx.maxPriorityFeePerGas is missing')),
BigInt(assertNotNull(tx.maxFeePerGas, 'tx.maxFeePerGas is missing')),
BigInt(tx.gas),
Expand Down Expand Up @@ -893,7 +893,7 @@ function serializeTransaction(tx: Transaction): Uint8Array | undefined {
// Stable v1.4.0 custom transaction type — signing payload (no signature)
let payload = RLP.encode([
BigInt(assertNotNull(tx.chainId, 'tx.chainId is missing')),
BigInt(tx.nonce),
BigInt(assertNotNull(tx.nonce, 'tx.nonce is missing')),
BigInt(assertNotNull(tx.maxPriorityFeePerGas, 'tx.maxPriorityFeePerGas is missing')),
BigInt(assertNotNull(tx.maxFeePerGas, 'tx.maxFeePerGas is missing')),
BigInt(tx.gas),
Expand Down
Loading