Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

EIP 7742 implementation #7518

Open
wants to merge 46 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
de19765
Get target_blob_count, max_blob_count from Engine Api
yerke26 Sep 30, 2024
eca6690
fix HeaderDecoder, restore Eip4844 changes, Add Eip7742
yerke26 Oct 1, 2024
7ac90df
Merge branch 'master' into feature/eip-7742
yerke26 Oct 1, 2024
9be8a53
fix HeaderDecoder
yerke26 Oct 1, 2024
c592412
move new params from ExecutionPayload to ExecutionPayloadV4
yerke26 Oct 1, 2024
f6ae01c
revert minor changes
yerke26 Oct 1, 2024
f803097
added new params to PayloadAttributes
yerke26 Oct 1, 2024
7043124
fix tests
yerke26 Oct 1, 2024
fef9cb2
fix whitespaces
yerke26 Oct 1, 2024
cc02ce5
add tests
yerke26 Oct 2, 2024
206d514
Merge branch 'master' into feature/eip-7742
yerke26 Oct 2, 2024
9fbb9c1
add missing fields
yerke26 Oct 4, 2024
8ada4e2
fix PayloadAttributes
yerke26 Oct 7, 2024
3ae0fed
add engine_forkchoiceUpdatedV4 to EngineRpcModule.Prague
yerke26 Oct 7, 2024
372868a
change to engine_forkchoiceUpdatedV4
yerke26 Oct 7, 2024
0bd75c6
Merge branch 'master' into feature/eip-7742
yerke26 Oct 7, 2024
4b5fbfc
add new fields to EngineModuleTests.V4
yerke26 Oct 7, 2024
5078b76
added new engine_forkchoiceUpdatedV4 to EngineRpcCapabilitiesProvider
yerke26 Oct 7, 2024
a92cdde
fix test cases
yerke26 Oct 8, 2024
c032480
Merge branch 'master' into feature/eip-7742
yerke26 Oct 8, 2024
4ab5fda
fix whitespace
yerke26 Oct 8, 2024
0ba2006
fix EngineRpcCapabilitiesProvider, revert ChainSpecBasedSpecProviderT…
yerke26 Oct 8, 2024
911d232
remove unnecessary comment
yerke26 Oct 8, 2024
f806f22
remove comments
yerke26 Oct 8, 2024
8dc3d6a
Merge branch 'master' into feature/eip-7742
yerke26 Oct 15, 2024
a993d59
Merge branch 'master' into feature/eip-7742
yerke26 Oct 30, 2024
ba1a3de
Merge branch 'master' into feature/eip-7742
yerke26 Nov 4, 2024
87e721a
add eip7742 logic to TryCalculateFeePerBlobGas
yerke26 Nov 4, 2024
e52fc98
remove MaxBlobCount, pass Spec to BlobGasCalculator
yerke26 Nov 4, 2024
49424d9
Merge branch 'master' into feature/eip-7742
yerke26 Nov 4, 2024
2162240
resolve merge conflicts
yerke26 Nov 4, 2024
9d4ec74
add MaxBlobCount in PayloadAttributes, refactoring, resolve comments
yerke26 Nov 5, 2024
d8e088c
fix tests
yerke26 Nov 5, 2024
793d085
Merge branch 'master' into feature/eip-7742
yerke26 Nov 5, 2024
a4fd1ef
fix Evm tool
yerke26 Nov 5, 2024
d022c4f
fix Evm tool
yerke26 Nov 5, 2024
47437f2
Merge branch 'master' into feature/eip-7742
yerke26 Nov 25, 2024
020ceef
resolve merge conflicts
yerke26 Nov 25, 2024
5699ad4
fix some tests
yerke26 Nov 26, 2024
81fb376
Merge branch 'master' into feature/eip-7742
yerke26 Nov 26, 2024
332322d
fix tests
yerke26 Nov 27, 2024
91822aa
fix AuraTests
yerke26 Nov 28, 2024
b4790dd
fix tests
yerke26 Nov 28, 2024
36464ad
fix JsonRpcTests
yerke26 Nov 28, 2024
459ebb7
fix TraceRpcModuleTests
yerke26 Nov 28, 2024
71963b7
fix tests
yerke26 Nov 28, 2024
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
5 changes: 3 additions & 2 deletions src/Nethermind/Ethereum.Test.Base/GeneralTestBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ protected EthereumTestResult RunTest(GeneralStateTest test, ITxTracer txTracer)
header.ExcessBlobGas = test.CurrentExcessBlobGas ?? (test.Fork is Cancun ? 0ul : null);
header.BlobGasUsed = BlobGasCalculator.CalculateBlobGas(test.Transaction);
header.RequestsHash = test.RequestsHash;
if (test.Fork is Prague) header.TargetBlobCount = 0;

