Skip to content

Commit 3ed89d6

Browse files
committed
bump to fulu and test with blob tx
1 parent 0d3e8ce commit 3ed89d6

2 files changed

Lines changed: 108 additions & 13 deletions

File tree

commands/test.ts

Lines changed: 107 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,13 @@
77
import { Command } from "jsr:@cliffy/command@^1.0.0-rc.7";
88
import {
99
type AddressLike,
10+
concat,
11+
decodeRlp,
12+
encodeRlp,
1013
getBytes,
14+
hexlify,
1115
keccak256,
16+
sha256,
1217
toUtf8String,
1318
Transaction,
1419
Wallet,
@@ -20,9 +25,99 @@ const STATIC_PREFUNDED = [
2025
"0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d",
2126
];
2227

28+
export type TxType = "legacy" | "blob";
29+
2330
const DEFAULT_VALUE_WEI = 100_000_000_000_000_000n; // 0.1 ETH
2431
const DEFAULT_GAS_LIMIT = 21000n;
2532
const DEFAULT_GAS_PRICE = 1_000_000_000n; // 1 gwei
33+
// EIP-1559/blob fee fields (blob tx is type 3, so it can't use gasPrice).
34+
const DEFAULT_PRIORITY_FEE = 1_000_000_000n; // 1 gwei
35+
const DEFAULT_MAX_FEE = 10_000_000_000n; // 10 gwei — covers devnet base fee
36+
const DEFAULT_MAX_BLOB_FEE = 1_000_000_000n; // 1 gwei — well above the 1 wei min
37+
38+
const BLOB_SIZE = 4096 * 32; // one EIP-4844 blob: 4096 field elements × 32 bytes
39+
40+
// kzg-wasm, loaded lazily so the legacy path never pays for the WASM init. Its
41+
// trusted setup is network-agnostic, so the bundled mainnet setup is correct on
42+
// any devnet. v1 exposes the EIP-7594 cell-proof ops required post-Osaka.
43+
type KzgApi = {
44+
blobToKZGCommitment: (blob: string) => string;
45+
computeCellsAndProofs: (blob: string) => [string[], string[]];
46+
};
47+
let kzgLib: Promise<KzgApi> | null = null;
48+
function getKzg(): Promise<KzgApi> {
49+
if (!kzgLib) {
50+
kzgLib = import("npm:kzg-wasm@^1.0.0").then((m) => m.loadKZG() as Promise<KzgApi>);
51+
}
52+
return kzgLib;
53+
}
54+
55+
const hx = (s: string): string => (s.startsWith("0x") ? s : `0x${s}`);
56+
57+
// A single valid blob: a short marker in the first field element (leading byte
58+
// left 0 so the element stays below the BLS modulus), the rest zero-filled.
59+
function makeBlob(marker: string): Uint8Array {
60+
const blob = new Uint8Array(BLOB_SIZE);
61+
blob.set(new TextEncoder().encode(marker).subarray(0, 31), 1);
62+
return blob;
63+
}
64+
65+
// versioned hash = 0x01 ‖ sha256(commitment)[1:] (VERSIONED_HASH_VERSION_KZG)
66+
function blobVersionedHash(commitment: string): string {
67+
const h = getBytes(sha256(hx(commitment)));
68+
h[0] = 0x01;
69+
return hexlify(h);
70+
}
71+
72+
// Build a post-Osaka (EIP-7594/PeerDAS) blob tx. ethers only serializes the
73+
// legacy EIP-4844 sidecar, so we let it produce the signed canonical tx
74+
// (0x03 ‖ rlp(body)) and re-wrap it as the cell-proof network form it can't
75+
// emit: 0x03 ‖ rlp([body, wrapper_version=1, blobs, commitments, cell_proofs])
76+
async function signBlobTx(wallet: Wallet, to: string, nonce: number, cid: bigint): Promise<string> {
77+
const kzg = await getKzg();
78+
const blob = hexlify(makeBlob("decker blob tx"));
79+
const commitment = hx(kzg.blobToKZGCommitment(blob));
80+
const cellProofs = kzg.computeCellsAndProofs(blob)[1].map(hx); // 128 proofs per blob
81+
82+
const tx = new Transaction();
83+
tx.type = 3;
84+
tx.to = to;
85+
tx.value = DEFAULT_VALUE_WEI;
86+
tx.nonce = nonce;
87+
tx.chainId = cid;
88+
tx.gasLimit = DEFAULT_GAS_LIMIT;
89+
tx.maxPriorityFeePerGas = DEFAULT_PRIORITY_FEE;
90+
tx.maxFeePerGas = DEFAULT_MAX_FEE;
91+
tx.maxFeePerBlobGas = DEFAULT_MAX_BLOB_FEE;
92+
tx.blobVersionedHashes = [blobVersionedHash(commitment)];
93+
// Sign the instance directly so we keep the canonical (sidecar-free) signed tx.
94+
tx.signature = wallet.signingKey.sign(tx.unsignedHash);
95+
96+
const body = decodeRlp(hx(tx.serialized.slice(4))); // strip 0x03, decode rlp(body)
97+
const wrapper = encodeRlp([body, "0x01", [blob], [commitment], cellProofs]);
98+
return concat(["0x03", wrapper]);
99+
}
100+
101+
async function signTx(
102+
type: TxType,
103+
wallet: Wallet,
104+
to: string,
105+
nonce: number,
106+
cid: bigint,
107+
): Promise<string> {
108+
if (type === "blob") return await signBlobTx(wallet, to, nonce, cid);
109+
const tx = Transaction.from({
110+
type: 0,
111+
to,
112+
value: DEFAULT_VALUE_WEI,
113+
gasLimit: DEFAULT_GAS_LIMIT,
114+
gasPrice: DEFAULT_GAS_PRICE,
115+
nonce,
116+
chainId: cid,
117+
data: "0x",
118+
});
119+
return await wallet.signTransaction(tx);
120+
}
26121

27122
function hhmmss(d: Date): string {
28123
const pad = (n: number) => String(n).padStart(2, "0");
@@ -102,6 +197,7 @@ export type TestOpts = {
102197
retries: number; // 0 = retry forever
103198
insecure: boolean;
104199
expectedExtraData?: string;
200+
type: TxType;
105201
};
106202

107203
export async function runTest(opts: TestOpts): Promise<number> {
@@ -120,19 +216,10 @@ export async function runTest(opts: TestOpts): Promise<number> {
120216
const nonce = await pendingNonce(el, wallet.address);
121217
console.log(`${dim("Nonce:")} ${accent(String(nonce))}`);
122218

123-
const tx = Transaction.from({
124-
type: 0,
125-
to: toAddress,
126-
value: DEFAULT_VALUE_WEI,
127-
gasLimit: DEFAULT_GAS_LIMIT,
128-
gasPrice: DEFAULT_GAS_PRICE,
129-
nonce,
130-
chainId: cid,
131-
data: "0x",
132-
});
133-
const signed = await wallet.signTransaction(tx);
219+
const signed = await signTx(opts.type, wallet, toAddress, nonce, cid);
134220

135-
console.log(`${dim("Sending transaction at")} ${accent(hhmmss(new Date()))}`);
221+
const label = opts.type === "blob" ? "blob transaction" : "transaction";
222+
console.log(`${dim(`Sending ${label} at`)} ${accent(hhmmss(new Date()))}`);
136223
const txHash = await sendRawTx(target, signed);
137224
console.log(`${dim("TX Hash:")} ${accent(txHash)}`);
138225

@@ -205,14 +292,22 @@ export const command = new Command()
205292
.option("--expected-extra-data <s:string>", "Verify block extra data matches this string", {
206293
default: "",
207294
})
295+
.option("--type <type:string>", "Transaction type to send: legacy or blob", {
296+
default: "legacy",
297+
})
208298
.action(async (opts) => {
299+
if (opts.type !== "legacy" && opts.type !== "blob") {
300+
console.error(err(`unknown --type ${JSON.stringify(opts.type)} (expected: legacy, blob)`));
301+
Deno.exit(2);
302+
}
209303
Deno.exit(await runTest({
210304
rpc: opts.rpc,
211305
elRpc: opts.elRpc,
212306
timeoutMs: parseDuration(opts.timeout),
213307
retries: opts.retries,
214308
insecure: opts.insecure,
215309
expectedExtraData: opts.expectedExtraData,
310+
type: opts.type,
216311
}));
217312
});
218313

recipes/rbuilder-reth2.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { relayWarmup } from "../scripts/relay-warmup.ts";
88
// (see containers/reth-rbuilder.ts and utils/binary-build.ts). The first run
99
// triggers a full cargo build; the binary is then cached under cache/bins/.
1010
export const recipe: Recipe = {
11-
artifacts: { generator: "l1", fork: "electra" },
11+
artifacts: { generator: "l1", fork: "fulu" },
1212
scripts: [
1313
relayWarmup({
1414
relays: [{ container: "mev-boost-relay-1" }],

0 commit comments

Comments
 (0)