Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
27 changes: 27 additions & 0 deletions src/Nethermind/Nethermind.Core/Eip7851Constants.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited
// SPDX-License-Identifier: LGPL-3.0-only

using System;

namespace Nethermind.Core;

/// <summary>
/// Represents the <see href="https://eips.ethereum.org/EIPS/eip-7851">EIP-7851</see>
/// (code-controlled EOA delegation) parameters.
/// </summary>
public static class Eip7851Constants
{
private static readonly byte[] _delegationHeader = [0xef, 0x01, 0x01];

/// <summary>
/// The ECDSA-disabled delegation designator prefix. Accounts whose code is exactly
/// <c>0xef0101 || delegate_address</c> can no longer authorize transactions or EIP-7702
/// delegation changes with their ECDSA key; only the delegated wallet code can update the
/// delegation via SETSELFDELEGATE.
/// </summary>
public static ReadOnlySpan<byte> DelegationHeader => _delegationHeader.AsSpan();

public static bool IsEcdsaDisabledDelegatedCode(ReadOnlySpan<byte> code) =>
code.Length == _delegationHeader.Length + Address.Size
&& DelegationHeader.SequenceEqual(code[.._delegationHeader.Length]);
}
1 change: 1 addition & 0 deletions src/Nethermind/Nethermind.Core/GasCostOf.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ public static class GasCostOf
public const ulong PerAuthBaseCost = Eip7702Constants.PerAuthBaseCost;
public const ulong TotalCostFloorPerTokenEip7623 = 10; // eip-7623
public const ulong TotalCostFloorPerTokenEip7976 = 16; // eip-7976
public const ulong SetSelfDelegate = 9500; // eip-7851

public const ulong CostPerStateByte = 1530; // eip-8037
public const ulong StateBytesPerStorageSet = 64; // eip-8037
Expand Down
6 changes: 6 additions & 0 deletions src/Nethermind/Nethermind.Core/Specs/IReleaseSpec.cs
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,12 @@ public interface IReleaseSpec : IEip1559Spec, IReceiptSpec
/// </summary>
public bool IsEip7954Enabled { get; }

/// <summary>
/// EIP-7851: Code-Controlled EOA Delegation.
/// SETSELFDELEGATE opcode and the 0xef0101 ECDSA-disabled delegation designator.
/// </summary>
public bool IsEip7851Enabled { get; }

/// <summary>
/// Precomputed gas cost and refund constants derived from this spec.
/// Values are cached per spec instance (singletons per fork) to avoid
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,5 +117,6 @@ public class ReleaseSpecDecorator(IReleaseSpec spec) : IReleaseSpec
public virtual bool IsEip7843Enabled => spec.IsEip7843Enabled;
public virtual bool IsEip7954Enabled => spec.IsEip7954Enabled;
public virtual bool IsEip8024Enabled => spec.IsEip8024Enabled;
public virtual bool IsEip7851Enabled => spec.IsEip7851Enabled;
public SpecGasCosts GasCosts => spec.GasCosts;
}
227 changes: 227 additions & 0 deletions src/Nethermind/Nethermind.Evm.Test/Eip7851Tests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
// SPDX-FileCopyrightText: 2026 Demerzel Solutions Limited
// SPDX-License-Identifier: LGPL-3.0-only

using System;
using Nethermind.Blockchain.Tracing;
using Nethermind.Core;
using Nethermind.Core.Crypto;
using Nethermind.Core.Extensions;
using Nethermind.Core.Specs;
using Nethermind.Core.Test.Builders;
using Nethermind.Crypto;
using Nethermind.Evm.State;
using Nethermind.Int256;
using Nethermind.Evm.Tracing;
using Nethermind.Evm.TransactionProcessing;
using Nethermind.Specs;
using Nethermind.Specs.Forks;
using Nethermind.Specs.Test;
using NUnit.Framework;

namespace Nethermind.Evm.Test;

