Skip to content

Commit bab25f6

Browse files
committed
core/types: add EIP-8130 (0x7B) transaction type
1 parent d0734fd commit bab25f6

7 files changed

Lines changed: 948 additions & 0 deletions

File tree

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
// Copyright 2026 The go-ethereum Authors
2+
// This file is part of the go-ethereum library.
3+
//
4+
// The go-ethereum library is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU Lesser General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// The go-ethereum library is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU Lesser General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU Lesser General Public License
15+
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
16+
17+
package types
18+
19+
import (
20+
"encoding/json"
21+
"errors"
22+
"fmt"
23+
"io"
24+
25+
"github.com/ethereum/go-ethereum/common"
26+
"github.com/ethereum/go-ethereum/common/hexutil"
27+
"github.com/ethereum/go-ethereum/rlp"
28+
)
29+
30+
// EIP-8130 account_changes entry type bytes. On the wire each entry is encoded
31+
// as type_byte || rlp([body fields...]).
32+
const (
33+
accountChangeTypeCreate = 0x00
34+
accountChangeTypeConfig = 0x01
35+
accountChangeTypeDelegation = 0x02
36+
)
37+
38+
// ActorChangeType is the operation performed by an ActorChange. The value is the
39+
// on-wire op byte; in RLP it encodes as a bare uint, in JSON as a string.
40+
type ActorChangeType uint8
41+
42+
const (
43+
ActorChangeAuthorize ActorChangeType = 0x01
44+
ActorChangeRevoke ActorChangeType = 0x02
45+
)
46+
47+
func (t ActorChangeType) MarshalJSON() ([]byte, error) {
48+
switch t {
49+
case ActorChangeAuthorize:
50+
return []byte(`"Authorize"`), nil
51+
case ActorChangeRevoke:
52+
return []byte(`"Revoke"`), nil
53+
default:
54+
return nil, fmt.Errorf("eip8130: invalid actor change type %d", uint8(t))
55+
}
56+
}
57+
58+
func (t *ActorChangeType) UnmarshalJSON(input []byte) error {
59+
switch string(input) {
60+
case `"Authorize"`:
61+
*t = ActorChangeAuthorize
62+
case `"Revoke"`:
63+
*t = ActorChangeRevoke
64+
default:
65+
return fmt.Errorf("eip8130: invalid actor change type %s", input)
66+
}
67+
return nil
68+
}
69+
70+
// InitialActor is an actor installed on a newly-created account. Wire form is
71+
// rlp([actorId, authenticator]).
72+
type InitialActor struct {
73+
ActorID common.Hash `json:"actorId"`
74+
Authenticator common.Address `json:"authenticator"`
75+
}
76+
77+
// ActorChange is a single actor authorize/revoke operation inside a ConfigChange.
78+
// Wire form is rlp([changeType, actorId, data]); data is opaque at this layer.
79+
type ActorChange struct {
80+
ChangeType ActorChangeType `json:"changeType"`
81+
ActorID common.Hash `json:"actorId"`
82+
Data hexutil.Bytes `json:"data"`
83+
}
84+
85+
// CreateEntry is the body of an AccountChange create entry. Wire form is
86+
// rlp([userSalt, code, [InitialActor, ...]]).
87+
type CreateEntry struct {
88+
UserSalt common.Hash `json:"userSalt"`
89+
Code hexutil.Bytes `json:"code"`
90+
InitialActors []InitialActor `json:"initialActors"`
91+
}
92+
93+
// ConfigChange is the body of an AccountChange config-change entry. Wire form is
94+
// rlp([chainId, sequence, [ActorChange, ...], auth]).
95+
type ConfigChange struct {
96+
ChainID uint64 `json:"chainId"`
97+
Sequence uint64 `json:"sequence"`
98+
ActorChanges []ActorChange `json:"actorChanges"`
99+
Auth hexutil.Bytes `json:"auth"`
100+
}
101+
102+
// Delegation is the body of an AccountChange delegation entry. Wire form is
103+
// rlp([target]); a zero target clears the existing delegation.
104+
type Delegation struct {
105+
Target common.Address `json:"target"`
106+
}
107+
108+
// AccountChange is a tagged-union entry inside Eip8130Tx.AccountChanges. Exactly
109+
// one of the body pointers is set. On the wire each entry is
110+
// type_byte || rlp([body fields...]); in JSON it is the body object with an added
111+
// "type" discriminator ("create" / "configChange" / "delegation").
112+
type AccountChange struct {
113+
Create *CreateEntry
114+
ConfigChange *ConfigChange
115+
Delegation *Delegation
116+
}
117+
118+
// EncodeRLP writes the entry as type_byte || rlp(body).
119+
func (a AccountChange) EncodeRLP(w io.Writer) error {
120+
var (
121+
typeByte byte
122+
body interface{}
123+
)
124+
switch {
125+
case a.Create != nil:
126+
typeByte, body = accountChangeTypeCreate, a.Create
127+
case a.ConfigChange != nil:
128+
typeByte, body = accountChangeTypeConfig, a.ConfigChange
129+
case a.Delegation != nil:
130+
typeByte, body = accountChangeTypeDelegation, a.Delegation
131+
default:
132+
return errors.New("eip8130: empty account change")
133+
}
134+
if _, err := w.Write([]byte{typeByte}); err != nil {
135+
return err
136+
}
137+
return rlp.Encode(w, body)
138+
}
139+
140+
// DecodeRLP reads the type byte and decodes the matching body. Returns rlp.EOL
141+
// at the end of the enclosing list so it composes with slice decoding.
142+
func (a *AccountChange) DecodeRLP(s *rlp.Stream) error {
143+
typeByte, err := s.Bytes()
144+
if err != nil {
145+
return err
146+
}
147+
if len(typeByte) != 1 {
148+
return errors.New("eip8130: invalid account change type byte")
149+
}
150+
switch typeByte[0] {
151+
case accountChangeTypeCreate:
152+
a.Create = new(CreateEntry)
153+
return s.Decode(a.Create)
154+
case accountChangeTypeConfig:
155+
a.ConfigChange = new(ConfigChange)
156+
return s.Decode(a.ConfigChange)
157+
case accountChangeTypeDelegation:
158+
a.Delegation = new(Delegation)
159+
return s.Decode(a.Delegation)
160+
default:
161+
return fmt.Errorf("eip8130: invalid account change type byte 0x%x", typeByte[0])
162+
}
163+
}
164+
165+
// MarshalJSON encodes the entry as its body object plus a "type" discriminator.
166+
func (a AccountChange) MarshalJSON() ([]byte, error) {
167+
var (
168+
typ string
169+
body interface{}
170+
)
171+
switch {
172+
case a.Create != nil:
173+
typ, body = "create", a.Create
174+
case a.ConfigChange != nil:
175+
typ, body = "configChange", a.ConfigChange
176+
case a.Delegation != nil:
177+
typ, body = "delegation", a.Delegation
178+
default:
179+
return nil, errors.New("eip8130: empty account change")
180+
}
181+
raw, err := json.Marshal(body)
182+
if err != nil {
183+
return nil, err
184+
}
185+
fields := map[string]json.RawMessage{}
186+
if err := json.Unmarshal(raw, &fields); err != nil {
187+
return nil, err
188+
}
189+
fields["type"], _ = json.Marshal(typ)
190+
return json.Marshal(fields)
191+
}
192+
193+
// UnmarshalJSON dispatches on the "type" discriminator.
194+
func (a *AccountChange) UnmarshalJSON(input []byte) error {
195+
var tag struct {
196+
Type string `json:"type"`
197+
}
198+
if err := json.Unmarshal(input, &tag); err != nil {
199+
return err
200+
}
201+
switch tag.Type {
202+
case "create":
203+
a.Create = new(CreateEntry)
204+
return json.Unmarshal(input, a.Create)
205+
case "configChange":
206+
a.ConfigChange = new(ConfigChange)
207+
return json.Unmarshal(input, a.ConfigChange)
208+
case "delegation":
209+
a.Delegation = new(Delegation)
210+
return json.Unmarshal(input, a.Delegation)
211+
default:
212+
return fmt.Errorf("eip8130: unknown account change type %q", tag.Type)
213+
}
214+
}

