Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
6ffb618
reject EL transactions that don't match their reported hash
pk910 Aug 26, 2026
74e7d56
Merge remote-tracking branch 'origin/master' into pk910/frame-transac…
pk910 Aug 27, 2026
5d528e7
replace go-ethereum's transaction type with spamoor's txtypes
pk910 Aug 27, 2026
449e95b
render frame transactions as JSON through txtypes
pk910 Aug 27, 2026
eaa8cb8
index EIP-8141 frame transactions
pk910 Aug 27, 2026
59ff016
persist the frames of a frame transaction
pk910 Aug 27, 2026
7061e80
keep frame receipts in blockdb
pk910 Aug 27, 2026
5bdda99
show frame transactions
pk910 Aug 27, 2026
d376041
do not read a frame transaction's missing recipient as a creation
pk910 Aug 27, 2026
4d5a827
decode block receipts whose frame logs omit their position
pk910 Aug 27, 2026
e4bd719
record a frame's result and its callers the same way in every store
pk910 Aug 27, 2026
383f9b1
Merge remote-tracking branch 'origin/master' into pk910/frame-transac…
pk910 Aug 28, 2026
ecb980c
read a frame transaction's frames from the transaction and its receipt
pk910 Aug 28, 2026
dd0f3c2
show a frame transaction's frames in a tab of their own
pk910 Aug 28, 2026
4a01386
Merge branch 'master' into pk910/frame-transactions
pk910 Aug 28, 2026
2f63dff
open the tab a nav item points at
pk910 Aug 28, 2026
a0e080c
say which frame an event or a call belongs to
pk910 Aug 28, 2026
7562f6c
say which frame a token transfer came from, and which one failed
pk910 Aug 28, 2026
eb72eb5
a frame transaction that reached the chain completed
pk910 Aug 28, 2026
06216d2
read a frame's caller before its target
pk910 Aug 28, 2026
c4d607b
measure a frame transaction's deadline from its inclusion, and show i…
pk910 Aug 28, 2026
0042d73
show what a frame transaction authorised, declared, and who took part
pk910 Aug 28, 2026
61230c0
space the two new lists like the ones they sit beside
pk910 Aug 28, 2026
384ceb4
say what each kind of frame is for
pk910 Aug 28, 2026
2e4fd73
follow one address through the page it appears on
pk910 Aug 28, 2026
a9a36c4
give every transaction a signatures tab
pk910 Aug 28, 2026
d0e7ee4
tell a frame's own failure apart from a batch undoing it
pk910 Aug 28, 2026
6d385cd
index every transaction a client reports, even one that re-encodes di…
pk910 Aug 28, 2026
5ac4395
recover an execution payload announced before the node serves it
pk910 Aug 28, 2026
c2e5eab
read the frame envelope the chain actually carries
pk910 Aug 28, 2026
9c99fa0
show a nonce key as the opaque identifier it is
pk910 Aug 29, 2026
b7dfe4d
wrap an expanded signature instead of widening the list
pk910 Aug 29, 2026
ff19336
drop the annotations on the signature field values
pk910 Aug 29, 2026
461820a
show the state changes a discarded call trace left behind
pk910 Aug 29, 2026
4713743
drop the frame batch predicate the durability rewrite left behind
pk910 Aug 29, 2026
4bdea33
Merge branch 'master' into pk910/frame-transactions
pk910 Sep 7, 2026
73b04c7
Show frame transactions as complete rather than failed in every view
pk910 Sep 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions blockdb/types/execdata_receiptmeta.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package types

import (
"fmt"
)

// receiptMetaSize is the encoded size of ReceiptMetaData, which is fixed. A version 2
// section is exactly this many bytes followed by an encoded FrameReceiptData.
var receiptMetaSize = (&ReceiptMetaData{}).SizeSSZ()

// EncodeReceiptMetaSection encodes the receipt metadata section.
//
// Frame content is appended after the fixed metadata rather than stored in a section of
// its own: the per-transaction index entry has a fixed width, so a new section pointer
// would change the object format and make every object already written unreadable. The
// version field on the metadata says whether the tail is there.
func EncodeReceiptMetaSection(meta *ReceiptMetaData, frames *FrameReceiptData) ([]byte, error) {
if frames == nil {
meta.Version = ReceiptMetaVersion1
} else {
meta.Version = ReceiptMetaVersion2
}

encoded, err := meta.MarshalSSZ()
if err != nil {
return nil, fmt.Errorf("marshal receipt metadata: %w", err)
}

if frames == nil {
return encoded, nil
}

frameData, err := frames.MarshalSSZ()
if err != nil {
return nil, fmt.Errorf("marshal frame receipt data: %w", err)
}

return append(encoded, frameData...), nil
}