/// <summary>
/// Tests for EIP-7851: SETSELFDELEGATE opcode and the 0xef0101 ECDSA-disabled delegation
/// designator.
/// </summary>
[TestFixture]
public class Eip7851Tests : VirtualMachineTestsBase
{
private static readonly Address NewDelegate = TestItem.AddressC;
// Must differ from the harness defaults (sender = AddressA, recipient = AddressB).
private static readonly Address Eoa = TestItem.PrivateKeyD.Address;
private static readonly Address Wallet = TestItem.AddressE;

private readonly EthereumEcdsa _ecdsa = new(1);

protected override ISpecProvider SpecProvider { get; } =
new TestSpecProvider(new OverridableReleaseSpec(Prague.Instance) { IsEip7851Enabled = true });

[SetUp]
public override void Setup()
{
base.Setup();
TestState.CreateAccount(TestItem.PrivateKeyA.Address, 1000.Ether);
TestState.Commit(SpecProvider.GenesisSpec);
TestState.CommitTree(0);
}

private void SetCode(Address account, byte[] code)
{
if (!TestState.AccountExists(account))
{
TestState.CreateAccount(account, 0);
}

TestState.InsertCode(account, ValueKeccak.Compute(code), code, Spec);
TestState.Commit(Spec);
}

private static byte[] Designator(ReadOnlySpan<byte> header, Address delegate_) =>
[.. header, .. delegate_.Bytes];

private byte[] WalletCodeSettingDelegate(Address delegateAddress) => Prepare.EvmCode
.PushData(delegateAddress)
.Op(Instruction.SETSELFDELEGATE)
.PushData(0)
.Op(Instruction.MSTORE)
.PushData(32)
.PushData(0)
.Op(Instruction.RETURN)
.Done;

private byte[] CallEoa(PrivateKey sender = null, ulong nonce = 0)
{
Transaction tx = Build.A.Transaction
.To(Eoa)
.WithNonce(nonce)
.WithGasLimit(100000)
.SignedAndResolved(_ecdsa, sender ?? TestItem.PrivateKeyA)
.TestObject;
Block block = Build.A.Block.WithNumber(BlockNumber).WithTimestamp(Timestamp).WithTransactions(tx).WithGasLimit(1000000).TestObject;
CallOutputTracer tracer = new();
_processor.Execute(tx, new BlockExecutionContext(block.Header, SpecProvider.GetSpec(block.Header)), tracer);
return tracer.ReturnValue;
}

[Test]
public void SetSelfDelegate_from_7702_context_disables_ecdsa_and_updates_delegate()
{
SetCode(Wallet, WalletCodeSettingDelegate(NewDelegate));
SetCode(Eoa, Designator(Eip7702Constants.DelegationHeader, Wallet));

byte[] returnValue = CallEoa();

using (Assert.EnterMultipleScope())
{
Assert.That(returnValue, Is.EqualTo(UInt256.One.ToBigEndian()), "must push 1 on success");
Assert.That(TestState.GetCode(Eoa), Is.EqualTo(Designator(Eip7851Constants.DelegationHeader, NewDelegate)));
}
}

[Test]
public void SetSelfDelegate_from_ecdsa_disabled_context_updates_delegate()
{
SetCode(Wallet, WalletCodeSettingDelegate(NewDelegate));
SetCode(Eoa, Designator(Eip7851Constants.DelegationHeader, Wallet));

byte[] returnValue = CallEoa();

using (Assert.EnterMultipleScope())
{
Assert.That(returnValue, Is.EqualTo(UInt256.One.ToBigEndian()));
Assert.That(TestState.GetCode(Eoa), Is.EqualTo(Designator(Eip7851Constants.DelegationHeader, NewDelegate)));
}
}

[Test]
public void SetSelfDelegate_with_zero_delegate_fails_without_state_change()
{
SetCode(Wallet, WalletCodeSettingDelegate(Address.Zero));
byte[] designator = Designator(Eip7702Constants.DelegationHeader, Wallet);
SetCode(Eoa, designator);

byte[] returnValue = CallEoa();

using (Assert.EnterMultipleScope())
{
Assert.That(returnValue, Is.EqualTo(UInt256.Zero.ToBigEndian()), "must push 0 for zero delegate");
Assert.That(TestState.GetCode(Eoa), Is.EqualTo(designator), "state must not change");
}
}

[Test]
public void SetSelfDelegate_outside_delegated_context_fails()
{
// The wallet executes its own code directly — the executing account's code is not a
// 23-byte designator, so the opcode must fail with 0.
byte[] walletCode = WalletCodeSettingDelegate(NewDelegate);
SetCode(Wallet, walletCode);

TestAllTracerWithOutput result = Execute(Prepare.EvmCode
.Call(Wallet, 50000)
.Op(Instruction.STOP)
.Done);

using (Assert.EnterMultipleScope())
{
Assert.That(result.StatusCode, Is.EqualTo(StatusCode.Success));
Assert.That(TestState.GetCode(Wallet), Is.EqualTo(walletCode), "wallet code must not change");
}
}

[Test]
public void SetSelfDelegate_in_static_context_halts_exceptionally()
{
SetCode(Wallet, WalletCodeSettingDelegate(NewDelegate));
byte[] designator = Designator(Eip7702Constants.DelegationHeader, Wallet);
SetCode(Eoa, designator);

// STATICCALL into the delegated EOA must fail (returns 0 on the caller's stack).
byte[] outer = Prepare.EvmCode
.PushData(0).PushData(0).PushData(0).PushData(0)
.PushData(Eoa)
.PushData(50000)
.Op(Instruction.STATICCALL)
.PushData(0)
.Op(Instruction.MSTORE)
.PushData(32)
.PushData(0)
.Op(Instruction.RETURN)
.Done;

TestAllTracerWithOutput result = Execute(outer);

using (Assert.EnterMultipleScope())
{
Assert.That(result.ReturnValue, Is.EqualTo(UInt256.Zero.ToBigEndian()), "STATICCALL must report failure");
Assert.That(TestState.GetCode(Eoa), Is.EqualTo(designator), "state must not change");
}
}

[Test]
public void Ecdsa_disabled_sender_transaction_is_rejected()
{
SetCode(Eoa, Designator(Eip7851Constants.DelegationHeader, Wallet));
TestState.AddToBalance(Eoa, 1.Ether, Spec);
TestState.Commit(Spec);

Transaction tx = Build.A.Transaction
.To(TestItem.AddressF)
.WithGasLimit(100000)
.SignedAndResolved(_ecdsa, TestItem.PrivateKeyD)
.TestObject;
Block block = Build.A.Block.WithNumber(BlockNumber).WithTimestamp(Timestamp).WithTransactions(tx).WithGasLimit(1000000).TestObject;

TransactionResult result = _processor.Execute(tx, new BlockExecutionContext(block.Header, SpecProvider.GetSpec(block.Header)), NullTxTracer.Instance);

Assert.That(result.TransactionExecuted, Is.False, "ECDSA-authenticated tx from an 0xef0101 account must be invalid");
}

[Test]
public void Authorization_from_ecdsa_disabled_account_is_skipped()
{
byte[] designator = Designator(Eip7851Constants.DelegationHeader, Wallet);
SetCode(Eoa, designator);

AuthorizationTuple authorization = _ecdsa.Sign(TestItem.PrivateKeyD, 1, NewDelegate, TestState.GetNonce(Eoa));
Transaction tx = Build.A.Transaction
.WithType(TxType.SetCode)
.To(TestItem.AddressF)
.WithAuthorizationCode([authorization])
.WithMaxFeePerGas(1.GWei)
.WithMaxPriorityFeePerGas(1.GWei)
.WithGasLimit(200000)
.SignedAndResolved(_ecdsa, TestItem.PrivateKeyA)
.TestObject;
Block block = Build.A.Block.WithNumber(BlockNumber).WithTimestamp(Timestamp).WithTransactions(tx).WithGasLimit(1000000).TestObject;

TransactionResult result = _processor.Execute(tx, new BlockExecutionContext(block.Header, SpecProvider.GetSpec(block.Header)), NullTxTracer.Instance);

using (Assert.EnterMultipleScope())
{
Assert.That(result.TransactionExecuted, Is.True);
Assert.That(TestState.GetCode(Eoa), Is.EqualTo(designator), "authorization from an ECDSA-disabled account must not change its delegation");
}
}
}
6 changes: 4 additions & 2 deletions src/Nethermind/Nethermind.Evm/ICodeInfoRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,14 @@ public interface ICodeInfoRepository
bool TryGetDelegation(Address address, IReleaseSpec spec, [NotNullWhen(true)] out Address? delegatedAddress);

