@@ -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.
21702181func 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