diff --git a/core/types/rlp_backwards_compat.libevm_test.go b/core/types/rlp_backwards_compat.libevm_test.go
new file mode 100644
index 00000000000..6d14c3566f6
--- /dev/null
+++ b/core/types/rlp_backwards_compat.libevm_test.go
@@ -0,0 +1,109 @@
+// Copyright 2024 the libevm authors.
+//
+// The libevm additions to go-ethereum are free software: you can redistribute
+// them and/or modify them under the terms of the GNU Lesser General Public License
+// as published by the Free Software Foundation, either version 3 of the License,
+// or (at your option) any later version.
+//
+// The libevm additions are distributed in the hope that they will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser
+// General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see
+// .
+
+package types_test
+
+import (
+ "encoding/hex"
+ "math/big"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ . "github.com/ava-labs/libevm/core/types"
+ "github.com/ava-labs/libevm/libevm/ethtest"
+ "github.com/ava-labs/libevm/rlp"
+)
+
+func TestHeaderRLPBackwardsCompatibility(t *testing.T) {
+ // This is a deliberate change-detector test that locks in backwards
+ // compatibility of RLP encoding.
+ rng := ethtest.NewPseudoRand(42)
+
+ const numExtraBytes = 16
+ hdr := &Header{
+ ParentHash: rng.Hash(),
+ UncleHash: rng.Hash(),
+ Coinbase: rng.Address(),
+ Root: rng.Hash(),
+ TxHash: rng.Hash(),
+ ReceiptHash: rng.Hash(),
+ // Bloom populated below
+ Difficulty: rng.Uint256().ToBig(),
+ Number: rng.BigUint64(),
+ GasLimit: rng.Uint64(),
+ GasUsed: rng.Uint64(),
+ Time: rng.Uint64(),
+ Extra: make([]byte, numExtraBytes), // populated below
+ MixDigest: rng.Hash(),
+ // Nonce populated below
+
+ BaseFee: rng.BigUint64(),
+ WithdrawalsHash: rng.HashPtr(),
+ BlobGasUsed: rng.Uint64Ptr(),
+ ExcessBlobGas: rng.Uint64Ptr(),
+ ParentBeaconRoot: rng.HashPtr(),
+ }
+ require.Equal(t, BloomByteLength, rng.Read(hdr.Bloom[:]))
+ require.Equal(t, len(BlockNonce{}), rng.Read(hdr.Nonce[:]))
+ require.Equal(t, numExtraBytes, rng.Read(hdr.Extra))
+
+ // WARNING: changing this hex might break backwards compatibility of RLP
+ // encoding (i.e. block hashes might change)!
+ const wantHex = `f9029aa01a571e7e4d774caf46053201cfe0001b3c355ffcc93f510e671e8809741f0eeda0756095410506ec72a2c287fe83ebf68efb0be177e61acec1c985277e90e52087941bfc3bc193012ba58912c01fb35a3454831a8971a00bc9f064144eb5965c5e5d1020f9f90392e7e06ded9225966abc7c754b410e61a0d942eab201424f4320ec1e1ffa9390baf941629b9349977b5d48e0502dbb9386a035d9d550a9c113f78689b4c161c4605609bb57b83061914c42ad244daa7fc38eb90100718d155798390a6c6782181d1bac1dd64cd956332b008412ddc735f2994e297c8a088c6bb4c637542295ba3cbc3cd399c8127076f4d834d74d5b11a36b6d02e2fe3a583216aa4ccef052df9a96e7a454256bebabdfc38c429079f25913e0f1d7416b2f056c4a115fc757012b1757d2d69f0e5fb87c08605098d9031fa37cd0df6942c5a2da12a4424b978febf5479896165caf573cf82fb3aa10f6ebf6b62bef8ed36b8ea3d4b1ddb80c99afafa37cb8f3393eb6d802f5bc6c8cd6bcd168a7e0061a718218b848d945135b6dff228a4e66bade4717e6f4d318ac98fca12a053af6f98805a764fb5d523cb6f69029522cab9ced907cc75718f7e2c79154ef3fc7a04b31d39ae246d689f23176d679a62ff328f530407cbafd0146f45b2ed635282e88b36f6a5752feff5b881fc7fa9ef217f81d889f073433138e6ba58857515405d28f2a8e904bcda3066d382675f37dd1a18507b5fba02812f2701021506f27190adb52a1313f6d28c77d66ae1aa3d3d6757a762476f488294c7768cddd9ccf881b5da1b6a47970a3a0c8a2b7b2c44161190c82d5e1c8b55e05c7354f1e5f6512924c941fb3d93667dc889bc9df25654e163c88859405c51041475fa03a8c304a732153e20300c3482832d07b65f97958360da414cb438ce252aec6c2`
+ want, err := hex.DecodeString(wantHex)
+ require.NoError(t, err, "hex.DecodeString()")
+
+ got, err := rlp.EncodeToBytes(hdr)
+ require.NoErrorf(t, err, "rlp.EncodeToBytes(%T)", hdr)
+ assert.Equalf(t, want, got, "rlp.EncodeToBytes(%T)", hdr)
+
+ t.Run("ParseTree", func(t *testing.T) {
+ got, err := rlp.ParseTree(got)
+ require.NoErrorf(t, err, "rlp.ParseTree(rlp.EncodeToBytes(%T))", hdr)
+
+ type (
+ l = rlp.ListNode
+ s = rlp.StringNode
+ )
+ u64Bytes := func(u uint64) []byte { return new(big.Int).SetUint64(u).Bytes() }
+ want := l{
+ s(hdr.ParentHash[:]),
+ s(hdr.UncleHash[:]),
+ s(hdr.Coinbase[:]),
+ s(hdr.Root[:]),
+ s(hdr.TxHash[:]),
+ s(hdr.ReceiptHash[:]),
+ s(hdr.Bloom[:]),
+ s(hdr.Difficulty.Bytes()),
+ s(hdr.Number.Bytes()),
+ s(u64Bytes(hdr.GasLimit)),
+ s(u64Bytes(hdr.GasUsed)),
+ s(u64Bytes(hdr.Time)),
+ s(hdr.Extra[:]),
+ s(hdr.MixDigest[:]),
+ s(hdr.Nonce[:]),
+ s(hdr.BaseFee.Bytes()),
+ s(hdr.WithdrawalsHash[:]),
+ s(u64Bytes(*hdr.BlobGasUsed)),
+ s(u64Bytes(*hdr.ExcessBlobGas)),
+ s(hdr.ParentBeaconRoot[:]),
+ }
+
+ assert.Equal(t, want, got)
+ })
+}
diff --git a/rlp/tree.libevm.go b/rlp/tree.libevm.go
new file mode 100644
index 00000000000..1928f17d074
--- /dev/null
+++ b/rlp/tree.libevm.go
@@ -0,0 +1,194 @@
+// Copyright 2024 the libevm authors.
+//
+// The libevm additions to go-ethereum are free software: you can redistribute
+// them and/or modify them under the terms of the GNU Lesser General Public License
+// as published by the Free Software Foundation, either version 3 of the License,
+// or (at your option) any later version.
+//
+// The libevm additions are distributed in the hope that they will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser
+// General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see
+// .
+
+package rlp
+
+import (
+ "encoding/binary"
+ "errors"
+ "io"
+)
+
+// An ItemNode is a parsed RLP item as part of a tree, which may have only a
+// root node. Nodes contain only their unpacked values, not their length- and
+// type-denoting tags.
+type ItemNode interface {
+ rlpItem()
+}
+
+var _ = []ItemNode{ListNode(nil), StringNode(nil), ByteNode(0)}
+
+// A ListNode is a slice of RLP items. It is the ItemNode equivalent of [List].
+type ListNode []ItemNode
+
+// A StringNode is an RLP [ItemNode] holding an arbitrary byte slice. It is the
+// ItemNode equivalent of [String].
+type StringNode []byte
+
+// An ByteNode is an RLP [ItemNode] representing an unsigned integer <= 127. It
+// is the ItemNode equivalent of [Byte].
+//
+// [ParseTree] will only return an ByteNode if the value is in the range [0,127]
+// but an ByteNode MAY be outside of this range for the purpose of re-encoding.
+type ByteNode byte
+
+func (ListNode) rlpItem() {}
+func (StringNode) rlpItem() {}
+func (ByteNode) rlpItem() {}
+
+var (
+ errConcatenated = errors.New("concatenated items outside of list")
+ errTrailingBytes = errors.New("trailing bytes after parsing")
+ errTooLong = errors.New("parsing >8 big-endian bytes")
+)
+
+// ParseTree parses the RLP-encoded buffer and returns one of the concrete
+// ItemNode types. All [StringNode] instances will be backed by the same memory
+// as the argument received by ParseTree.
+func ParseTree(rlp []byte) (ItemNode, error) {
+ return parse(rlp, false /*inList*/)
+}
+
+// parseList is a convenience wrapper around [slicer.short] and [slicer.long],
+// returning their return buffer as a [ListNode].
+func parseList(str []byte, err error) (ItemNode, error) {
+ if err != nil {
+ return nil, err
+ }
+ return parse(str, true)
+}
+
+func parse(rlp []byte, inList bool) (ItemNode, error) {
+ buf := &slicer{buf: rlp, i: 0}
+ var items []ItemNode
+
+ for eof := false; !eof; {
+ switch tag, err := buf.byte(); {
+ case err == io.EOF:
+ eof = true
+
+ case err != nil:
+ // Impossible but being defensive in case of a future refactor.
+ return nil, err
+
+ case tag <= 0x7f:
+ items = append(items, ByteNode(tag))
+
+ case tag <= 0xb7:
+ str, err := buf.short(tag, 0x80)
+ if err != nil {
+ return nil, err
+ }
+ items = append(items, StringNode(str))
+
+ case tag <= 0xbf:
+ str, err := buf.long(tag, 0xb7)
+ if err != nil {
+ return nil, err
+ }
+ items = append(items, StringNode(str))
+
+ case tag <= 0xf7:
+ list, err := parseList(buf.short(tag, 0xc0))
+ if err != nil {
+ return nil, err
+ }
+ items = append(items, list)
+
+ default:
+ list, err := parseList(buf.long(tag, 0xf7))
+ if err != nil {
+ return nil, err
+ }
+ items = append(items, list)
+ }
+
+ if !inList && len(items) > 1 {
+ return nil, errConcatenated
+ }
+ }
+
+ if n := buf.left(); n > 0 {
+ return nil, errTrailingBytes
+ }
+ if inList {
+ return ListNode(items), nil
+ }
+ return items[0], nil
+}
+
+// A slicer is a byte-slice reader that returns slices backed by the same memory
+// as its buffer.
+type slicer struct {
+ buf []byte
+ i uint64
+}
+
+func (s *slicer) len() uint64 {
+ return uint64(len(s.buf))
+}
+
+func (s *slicer) left() uint64 {
+ return s.len() - s.i
+}
+
+// next returns the next `n` bytes.
+func (s *slicer) next(n uint64) ([]byte, error) {
+ if n > s.left() {
+ return nil, io.EOF
+ }
+ b := s.buf[s.i : s.i+n]
+ s.i += n
+ return b, nil
+}
+
+func (s *slicer) byte() (byte, error) {
+ b, err := s.next(1)
+ if err != nil {
+ return 0, err
+ }
+ return b[0], nil
+}
+
+// short returns the bytes encoding either a string or a list of <=55 bytes.
+func (s *slicer) short(tag, base byte) ([]byte, error) {
+ return s.next(uint64(tag - base))
+}
+
+// long returns the bytes encoding either a string or a list of >55 bytes, first
+// reading the length.
+func (s *slicer) long(tag, base byte) ([]byte, error) {
+ n, err := s.bigEndian(uint64(tag - base))
+ if err != nil {
+ return nil, err
+ }
+ return s.next(n)
+}
+
+// bigEndian returns the next `nBytes` bytes interpreted as a big-endian uint64.
+func (s *slicer) bigEndian(nBytes uint64) (uint64, error) {
+ if nBytes > 8 {
+ return 0, errTooLong
+ }
+ buf, err := s.next(nBytes)
+ if err != nil {
+ return 0, err
+ }
+
+ var padded [8]byte
+ copy(padded[8-len(buf):], buf)
+ return binary.BigEndian.Uint64(padded[:]), nil
+}
diff --git a/rlp/tree.libevm_test.go b/rlp/tree.libevm_test.go
new file mode 100644
index 00000000000..fc8ad3b322f
--- /dev/null
+++ b/rlp/tree.libevm_test.go
@@ -0,0 +1,144 @@
+// Copyright 2024 the libevm authors.
+//
+// The libevm additions to go-ethereum are free software: you can redistribute
+// them and/or modify them under the terms of the GNU Lesser General Public License
+// as published by the Free Software Foundation, either version 3 of the License,
+// or (at your option) any later version.
+//
+// The libevm additions are distributed in the hope that they will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser
+// General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see
+// .
+
+package rlp
+
+import (
+ "bytes"
+ "encoding/hex"
+ "math/rand"
+ "runtime"
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+ "github.com/google/go-cmp/cmp/cmpopts"
+ "github.com/stretchr/testify/require"
+)
+
+func TestParseTreeNoErrorOnUpstreamTests(t *testing.T) {
+ for _, tt := range encTests {
+ t.Run("", func(t *testing.T) {
+ if tt.error != "" {
+ return
+ }
+ switch tt.val.(type) {
+ // These return arbitrary byte slices, not real RLP.
+ case *testEncoder:
+ return
+ case struct{ E testEncoderValueMethod }:
+ return
+ case undecodableEncoder:
+ return
+ }
+
+ buf, err := hex.DecodeString(tt.output)
+ require.NoErrorf(t, err, "hex.DecodeString(%T.output)", tt)
+
+ _, err = ParseTree(buf)
+ require.NoErrorf(t, err, "ParseTree(%T => %#x)", tt.val, buf)
+ })
+ }
+}
+
+func TestParseTree(t *testing.T) {
+ t.Parallel()
+
+ type test struct {
+ fuzzed bool
+ fuzzSeed int64
+ value any
+ want ItemNode
+ }
+ tests := []test{{
+ value: []byte(nil),
+ want: StringNode{},
+ }}
+
+ for i := 0; i < 3e3; i++ {
+ seed := int64(i)
+ rng := rand.New(rand.NewSource(seed)) //nolint:gosec // Not security; reproducible (deterministic) fuzzing is useful
+ val, node := randomItemNode(rng)
+ tests = append(tests, test{
+ fuzzed: true,
+ fuzzSeed: seed,
+ value: val,
+ want: node,
+ })
+ }
+
+ throttle := make(chan struct{}, runtime.GOMAXPROCS(0))
+ for _, tt := range tests {
+ tt := tt
+ t.Run("", func(t *testing.T) {
+ t.Parallel()
+ throttle <- struct{}{}
+ defer func() { <-throttle }()
+
+ if tt.fuzzed {
+ t.Logf("Fuzzing seed: %d", tt.fuzzSeed)
+ }
+
+ buf := bytes.NewBuffer(nil)
+ require.NoError(t, Encode(buf, tt.value))
+ t.Logf("RLP encoding of %T: %#x\nValue: %x", tt.value, buf.Bytes(), tt.value)
+
+ got, err := ParseTree(buf.Bytes())
+ require.NoError(t, err)
+
+ if diff := cmp.Diff(tt.want, got, cmpopts.EquateEmpty()); diff != "" {
+ t.Errorf("%s", diff)
+ }
+ })
+ }
+}
+
+func randomItemNode(rng *rand.Rand) (any, ItemNode) {
+ switch rng.Intn(3) {
+ case 0:
+ v := byte(rng.Intn(128))
+ if v == 0 {
+ // https://ethereum.org/en/developers/docs/data-structures-and-encoding/rlp/#:~:text=thus%20making%20the%20integer%20value%20zero%20equivalent%20to%20the%20empty%20byte%20array
+ return v, StringNode{}
+ }
+ return v, ByteNode(v)
+
+ case 1:
+ v := make([]byte, rng.Intn(110)) // >55 is a "long" string so ~half of each
+ rng.Read(v) //nolint:gosec // Documented as returning nil error
+ v = bytes.TrimLeft(v, "\x00")
+
+ var node ItemNode
+ switch n := len(v); {
+ case n == 1 && v[0] < 128:
+ node = ByteNode(v[0])
+ default:
+ node = StringNode(v)
+ }
+ return v, node
+
+ default:
+ var (
+ vals []any
+ list ListNode
+ )
+ for i, n := 0, rng.Intn(5); i < n; i++ {
+ v, item := randomItemNode(rng)
+ vals = append(vals, v)
+ list = append(list, item)
+ }
+ return vals, list
+ }
+}