/// <remarks>
/// Parses delegation code to extract the contained address.
/// Parses delegation code to extract the contained address. Accepts both the EIP-7702
/// (<c>0xef0100</c>) and the EIP-7851 ECDSA-disabled (<c>0xef0101</c>) designators — calls
/// to an ECDSA-disabled account still execute its delegate.
/// <b>Assumes </b><paramref name="code"/> <b>is delegation code!</b>
/// </remarks>
static bool TryGetDelegatedAddress(ReadOnlySpan<byte> code, [NotNullWhen(true)] out Address? address)
{
if (Eip7702Constants.IsDelegatedCode(code))
if (Eip7702Constants.IsDelegatedCode(code) || Eip7851Constants.IsEcdsaDisabledDelegatedCode(code))
{
address = new Address(code[Eip7702Constants.DelegationHeader.Length..]);
return true;
Expand Down
1 change: 1 addition & 0 deletions src/Nethermind/Nethermind.Evm/Instruction.cs
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ public enum Instruction : byte
RETURN = 0xf3,
DELEGATECALL = 0xf4,
CREATE2 = 0xf5,
SETSELFDELEGATE = 0xf6, // EIP-7851 (opcode value TBD in the spec; placeholder)
STATICCALL = 0xfa,
REVERT = 0xfd,
INVALID = 0xfe,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.X86;
using Nethermind.Core;
using Nethermind.Core.Crypto;
using Nethermind.Core.Specs;
using Nethermind.Evm.GasPolicy;
using Nethermind.Evm.State;
Expand Down Expand Up @@ -283,6 +284,57 @@ public static EvmExceptionType InstructionSelfDestruct<TGasPolicy, TEip8037, TEi
return EvmExceptionType.StaticCallViolation;
}

/// <summary>
/// Executes the SETSELFDELEGATE opcode (EIP-7851).
/// Updates the executing account's delegation designator to the ECDSA-disabled form
/// <c>0xef0101 || delegate</c>, permanently disabling its ECDSA authority.
/// </summary>
/// <remarks>
/// Pushes 1 on success. Pushes 0 without any state change when the delegate address is zero
/// or the executing account's state code is not a 23-byte EIP-7702/EIP-7851 delegation
/// designator (i.e. the code is not running in a delegated EOA's own context).
/// </remarks>
[SkipLocalsInit]
public static EvmExceptionType InstructionSetSelfDelegate<TGasPolicy, TTracingInst>(VirtualMachine<TGasPolicy> vm, ref EvmStack stack, ref TGasPolicy gas, ref int programCounter)
where TGasPolicy : struct, IGasPolicy<TGasPolicy>
where TTracingInst : struct, IFlag
{
VmState<TGasPolicy> vmState = vm.VmState;
if (vmState.IsStatic)
goto StaticCallViolation;

if (!TGasPolicy.UpdateGas(ref gas, GasCostOf.SetSelfDelegate))
goto OutOfGas;

Address delegateAddress = stack.PopAddress();
if (delegateAddress is null)
goto StackUnderflow;

IWorldState state = vm.WorldState;
Address executingAccount = vmState.Env.ExecutingAccount;
byte[]? currentCode = state.GetCode(executingAccount);
bool isDelegatedContext = currentCode is not null
&& (Eip7702Constants.IsDelegatedCode(currentCode) || Eip7851Constants.IsEcdsaDisabledDelegatedCode(currentCode));

if (delegateAddress == Address.Zero || !isDelegatedContext)
return stack.PushZero<TTracingInst>();

byte[] newCode = new byte[Eip7851Constants.DelegationHeader.Length + Address.Size];
Eip7851Constants.DelegationHeader.CopyTo(newCode);
delegateAddress.Bytes.CopyTo(newCode.AsSpan(Eip7851Constants.DelegationHeader.Length));
ValueHash256 codeHash = ValueKeccak.Compute(newCode);
state.InsertCode(executingAccount, in codeHash, newCode, vm.Spec);

return stack.PushOne<TTracingInst>();
// Jump forward to be unpredicted by the branch predictor.
OutOfGas:
return EvmExceptionType.OutOfGas;
StackUnderflow:
return EvmExceptionType.StackUnderflow;
StaticCallViolation:
return EvmExceptionType.StaticCallViolation;
}

/// <summary>
/// Handles invalid opcodes by deducting a high gas cost and returning a BadInstruction error.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,11 @@ public static unsafe partial class EvmInstructions
lookup[(int)Instruction.REVERT] = &InstructionRevert;
}

if (spec.IsEip7851Enabled)
{
lookup[(int)Instruction.SETSELFDELEGATE] = &InstructionSetSelfDelegate<TGasPolicy, TTracingInst>;
}

// Final opcodes.
lookup[(int)Instruction.INVALID] = &InstructionInvalid;
lookup[(int)Instruction.SELFDESTRUCT] = (spec.IsEip8037Enabled, spec.IsEip7708Enabled) switch
Expand Down
Loading
Loading