Skip to content

Commit c211f43

Browse files
s1nacdetrio
andauthored
Add relayer for realistic test suites from client RPC data (#17)
* Add relayer for realistic test suites from client RPC data * python script to do eth_getProof queries and example result * Modify realistic relayer to accept output from python script * Fix realistic python script to get witnesses from prev block * Update getproof fixture for new block * Add rpc client in TS for realistic token relayer * Rm python realistic rpc * Use dataStart for typed array params to bignum methods * Fix formatting issues * ci: add token:realistic task * relayer: reuse sortAddrsByHash func in getTestsTxes * Fix linting errors * relayer: make getAccount private Co-authored-by: cdetrio <[email protected]>
1 parent b2c0175 commit c211f43

File tree

11 files changed

+1091
-39
lines changed

11 files changed

+1091
-39
lines changed

.circleci/config.yml

+3
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@
2727
- run:
2828
name: Run token ee with generated input
2929
command: npm run token
30+
- run:
31+
name: Run token with realistic rpc input
32+
command: npm run token:realistic
3033
- run:
3134
name: Run EVM ee with generated input
3235
command: npm run evm

assembly/main.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -140,19 +140,19 @@ export function processBlock(preStateRoot: Uint8Array, blockData: Uint8Array): U
140140
value = padBuf(value, 32)
141141
let fromBalance = padBuf(fromAccount[1], 32)
142142
let newFromBalance = new ArrayBuffer(32)
143-
bignum_sub256(fromBalance.buffer as usize, value.buffer as usize, newFromBalance as usize)
143+
bignum_sub256(fromBalance.dataStart, value.dataStart, newFromBalance as usize)
144144

145145
let toBalance = padBuf(toAccount[1], 32)
146146
let newToBalance = new ArrayBuffer(32)
147-
bignum_add256(toBalance.buffer as usize, value.buffer as usize, newToBalance as usize)
147+
bignum_add256(toBalance.dataStart, value.dataStart, newToBalance as usize)
148148

149149
let paddedNonce = padBuf(nonce, 32)
150150
let fromNonce = padBuf(fromAccount[0], 32)
151151
let newFromNonce = new ArrayBuffer(32)
152152
let one256 = new ArrayBuffer(32)
153153
let onedv = new DataView(one256)
154154
onedv.setUint8(31, 1)
155-
bignum_add256(fromNonce.buffer as usize, one256 as usize, newFromNonce as usize)
155+
bignum_add256(fromNonce.dataStart, one256 as usize, newFromNonce as usize)
156156

157157
// Encode updated accounts
158158
let newFromAccount = encodeAccount(

assembly/token.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -134,19 +134,19 @@ export function processBlock(preStateRoot: Uint8Array, blockData: Uint8Array): U
134134
value = padBuf(value, 32)
135135
let fromBalance = padBuf(fromAccount[1], 32)
136136
let newFromBalance = new ArrayBuffer(32)
137-
bignum_sub256(fromBalance.buffer as usize, value.buffer as usize, newFromBalance as usize)
137+
bignum_sub256(fromBalance.dataStart, value.dataStart, newFromBalance as usize)
138138

139139
let toBalance = padBuf(toAccount[1], 32)
140140
let newToBalance = new ArrayBuffer(32)
141-
bignum_add256(toBalance.buffer as usize, value.buffer as usize, newToBalance as usize)
141+
bignum_add256(toBalance.dataStart, value.dataStart, newToBalance as usize)
142142

143143
let paddedNonce = padBuf(nonce, 32)
144144
let fromNonce = padBuf(fromAccount[0], 32)
145145
let newFromNonce = new ArrayBuffer(32)
146146
let one256 = new ArrayBuffer(32)
147147
let onedv = new DataView(one256)
148148
onedv.setUint8(31, 1)
149-
bignum_add256(fromNonce.buffer as usize, one256 as usize, newFromNonce as usize)
149+
bignum_add256(fromNonce.dataStart, one256 as usize, newFromNonce as usize)
150150

151151
// Encode updated accounts
152152
let newFromAccount = encodeAccount(

package.json

+4
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
"token:relayer": "ts-node src/relayer/bin.ts",
99
"token:build": "gulp token",
1010
"token:run": "scout.ts turbo-token.yaml",
11+
"token:relayer:rpc": "ts-node src/relayer/rpc.ts test/fixture/eth_getproof_result.json",
12+
"token:relayer:realistic": "ts-node src/relayer/bin.ts --realistic test/fixture/eth_getproof_result.json",
13+
"token:realistic": "npm run token:relayer:realistic && npm run token:build && scout.ts turbo-token-realistic.yaml",
1114
"evm": "npm run evm:relayer && npm run evm:build && npm run evm:run",
1215
"evm:relayer": "ts-node src/relayer/bin.ts --basicEvm",
1316
"evm:build": "gulp evm",
@@ -48,6 +51,7 @@
4851
"wabt": "^1.0.11"
4952
},
5053
"dependencies": {
54+
"axios": "^0.19.0",
5155
"bn.js": "^5.0.0",
5256
"ethereumjs-account": "^3.0.0",
5357
"ethereumjs-testing": "git+https://github.com/ethereumjs/ethereumjs-testing.git#v1.2.7",

src/relayer/bin.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
// tslint:disable:no-console
22
import { generateTestSuite, TestSuite, stateTestRunner, RunnerArgs, TestGetterArgs } from './lib'
33
import { basicEvmTestSuite } from './basic-evm'
4+
import { generateRealisticTestSuite } from './realistic'
45
const fs = require('fs')
56
const yaml = require('js-yaml')
67
const testing = require('ethereumjs-testing')
78

89
async function main() {
910
const args = process.argv
1011

11-
if (args.length === 4 && args[2] == '--stateTest') {
12+
if (args.length === 4 && args[2] === '--stateTest') {
1213
const testCase = args[3]
1314
const testGetterArgs: TestGetterArgs = { test: testCase }
1415
const runnerArgs: RunnerArgs = {
@@ -38,6 +39,10 @@ async function main() {
3839
.catch((err: any) => {
3940
console.log('Err: ', err)
4041
})
42+
} else if (args.length === 4 && args[2] === '--realistic') {
43+
const rpcData = JSON.parse(fs.readFileSync(process.argv[3]))
44+
const testSuite = await generateRealisticTestSuite(rpcData)
45+
writeScoutConfig(testSuite, 'turbo-token-realistic.yaml', 'build/token_with_keccak.wasm')
4146
} else if (args.length === 3 && args[2] === '--basicEvm') {
4247
const testSuite = await basicEvmTestSuite()
4348
writeScoutConfig(testSuite, 'basic-evm.yaml', 'build/evm_with_keccak.wasm')

src/relayer/lib.ts

+25-32
Original file line numberDiff line numberDiff line change
@@ -120,21 +120,7 @@ async function generateTxes(trie: any, accounts: AccountInfo[], count = 50) {
120120
const unsortedAddrs = Object.keys(toProve).map(s => Buffer.from(s, 'hex'))
121121
const keys = unsortedAddrs.map(a => keccak256(a))
122122
keys.sort(Buffer.compare)
123-
// Sort addresses based on their hashes.
124-
// Naive algorithm
125-
const sortedAddrs = new Array(keys.length).fill(undefined)
126-
for (const a of unsortedAddrs) {
127-
let idx = -1
128-
const h = keccak256(a)
129-
for (let i = 0; i < keys.length; i++) {
130-
const k = keys[i]
131-
if (h.equals(k)) {
132-
idx = i
133-
}
134-
}
135-
assert(idx >= 0)
136-
sortedAddrs[idx] = a
137-
}
123+
const sortedAddrs = sortAddrsByHash(unsortedAddrs)
138124

139125
const proof = await makeMultiproof(trie, keys)
140126
// Verify proof is valid
@@ -173,6 +159,29 @@ export async function transfer(trie: any, tx: SimulationData) {
173159
await putAccount(trie, to, toAcc)
174160
}
175161

162+
// Sort addresses based on their hashes.
163+
// Naive algorithm
164+
export function sortAddrsByHash(addrs: Buffer[]): Buffer[] {
165+
const keys = addrs.map((a: Buffer) => keccak256(a))
166+
keys.sort(Buffer.compare)
167+
const sortedAddrs = new Array(keys.length).fill(undefined)
168+
169+
for (const a of addrs) {
170+
let idx = -1
171+
const h = keccak256(a)
172+
for (let i = 0; i < keys.length; i++) {
173+
const k = keys[i]
174+
if (h.equals(k)) {
175+
idx = i
176+
}
177+
}
178+
assert(idx >= 0)
179+
sortedAddrs[idx] = a
180+
}
181+
182+
return sortedAddrs
183+
}
184+
176185
export async function generateAccounts(trie: any, count = 500): Promise<AccountInfo[]> {
177186
const accounts = []
178187
for (let i = 0; i < count; i++) {
@@ -260,23 +269,7 @@ export async function getTestsTxes(trie: any, accounts: AccountInfo[], test: any
260269
const unsortedAddrs = Object.keys(toProve).map(s => Buffer.from(s, 'hex'))
261270
const keys = unsortedAddrs.map(a => keccak256(a))
262271
keys.sort(Buffer.compare)
263-
264-
// Sort addresses based on their hashes.
265-
// Naive algorithm
266-
const sortedAddrs = new Array(keys.length).fill(undefined)
267-
for (const a of unsortedAddrs) {
268-
let idx = -1
269-
const h = keccak256(a)
270-
271-
for (let i = 0; i < keys.length; i++) {
272-
const k = keys[i]
273-
if (h.equals(k)) {
274-
idx = i
275-
}
276-
}
277-
assert(idx >= 0)
278-
sortedAddrs[idx] = a
279-
}
272+
const sortedAddrs = sortAddrsByHash(unsortedAddrs)
280273

281274
const proof = await makeMultiproof(trie, keys)
282275

src/relayer/realistic.ts

+125
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import BN = require('bn.js')
2+
import { keccak256, stripZeros } from 'ethereumjs-util'
3+
import { encode } from 'rlp'
4+
import { Multiproof, makeMultiproof } from '../multiproof'
5+
import { TestSuite, sortAddrsByHash, rawMultiproof, SimulationData, transfer } from './lib'
6+
const { promisify } = require('util')
7+
const Trie = require('merkle-patricia-tree/secure')
8+
9+
export interface AccountData {
10+
address: Buffer
11+
accountProof: Buffer[]
12+
nonce: BN
13+
balance: BN
14+
codeHash: Buffer
15+
storageHash: Buffer
16+
storageProof: Buffer[]
17+
}
18+
19+
export interface TransactionData {
20+
to: Buffer
21+
value: BN
22+
nonce: BN
23+
from: Buffer
24+
}
25+
26+
export async function generateRealisticTestSuite(data: any): Promise<TestSuite> {
27+
const trie = new Trie()
28+
29+
const accData = []
30+
const addrs = []
31+
for (const acc of data.accounts) {
32+
accData.push(accountDataFromJSON(acc.result))
33+
addrs.push(accData[accData.length - 1].address)
34+
}
35+
36+
const multiproof = await turboproofFromAccountData(trie, accData)
37+
const preStateRoot = trie.root
38+
39+
const sortedAddrs = sortAddrsByHash(addrs)
40+
const txes = []
41+
const simulationData = []
42+
for (const rawTx of data.transactions) {
43+
const rawTxData = rawTx.result ? rawTx.result : rawTx
44+
const txData = transactionDataFromJSON(rawTxData)
45+
simulationData.push({
46+
to: txData.to,
47+
value: txData.value,
48+
nonce: txData.nonce,
49+
from: txData.from,
50+
})
51+
const toIdx = sortedAddrs.findIndex((a: Buffer) => a.equals(txData.to))
52+
const fromIdx = sortedAddrs.findIndex((a: Buffer) => a.equals(txData.from))
53+
if (toIdx === -1 || fromIdx === -1) {
54+
throw new Error('Invalid transaction sender/recipient')
55+
}
56+
txes.push([
57+
toIdx,
58+
stripZeros(txData.value.toBuffer('be', 32)),
59+
stripZeros(txData.nonce.toBuffer('be', 32)),
60+
fromIdx,
61+
])
62+
}
63+
64+
const blockData = encode([txes, sortedAddrs, ...rawMultiproof(multiproof, true)])
65+
66+
// Apply txes on top of trie to compute post state root
67+
for (const tx of simulationData as SimulationData[]) {
68+
await transfer(trie, tx)
69+
}
70+
71+
return {
72+
preStateRoot,
73+
blockData,
74+
postStateRoot: trie.root,
75+
}
76+
}
77+
78+
export async function turboproofFromAccountData(
79+
trie: any,
80+
data: AccountData[],
81+
): Promise<Multiproof> {
82+
const putRaw = promisify(trie._putRaw.bind(trie))
83+
const addrs = []
84+
const preStateRoot = keccak256(data[0].accountProof[0])
85+
trie.root = preStateRoot
86+
for (const accountData of data) {
87+
addrs.push(accountData.address)
88+
for (const node of accountData.accountProof) {
89+
await putRaw(keccak256(node), node)
90+
}
91+
}
92+
const keys = addrs.map((a: Buffer) => keccak256(a))
93+
keys.sort(Buffer.compare)
94+
95+
return makeMultiproof(trie, keys)
96+
}
97+
98+
export function accountDataFromJSON(data: any): AccountData {
99+
return {
100+
address: toBuffer(data.address),
101+
accountProof: data.accountProof.map((n: string) => toBuffer(n)),
102+
nonce: toBN(data.nonce),
103+
balance: toBN(data.balance),
104+
codeHash: toBuffer(data.codeHash),
105+
storageHash: toBuffer(data.storageHash),
106+
storageProof: data.storageProof.map((n: string) => toBuffer(n)),
107+
}
108+
}
109+
110+
export function transactionDataFromJSON(data: any): TransactionData {
111+
return {
112+
to: toBuffer(data.to),
113+
value: toBN(data.value),
114+
nonce: toBN(data.nonce),
115+
from: toBuffer(data.from),
116+
}
117+
}
118+
119+
export function toBuffer(str: string): Buffer {
120+
return Buffer.from(str.slice(2), 'hex')
121+
}
122+
123+
function toBN(str: string): BN {
124+
return new BN(str.slice(2), 16)
125+
}

src/relayer/rpc.ts

+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import * as fs from 'fs'
2+
import axios from 'axios'
3+
4+
// Based on @cdetrio's python script:
5+
// https://github.com/ewasm/biturbo/blob/7dccdbcff4e01e3ed7bec2659a9a377a4703565d/test/fetch_realistic_rpc.py
6+
7+
const ENDPOINT = 'http://localhost:8545'
8+
const blockNumber = 9125141
9+
10+
async function getBlockByNumber(n: number): Promise<any> {
11+
const res = await axios.post(ENDPOINT, {
12+
method: 'eth_getBlockByNumber',
13+
params: [toHex(n), true],
14+
id: 1,
15+
})
16+
17+
return res.data
18+
}
19+
20+
async function getProof(n: number, addr: string): Promise<any> {
21+
const res = await axios.post(ENDPOINT, {
22+
method: 'eth_getProof',
23+
params: [addr, [], toHex(n)],
24+
id: 1,
25+
})
26+
27+
const data = res.data
28+
if (data.error) {
29+
throw new Error(`eth_getProof error (${data.error.code}): ${data.error.message}`)
30+
}
31+
32+
return data
33+
}
34+
35+
async function getBlockWitnesses(n: number): Promise<any> {
36+
const res = await getBlockByNumber(n)
37+
const block = res.result
38+
const txes = []
39+
const accounts: any = {}
40+
for (const tx of block.transactions) {
41+
// Skip create txes
42+
if (!tx.to) continue
43+
txes.push(tx)
44+
45+
if (accounts[tx.to] === undefined) {
46+
accounts[tx.to] = await getProof(n - 1, tx.to)
47+
}
48+
if (accounts[tx.from] === undefined) {
49+
accounts[tx.from] = await getProof(n - 1, tx.from)
50+
}
51+
}
52+
53+
return { transactions: txes, accounts: Object.values(accounts) }
54+
}
55+
56+
async function main() {
57+
const res = await getBlockWitnesses(blockNumber)
58+
let path = 'test/fixture/eth_getproof_result.json'
59+
if (process.argv.length === 3) {
60+
path = process.argv[2]
61+
}
62+
fs.writeFileSync(path, JSON.stringify(res, null, 2))
63+
}
64+
65+
function toHex(n: number): string {
66+
return '0x' + n.toString(16)
67+
}
68+
69+
main()
70+
.then()
71+
.catch(e => {
72+
throw new Error(e)
73+
})

test/fixture/eth_getProof_sample.json

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"preStateRoot": "0x9a68e09e35376855b0b37f82592d503768f6a305a91e4ab17cc9d7ee8880a966",
3+
"accounts": [
4+
{"jsonrpc":"2.0","id":1,"result":{"address":"0x85a43fe911f777f9238ac25c77125d51c280df85","accountProof":["0xf8f180a086fd41bb68a0f4a475e60980095758be85644ded4f299234ed297830513ff26d8080a01a697e814758281972fcd13bc9707dbcd2f195986b05463d7b78426508445a04a0b5d7a91be5ee273cce27e2ad9a160d2faadd5a6ba518d384019b68728a4f62f4a0c2c799b60a0cd6acd42c1015512872e86c186bcf196e85061e76842f3b7cf86080a02e0d86c3befd177f574a20ac63804532889077e955320c9361cd10b7cc6f580980a06301b39b2ea8a44df8b0356120db64b788e71f52e1d7a6309d0d2e5b86fee7cb8080a00441e31691969e770b8749c6d63814e01096a9606ede6699531be56c86b33e93808080","0xf8518080808080a0ed2fba131fadeadeb1082f565fff16ceb008f693056e3140204716c0739cf1e08080a0645793ede6fb93c6830662130970fd4a3812f50e0d0a1c1d491ad1e22a71f56f8080808080808080","0xf8518080a03e1ce0d57176a97c1d5aee9587d872d197068db6d766e3dd4544ded62ef4b5068080808080808080a0ceaa292ecbd4cc29c0e85120e376dfc52a903f79b275f2efc0c066b1811e62258080808080","0xf8889f39b523aa40ae4d154a1b8161938d550dda1caae5a2ef78cec049cfef2170cfb866f86402a0fffffffffffffffffffffffffffffffffffffffffffffffffd39750f44ebfff7a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421a0c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470"],"balance":"0xfffffffffffffffffffffffffffffffffffffffffffffffffd39750f44ebfff7","codeHash":"0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470","nonce":"0x2","storageHash":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421","storageProof":[]}},
5+
{"jsonrpc":"2.0","id":1,"result":{"address":"0xb3c02212ef4317e3dbae99d8368346220e9802ff","accountProof":["0xf8f180a086fd41bb68a0f4a475e60980095758be85644ded4f299234ed297830513ff26d8080a01a697e814758281972fcd13bc9707dbcd2f195986b05463d7b78426508445a04a0b5d7a91be5ee273cce27e2ad9a160d2faadd5a6ba518d384019b68728a4f62f4a0c2c799b60a0cd6acd42c1015512872e86c186bcf196e85061e76842f3b7cf86080a02e0d86c3befd177f574a20ac63804532889077e955320c9361cd10b7cc6f580980a06301b39b2ea8a44df8b0356120db64b788e71f52e1d7a6309d0d2e5b86fee7cb8080a00441e31691969e770b8749c6d63814e01096a9606ede6699531be56c86b33e93808080","0xf85180808080a0998f1a60662f16e1b7d5626752bea5ff7a672769512921d900ebd1758c504b278080a0f4e2c36a4945f95fac2dec5ddcc12a290c0bf7a13fbecba475ad3c78a6033097808080808080808080","0xf871a0201bdb62e37bf9425e0091e45881e017589840ff24c569f803f59cdfb815dfadb84ef84c8088016345785d8a0000a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421a0c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470"],"balance":"0x16345785d8a0000","codeHash":"0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470","nonce":"0x0","storageHash":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421","storageProof":[]}},
6+
{"jsonrpc":"2.0","id":1,"result":{"address":"0x49430da50f7955222c4cfeb009997409db3dbcd2","accountProof":["0xf8f180a086fd41bb68a0f4a475e60980095758be85644ded4f299234ed297830513ff26d8080a01a697e814758281972fcd13bc9707dbcd2f195986b05463d7b78426508445a04a0b5d7a91be5ee273cce27e2ad9a160d2faadd5a6ba518d384019b68728a4f62f4a0c2c799b60a0cd6acd42c1015512872e86c186bcf196e85061e76842f3b7cf86080a02e0d86c3befd177f574a20ac63804532889077e955320c9361cd10b7cc6f580980a06301b39b2ea8a44df8b0356120db64b788e71f52e1d7a6309d0d2e5b86fee7cb8080a00441e31691969e770b8749c6d63814e01096a9606ede6699531be56c86b33e93808080","0xf8518080808080a0ed2fba131fadeadeb1082f565fff16ceb008f693056e3140204716c0739cf1e08080a0645793ede6fb93c6830662130970fd4a3812f50e0d0a1c1d491ad1e22a71f56f8080808080808080","0xf8518080a03e1ce0d57176a97c1d5aee9587d872d197068db6d766e3dd4544ded62ef4b5068080808080808080a0ceaa292ecbd4cc29c0e85120e376dfc52a903f79b275f2efc0c066b1811e62258080808080","0xf8709f3dd96f46dd800a6ca7688da65bf9f97b429f8d24467228379f67690f889018b84ef84c8088016345785d8a0000a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421a0c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470"],"balance":"0x16345785d8a0000","codeHash":"0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470","nonce":"0x0","storageHash":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421","storageProof":[]}}
7+
],
8+
"transactions": [
9+
{"jsonrpc":"2.0","id":1,"result":{"blockHash":"0xf1add87570527cf773c478c1209434da65aff453571761563af1fa1455280915","blockNumber":"0x3","from":"0x85a43fe911f777f9238ac25c77125d51c280df85","gas":"0x5208","gasPrice":"0x1","hash":"0x0eb40191d2e6fa7dbaabeccb4ca703c982d6fc777553f7a5ccbc936c4ce94e7f","input":"0x","nonce":"0x2","to":"0xb3c02212ef4317e3dbae99d8368346220e9802ff","transactionIndex":"0x0","value":"0x2ea11e32ad50000","v":"0xa96","r":"0x2eb2c365014d918ee85ec095631ea1926b58417bbf0c2f29aed88ba08da00393","s":"0x41f9a91b497e727f1081b39d89ee25fc3ec19369879dd0e8a46f93a516550682"}},
10+
{"jsonrpc":"2.0","id":1,"result":{"blockHash":"0x9f1e28dbca4c23725b90b803a2dd791f6dd7376a6c60a73536fa22cf5ba0f78c","blockNumber":"0x4","from":"0x85a43fe911f777f9238ac25c77125d51c280df85","gas":"0x5208","gasPrice":"0x1","hash":"0x5985285c1f85af666aa841525d083a81a29abf1c7f670bd7ce2813424edb3891","input":"0x","nonce":"0x3","to":"0x49430da50f7955222c4cfeb009997409db3dbcd2","transactionIndex":"0x0","value":"0x30d98d59a960000","v":"0xa96","r":"0x6319bce4b5b325419c09b9d69b865ef860d091932efc87dd30723e50aa44b3c3","s":"0x7e77f07c91362e804614733a9c7552850027d9b3675457a0444e6b74fb0bde3"}}
11+
]
12+
}

0 commit comments

Comments
 (0)