Skip to content

Commit c2e5eab

Browse files
committed
read the frame envelope the chain actually carries
EIP-8141's envelope is composable: EIP-8250 replaces the nonce with a keyed sequence and EIP-8272 appends recent root references, independently, so four shapes occur and which one a payload used is a property of the transaction rather than of the chain. txtypes now decodes all four and says which it found, so the shape is named on the page instead of assumed. Frame durability comes from txtypes too rather than being restated here. That brings EIP-7906's POST_TX with it: an assertion frame that fails reverts the entire execution body, which is not an atomic batch unwind, and computing durability from batch structure alone reported the frames before it as successes when nothing survived. What the envelope carries is now shown as well. The frames tab names the nonce keys a transaction is sequenced in, where it is sequenced in domains of its own rather than against the sender's account nonce, and lists the recent roots it declared. Storage on the protocol's own accounts is labelled for what it is: NONCE_MANAGER and RECENT_ROOTS are written by validation, not by any frame, and read as anonymous addresses otherwise.
1 parent 5ac4395 commit c2e5eab

9 files changed

Lines changed: 412 additions & 78 deletions

File tree

go.mod

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ require (
1111
github.com/ethpandaops/ethcore v0.0.0-20260807103219-0fb0622e156b
1212
github.com/ethpandaops/ethwallclock v0.4.0
1313
github.com/ethpandaops/go-eth2-client v0.1.7
14-
github.com/ethpandaops/spamoor v1.2.4-0.20260827152838-d35c83e6da78
14+
github.com/ethpandaops/spamoor v1.2.4-0.20260828234109-9ac9b69d9050
1515
github.com/ethpandaops/xatu v1.22.1-0.20260824050538-619c572d19c3
1616
github.com/ethpandaops/xatu-cbt v0.0.0-20260825024339-8eadec716ac8
1717
github.com/go-redis/redis/v8 v8.11.5
@@ -177,7 +177,7 @@ require (
177177
go.uber.org/zap v1.28.0 // indirect
178178
go.yaml.in/yaml/v3 v3.0.5 // indirect
179179
golang.org/x/mod v0.38.0 // indirect
180-
golang.org/x/telemetry v0.0.0-20260708182218-49f421fb7959 // indirect
180+
golang.org/x/telemetry v0.0.0-20260804195142-bdd03c3c8848 // indirect
181181
golang.org/x/tools v0.48.0 // indirect
182182
google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 // indirect
183183
google.golang.org/genproto/googleapis/rpc v0.0.0-20260720211330-0afa2a65878a // indirect
@@ -192,9 +192,9 @@ require (
192192
github.com/Masterminds/semver/v3 v3.5.0 // indirect
193193
github.com/Microsoft/go-winio v0.6.2 // indirect
194194
github.com/beorn7/perks v1.0.1 // indirect
195-
github.com/bits-and-blooms/bitset v1.24.4 // indirect
195+
github.com/bits-and-blooms/bitset v1.24.6 // indirect
196196
github.com/cespare/xxhash/v2 v2.3.0 // indirect
197-
github.com/consensys/gnark-crypto v0.20.1 // indirect
197+
github.com/consensys/gnark-crypto v0.21.0 // indirect
198198
github.com/deckarep/golang-set/v2 v2.8.0 // indirect
199199
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 // indirect
200200
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect

go.sum

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@ github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz
4141
github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
4242
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
4343
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
44-
github.com/bits-and-blooms/bitset v1.24.4 h1:95H15Og1clikBrKr/DuzMXkQzECs1M6hhoGXLwLQOZE=
45-
github.com/bits-and-blooms/bitset v1.24.4/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
44+
github.com/bits-and-blooms/bitset v1.24.6 h1:qcrftZUVBIwfs+m+nhoCBAPT+ZPZZjti8SbHbDQQkZ4=
45+
github.com/bits-and-blooms/bitset v1.24.6/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
4646
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
4747
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
4848
github.com/chuckpreslar/emission v0.0.0-20170206194824-a7ddd980baf9 h1:xz6Nv3zcwO2Lila35hcb0QloCQsc38Al13RNEzWRpX4=
@@ -72,8 +72,8 @@ github.com/cockroachdb/swiss v0.0.0-20251224182025-b0f6560f979b h1:VXvSNzmr8hMj8
7272
github.com/cockroachdb/swiss v0.0.0-20251224182025-b0f6560f979b/go.mod h1:yBRu/cnL4ks9bgy4vAASdjIW+/xMlFwuHKqtmh3GZQg=
7373
github.com/cockroachdb/tokenbucket v0.0.0-20250429170803-42689b6311bb h1:3bCgBvB8PbJVMX1ouCcSIxvsqKPYM7gs72o0zC76n9g=
7474
github.com/cockroachdb/tokenbucket v0.0.0-20250429170803-42689b6311bb/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ=
75-
github.com/consensys/gnark-crypto v0.20.1 h1:PXDUBvk8AzhvWowHLWBEAfUQcV1/aZgWIqD6eMpXmDg=
76-
github.com/consensys/gnark-crypto v0.20.1/go.mod h1:RBWrSgy+IDbGR69RRV313th3M/aZU1ubk2om+qHuTSc=
75+
github.com/consensys/gnark-crypto v0.21.0 h1:FDHibVIk4T5LkOKAkiN38g8gEvOxNcM10mLHOqvFTD0=
76+
github.com/consensys/gnark-crypto v0.21.0/go.mod h1:hdTjDNjdkYJ1oVuc8emh9XEhfM1SbyZhJigFqItiOLk=
7777
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
7878
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
7979
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
@@ -123,8 +123,8 @@ github.com/ethpandaops/ethwallclock v0.4.0 h1:+sgnhf4pk6hLPukP076VxkiLloE4L0Yk1y
123123
github.com/ethpandaops/ethwallclock v0.4.0/go.mod h1:y0Cu+mhGLlem19vnAV2x0hpFS5KZ7oOi2SWYayv9l24=
124124
github.com/ethpandaops/go-eth2-client v0.1.7 h1:EzZP50rrpVsjk2U8cOpN+kgNDTd/9IZNQXkh9t8G9AQ=
125125
github.com/ethpandaops/go-eth2-client v0.1.7/go.mod h1:MXQukU/345puJmB2EikoaeQIFizk3zbekPxwYyxG/t8=
126-
github.com/ethpandaops/spamoor v1.2.4-0.20260827152838-d35c83e6da78 h1:ED6ftVF7qiH3bCayZ32UxKME1kqcOrJsFCQWC+KdaII=
127-
github.com/ethpandaops/spamoor v1.2.4-0.20260827152838-d35c83e6da78/go.mod h1:cctZJLYwi94DIf4if6X+CCoLkV/PeHiVrpfK9JwEAho=
126+
github.com/ethpandaops/spamoor v1.2.4-0.20260828234109-9ac9b69d9050 h1:h+0THK4avP0BBQtRuR0aPy5LT8HTsDShumJmn9BehLE=
127+
github.com/ethpandaops/spamoor v1.2.4-0.20260828234109-9ac9b69d9050/go.mod h1:9P2OdmRA+4kS/g5SO8KiH67KZhl9hHI/WAiLRhM28EQ=
128128
github.com/ethpandaops/xatu v1.22.1-0.20260824050538-619c572d19c3 h1:83LDwFEAijcssPdQ8ojzzbi+2mD3a+zL1b4V4cY1sMo=
129129
github.com/ethpandaops/xatu v1.22.1-0.20260824050538-619c572d19c3/go.mod h1:sDLPtT/bQgK+vExtPq77y2W4yHq9tRdiDu852yptTcU=
130130
github.com/ethpandaops/xatu-cbt v0.0.0-20260825024339-8eadec716ac8 h1:YfAXAt5fQcEnzPccCfByhXY2Vy9FlGrOp+NxGLx3QNY=
@@ -825,8 +825,8 @@ golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
825825
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
826826
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
827827
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
828-
golang.org/x/telemetry v0.0.0-20260708182218-49f421fb7959 h1:RJhm5l6Fo4rmEIcndxDllNhhf/fAx8qIm4t6A7vpm2A=
829-
golang.org/x/telemetry v0.0.0-20260708182218-49f421fb7959/go.mod h1:LV7u5Oco+Z/g6XI7PqN+EUUUGGkEcmB1uj2ceI0fOVg=
828+
golang.org/x/telemetry v0.0.0-20260804195142-bdd03c3c8848 h1:gw6MYMPjxGBM77qPFYzz0c2naANqd2FF66wZRteou4s=
829+
golang.org/x/telemetry v0.0.0-20260804195142-bdd03c3c8848/go.mod h1:LV7u5Oco+Z/g6XI7PqN+EUUUGGkEcmB1uj2ceI0fOVg=
830830
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
831831
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
832832
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -925,5 +925,5 @@ modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
925925
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
926926
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
927927
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
928-
modernc.org/sqlite v1.54.0 h1:JCxR4qwkJvOaqAoYcgDoO25Nc+ROg6EJ2LfBVzdrgog=
929-
modernc.org/sqlite v1.54.0/go.mod h1:4ntCLuNmnH8+GNqjka1wNg7KJd5/Hi5FYp8K+XQ7GZw=
928+
modernc.org/sqlite v1.55.0 h1:hIFh0MCH0rGinQ/4KYb5/UbCkRkb+UP+OkLCVWa5MTM=
929+
modernc.org/sqlite v1.55.0/go.mod h1:4ntCLuNmnH8+GNqjka1wNg7KJd5/Hi5FYp8K+XQ7GZw=

handlers/transaction.go

Lines changed: 106 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1192,6 +1192,8 @@ func annotateStateChangeRoles(pageData *models.TransactionPageData) {
11921192

11931193
// The sender paying its own fee is the ordinary case and says nothing.
11941194
account.IsPayer = !pageData.PayerIsSender && sameAddress(account.Address, pageData.PayerAddr)
1195+
1196+
account.PredeployName = framePredeployNames[common.BytesToAddress(account.Address)]
11951197
}
11961198
}
11971199

@@ -1948,6 +1950,7 @@ var frameModeNames = map[uint8]string{
19481950
uint8(txtypes.FrameModeDefault): "Default",
19491951
uint8(txtypes.FrameModeVerify): "Verify",
19501952
uint8(txtypes.FrameModeSender): "Sender",
1953+
uint8(txtypes.FrameModePostTx): "Post-tx",
19511954
}
19521955

19531956
// frameSpeciesNames turn the mempool rules' species names into readable ones.
@@ -1968,6 +1971,7 @@ var frameSpeciesNames = map[txtypes.FrameSpecies]string{
19681971
txtypes.SpeciesDeploy: "Account deploy",
19691972
txtypes.SpeciesUserOp: "User operation",
19701973
txtypes.SpeciesPostOp: "Settlement",
1974+
txtypes.SpeciesPostTx: "Assertion",
19711975
txtypes.SpeciesOther: "Other",
19721976
}
19731977

@@ -1996,6 +2000,10 @@ var frameSpeciesInfo = map[txtypes.FrameSpecies]string{
19962000
txtypes.SpeciesPostOp: "A call made after the operations have run, entered by the ENTRY_POINT predeploy rather " +
19972001
"than by the sender. It is where a paymaster squares up once the real cost is known.",
19982002

2003+
txtypes.SpeciesPostTx: "Runs after the transaction's operations, reading what they did through TXTRACE and " +
2004+
"asserting something about it. If it reverts, the whole execution body is reverted with it - not just " +
2005+
"its own atomic batch - though the transaction still reaches the chain and its fee is still owed.",
2006+
19992007
txtypes.SpeciesOther: "The frame's mode and approval flags match none of the shapes the mempool rules name.",
20002008
}
20012009

@@ -2135,6 +2143,7 @@ func applyFrameResults(pageData *models.TransactionPageData, results []bdbtypes.
21352143
}
21362144

21372145
markRolledBackFrames(pageData.Frames)
2146+
applyFrameBodyReverted(pageData)
21382147
summarizeFrames(pageData)
21392148

21402149
pageData.FrameResultsMissing = false
@@ -2162,50 +2171,92 @@ func applyFrameReceiptExtra(pageData *models.TransactionPageData, receipt *txtyp
21622171
applyFrameResults(pageData, results)
21632172
}
21642173

2165-
// markRolledBackFrames flags the frames of every atomic batch that failed.
2174+
// markRolledBackFrames flags the frames whose effects did not survive the transaction.
21662175
//
2167-
// A frame that ran before the failure keeps the success status it earned, but the batch
2168-
// took its state changes back with it, so showing a plain success would say it did
2169-
// something it did not.
2176+
// A frame's status says whether it ran, not whether what it did lasted. Two rules discard
2177+
// the effects of a frame that reports success: an atomic batch that fails is unrolled,
2178+
// and a failing POST_TX frame reverts the whole execution body. Both live in txtypes,
2179+
// which owns the rules, so the shapes and statuses the page already holds are handed back
2180+
// to it rather than the rules being restated here.
21702181
func markRolledBackFrames(frames []*models.TransactionPageDataFrame) {
2171-
batchStart := 0
2182+
durable := frameDurability(frames)
2183+
2184+
// A batch names what undid it, which the durability answer alone does not carry.
2185+
undoneBy := batchFailures(frames)
21722186

21732187
for i, frame := range frames {
2174-
if frame.AtomicBatch && i+1 < len(frames) {
2188+
if durable[i] || uint64(frame.Status) != txtypes.FrameStatusSuccess {
21752189
continue
21762190
}
21772191

2178-
batch := frames[batchStart : i+1]
2179-
batchStart = i + 1
2192+
frame.RolledBack = true
2193+
frame.StatusText = frameStatusText(frame.Status, true)
21802194

2181-
failedIndex := -1
2195+
if idx, ok := undoneBy[i]; ok {
2196+
frame.BatchFailedIndex = int(idx)
2197+
}
2198+
}
2199+
}
21822200

2183-
for _, member := range batch {
2184-
if uint64(member.Status) == txtypes.FrameStatusFailed {
2185-
failedIndex = int(member.Index)
2201+
// frameDurability asks txtypes which frames' effects survived, rebuilding the shapes it
2202+
// needs from what the page holds: a frame's mode and flags decide the validation prefix,
2203+
// the atomic batches and whether a POST_TX frame is present.
2204+
//
2205+
// Frames raised from a receipt alone carry no mode or flags, which reads as a transaction
2206+
// with no prefix and no batches - and the answer is then simply whether each frame
2207+
// succeeded, which is all that can be known without the transaction.
2208+
func frameDurability(frames []*models.TransactionPageDataFrame) []bool {
2209+
tx := &txtypes.FrameTx{Frames: make([]*txtypes.Frame, len(frames))}
2210+
extra := &txtypes.FrameReceiptExtra{Frames: make([]*txtypes.FrameReceipt, len(frames))}
21862211

2187-
break
2188-
}
2212+
for i, frame := range frames {
2213+
// The batch bit is rebuilt from AtomicBatch rather than read out of Flags: the
2214+
// two are the same fact, and taking the meaning rather than the encoding keeps
2215+
// this right whichever of them a caller filled in.
2216+
flags := frame.Flags & txtypes.ApproveScopeMask
2217+
if frame.AtomicBatch {
2218+
flags |= txtypes.AtomicBatchFlag
2219+
}
2220+
2221+
tx.Frames[i] = &txtypes.Frame{
2222+
Mode: txtypes.FrameMode(frame.Mode),
2223+
Flags: flags,
21892224
}
2225+
extra.Frames[i] = &txtypes.FrameReceipt{Status: uint64(frame.Status)}
2226+
}
2227+
2228+
return extra.DurableFrames(tx)
2229+
}
2230+
2231+
// batchFailures maps each frame of a failed atomic batch to the frame whose failure undid
2232+
// it, so a rolled-back frame can name its cause.
2233+
func batchFailures(frames []*models.TransactionPageDataFrame) map[int]uint32 {
2234+
undoneBy := make(map[int]uint32, len(frames))
2235+
batchStart := 0
21902236

2191-
if failedIndex < 0 {
2237+
for i, frame := range frames {
2238+
if frame.AtomicBatch && i+1 < len(frames) {
21922239
continue
21932240
}
21942241

2242+
batch := frames[batchStart : i+1]
2243+
start := batchStart
2244+
batchStart = i + 1
2245+
21952246
for _, member := range batch {
2196-
// Only a frame that succeeded had anything taken back from it. The frame
2197-
// that failed, and the ones after it that never ran, are told by their own
2198-
// status - and a lone failed frame is not a batch that rolled back, it is
2199-
// just a failure.
2200-
if uint64(member.Status) != txtypes.FrameStatusSuccess {
2247+
if uint64(member.Status) != txtypes.FrameStatusFailed {
22012248
continue
22022249
}
22032250

2204-
member.RolledBack = true
2205-
member.BatchFailedIndex = failedIndex
2206-
member.StatusText = frameStatusText(member.Status, true)
2251+
for offset := range batch {
2252+
undoneBy[start+offset] = member.Index
2253+
}
2254+
2255+
break
22072256
}
22082257
}
2258+
2259+
return undoneBy
22092260
}
22102261

22112262
// frameSpecies classifies a frame for display.
@@ -2234,6 +2285,7 @@ func frameCaller(frame *txtypes.Frame, sender common.Address) common.Address {
22342285
return sender
22352286
}
22362287

2288+
// DEFAULT, VERIFY and POST_TX frames are all entered by the predeploy.
22372289
return txtypes.EntryPoint
22382290
}
22392291

@@ -2487,6 +2539,19 @@ func summarizeFrames(pageData *models.TransactionPageData) {
24872539
applyFrameTxStatus(pageData)
24882540
}
24892541

2542+
// applyFrameBodyReverted records whether an assertion frame took the whole execution body
2543+
// with it, which the frame statuses alone do not say: the frames it reverted still report
2544+
// the success they earned.
2545+
func applyFrameBodyReverted(pageData *models.TransactionPageData) {
2546+
for _, frame := range pageData.Frames {
2547+
if frame.Mode == uint8(txtypes.FrameModePostTx) && uint64(frame.Status) == txtypes.FrameStatusFailed {
2548+
pageData.FrameBodyReverted = true
2549+
2550+
return
2551+
}
2552+
}
2553+
}
2554+
24902555
// applyFrameTxStatus states the transaction's own outcome from its frames.
24912556
//
24922557
// A frame transaction that reached the chain ran and paid: its validation prefix
@@ -2522,6 +2587,19 @@ func applyFrameTxStatus(pageData *models.TransactionPageData) {
25222587
parts = append(parts, fmt.Sprintf("%d never ran", pageData.FrameSkippedCount))
25232588
}
25242589

2590+
if pageData.FrameBodyReverted {
2591+
pageData.StatusText = "Reverted"
2592+
pageData.FrameIncomplete = true
2593+
pageData.FrameStatusDetail = fmt.Sprintf(
2594+
"An assertion frame failed, which reverts everything the transaction did after its validation "+
2595+
"frames - not just its own atomic batch. The transaction still reached the chain and its fee "+
2596+
"was still owed: of its %d frames, only the validation ones left anything behind.",
2597+
len(pageData.Frames),
2598+
)
2599+
2600+
return
2601+
}
2602+
25252603
if len(parts) == 0 {
25262604
pageData.StatusText = "Success"
25272605
pageData.FrameIncomplete = false
@@ -2642,6 +2720,11 @@ func applyFrameTxEnvelope(pageData *models.TransactionPageData, frameTx *txtypes
26422720
buildFrameSignatures(pageData, frameTx)
26432721
buildFrameRecentRoots(pageData, frameTx)
26442722

2723+
// Which of EIP-8141's extensions the payload used. A chain can run 8250 and 8272
2724+
// independently, so this is a property of the transaction rather than of the chain.
2725+
pageData.FrameExtensions = frameTx.Extensions.String()
2726+
pageData.FrameHasKeyedNonces = frameTx.HasKeyedNonces()
2727+
26452728
if !pageData.NonceIsAccount {
26462729
pageData.NonceKeys = make([]string, 0, len(frameTx.NonceKeys))
26472730
for _, key := range frameTx.NonceKeys {

0 commit comments

Comments
 (0)