Skip to content

Commit 8ca7fdb

Browse files
committed
Add nested execution plan prototype
1 parent a241765 commit 8ca7fdb

3 files changed

Lines changed: 247 additions & 0 deletions

File tree

gossip/blockproc/bundle/bundle.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ package bundle
1818

1919
import (
2020
"bytes"
21+
"encoding/binary"
2122
"fmt"
23+
"io"
2224
"math"
2325

2426
"github.com/ethereum/go-ethereum/common"
@@ -152,6 +154,24 @@ func (r BlockRange) IsInRange(blockNum uint64) bool {
152154
return blockNum >= r.Earliest && blockNum <= r.Latest
153155
}
154156

157+
func (r BlockRange) encode(writer io.Writer) error {
158+
data := make([]byte, 16)
159+
binary.BigEndian.PutUint64(data[0:8], r.Earliest)
160+
binary.BigEndian.PutUint64(data[8:16], r.Latest)
161+
_, err := writer.Write(data)
162+
return err
163+
}
164+
165+
func (r *BlockRange) decode(reader io.Reader) error {
166+
data := make([]byte, 16)
167+
if _, err := io.ReadFull(reader, data); err != nil {
168+
return err
169+
}
170+
r.Earliest = binary.BigEndian.Uint64(data[0:8])
171+
r.Latest = binary.BigEndian.Uint64(data[8:16])
172+
return nil
173+
}
174+
155175
// Hash computes the execution plan hash
156176
// The hash is computed with Keccak256, and is based on the RLP encoding of the type
157177
// rlp([Steps, Flags]), where Steps is of type [[{20 bytes}, {32 bytes}]...] where

gossip/blockproc/bundle/plan.go

Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
package bundle
2+
3+
import (
4+
"bytes"
5+
"errors"
6+
"io"
7+
"maps"
8+
"slices"
9+
10+
"github.com/ethereum/go-ethereum/common"
11+
"github.com/ethereum/go-ethereum/core/types"
12+
"github.com/ethereum/go-ethereum/crypto"
13+
"github.com/ethereum/go-ethereum/rlp"
14+
)
15+
16+
type TransactionBundle2 struct {
17+
Transactions map[TxReference]*types.Transaction
18+
Plan ExecutionPlan2
19+
}
20+
21+
const (
22+
bundleEncodingVersion2 byte = 2
23+
)
24+
25+
type bundleEncodingV2 struct {
26+
Bundle types.Transactions
27+
Plan []byte
28+
}
29+
30+
func (b *TransactionBundle2) Encode() []byte {
31+
32+
// Create canonical form of list of included transactions.
33+
transactions := slices.Collect(maps.Values(b.Transactions))
34+
slices.SortFunc(transactions, func(a, b *types.Transaction) int {
35+
hashA := a.Hash()
36+
hashB := b.Hash()
37+
return bytes.Compare(hashA[:], hashB[:])
38+
})
39+
40+
// TODO: check error handling
41+
encodedPlan := bytes.NewBuffer(nil)
42+
_ = b.Plan.encode(encodedPlan)
43+
44+
buffer := bytes.Buffer{}
45+
// encode into a buffer can only fail due to OOM
46+
// since we are encoding a struct with fixed fields, we can ignore the error
47+
_ = rlp.Encode(&buffer, bundleEncodingVersion2)
48+
_ = rlp.Encode(&buffer, bundleEncodingV2{
49+
transactions,
50+
encodedPlan.Bytes(),
51+
})
52+
return buffer.Bytes()
53+
}
54+
55+
func (b *TransactionBundle2) Decode(data []byte) error {
56+
var version byte
57+
if err := rlp.DecodeBytes(data, &version); err != nil {
58+
return err
59+
}
60+
if version != bundleEncodingVersion2 {
61+
return errors.New("unsupported bundle encoding version")
62+
}
63+
64+
var decoded bundleEncodingV2
65+
if err := rlp.DecodeBytes(data, &decoded); err != nil {
66+
return err
67+
}
68+
69+
b.Transactions = make(map[TxReference]*types.Transaction)
70+
for _, tx := range decoded.Bundle {
71+
if !tx.Protected() {
72+
return errors.New("unsupported transaction type in bundle")
73+
}
74+
signer := types.LatestSignerForChainID(tx.ChainId())
75+
sender, err := types.Sender(signer, tx)
76+
if err != nil {
77+
return err
78+
}
79+
txData := TxReference{
80+
From: sender,
81+
Hash: tx.Hash(),
82+
}
83+
b.Transactions[txData] = tx
84+
}
85+
86+
return b.Plan.decode(bytes.NewReader(decoded.Plan))
87+
}
88+
89+
type ExecutionPlan2 struct {
90+
Group Group
91+
Range BlockRange
92+
}
93+
94+
func (p *ExecutionPlan2) Hash() common.Hash {
95+
hasher := crypto.NewKeccakState()
96+
_ = p.encode(hasher)
97+
return common.BytesToHash(hasher.Sum(nil))
98+
}
99+
100+
func (p *ExecutionPlan2) encode(writer io.Writer) error {
101+
return errors.Join(
102+
p.Group.encode(writer),
103+
p.Range.encode(writer),
104+
)
105+
}
106+
107+
func (p *ExecutionPlan2) decode(reader io.Reader) error {
108+
return errors.Join(
109+
p.Group.decode(reader),
110+
p.Range.decode(reader),
111+
)
112+
}
113+
114+
type Group struct {
115+
Flags ExecutionFlags
116+
Steps []GroupOrTransaction
117+
}
118+
119+
const (
120+
groupEncodingMarker byte = 0x00
121+
txReferenceEncodingMarker byte = 0x01
122+
)
123+
124+
func (g *Group) encode(writer io.Writer) error {
125+
_, err := writer.Write([]byte{
126+
byte(g.Flags),
127+
byte(len(g.Steps)), // TODO: limit number of steps to 255
128+
})
129+
if err != nil {
130+
return err
131+
}
132+
for _, step := range g.Steps {
133+
var mark byte
134+
switch step.(type) {
135+
case *Group:
136+
mark = groupEncodingMarker
137+
case *TxReference:
138+
mark = txReferenceEncodingMarker
139+
}
140+
_, err = writer.Write([]byte{mark})
141+
if err != nil {
142+
return err
143+
}
144+
if err := step.encode(writer); err != nil {
145+
return err
146+
}
147+
}
148+
return err
149+
}
150+
151+
func (g *Group) decode(reader io.Reader) error {
152+
header := make([]byte, 2)
153+
if _, err := io.ReadFull(reader, header); err != nil {
154+
return err
155+
}
156+
g.Flags = ExecutionFlags(header[0])
157+
nSteps := int(header[1])
158+
g.Steps = make([]GroupOrTransaction, 0, nSteps)
159+
for range nSteps {
160+
marker := make([]byte, 1)
161+
if _, err := io.ReadFull(reader, marker); err != nil {
162+
return err
163+
}
164+
var step GroupOrTransaction
165+
switch marker[0] {
166+
case groupEncodingMarker:
167+
step = &Group{}
168+
case txReferenceEncodingMarker:
169+
step = &TxReference{}
170+
default:
171+
return errors.New("unknown step marker")
172+
}
173+
if err := step.decode(reader); err != nil {
174+
return err
175+
}
176+
g.Steps = append(g.Steps, step)
177+
}
178+
return nil
179+
}
180+
181+
// TxReference represents a single step in an execution plan, referencing a
182+
// transaction to be processed at this point of the plan.
183+
type TxReference struct {
184+
// From is the sender of the transaction.
185+
From common.Address
186+
// Hash is the transaction hash to be signed (not the hash of the
187+
// transaction including its signature) where the bundle-only marker has
188+
// been removed.
189+
Hash common.Hash
190+
}
191+
192+
func (t *TxReference) encode(writer io.Writer) error {
193+
_, err1 := writer.Write(t.From.Bytes())
194+
_, err2 := writer.Write(t.Hash.Bytes())
195+
return errors.Join(err1, err2)
196+
}
197+
198+
func (t *TxReference) decode(reader io.Reader) error {
199+
from := make([]byte, common.AddressLength)
200+
if _, err := io.ReadFull(reader, from); err != nil {
201+
return err
202+
}
203+
hash := make([]byte, common.HashLength)
204+
if _, err := io.ReadFull(reader, hash); err != nil {
205+
return err
206+
}
207+
t.From = common.BytesToAddress(from)
208+
t.Hash = common.BytesToHash(hash)
209+
return nil
210+
}
211+
212+
type GroupOrTransaction interface {
213+
encode(writer io.Writer) error
214+
decode(reader io.Reader) error
215+
}
216+
217+
var _ GroupOrTransaction = (*Group)(nil)
218+
var _ GroupOrTransaction = (*TxReference)(nil)
219+
220+
// TODO:
221+
// - implement plan serialization
222+
// - implement plan hashing
223+
// - implement plan debugging
224+
// - implement bundle validation
225+
// - implement bundle execution
226+
// - implement bundle builder for new format
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
package bundle

0 commit comments

Comments
 (0)