// DecodeReceiptMetaSection decodes a receipt metadata section, returning the frame
// content when the section carries any. Sections written before frame transactions
// existed decode with a nil frame result.
func DecodeReceiptMetaSection(raw []byte) (*ReceiptMetaData, *FrameReceiptData, error) {
if len(raw) < receiptMetaSize {
return nil, nil, fmt.Errorf("receipt metadata truncated: need %d bytes, got %d", receiptMetaSize, len(raw))
}

meta := &ReceiptMetaData{}
if err := meta.UnmarshalSSZ(raw[:receiptMetaSize]); err != nil {
return nil, nil, fmt.Errorf("unmarshal receipt metadata: %w", err)
}

tail := raw[receiptMetaSize:]
if meta.Version < ReceiptMetaVersion2 || len(tail) == 0 {
return meta, nil, nil
}

frames := &FrameReceiptData{}
if err := frames.UnmarshalSSZ(tail); err != nil {
return nil, nil, fmt.Errorf("unmarshal frame receipt data: %w", err)
}

return meta, frames, nil
}
138 changes: 138 additions & 0 deletions blockdb/types/execdata_receiptmeta_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package types

import (
"testing"
)

func sampleReceiptMeta() *ReceiptMetaData {
meta := &ReceiptMetaData{
Version: ReceiptMetaVersion1,
Status: 1,
TxType: 6,
CumulativeGasUsed: 0x4e86,
GasUsed: 0x4e86,
}
meta.EffectiveGasPrice.SetUint64(0x773642f2)
copy(meta.From[:], []byte{0x7a, 0x11})
copy(meta.To[:], []byte{0x30, 0x59})

return meta
}

// A receipt with no frame content encodes exactly as it always did, so objects written
// before frame transactions existed and objects written after are the same shape.
func TestReceiptMetaSectionWithoutFramesIsUnchanged(t *testing.T) {
meta := sampleReceiptMeta()

encoded, err := EncodeReceiptMetaSection(meta, nil)
if err != nil {
t.Fatalf("encode failed: %v", err)
}

if len(encoded) != receiptMetaSize {
t.Fatalf("encoded %d bytes, want the fixed %d", len(encoded), receiptMetaSize)
}

decoded, frames, err := DecodeReceiptMetaSection(encoded)
if err != nil {
t.Fatalf("decode failed: %v", err)
}

if frames != nil {
t.Error("a receipt with no frame content must decode without any")
}

if decoded.Version != ReceiptMetaVersion1 {
t.Errorf("version = %d, want %d", decoded.Version, ReceiptMetaVersion1)
}

if decoded.GasUsed != meta.GasUsed || decoded.Status != meta.Status {
t.Error("receipt metadata did not survive the round trip")
}
}

func TestReceiptMetaSectionRoundTripsFrames(t *testing.T) {
meta := sampleReceiptMeta()

frames := &FrameReceiptData{
Frames: []FrameReceiptEntry{
{Status: 1, ExecGasUsed: 0x33, StateGasUsed: 0, LogCount: 0},
{Status: 2, ExecGasUsed: 0, StateGasUsed: 0, LogCount: 0},
{Status: 1, ExecGasUsed: 21000, StateGasUsed: 5, LogCount: 3},
},
}
copy(frames.Payer[:], []byte{0x6d, 0xf3, 0x54, 0x38})

encoded, err := EncodeReceiptMetaSection(meta, frames)
if err != nil {
t.Fatalf("encode failed: %v", err)
}

if len(encoded) <= receiptMetaSize {
t.Fatalf("encoded %d bytes, want more than the fixed %d", len(encoded), receiptMetaSize)
}

// The version has to say the tail is there, or a reader will not look for it.
if meta.Version != ReceiptMetaVersion2 {
t.Errorf("version = %d, want %d", meta.Version, ReceiptMetaVersion2)
}

decodedMeta, decodedFrames, err := DecodeReceiptMetaSection(encoded)
if err != nil {
t.Fatalf("decode failed: %v", err)
}

if decodedMeta.GasUsed != meta.GasUsed {
t.Error("receipt metadata did not survive alongside the frames")
}

if decodedFrames == nil {
t.Fatal("frame content was lost")
}

if decodedFrames.Payer != frames.Payer {
t.Errorf("payer = %x, want %x", decodedFrames.Payer, frames.Payer)
}

if len(decodedFrames.Frames) != len(frames.Frames) {
t.Fatalf("frames = %d, want %d", len(decodedFrames.Frames), len(frames.Frames))
}

for i := range frames.Frames {
if decodedFrames.Frames[i] != frames.Frames[i] {
t.Errorf("frame %d = %+v, want %+v", i, decodedFrames.Frames[i], frames.Frames[i])
}
}
}