Stopwatch stopwatch = Stopwatch.StartNew();
IReleaseSpec? spec = specProvider.GetSpec((ForkActivation)test.CurrentNumber);
Expand All @@ -129,14 +130,14 @@ protected EthereumTestResult RunTest(GeneralStateTest test, ITxTracer txTracer)
BlobGasUsed = (ulong)test.ParentBlobGasUsed,
ExcessBlobGas = (ulong)test.ParentExcessBlobGas,
};
header.ExcessBlobGas = BlobGasCalculator.CalculateExcessBlobGas(parent, spec);
header.ExcessBlobGas = BlobGasCalculator.CalculateExcessBlobGas(parent, spec, header);
}

ValidationResult txIsValid = _txValidator.IsWellFormed(test.Transaction, spec);

if (txIsValid)
{
transactionProcessor.Execute(test.Transaction, new BlockExecutionContext(header), txTracer);
transactionProcessor.Execute(test.Transaction, new BlockExecutionContext(header, spec), txTracer);
}
else
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,8 @@ protected override BlockProcessor CreateBlockProcessor()
blockGasLimitContractTransition.Key,
new ReadOnlyTxProcessingEnv(
WorldStateManager,
BlockTree.AsReadOnly(), SpecProvider, LimboLogs.Instance));
BlockTree.AsReadOnly(), SpecProvider, LimboLogs.Instance),
SpecProvider);

GasLimitOverrideCache = new AuRaContractGasLimitOverride.Cache();
GasLimitCalculator = new AuRaContractGasLimitOverride(new[] { gasLimitContract }, GasLimitOverrideCache, false, new FollowOtherMiners(SpecProvider), LimboLogs.Instance);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using Nethermind.Consensus.AuRa.Contracts;
using Nethermind.Core;
using Nethermind.Core.Extensions;
using Nethermind.Specs;
using NSubstitute;
using NUnit.Framework;

Expand All @@ -17,15 +18,15 @@ public class ReportingValidatorContractTests
[Test]
public void Should_generate_malicious_transaction()
{
ReportingValidatorContract contract = new(AbiEncoder.Instance, new Address("0x1000000000000000000000000000000000000001"), Substitute.For<ISigner>());
ReportingValidatorContract contract = new(AbiEncoder.Instance, new Address("0x1000000000000000000000000000000000000001"), Substitute.For<ISigner>(), TestSpecProvider.Instance);
Transaction transaction = contract.ReportMalicious(new Address("0x75df42383afe6bf5194aa8fa0e9b3d5f9e869441"), 10, []);
transaction.Data.AsArray().ToHexString().Should().Be("c476dd4000000000000000000000000075df42383afe6bf5194aa8fa0e9b3d5f9e869441000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000");
}

