Skip to content

Commit 8986f63

Browse files
committed
Tempo network support
1 parent f08b45e commit 8986f63

File tree

5 files changed

+714
-36
lines changed

5 files changed

+714
-36
lines changed

evm/evm-normalization/src/data.ts

Lines changed: 76 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,11 @@ export interface BlockHeader {
3636
excessBlobGas?: Qty,
3737
parentBeaconBlockRoot?: Bytes32,
3838
requestsHash?: Bytes32,
39-
l1BlockNumber?: number
39+
l1BlockNumber?: number,
40+
// Tempo-specific block header fields
41+
mainBlockGeneralGasLimit?: Qty,
42+
sharedGasLimit?: Qty,
43+
timestampMillisPart?: Qty,
4044
}
4145

4246

@@ -56,14 +60,75 @@ export interface EIP7702Authorization {
5660
}
5761

5862

63+
export interface TempoCall {
64+
to?: Bytes20,
65+
value: Qty,
66+
input: Bytes
67+
}
68+
69+
70+
export interface TempoSecp256k1Signature {
71+
type: 'secp256k1',
72+
r: Bytes,
73+
s: Bytes,
74+
yParity?: number,
75+
v?: number,
76+
}
77+
78+
79+
export interface TempoP256Signature {
80+
type: 'p256',
81+
r: Bytes,
82+
s: Bytes,
83+
pubKeyX: Bytes,
84+
pubKeyY: Bytes,
85+
preHash: boolean,
86+
}
87+
88+
89+
export interface TempoWebAuthnSignature {
90+
type: 'webAuthn',
91+
r: Bytes,
92+
s: Bytes,
93+
pubKeyX: Bytes,
94+
pubKeyY: Bytes,
95+
webauthnData: Bytes,
96+
}
97+
98+
99+
export type TempoPrimitiveSignature =
100+
| TempoSecp256k1Signature
101+
| TempoP256Signature
102+
| TempoWebAuthnSignature
103+
104+
105+
export interface TempoKeychainSignature {
106+
userAddress: Bytes20,
107+
signature: TempoPrimitiveSignature,
108+
version?: string
109+
}
110+
111+
112+
export type TempoSignature = TempoPrimitiveSignature | TempoKeychainSignature
113+
114+
115+
export interface TempoFeePayerSignature {
116+
v: number,
117+
r: Bytes,
118+
s: Bytes
119+
}
120+
121+
59122
export interface Transaction {
60123
transactionIndex: number,
61124
hash: Bytes32,
62125
nonce: number,
63126
from: Bytes20,
64127
to?: Bytes20,
65-
input: Bytes,
66-
value: Qty,
128+
// Optional for Tempo 0x76 transactions which use batched `calls` instead of `input`
129+
input?: Bytes,
130+
// Optional for Tempo 0x76 transactions which use batched `calls`
131+
value?: Qty,
67132
type?: number,
68133
gas: Qty,
69134
gasPrice?: Qty,
@@ -78,6 +143,14 @@ export interface Transaction {
78143
maxFeePerBlobGas?: Qty,
79144
blobVersionedHashes?: Bytes32[],
80145
authorizationList?: EIP7702Authorization[],
146+
// Tempo 0x76 transaction fields
147+
calls?: TempoCall[],
148+
nonceKey?: Bytes,
149+
signature?: TempoSignature,
150+
feeToken?: Bytes20,
151+
feePayerSignature?: TempoFeePayerSignature,
152+
validBefore?: Qty,
153+
validAfter?: Qty,
81154
// transaction receipt
82155
contractAddress?: Bytes20,
83156
cumulativeGasUsed?: Qty,

evm/evm-normalization/src/mapping.ts

Lines changed: 83 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ import {
88
Transaction,
99
AccessListItem,
1010
EIP7702Authorization,
11+
TempoCall,
12+
TempoPrimitiveSignature,
13+
TempoKeychainSignature,
14+
TempoSignature,
15+
TempoFeePayerSignature,
1116
Log,
1217
Trace,
1318
TraceCreateAction,
@@ -407,15 +412,80 @@ function mapEIP7702Authorization(src: rpc.EIP7702Authorization): EIP7702Authoriz
407412
}
408413

409414

415+
function mapTempoCall(src: rpc.TempoCall): TempoCall {
416+
return {
417+
to: src.to ? src.to.toLowerCase() : undefined,
418+
value: src.value,
419+
input: src.input
420+
}
421+
}
422+
423+
424+
function mapTempoPrimitiveSignature(src: rpc.TempoPrimitiveSignature): TempoPrimitiveSignature {
425+
switch (src.type) {
426+
case 'secp256k1':
427+
return {
428+
type: 'secp256k1',
429+
r: src.r,
430+
s: src.s,
431+
yParity: src.yParity != null ? qty2Int(src.yParity) : undefined,
432+
v: src.v != null ? qty2Int(src.v) : undefined,
433+
}
434+
case 'p256':
435+
return {
436+
type: 'p256',
437+
r: src.r,
438+
s: src.s,
439+
pubKeyX: src.pubKeyX,
440+
pubKeyY: src.pubKeyY,
441+
preHash: src.preHash,
442+
}
443+
case 'webAuthn':
444+
return {
445+
type: 'webAuthn',
446+
r: src.r,
447+
s: src.s,
448+
pubKeyX: src.pubKeyX,
449+
pubKeyY: src.pubKeyY,
450+
webauthnData: src.webauthnData,
451+
}
452+
default:
453+
throw unexpectedCase((src as any).type)
454+
}
455+
}
456+
457+
458+
function mapTempoSignature(src: rpc.TempoSignatureObject): TempoSignature {
459+
if ('userAddress' in src) {
460+
let result: TempoKeychainSignature = {
461+
userAddress: src.userAddress.toLowerCase(),
462+
signature: mapTempoPrimitiveSignature(src.signature)
463+
}
464+
if (src.version != null) result.version = src.version
465+
return result
466+
}
467+
return mapTempoPrimitiveSignature(src as rpc.TempoPrimitiveSignature)
468+
}
469+
470+
471+
function mapTempoFeePayerSignature(src: {v: string, r: string, s: string}): TempoFeePayerSignature {
472+
return {
473+
v: qty2Int(src.v),
474+
r: src.r,
475+
s: src.s
476+
}
477+
}
478+
479+
410480
function mapTransaction(src: rpc.Transaction, receipt?: rpc.Receipt): Transaction {
411481
return {
412482
transactionIndex: qty2Int(src.transactionIndex),
413483
hash: src.hash,
414484
nonce: qty2Int(src.nonce),
415485
from: src.from.toLowerCase(),
416486
to: src.to ? src.to.toLowerCase() : undefined,
417-
input: src.input,
418-
value: src.value,
487+
input: src.input ?? undefined,
488+
value: src.value ?? undefined,
419489
type: src.type ? qty2Int(src.type) : undefined,
420490
gas: src.gas,
421491
gasPrice: src.gasPrice ?? undefined,
@@ -430,6 +500,13 @@ function mapTransaction(src: rpc.Transaction, receipt?: rpc.Receipt): Transactio
430500
maxFeePerBlobGas: src.maxFeePerBlobGas ?? undefined,
431501
blobVersionedHashes: src.blobVersionedHashes ?? undefined,
432502
authorizationList: src.authorizationList?.map(mapEIP7702Authorization),
503+
calls: src.calls?.map(mapTempoCall),
504+
nonceKey: src.nonceKey ?? undefined,
505+
signature: src.signature ? mapTempoSignature(src.signature) : undefined,
506+
feeToken: src.feeToken ? src.feeToken.toLowerCase() : undefined,
507+
feePayerSignature: src.feePayerSignature ? mapTempoFeePayerSignature(src.feePayerSignature) : undefined,
508+
validBefore: src.validBefore ?? undefined,
509+
validAfter: src.validAfter ?? undefined,
433510
contractAddress: receipt?.contractAddress ? receipt?.contractAddress.toLowerCase() : undefined,
434511
cumulativeGasUsed: receipt?.cumulativeGasUsed,
435512
effectiveGasPrice: receipt?.effectiveGasPrice ?? undefined,
@@ -474,7 +551,10 @@ function mapBlockHeader(src: rpc.GetBlock): BlockHeader {
474551
excessBlobGas: src.excessBlobGas ?? undefined,
475552
parentBeaconBlockRoot: src.parentBeaconBlockRoot ?? undefined,
476553
requestsHash: src.requestsHash ?? undefined,
477-
l1BlockNumber: src.l1BlockNumber ? qty2Int(src.l1BlockNumber) : undefined
554+
l1BlockNumber: src.l1BlockNumber ? qty2Int(src.l1BlockNumber) : undefined,
555+
mainBlockGeneralGasLimit: src.mainBlockGeneralGasLimit ?? undefined,
556+
sharedGasLimit: src.sharedGasLimit ?? undefined,
557+
timestampMillisPart: src.timestampMillisPart ?? undefined,
478558
}
479559
}
480560

evm/evm-rpc/src/chain-utils.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {GetBlock, Log, Receipt, Transaction} from './rpc-data'
33
import {Qty} from './types'
44
import {
55
blockHash,
6+
tempoBlockHash,
67
logsBloom,
78
receiptsRoot,
89
recoverTxSender,
@@ -17,15 +18,25 @@ export class ChainUtils {
1718
public isHyperliquidMainnet: boolean
1819
public isHyperliquidTestnet: boolean
1920
public isStable: boolean
21+
public isTempo: boolean
2022

2123
constructor(chainId: Qty) {
2224
this.isPolygonMainnet = chainId == '0x89'
2325
this.isHyperliquidMainnet = chainId == '0x3e7'
2426
this.isHyperliquidTestnet = chainId == '0x3e6'
2527
this.isStable = chainId == '0x3dc' || chainId == '0x899' // Chain ID 988 (mainnet) or 2201 (testnet)
28+
// Tempo mainnet (4217), Moderato testnet (42431), Andantino testnet (42429)
29+
// https://drpc.org/chainlist/tempo-mainnet-rpc
30+
// https://drpc.org/chainlist/tempo-moderato-testnet-rpc
31+
// https://drpc.org/chainlist/tempo-testnet-rpc
32+
this.isTempo = chainId == '0x1079' || chainId == '0xa5bf' || chainId == '0xa5bd'
2633
}
2734

2835
calculateBlockHash(block: GetBlock) {
36+
// Tempo extends the Ethereum header with additional fields
37+
if (this.isTempo) {
38+
return tempoBlockHash(block)
39+
}
2940
return blockHash(block)
3041
}
3142

@@ -52,6 +63,18 @@ export class ChainUtils {
5263
transactions = transactions.filter(tx => !isHyperliquidSystemTx(tx))
5364
}
5465

66+
if (this.isTempo) {
67+
// Tempo 0x77 tx type appears on the deprecated Andantino testnet, but:
68+
// - is not documented anywhere,
69+
// - is absent from the latest (Moderato) testnet,
70+
// so skip verification for blocks containing 0x77 transactions
71+
for (let tx of transactions) {
72+
if (tx.type == '0x77') {
73+
return block.transactionsRoot
74+
}
75+
}
76+
}
77+
5578
return transactionsRoot(transactions)
5679
}
5780

@@ -101,6 +124,13 @@ export class ChainUtils {
101124
if (isHyperliquidSystemTx(transaction)) return
102125
}
103126

127+
// Tempo system transactions are legacy txs with a fake signature (r=0, s=0)
128+
// and sender set to Address::ZERO. They cannot be ECDSA-recovered.
129+
// https://github.com/tempoxyz/tempo/blob/main/crates/primitives/src/transaction/envelope.rs
130+
if (this.isTempo) {
131+
if (isTempoSystemTx(transaction)) return
132+
}
133+
104134
return recoverTxSender(transaction)
105135
}
106136
}
@@ -112,6 +142,13 @@ function isHyperliquidSystemTx(tx: Transaction) {
112142
}
113143

114144

145+
function isTempoSystemTx(tx: Transaction) {
146+
// Tempo system transactions are legacy (type 0x0) with a fake signature (r=0, s=0).
147+
// https://github.com/tempoxyz/tempo/blob/main/crates/primitives/src/transaction/envelope.rs
148+
return tx.type == '0x0' && tx.r == '0x0' && tx.s == '0x0'
149+
}
150+
151+
115152
function isHyperliquidSystemReceipt(receipt: Receipt) {
116153
// https://github.com/hl-archive-node/nanoreth/blob/732f8c574db2dde90344a29b0292189a5cddd2d1/src/addons/hl_node_compliance.rs#L365
117154
return receipt.cumulativeGasUsed == '0x0'

0 commit comments

Comments
 (0)