core/types/eip8130_call.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// Copyright 2026 The go-ethereum Authors
2+
// This file is part of the go-ethereum library.
3+
//
4+
// The go-ethereum library is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU Lesser General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// The go-ethereum library is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU Lesser General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU Lesser General Public License
15+
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
16+
17+
package types
18+
19+
import (
20+
"github.com/ethereum/go-ethereum/common"
21+
"github.com/ethereum/go-ethereum/common/hexutil"
22+
)
23+
24+
// Call is a single call dispatched by the protocol during EIP-8130 transaction
25+
// execution. The wire form is rlp([to, data]); the dispatched call carries no
26+
// value. Calls are grouped into phases on the transaction as [][]Call, which
27+
// encodes as rlp([rlp([Call, ...]), ...]).
28+
type Call struct {
29+
To common.Address `json:"to"`
30+
Data hexutil.Bytes `json:"data"`
31+
}

core/types/eip8130_tx.go

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
// Copyright 2026 The go-ethereum Authors
2+
// This file is part of the go-ethereum library.
3+
//
4+
// The go-ethereum library is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU Lesser General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// The go-ethereum library is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU Lesser General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU Lesser General Public License
15+
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
16+
17+
package types
18+
19+
import (
20+
"bytes"
21+
"math/big"
22+
23+
"github.com/ethereum/go-ethereum/common"
24+
"github.com/ethereum/go-ethereum/rlp"
25+
)
26+
27+
// Eip8130Tx is a field-aware representation of an EIP-8130 transaction. The nested
28+
// account_changes and calls structures are modelled as typed Go values; the
29+
// authorization blobs are kept as raw bytes.
30+
type Eip8130Tx struct {
31+
ChainID *big.Int
32+
Sender *common.Address `rlp:"nil"` // nil means the empty EOA path
33+
NonceKey *big.Int
34+
NonceSequence uint64
35+
Expiry uint64
36+
GasTipCap *big.Int // a.k.a. maxPriorityFeePerGas
37+
GasFeeCap *big.Int // a.k.a. maxFeePerGas
38+
GasLimit uint64
39+
AccountChanges []AccountChange // account-mutation entries applied before calls execute
40+
Calls [][]Call // calls grouped into phases
41+
Metadata []byte // opaque attribution bytes; not interpreted by the protocol
42+
Payer *common.Address `rlp:"nil"` // nil means self-pay
43+
SenderAuth []byte
44+
PayerAuth []byte
45+
}
46+
47+
// copy creates a deep copy of the transaction data and initializes all fields.
48+
func (tx *Eip8130Tx) copy() TxData {
49+
cpy := &Eip8130Tx{
50+
Sender: copyAddressPtr(tx.Sender),
51+
NonceSequence: tx.NonceSequence,
52+
Expiry: tx.Expiry,
53+
GasLimit: tx.GasLimit,
54+
AccountChanges: append([]AccountChange(nil), tx.AccountChanges...),
55+
Calls: copyCalls(tx.Calls),
56+
Metadata: common.CopyBytes(tx.Metadata),
57+
Payer: copyAddressPtr(tx.Payer),
58+
SenderAuth: common.CopyBytes(tx.SenderAuth),
59+
PayerAuth: common.CopyBytes(tx.PayerAuth),
60+
// These are copied below.
61+
ChainID: new(big.Int),
62+
NonceKey: new(big.Int),
63+
GasTipCap: new(big.Int),
64+
GasFeeCap: new(big.Int),
65+
}
66+
if tx.ChainID != nil {
67+
cpy.ChainID.Set(tx.ChainID)
68+
}
69+
if tx.NonceKey != nil {
70+
cpy.NonceKey.Set(tx.NonceKey)
71+
}
72+
if tx.GasTipCap != nil {
73+
cpy.GasTipCap.Set(tx.GasTipCap)
74+
}
75+
if tx.GasFeeCap != nil {
76+
cpy.GasFeeCap.Set(tx.GasFeeCap)
77+
}
78+
return cpy
79+
}
80+
81+
// accessors for innerTx.
82+
func (tx *Eip8130Tx) txType() byte { return Eip8130TxType }
83+
func (tx *Eip8130Tx) chainID() *big.Int { return tx.ChainID }
84+
func (tx *Eip8130Tx) accessList() AccessList { return nil }
85+
func (tx *Eip8130Tx) data() []byte { return nil }
86+
func (tx *Eip8130Tx) gas() uint64 { return tx.GasLimit }
87+
func (tx *Eip8130Tx) gasFeeCap() *big.Int { return tx.GasFeeCap }
88+
func (tx *Eip8130Tx) gasTipCap() *big.Int { return tx.GasTipCap }
89+
func (tx *Eip8130Tx) gasPrice() *big.Int { return tx.GasFeeCap }
90+
func (tx *Eip8130Tx) value() *big.Int { return common.Big0 }
91+
func (tx *Eip8130Tx) nonce() uint64 { return tx.NonceSequence }
92+
func (tx *Eip8130Tx) to() *common.Address { return nil }
93+
func (tx *Eip8130Tx) isSystemTx() bool { return false }
94+
95+
func (tx *Eip8130Tx) effectiveGasPrice(dst *big.Int, baseFee *big.Int) *big.Int {
96+
if baseFee == nil {
97+
return dst.Set(tx.GasFeeCap)
98+
}
99+
tip := dst.Sub(tx.GasFeeCap, baseFee)
100+
if tip.Cmp(tx.GasTipCap) > 0 {
101+
tip.Set(tx.GasTipCap)
102+
}
103+
return tip.Add(tip, baseFee)
104+
}
105+
106+
// rawSignatureValues returns zeroes. EIP-8130 carries its authorization in the
107+
// sender_auth and payer_auth fields rather than in v, r, s.
108+
func (tx *Eip8130Tx) rawSignatureValues() (v, r, s *big.Int) {
109+
return common.Big0, common.Big0, common.Big0
110+
}
111+
112+
// setSignatureValues only records the chain ID. EIP-8130 has no canonical
113+
// v, r, s; the authorization lives in the transaction fields.
114+
func (tx *Eip8130Tx) setSignatureValues(chainID, v, r, s *big.Int) {
115+
tx.ChainID = chainID
116+
}
117+
118+
// copyCalls deep-copies the call phases.
119+
func copyCalls(calls [][]Call) [][]Call {
120+
if calls == nil {
121+
return nil
122+
}
123+
cpy := make([][]Call, len(calls))
124+
for i, phase := range calls {
125+
cpy[i] = append([]Call(nil), phase...)
126+
}
127+
return cpy
128+
}
129+
130+
func (tx *Eip8130Tx) encode(b *bytes.Buffer) error {
131+
// Empty account_changes and calls slices encode as the canonical RLP empty
132+
// list (0xc0), keeping the 2718 stream's element count stable.
133+
return rlp.Encode(b, tx)
134+
}
135+
136+
func (tx *Eip8130Tx) decode(input []byte) error {
137+
return rlp.DecodeBytes(input, tx)
138+
}
139+
140+
// sigHash is not implemented: EIP-8130 puts its authorization in the sender_auth
141+
// and payer_auth fields, not in the canonical (v, r, s) fields, and any recovery
142+
// uses an EIP-8130-specific payload, so the generic signing-hash path does not apply.
143+
func (tx *Eip8130Tx) sigHash(*big.Int) common.Hash {
144+
panic("eip8130 transactions are not signed")
145+
}

0 commit comments

Comments
 (0)