[Test]
public void Should_generate_benign_transaction()
{
ReportingValidatorContract contract = new(AbiEncoder.Instance, new Address("0x1000000000000000000000000000000000000001"), Substitute.For<ISigner>());
ReportingValidatorContract contract = new(AbiEncoder.Instance, new Address("0x1000000000000000000000000000000000000001"), Substitute.For<ISigner>(), TestSpecProvider.Instance);
Transaction transaction = contract.ReportBenign(new Address("0x75df42383afe6bf5194aa8fa0e9b3d5f9e869441"), 10);
transaction.Data.AsArray().ToHexString().Should().Be("d69f13bb00000000000000000000000075df42383afe6bf5194aa8fa0e9b3d5f9e869441000000000000000000000000000000000000000000000000000000000000000a");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ protected override TxPoolTxSource CreateTxPoolTxSource()
TxPoolTxSource txPoolTxSource = base.CreateTxPoolTxSource();

TxPriorityContract = new TxPriorityContract(AbiEncoder.Instance, TestItem.AddressA,
new ReadOnlyTxProcessingEnv(WorldStateManager, BlockTree.AsReadOnly(), SpecProvider, LimboLogs.Instance));
new ReadOnlyTxProcessingEnv(WorldStateManager, BlockTree.AsReadOnly(), SpecProvider, LimboLogs.Instance), SpecProvider);

Priorities = new DictionaryContractDataStore<TxPriorityContract.Destination>(
new TxPriorityContract.DestinationSortedListContractDataStoreCollection(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
using Nethermind.Evm.Tracing;
using Nethermind.Evm.TransactionProcessing;
using Nethermind.Logging;
using Nethermind.Specs;
using Nethermind.State;
using NSubstitute;
using NUnit.Framework;
Expand Down Expand Up @@ -52,7 +53,8 @@ public void constructor_throws_ArgumentNullException_on_null_contractAddress()
null,
_stateProvider,
_readOnlyTxProcessorSource,
new Signer(0, TestItem.PrivateKeyD, LimboLogs.Instance));
new Signer(0, TestItem.PrivateKeyD, LimboLogs.Instance),
TestSpecProvider.Instance);
action.Should().Throw<ArgumentNullException>();
}

Expand All @@ -78,12 +80,13 @@ public void finalize_change_should_call_correct_transaction()
_contractAddress,
_stateProvider,
_readOnlyTxProcessorSource,
new Signer(0, TestItem.PrivateKeyD, LimboLogs.Instance));
new Signer(0, TestItem.PrivateKeyD, LimboLogs.Instance),
TestSpecProvider.Instance);

contract.FinalizeChange(_block.Header);

_transactionProcessor.Received().Execute(
Arg.Is<Transaction>(t => IsEquivalentTo(expectation, t)), Arg.Is<BlockExecutionContext>(blkCtx => blkCtx.Header.Equals(_block.Header)), Arg.Any<ITxTracer>());
Arg.Is<Transaction>(t => IsEquivalentTo(expectation, t)), Arg.Is<BlockHeader>(blockHeader => blockHeader.Equals(_block.Header)), Arg.Any<ITxTracer>());
}

private static bool IsEquivalentTo(Transaction expected, Transaction item)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
using NSubstitute;
using NUnit.Framework;
using Nethermind.Evm;
using Nethermind.Specs;

namespace Nethermind.AuRa.Test.Reward
{
Expand Down Expand Up @@ -60,21 +61,21 @@ public void SetUp()
[Test]
public void constructor_throws_ArgumentNullException_on_null_auraParameters()
{
Action action = () => new AuRaRewardCalculator(null, _abiEncoder, _transactionProcessor);
Action action = () => new AuRaRewardCalculator(null, _abiEncoder, _transactionProcessor, TestSpecProvider.Instance);
action.Should().Throw<ArgumentNullException>();
}

[Test]
public void constructor_throws_ArgumentNullException_on_null_encoder()
{
Action action = () => new AuRaRewardCalculator(_auraParameters, null, _transactionProcessor);
Action action = () => new AuRaRewardCalculator(_auraParameters, null, _transactionProcessor, TestSpecProvider.Instance);
action.Should().Throw<ArgumentNullException>();
}

[Test]
public void constructor_throws_ArgumentNullException_on_null_transactionProcessor()
{
Action action = () => new AuRaRewardCalculator(_auraParameters, _abiEncoder, null);
Action action = () => new AuRaRewardCalculator(_auraParameters, _abiEncoder, null, TestSpecProvider.Instance);
action.Should().Throw<ArgumentNullException>();
}

Expand All @@ -86,7 +87,7 @@ public void constructor_throws_ArgumentException_on_BlockRewardContractTransitio
{2, Address.FromNumber(2)}
};