// A section written before frame transactions existed carries version 1 and no tail. It
// must keep decoding, which is the whole reason the frames go behind a version rather
// than into a new section of the object.
func TestReceiptMetaSectionDecodesLegacySections(t *testing.T) {
meta := sampleReceiptMeta()
meta.Version = ReceiptMetaVersion1

legacy, err := meta.MarshalSSZ()
if err != nil {
t.Fatalf("marshal failed: %v", err)
}

decoded, frames, err := DecodeReceiptMetaSection(legacy)
if err != nil {
t.Fatalf("decode of a legacy section failed: %v", err)
}

if frames != nil {
t.Error("a legacy section has no frame content")
}

if decoded.CumulativeGasUsed != meta.CumulativeGasUsed {
t.Error("legacy receipt metadata did not decode")
}
}

func TestReceiptMetaSectionRejectsTruncatedInput(t *testing.T) {
if _, _, err := DecodeReceiptMetaSection(make([]byte, receiptMetaSize-1)); err == nil {
t.Error("a section shorter than the fixed metadata must not decode")
}
}
42 changes: 42 additions & 0 deletions blockdb/types/execdata_sections.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const (
CallTypeCreate = 3
CallTypeCreate2 = 4
CallTypeSelfDestruct = 5
CallTypeFrame = 6
)

// Call status constants for binary encoding.
Expand Down Expand Up @@ -129,8 +130,49 @@ type BlockReceiptMeta struct {
// Receipt metadata version. Bump when adding new fields.
const (
ReceiptMetaVersion1 = 1

// ReceiptMetaVersion2 marks a section that carries frame content after the fixed
// metadata: the payer and per-frame results of an EIP-8141 frame transaction, which
// a receipt reports and no other section holds.
ReceiptMetaVersion2 = 2
)

// FrameStatusUnknown marks a frame the client reported no result for, which is neither
// a success nor a failure. EIP-8141's own statuses start at zero, and zero is how a
// failure is spelled, so the absence of a result needs a value of its own.
const FrameStatusUnknown uint8 = 255

// FrameReceiptEntry is the result a receipt reports for one frame.
type FrameReceiptEntry struct {
// Status is EIP-8141's per-frame status: 0 failed, 1 success, 2 skipped. A skipped
// frame is neither - an earlier frame in its atomic batch failed and it never ran.
Status uint8

// EIP-8037 accounts for gas in two dimensions, and the receipt reports both per
// frame. State gas is a final attribution rather than a running total: a later frame
// can retroactively reduce an earlier one through a state-gas refill.
ExecGasUsed uint64
StateGasUsed uint64

// LogCount is how many logs the frame emitted. The transaction's logs are the
// per-frame lists concatenated in frame order, so these counts partition the events
// section and attribute each log to the frame that emitted it.
LogCount uint32
}

// FrameReceiptData is the frame-transaction content of a receipt.
//
// A frame transaction's own fields - its targets, values, calldata and gas budgets - are
// recoverable from the transaction in the beacon block. What only the receipt holds is
// who paid and what each frame did, so that is what is kept here.
type FrameReceiptData struct {
// Payer settled the transaction's fee. For a sponsored transaction it is a paymaster
// rather than the sender, which is the point of the field.
Payer [20]byte

Frames []FrameReceiptEntry `ssz-max:"64"`
}

// ReceiptMetaData holds per-transaction receipt metadata needed to
// reconstruct a full eth_getTransactionReceipt JSON response.
// Stored in the ReceiptMeta section (bitmap flag 0x08) of the execution
Expand Down
Loading