Skip to content

Commit f71e426

Browse files
authored
Add toJSON() method for BAL (#4246)
* New toJSON() method for BAL * Spelling fix
1 parent d9bc56d commit f71e426

File tree

2 files changed

+125
-1
lines changed

2 files changed

+125
-1
lines changed

packages/util/src/bal.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,69 @@ export class BlockLevelAccessList {
454454
* Per EIP-7928: "if the account had a positive balance pre-transaction,
455455
* the balance change to zero MUST be recorded."
456456
*/
457+
/**
458+
* Converts the internal representation to the JSON format (BALJSONBlockAccessList).
459+
* Inverse of createBlockLevelAccessListFromJSON().
460+
*/
461+
public toJSON(): BALJSONBlockAccessList {
462+
const result: BALJSONBlockAccessList = []
463+
464+
for (const [address, access] of Object.entries(this.accesses)
465+
.sort(([a], [b]) => a.localeCompare(b))
466+
.filter(([addr]) => addr !== systemAddress)) {
467+
const storageChanges: BALJSONSlotChanges[] = (
468+
Object.entries(access.storageChanges) as [BALStorageKeyHex, BALRawStorageChange[]][]
469+
)
470+
.sort(([a], [b]) => a.localeCompare(b))
471+
.map(([slot, changes]) => ({
472+
slot,
473+
slotChanges: changes
474+
.sort((a, b) => a[0] - b[0])
475+
.map(([index, value]) => ({
476+
blockAccessIndex: indexToHex(index),
477+
postValue: padToEvenHex(bytesToHex(value)),
478+
})),
479+
}))
480+
481+
const storageReads: BALStorageKeyHex[] = Array.from(access.storageReads).sort((a, b) =>
482+
Number(
483+
(a === '0x' ? 0n : hexToBigInt(a as `0x${string}`)) -
484+
(b === '0x' ? 0n : hexToBigInt(b as `0x${string}`)),
485+
),
486+
)
487+
488+
const balanceChanges: BALJSONBalanceChange[] = Array.from(access.balanceChanges.entries())
489+
.sort(([a], [b]) => a - b)
490+
.map(([index, balance]) => ({
491+
blockAccessIndex: indexToHex(index),
492+
postBalance: balance,
493+
}))
494+
495+
const nonceChanges: BALJSONNonceChange[] = Array.from(access.nonceChanges.entries())
496+
.sort(([a], [b]) => a - b)
497+
.map(([index, nonce]) => ({
498+
blockAccessIndex: indexToHex(index),
499+
postNonce: nonce,
500+
}))
501+
502+
const codeChanges: BALJSONCodeChange[] = access.codeChanges.map(([index, code]) => ({
503+
blockAccessIndex: indexToHex(index),
504+
newCode: bytesToHex(code),
505+
}))
506+
507+
result.push({
508+
address: address as BALAddressHex,
509+
nonceChanges,
510+
balanceChanges,
511+
codeChanges,
512+
storageChanges,
513+
storageReads,
514+
})
515+
}
516+
517+
return result
518+
}
519+
457520
public cleanupSelfdestructed(addresses: Array<BALAddressHex>): void {
458521
for (const address of addresses) {
459522
const access = this.accesses[address]
@@ -641,5 +704,9 @@ function normalizeStorageKeyHex(hex: PrefixedHexString): BALStorageKeyHex {
641704
return `0x${padToEven(stripped)}` as BALStorageKeyHex
642705
}
643706

707+
function indexToHex(index: BALAccessIndexNumber): BALAccessIndexHex {
708+
return padToEvenHex(`0x${index.toString(16)}`) as BALAccessIndexHex
709+
}
710+
644711
// Address to ignore
645712
const systemAddress = '0xfffffffffffffffffffffffffffffffffffffffe'

packages/util/test/bal.spec.ts

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ import {
55
BlockLevelAccessList,
66
createBlockLevelAccessList,
77
createBlockLevelAccessListFromJSON,
8+
createBlockLevelAccessListFromRLP,
89
} from '../src/bal.ts'
9-
import { bytesToHex } from '../src/bytes.ts'
10+
import { bytesToHex, hexToBytes } from '../src/bytes.ts'
1011
import { KECCAK256_RLP_ARRAY_S } from '../src/constants.ts'
1112
import type { PrefixedHexString } from '../src/types.ts'
1213
import bal_all_transaction_types from './testdata/bal/bal_all_transaction_types.json' with {
@@ -124,4 +125,60 @@ describe('JSON', () => {
124125
assert.deepEqual(bytesToHex(bal.serialize()), balAllTransactionTypesRLP)
125126
assert.deepEqual(bytesToHex(bal.hash()), balAllTransactionTypesHash)
126127
})
128+
129+
it('toJSON() should produce correct JSON from Accesses data', () => {
130+
// bal_simple and bal_all_transaction_types use even-padded hex,
131+
// so toJSON() output should match the JSON test data directly.
132+
let bal = new BlockLevelAccessList(balSimple)
133+
assert.deepEqual(bal.toJSON(), bal_simple as BALJSONBlockAccessList)
134+
135+
bal = new BlockLevelAccessList(balAllTransactionTypes)
136+
assert.deepEqual(bal.toJSON(), bal_all_transaction_types as BALJSONBlockAccessList)
137+
})
138+
139+
it('toJSON() roundtrip: JSON -> internal -> toJSON()', () => {
140+
// For already-normalized JSON (even-padded hex), toJSON() output matches input.
141+
let bal = createBlockLevelAccessListFromJSON(bal_simple as BALJSONBlockAccessList)
142+
assert.deepEqual(bal.toJSON(), bal_simple as BALJSONBlockAccessList)
143+
144+
bal = createBlockLevelAccessListFromJSON(bal_all_transaction_types as BALJSONBlockAccessList)
145+
assert.deepEqual(bal.toJSON(), bal_all_transaction_types as BALJSONBlockAccessList)
146+
147+
// bal_empty_block_no_coinbase uses un-normalized hex (e.g. "0x0" vs "0x00"),
148+
// so direct JSON comparison won't match. Verify semantic roundtrip instead:
149+
// JSON -> internal -> toJSON() -> internal -> RLP/hash must still match.
150+
bal = createBlockLevelAccessListFromJSON(bal_empty_block_no_coinbase as BALJSONBlockAccessList)
151+
const roundtripJSON = bal.toJSON()
152+
const bal2 = createBlockLevelAccessListFromJSON(roundtripJSON)
153+
assert.deepEqual(bal2.accesses, balEmptyBlockNoCoinbase)
154+
assert.deepEqual(bytesToHex(bal2.serialize()), balEmptyBlockNoCoinbaseRLP)
155+
assert.deepEqual(bytesToHex(bal2.hash()), balEmptyBlockNoCoinbaseHash)
156+
})
157+
})
158+
159+
describe('RLP', () => {
160+
it('serialize() should produce correct RLP output', () => {
161+
let bal = new BlockLevelAccessList(balSimple)
162+
assert.deepEqual(bytesToHex(bal.serialize()), balSimpleRLP)
163+
164+
bal = new BlockLevelAccessList(balEmptyBlockNoCoinbase)
165+
assert.deepEqual(bytesToHex(bal.serialize()), balEmptyBlockNoCoinbaseRLP)
166+
167+
bal = new BlockLevelAccessList(balAllTransactionTypes)
168+
assert.deepEqual(bytesToHex(bal.serialize()), balAllTransactionTypesRLP)
169+
})
170+
171+
it('serialize() roundtrip: RLP -> internal -> serialize()', () => {
172+
let bal = createBlockLevelAccessListFromRLP(hexToBytes(balSimpleRLP))
173+
assert.deepEqual(bytesToHex(bal.serialize()), balSimpleRLP)
174+
assert.deepEqual(bytesToHex(bal.hash()), balSimpleHash)
175+
176+
bal = createBlockLevelAccessListFromRLP(hexToBytes(balEmptyBlockNoCoinbaseRLP))
177+
assert.deepEqual(bytesToHex(bal.serialize()), balEmptyBlockNoCoinbaseRLP)
178+
assert.deepEqual(bytesToHex(bal.hash()), balEmptyBlockNoCoinbaseHash)
179+
180+
bal = createBlockLevelAccessListFromRLP(hexToBytes(balAllTransactionTypesRLP))
181+
assert.deepEqual(bytesToHex(bal.serialize()), balAllTransactionTypesRLP)
182+
assert.deepEqual(bytesToHex(bal.hash()), balAllTransactionTypesHash)
183+
})
127184
})

0 commit comments

Comments
 (0)