Action action = () => new AuRaRewardCalculator(_auraParameters, _abiEncoder, _transactionProcessor);
Action action = () => new AuRaRewardCalculator(_auraParameters, _abiEncoder, _transactionProcessor, TestSpecProvider.Instance);
action.Should().Throw<ArgumentException>();
}

Expand All @@ -96,7 +97,7 @@ public void constructor_throws_ArgumentException_on_BlockRewardContractTransitio
public void calculates_rewards_correctly_before_contract_transition(long blockNumber, ulong expectedReward)
{
_block.Header.Number = blockNumber;
AuRaRewardCalculator calculator = new(_auraParameters, _abiEncoder, _transactionProcessor);
AuRaRewardCalculator calculator = new(_auraParameters, _abiEncoder, _transactionProcessor, TestSpecProvider.Instance);
BlockReward[] result = calculator.CalculateRewards(_block);
result.Should().BeEquivalentTo(new BlockReward(_block.Beneficiary, expectedReward));
}
Expand All @@ -105,7 +106,7 @@ public void calculates_rewards_correctly_before_contract_transition(long blockNu
public void calculates_rewards_correctly_for_genesis()
{
_block.Header.Number = 0;
AuRaRewardCalculator calculator = new(_auraParameters, _abiEncoder, _transactionProcessor);
AuRaRewardCalculator calculator = new(_auraParameters, _abiEncoder, _transactionProcessor, TestSpecProvider.Instance);
BlockReward[] result = calculator.CalculateRewards(_block);
result.Should().BeEmpty();
}
Expand All @@ -117,7 +118,7 @@ public void calculates_rewards_correctly_after_contract_transition(long blockNum
_block.Header.Number = blockNumber;
BlockReward expected = new(_block.Beneficiary, expectedReward, BlockRewardType.External);
SetupBlockRewards(new Dictionary<Address, BlockReward[]>() { { _address10, new[] { expected } } });
AuRaRewardCalculator calculator = new(_auraParameters, _abiEncoder, _transactionProcessor);
AuRaRewardCalculator calculator = new(_auraParameters, _abiEncoder, _transactionProcessor, TestSpecProvider.Instance);
BlockReward[] result = calculator.CalculateRewards(_block);
result.Should().BeEquivalentTo(expected);
}
Expand All @@ -143,7 +144,7 @@ public void calculates_rewards_correctly_after_subsequent_contract_transitions(l
_block.Header.Number = blockNumber;
BlockReward expected = new(_block.Beneficiary, expectedReward, BlockRewardType.External);
SetupBlockRewards(new Dictionary<Address, BlockReward[]>() { { address, new[] { expected } } });
AuRaRewardCalculator calculator = new(_auraParameters, _abiEncoder, _transactionProcessor);
AuRaRewardCalculator calculator = new(_auraParameters, _abiEncoder, _transactionProcessor, TestSpecProvider.Instance);
BlockReward[] result = calculator.CalculateRewards(_block);
result.Should().BeEquivalentTo(expected);
}
Expand All @@ -166,7 +167,7 @@ public void calculates_rewards_correctly_for_uncles(long blockNumber, ulong expe
};

SetupBlockRewards(new Dictionary<Address, BlockReward[]>() { { _address10, expected } });
AuRaRewardCalculator calculator = new(_auraParameters, _abiEncoder, _transactionProcessor);
AuRaRewardCalculator calculator = new(_auraParameters, _abiEncoder, _transactionProcessor, TestSpecProvider.Instance);
BlockReward[] result = calculator.CalculateRewards(_block);
result.Should().BeEquivalentTo(expected);
}
Expand All @@ -189,7 +190,7 @@ public void calculates_rewards_correctly_for_external_addresses()
};

