Skip to content

Commit 8a24b56

Browse files
authored
cmd/evm: implement input txs via rlp in t8n tool (#23138)
In many cases, it's desireable to use already-signed transactions as input to the state transition, instead of having the evm sign them internally (for example to use malformed or not-yet-valid transactions). This PR adds support + docs for that feature.
1 parent 0658712 commit 8a24b56

File tree

8 files changed

+240
-36
lines changed

8 files changed

+240
-36
lines changed

cmd/evm/README.md

Lines changed: 93 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@ The `evm t8n` tool is a stateless state transition utility. It is a utility
44
which can
55

66
1. Take a prestate, including
7-
- Accounts,
8-
- Block context information,
9-
- Previous blockshashes (*optional)
7+
- Accounts,
8+
- Block context information,
9+
- Previous blockshashes (*optional)
1010
2. Apply a set of transactions,
1111
3. Apply a mining-reward (*optional),
1212
4. And generate a post-state, including
13-
- State root, transaction root, receipt root,
14-
- Information about rejected transactions,
15-
- Optionally: a full or partial post-state dump
13+
- State root, transaction root, receipt root,
14+
- Information about rejected transactions,
15+
- Optionally: a full or partial post-state dump
1616

1717
## Specification
1818

@@ -37,6 +37,8 @@ Command line params that has to be supported are
3737
--output.result result Determines where to put the result (stateroot, txroot etc) of the post-state.
3838
`stdout` - into the stdout output
3939
`stderr` - into the stderr output
40+
--output.body value If set, the RLP of the transactions (block body) will be written to this file.
41+
--input.txs stdin stdin or file name of where to find the transactions to apply. If the file prefix is '.rlp', then the data is interpreted as an RLP list of signed transactions.The '.rlp' format is identical to the output.body format. (default: "txs.json")
4042
--state.fork value Name of ruleset to use.
4143
--state.chainid value ChainID to use (default: 1)
4244
--state.reward value Mining reward. Set to -1 to disable (default: 0)
@@ -110,7 +112,10 @@ Two resulting files:
110112
}
111113
],
112114
"rejected": [
113-
1
115+
{
116+
"index": 1,
117+
"error": "nonce too low: address 0x8A8eAFb1cf62BfBeb1741769DAE1a9dd47996192, tx: 0 state: 1"
118+
}
114119
]
115120
}
116121
```
@@ -156,7 +161,10 @@ Output:
156161
}
157162
],
158163
"rejected": [
159-
1
164+
{
165+
"index": 1,
166+
"error": "nonce too low: address 0x8A8eAFb1cf62BfBeb1741769DAE1a9dd47996192, tx: 0 state: 1"
167+
}
160168
]
161169
}
162170
}
@@ -168,9 +176,9 @@ Mining rewards and ommer rewards might need to be added. This is how those are a
168176

169177
- `block_reward` is the block mining reward for the miner (`0xaa`), of a block at height `N`.
170178
- For each ommer (mined by `0xbb`), with blocknumber `N-delta`
171-
- (where `delta` is the difference between the current block and the ommer)
172-
- The account `0xbb` (ommer miner) is awarded `(8-delta)/ 8 * block_reward`
173-
- The account `0xaa` (block miner) is awarded `block_reward / 32`
179+
- (where `delta` is the difference between the current block and the ommer)
180+
- The account `0xbb` (ommer miner) is awarded `(8-delta)/ 8 * block_reward`
181+
- The account `0xaa` (block miner) is awarded `block_reward / 32`
174182

175183
To make `state_t8n` apply these, the following inputs are required:
176184

@@ -220,7 +228,7 @@ Output:
220228
### Future EIPS
221229

222230
It is also possible to experiment with future eips that are not yet defined in a hard fork.
223-
Example, putting EIP-1344 into Frontier:
231+
Example, putting EIP-1344 into Frontier:
224232
```
225233
./evm t8n --state.fork=Frontier+1344 --input.pre=./testdata/1/pre.json --input.txs=./testdata/1/txs.json --input.env=/testdata/1/env.json
226234
```
@@ -229,41 +237,102 @@ Example, putting EIP-1344 into Frontier:
229237

230238
The `BLOCKHASH` opcode requires blockhashes to be provided by the caller, inside the `env`.
231239
If a required blockhash is not provided, the exit code should be `4`:
232-
Example where blockhashes are provided:
240+
Example where blockhashes are provided:
233241
```
234-
./evm t8n --input.alloc=./testdata/3/alloc.json --input.txs=./testdata/3/txs.json --input.env=./testdata/3/env.json --trace
242+
./evm --verbosity=1 t8n --input.alloc=./testdata/3/alloc.json --input.txs=./testdata/3/txs.json --input.env=./testdata/3/env.json --trace
243+
INFO [07-27|11:53:40.960] Trie dumping started root=b7341d..857ea1
244+
INFO [07-27|11:53:40.960] Trie dumping complete accounts=3 elapsed="103.298µs"
245+
INFO [07-27|11:53:40.960] Wrote file file=alloc.json
246+
INFO [07-27|11:53:40.960] Wrote file file=result.json
247+
235248
```
249+
236250
```
237251
cat trace-0-0x72fadbef39cd251a437eea619cfeda752271a5faaaa2147df012e112159ffb81.jsonl | grep BLOCKHASH -C2
238252
```
239253
```
240-
{"pc":0,"op":96,"gas":"0x5f58ef8","gasCost":"0x3","memory":"0x","memSize":0,"stack":[],"returnStack":[],"returnData":"0x","depth":1,"refund":0,"opName":"PUSH1","error":""}
241-
{"pc":2,"op":64,"gas":"0x5f58ef5","gasCost":"0x14","memory":"0x","memSize":0,"stack":["0x1"],"returnStack":[],"returnData":"0x","depth":1,"refund":0,"opName":"BLOCKHASH","error":""}
242-
{"pc":3,"op":0,"gas":"0x5f58ee1","gasCost":"0x0","memory":"0x","memSize":0,"stack":["0xdac58aa524e50956d0c0bae7f3f8bb9d35381365d07804dd5b48a5a297c06af4"],"returnStack":[],"returnData":"0x","depth":1,"refund":0,"opName":"STOP","error":""}
243-
{"output":"","gasUsed":"0x17","time":142709}
254+
{"pc":0,"op":96,"gas":"0x5f58ef8","gasCost":"0x3","memory":"0x","memSize":0,"stack":[],"returnData":"0x","depth":1,"refund":0,"opName":"PUSH1","error":""}
255+
{"pc":2,"op":64,"gas":"0x5f58ef5","gasCost":"0x14","memory":"0x","memSize":0,"stack":["0x1"],"returnData":"0x","depth":1,"refund":0,"opName":"BLOCKHASH","error":""}
256+
{"pc":3,"op":0,"gas":"0x5f58ee1","gasCost":"0x0","memory":"0x","memSize":0,"stack":["0xdac58aa524e50956d0c0bae7f3f8bb9d35381365d07804dd5b48a5a297c06af4"],"returnData":"0x","depth":1,"refund":0,"opName":"STOP","error":""}
257+
{"output":"","gasUsed":"0x17","time":156276}
244258
```
245259

246260
In this example, the caller has not provided the required blockhash:
247261
```
248262
./evm t8n --input.alloc=./testdata/4/alloc.json --input.txs=./testdata/4/txs.json --input.env=./testdata/4/env.json --trace
249-
```
250-
```
251263
ERROR(4): getHash(3) invoked, blockhash for that block not provided
252264
```
253265
Error code: 4
266+
254267
### Chaining
255268

256269
Another thing that can be done, is to chain invocations:
257270
```
258271
./evm t8n --input.alloc=./testdata/1/alloc.json --input.txs=./testdata/1/txs.json --input.env=./testdata/1/env.json --output.alloc=stdout | ./evm t8n --input.alloc=stdin --input.env=./testdata/1/env.json --input.txs=./testdata/1/txs.json
259-
INFO [01-21|22:41:22.963] rejected tx index=1 hash=0557ba..18d673 from=0x8A8eAFb1cf62BfBeb1741769DAE1a9dd47996192 error="nonce too low: address 0x8A8eAFb1cf62BfBeb1741769DAE1a9dd47996192, tx: 0 state: 1"
260-
INFO [01-21|22:41:22.966] rejected tx index=0 hash=0557ba..18d673 from=0x8A8eAFb1cf62BfBeb1741769DAE1a9dd47996192 error="nonce too low: address 0x8A8eAFb1cf62BfBeb1741769DAE1a9dd47996192, tx: 0 state: 1"
261-
INFO [01-21|22:41:22.967] rejected tx index=1 hash=0557ba..18d673 from=0x8A8eAFb1cf62BfBeb1741769DAE1a9dd47996192 error="nonce too low: address 0x8A8eAFb1cf62BfBeb1741769DAE1a9dd47996192, tx: 0 state: 1"
272+
INFO [07-27|11:53:41.049] rejected tx index=1 hash=0557ba..18d673 from=0x8A8eAFb1cf62BfBeb1741769DAE1a9dd47996192 error="nonce too low: address 0x8A8eAFb1cf62BfBeb1741769DAE1a9dd47996192, tx: 0 state: 1"
273+
INFO [07-27|11:53:41.050] Trie dumping started root=84208a..ae4e13
274+
INFO [07-27|11:53:41.050] Trie dumping complete accounts=3 elapsed="59.412µs"
275+
INFO [07-27|11:53:41.050] Wrote file file=result.json
276+
INFO [07-27|11:53:41.051] rejected tx index=0 hash=0557ba..18d673 from=0x8A8eAFb1cf62BfBeb1741769DAE1a9dd47996192 error="nonce too low: address 0x8A8eAFb1cf62BfBeb1741769DAE1a9dd47996192, tx: 0 state: 1"
277+
INFO [07-27|11:53:41.051] rejected tx index=1 hash=0557ba..18d673 from=0x8A8eAFb1cf62BfBeb1741769DAE1a9dd47996192 error="nonce too low: address 0x8A8eAFb1cf62BfBeb1741769DAE1a9dd47996192, tx: 0 state: 1"
278+
INFO [07-27|11:53:41.052] Trie dumping started root=84208a..ae4e13
279+
INFO [07-27|11:53:41.052] Trie dumping complete accounts=3 elapsed="45.734µs"
280+
INFO [07-27|11:53:41.052] Wrote file file=alloc.json
281+
INFO [07-27|11:53:41.052] Wrote file file=result.json
262282
263283
```
264-
What happened here, is that we first applied two identical transactions, so the second one was rejected.
284+
What happened here, is that we first applied two identical transactions, so the second one was rejected.
265285
Then, taking the poststate alloc as the input for the next state, we tried again to include
266286
the same two transactions: this time, both failed due to too low nonce.
267287

268288
In order to meaningfully chain invocations, one would need to provide meaningful new `env`, otherwise the
269289
actual blocknumber (exposed to the EVM) would not increase.
290+
291+
### Transactions in RLP form
292+
293+
It is possible to provide already-signed transactions as input to, using an `input.txs` which ends with the `rlp` suffix.
294+
The input format for RLP-form transactions is _identical_ to the _output_ format for block bodies. Therefore, it's fully possible
295+
to use the evm to go from `json` input to `rlp` input.
296+
297+
The following command takes **json** the transactions in `./testdata/13/txs.json` and signs them. After execution, they are output to `signed_txs.rlp`.:
298+
```
299+
./evm t8n --state.fork=London --input.alloc=./testdata/13/alloc.json --input.txs=./testdata/13/txs.json --input.env=./testdata/13/env.json --output.result=alloc_jsontx.json --output.body=signed_txs.rlp
300+
INFO [07-27|11:53:41.124] Trie dumping started root=e4b924..6aef61
301+
INFO [07-27|11:53:41.124] Trie dumping complete accounts=3 elapsed="94.284µs"
302+
INFO [07-27|11:53:41.125] Wrote file file=alloc.json
303+
INFO [07-27|11:53:41.125] Wrote file file=alloc_jsontx.json
304+
INFO [07-27|11:53:41.125] Wrote file file=signed_txs.rlp
305+
306+
```
307+
308+
The `output.body` is the rlp-list of transactions, encoded in hex and placed in a string a'la `json` encoding rules:
309+
```
310+
cat signed_txs.rlp
311+
"0xf8d2b86702f864010180820fa08284d09411111111111111111111111111111111111111118080c001a0b7dfab36232379bb3d1497a4f91c1966b1f932eae3ade107bf5d723b9cb474e0a06261c359a10f2132f126d250485b90cf20f30340801244a08ef6142ab33d1904b86702f864010280820fa08284d09411111111111111111111111111111111111111118080c080a0d4ec563b6568cd42d998fc4134b36933c6568d01533b5adf08769270243c6c7fa072bf7c21eac6bbeae5143371eef26d5e279637f3bd73482b55979d76d935b1e9"
312+
```
313+
314+
We can use `rlpdump` to check what the contents are:
315+
```
316+
rlpdump -hex $(cat signed_txs.rlp | jq -r )
317+
[
318+
02f864010180820fa08284d09411111111111111111111111111111111111111118080c001a0b7dfab36232379bb3d1497a4f91c1966b1f932eae3ade107bf5d723b9cb474e0a06261c359a10f2132f126d250485b90cf20f30340801244a08ef6142ab33d1904,
319+
02f864010280820fa08284d09411111111111111111111111111111111111111118080c080a0d4ec563b6568cd42d998fc4134b36933c6568d01533b5adf08769270243c6c7fa072bf7c21eac6bbeae5143371eef26d5e279637f3bd73482b55979d76d935b1e9,
320+
]
321+
```
322+
Now, we can now use those (or any other already signed transactions), as input, like so:
323+
```
324+
./evm t8n --state.fork=London --input.alloc=./testdata/13/alloc.json --input.txs=./signed_txs.rlp --input.env=./testdata/13/env.json --output.result=alloc_rlptx.json
325+
INFO [07-27|11:53:41.253] Trie dumping started root=e4b924..6aef61
326+
INFO [07-27|11:53:41.253] Trie dumping complete accounts=3 elapsed="128.445µs"
327+
INFO [07-27|11:53:41.253] Wrote file file=alloc.json
328+
INFO [07-27|11:53:41.255] Wrote file file=alloc_rlptx.json
329+
330+
```
331+
332+
You might have noticed that the results from these two invocations were stored in two separate files.
333+
And we can now finally check that they match.
334+
```
335+
cat alloc_jsontx.json | jq .stateRoot && cat alloc_rlptx.json | jq .stateRoot
336+
"0xe4b924a6adb5959fccf769d5b7bb2f6359e26d1e76a2443c5a91a36d826aef61"
337+
"0xe4b924a6adb5959fccf769d5b7bb2f6359e26d1e76a2443c5a91a36d826aef61"
338+
```

cmd/evm/internal/t8ntool/flags.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,10 @@ var (
7979
Value: "env.json",
8080
}
8181
InputTxsFlag = cli.StringFlag{
82-
Name: "input.txs",
83-
Usage: "`stdin` or file name of where to find the transactions to apply.",
82+
Name: "input.txs",
83+
Usage: "`stdin` or file name of where to find the transactions to apply. " +
84+
"If the file prefix is '.rlp', then the data is interpreted as an RLP list of signed transactions." +
85+
"The '.rlp' format is identical to the output.body format.",
8486
Value: "txs.json",
8587
}
8688
RewardFlag = cli.Int64Flag{

cmd/evm/internal/t8ntool/transition.go

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"math/big"
2626
"os"
2727
"path"
28+
"strings"
2829

2930
"github.com/ethereum/go-ethereum/common"
3031
"github.com/ethereum/go-ethereum/common/hexutil"
@@ -72,6 +73,7 @@ type input struct {
7273
Alloc core.GenesisAlloc `json:"alloc,omitempty"`
7374
Env *stEnv `json:"env,omitempty"`
7475
Txs []*txWithKey `json:"txs,omitempty"`
76+
TxRlp string `json:"txsRlp,omitempty"`
7577
}
7678

7779
func Main(ctx *cli.Context) error {
@@ -199,11 +201,44 @@ func Main(ctx *cli.Context) error {
199201
}
200202
defer inFile.Close()
201203
decoder := json.NewDecoder(inFile)
202-
if err := decoder.Decode(&txsWithKeys); err != nil {
203-
return NewError(ErrorJson, fmt.Errorf("failed unmarshaling txs-file: %v", err))
204+
if strings.HasSuffix(txStr, ".rlp") {
205+
var body hexutil.Bytes
206+
if err := decoder.Decode(&body); err != nil {
207+
return err
208+
}
209+
var txs types.Transactions
210+
if err := rlp.DecodeBytes(body, &txs); err != nil {
211+
return err
212+
}
213+
for _, tx := range txs {
214+
txsWithKeys = append(txsWithKeys, &txWithKey{
215+
key: nil,
216+
tx: tx,
217+
})
218+
}
219+
} else {
220+
if err := decoder.Decode(&txsWithKeys); err != nil {
221+
return NewError(ErrorJson, fmt.Errorf("failed unmarshaling txs-file: %v", err))
222+
}
204223
}
205224
} else {
206-
txsWithKeys = inputData.Txs
225+
if len(inputData.TxRlp) > 0 {
226+
// Decode the body of already signed transactions
227+
body := common.FromHex(inputData.TxRlp)
228+
var txs types.Transactions
229+
if err := rlp.DecodeBytes(body, &txs); err != nil {
230+
return err
231+
}
232+
for _, tx := range txs {
233+
txsWithKeys = append(txsWithKeys, &txWithKey{
234+
key: nil,
235+
tx: tx,
236+
})
237+
}
238+
} else {
239+
// JSON encoded transactions
240+
txsWithKeys = inputData.Txs
241+
}
207242
}
208243
// We may have to sign the transactions.
209244
signer := types.MakeSigner(chainConfig, big.NewInt(int64(prestate.Env.Number)))
@@ -365,13 +400,15 @@ func dispatchOutput(ctx *cli.Context, baseDir string, result *ExecutionResult, a
365400
return NewError(ErrorJson, fmt.Errorf("failed marshalling output: %v", err))
366401
}
367402
os.Stdout.Write(b)
403+
os.Stdout.Write([]byte("\n"))
368404
}
369405
if len(stdErrObject) > 0 {
370406
b, err := json.MarshalIndent(stdErrObject, "", " ")
371407
if err != nil {
372408
return NewError(ErrorJson, fmt.Errorf("failed marshalling output: %v", err))
373409
}
374410
os.Stderr.Write(b)
411+
os.Stderr.Write([]byte("\n"))
375412
}
376413
return nil
377414
}

cmd/evm/testdata/13/alloc.json

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"0x1111111111111111111111111111111111111111" : {
3+
"balance" : "0x010000000000",
4+
"code" : "0xfe",
5+
"nonce" : "0x01",
6+
"storage" : {
7+
}
8+
},
9+
"0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b" : {
10+
"balance" : "0x010000000000",
11+
"code" : "0x",
12+
"nonce" : "0x01",
13+
"storage" : {
14+
}
15+
},
16+
"0xd02d72e067e77158444ef2020ff2d325f929b363" : {
17+
"balance" : "0x01000000000000",
18+
"code" : "0x",
19+
"nonce" : "0x01",
20+
"storage" : {
21+
}
22+
}
23+
}

cmd/evm/testdata/13/env.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"currentCoinbase" : "0x2adc25665018aa1fe0e6bc666dac8fc2697ff9ba",
3+
"currentDifficulty" : "0x020000",
4+
"currentNumber" : "0x01",
5+
"currentTimestamp" : "0x079e",
6+
"previousHash" : "0xcb23ee65a163121f640673b41788ee94633941405f95009999b502eedfbbfd4f",
7+
"currentGasLimit" : "0x40000000",
8+
"currentBaseFee" : "0x036b",
9+
"blockHashes" : {
10+
"0" : "0xcb23ee65a163121f640673b41788ee94633941405f95009999b502eedfbbfd4f"
11+
}
12+
}

cmd/evm/testdata/13/readme.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
## Input transactions in RLP form
2+
3+
This testdata folder is used to examplify how transaction input can be provided in rlp form.
4+
Please see the README in `evm` folder for how this is performed.

cmd/evm/testdata/13/txs.json

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
[
2+
{
3+
"input" : "0x",
4+
"gas" : "0x84d0",
5+
"nonce" : "0x1",
6+
"to" : "0x1111111111111111111111111111111111111111",
7+
"value" : "0x0",
8+
"v" : "0x0",
9+
"r" : "0x0",
10+
"s" : "0x0",
11+
"secretKey" : "0x41f6e321b31e72173f8ff2e292359e1862f24fba42fe6f97efaf641980eff298",
12+
"chainId" : "0x1",
13+
"type" : "0x2",
14+
"maxFeePerGas" : "0xfa0",
15+
"maxPriorityFeePerGas" : "0x0",
16+
"accessList" : []
17+
},
18+
{
19+
"input" : "0x",
20+
"gas" : "0x84d0",
21+
"nonce" : "0x2",
22+
"to" : "0x1111111111111111111111111111111111111111",
23+
"value" : "0x0",
24+
"v" : "0x0",
25+
"r" : "0x0",
26+
"s" : "0x0",
27+
"secretKey" : "0x41f6e321b31e72173f8ff2e292359e1862f24fba42fe6f97efaf641980eff298",
28+
"chainId" : "0x1",
29+
"type" : "0x2",
30+
"maxFeePerGas" : "0xfa0",
31+
"maxPriorityFeePerGas" : "0x0",
32+
"accessList" : []
33+
}
34+
]

0 commit comments

Comments
 (0)