SetupBlockRewards(new Dictionary<Address, BlockReward[]>() { { _address10, expected } });
AuRaRewardCalculator calculator = new(_auraParameters, _abiEncoder, _transactionProcessor);
AuRaRewardCalculator calculator = new(_auraParameters, _abiEncoder, _transactionProcessor, TestSpecProvider.Instance);
BlockReward[] result = calculator.CalculateRewards(_block);
result.Should().BeEquivalentTo(expected);
}
Expand All @@ -198,7 +199,7 @@ private void SetupBlockRewards(IDictionary<Address, BlockReward[]> rewards)
{
_transactionProcessor.When(x => x.Execute(
Arg.Is<Transaction>(t => CheckTransaction(t, rewards.Keys, _rewardData)),
Arg.Is<BlockExecutionContext>(blkCtx => blkCtx.Header.Equals(_block.Header)),
Arg.Is<BlockHeader>(header => header.Equals(_block.Header)),
Arg.Is<ITxTracer>(t => t is CallOutputTracer)))
.Do(args =>
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ public async Task registry_contract_returns_not_found_when_key_doesnt_exist()
public async Task registry_contract_returns_not_found_when_contract_doesnt_exist()
{
using TestTxPermissionsBlockchain chain = await TestContractBlockchain.ForTest<TestTxPermissionsBlockchain, TxCertifierFilterTests>();
RegisterContract contract = new(AbiEncoder.Instance, Address.FromNumber(1000), chain.ReadOnlyTransactionProcessorSource);
RegisterContract contract = new(AbiEncoder.Instance, Address.FromNumber(1000), chain.ReadOnlyTransactionProcessorSource, _specProvider);
contract.TryGetAddress(chain.BlockTree.Head.Header, CertifierContract.ServiceTransactionContractRegistryName, out Address _).Should().BeFalse();
}

Expand All @@ -141,11 +141,12 @@ protected override BlockProcessor CreateBlockProcessor()
WorldStateManager,
BlockTree.AsReadOnly(), SpecProvider,
LimboLogs.Instance);
RegisterContract = new RegisterContract(abiEncoder, ChainSpec.Parameters.Registrar, ReadOnlyTransactionProcessorSource);
RegisterContract = new RegisterContract(abiEncoder, ChainSpec.Parameters.Registrar, ReadOnlyTransactionProcessorSource, Substitute.For<ISpecProvider>());
CertifierContract = new CertifierContract(
abiEncoder,
RegisterContract,
ReadOnlyTransactionProcessorSource);
ReadOnlyTransactionProcessorSource,
Substitute.For<ISpecProvider>());

return new AuRaBlockProcessor(
SpecProvider,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ public void SetUp()
.Encode(AbiEncodingStyle.IncludeSignature, Arg.Is<AbiSignature>(s => s.Name == "finalizeChange"), Arg.Any<object[]>())
.Returns(_finalizeChangeData.TransactionData);

_validatorContract = new ValidatorContract(_transactionProcessor, _abiEncoder, _contractAddress, _stateProvider, _readOnlyTxProcessorSource, new Signer(0, TestItem.PrivateKeyD, LimboLogs.Instance));
_validatorContract = new ValidatorContract(_transactionProcessor, _abiEncoder, _contractAddress, _stateProvider, _readOnlyTxProcessorSource, new Signer(0, TestItem.PrivateKeyD, LimboLogs.Instance), TestSpecProvider.Instance);
}

[TearDown]
Expand Down Expand Up @@ -184,13 +184,13 @@ public void loads_initial_validators_from_contract(long blockNumber)
_transactionProcessor.Received()
.CallAndRestore(
Arg.Is<Transaction>(t => CheckTransaction(t, _getValidatorsData)),
Arg.Is<BlockExecutionContext>(blkCtx => blkCtx.Header.Equals(_parentHeader)),
Arg.Is<BlockHeader>(blockHeader => blockHeader.Equals(_parentHeader)),
Arg.Is<ITxTracer>(t => t is CallOutputTracer));

// finalizeChange should be called
_transactionProcessor.Received(finalizeChangeCalled ? 1 : 0)
.Execute(Arg.Is<Transaction>(t => CheckTransaction(t, _finalizeChangeData)),
Arg.Is<BlockExecutionContext>(blkCtx => blkCtx.Header.Equals(block.Header)),
Arg.Is<BlockHeader>(blockHeader => blockHeader.Equals(block.Header)),
Arg.Is<ITxTracer>(t => t is CallOutputTracer));

// initial validator should be true
Expand Down Expand Up @@ -603,7 +603,7 @@ private void ValidateFinalizationForChain(ConsecutiveInitiateChangeTestParameter
// finalizeChange should be called or not based on test spec
_transactionProcessor.Received(chain.ExpectedFinalizationCount)
.Execute(Arg.Is<Transaction>(t => CheckTransaction(t, _finalizeChangeData)),
Arg.Is<BlockExecutionContext>(blkCtx => blkCtx.Header.Equals(_block.Header)),
Arg.Is<BlockHeader>(header => header.Equals(_block.Header)),
Arg.Is<ITxTracer>(t => t is CallOutputTracer));

_transactionProcessor.ClearReceivedCalls();
Expand Down Expand Up @@ -634,7 +634,7 @@ private void SetupInitialValidators(BlockHeader header, BlockHeader parentHeader

_transactionProcessor.When(x => x.CallAndRestore(
Arg.Is<Transaction>(t => CheckTransaction(t, _getValidatorsData)),
Arg.Any<BlockExecutionContext>(),
Arg.Any<BlockHeader>(),
Arg.Is<ITxTracer>(t => t is CallOutputTracer)))
.Do(args =>
args.Arg<ITxTracer>().MarkAsSuccess(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,6 @@ public void Test_StoreBeaconRoot_AccessListNotNull()

transaction.Hash = transaction.CalculateHash();
_transactionProcessor.Received().Execute(Arg.Is<Transaction>(t =>
t.Hash == transaction.Hash), header, NullTxTracer.Instance);
t.Hash == transaction.Hash), new BlockExecutionContext(header, Cancun.Instance), NullTxTracer.Instance);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,5 @@ public TestEip4844Config(ulong? maxBlobGasPerBlock = null)

public ulong GasPerBlob => Eip4844Constants.GasPerBlob;

public int GetMaxBlobsPerBlock() => (int)(MaxBlobGasPerBlock / GasPerBlob);
public ulong GetMaxBlobsPerBlock() => MaxBlobGasPerBlock / GasPerBlob;
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using Nethermind.Core.Eip2930;
using Nethermind.Core.Specs;
using Nethermind.Crypto;
using Nethermind.Evm;
using Nethermind.Evm.Tracing;
using Nethermind.Evm.TransactionProcessing;
using Nethermind.Int256;
Expand Down Expand Up @@ -62,7 +63,7 @@ public void StoreBeaconRoot(Block block, IReleaseSpec spec, ITxTracer tracer)

transaction.Hash = transaction.CalculateHash();

processor.Execute(transaction, header, tracer);
processor.Execute(transaction, new BlockExecutionContext(header, spec), tracer);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ private void OnHeadChanged(object? sender, BlockReplacementEventArgs e)
BlockGasLimit = e.Block!.GasLimit;
CurrentBaseFee = e.Block.Header.BaseFeePerGas;
CurrentFeePerBlobGas =
BlobGasCalculator.TryCalculateFeePerBlobGas(e.Block.Header, out UInt256 currentFeePerBlobGas)
BlobGasCalculator.TryCalculateFeePerBlobGas(e.Block.Header, out UInt256 currentFeePerBlobGas,
SpecProvider.GetSpec(e.Block.Header))
? currentFeePerBlobGas
: UInt256.Zero;
HeadChanged?.Invoke(sender, e);
Expand Down
Loading
Loading