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

Refactor TransactionForRpc (Alternative) #7483

Open
wants to merge 180 commits into
base: master
Choose a base branch
from

Conversation

emlautarom1
Copy link
Contributor

@emlautarom1 emlautarom1 commented Sep 23, 2024

Partially solves #7313

Changes

  • Make TransactionForRpc an abstract base class
  • Introduce multiple subclasses of TransactionForRpc for specific Transaction types (Legacy, AccessList, EIP1559, Blob and Optimism)
  • Support JSON (de)serializing
  • Removed TransactionForRpc -> Transaction "default" conversions: conversions now use domain specific defaults (ex. GasLimit defaults to 90_000) instead of C# specific.
  • Introduced mechanism to register new subclasses.

Types of changes

What types of changes does your code introduce?

  • Bugfix (a non-breaking change that fixes an issue)
  • New feature (a non-breaking change that adds functionality)
  • Breaking change (a change that causes existing functionality not to work as expected)
  • Optimization
  • Refactoring
  • Documentation update
  • Build-related changes
  • Other: Description

Testing

Requires testing

  • Yes
  • No

If yes, did you write tests?

  • Yes
  • No

Notes on testing

Documentation

Requires documentation update

  • Yes
  • No

Requires explanation in Release Notes

  • Yes
  • No

Remarks

This PR is an alternative version of #7446 that does not introduce a RpcGenericTransaction for deserializing purposes; instead we reuse the same type hierarchy for both serialization and deserialization. As a quick summary, this PR introduces the following changes:

When deserializing

  • We read JSON and construct a specific subclass of TransactionForRpc based on the Type field of the incoming JSON object (if this field is missing we default to Legacy transactions).
  • Each subclass should implement a ToTransaction method that constructs a proper Transaction object while setting up the appropriate domain defaults when fields are missing (ex. if the transaction is AccessList, then the resulting AccessList field in Transaction is an empty access list, not null).

When serializing

  • We have multiple Converters that know how to take a Transaction and build a TransactionForRpc (the above mentioned abstract base class). Each converter knows how to take a Transaction with a specific TxType and build a subclass (ex. there is a converter from Transaction to BlobTransactionForRpc). Each converter is based on the Ethereum JSON-RPC spec to ensure no fields are missing.
  • To write JSON, we instruct the serializer to use the runtime type rather than the static type, that is, don't use TransactionForRpc but, for example, EIP1559TransactionForRpc.

@emlautarom1 emlautarom1 mentioned this pull request Sep 30, 2024
16 tasks
Comment on lines +73 to +81
tx.SourceHash = SourceHash ?? throw new ArgumentNullException(nameof(SourceHash));
tx.SenderAddress = From ?? throw new ArgumentNullException(nameof(From));
tx.To = To;
tx.Mint = Mint ?? 0;
tx.Value = Value ?? throw new ArgumentNullException(nameof(Value));
// TODO: Unsafe cast
tx.GasLimit = (long)(Gas ?? throw new ArgumentNullException(nameof(Gas)));
tx.IsOPSystemTransaction = IsSystemTx ?? false;
tx.Data = Input ?? throw new ArgumentNullException(nameof(Input));
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to the official OP repo these fields are required and we should reject transactions that don't provide it:

https://github.com/ethereum-optimism/op-geth/blob/c283254e5447f127e3b9350860985911dab9cd2f/core/types/transaction_marshalling_test.go#L27-L67

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deposit transaction should never come from RPC? Maybe we should just fail here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if we get an invalid (that is, missing fields) deposit transaction on eth_sendTransaction? With these checks we'll fail with a null argument exception. Do you have something different in mind?

@emlautarom1 emlautarom1 marked this pull request as ready for review September 30, 2024 21:19
Copy link
Member

@LukaszRozmej LukaszRozmej left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Close but few things came up.

Comment on lines 93 to 96
// TODO: For some reason we might get an object wrapped in a String
using JsonDocument jsonObject = document.RootElement.ValueKind == JsonValueKind.String
? JsonDocument.Parse(document.RootElement.GetString()!)
: document;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah not the biggest fan of that, can we try using type discriminators: https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/polymorphism?pivots=dotnet-8-0#customize-the-type-discriminator-name

[JsonPolymorphic(TypeDiscriminatorPropertyName = "TxType")]
[JsonDerivedType(typeof(LegacyTransactionForRpc), typeDiscriminator: "Legacy")] 

or [JsonDerivedType(typeof(LegacyTransactionForRpc), typeDiscriminator: 0)]

as we would have to make it pluginable maybe we should create custom resolver: https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/polymorphism?pivots=dotnet-8-0#configure-polymorphism-with-the-contract-model which would register all the tx types.

Comment on lines 287 to 292
// TODO: We might want to lift `Nonce` to `TransactionForRpc`
UInt256? nonce = null;
if (rpcTx is LegacyTransactionForRpc legacyTx)
{
nonce = legacyTx.Nonce;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will deposit transaction interact with JSON RPC? Shouldn't it nonce be expected to be outputted there?

Otherwise: true, but Nonce sounds like such a basic transaction functionality that maybe it is better to make an exception, put it in base class and just ignore it or DepositTransaction? Or this shouldn't take in TransactionForRpc but LegacyTransactionForRpc? Or maybe create a class in between called like UserTransactionForRpc?

Comment on lines +73 to +81
tx.SourceHash = SourceHash ?? throw new ArgumentNullException(nameof(SourceHash));
tx.SenderAddress = From ?? throw new ArgumentNullException(nameof(From));
tx.To = To;
tx.Mint = Mint ?? 0;
tx.Value = Value ?? throw new ArgumentNullException(nameof(Value));
// TODO: Unsafe cast
tx.GasLimit = (long)(Gas ?? throw new ArgumentNullException(nameof(Gas)));
tx.IsOPSystemTransaction = IsSystemTx ?? false;
tx.Data = Input ?? throw new ArgumentNullException(nameof(Input));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deposit transaction should never come from RPC? Maybe we should just fail here.

- Use `IEnumerable` instead of `List`
@emlautarom1
Copy link
Contributor Author

Will deposit transaction interact with JSON RPC? Shouldn't it nonce be expected to be outputted there?

Otherwise: true, but Nonce sounds like such a basic transaction functionality that maybe it is better to make an exception, put it in base class and just ignore it or DepositTransaction? Or this shouldn't take in TransactionForRpc but LegacyTransactionForRpc? Or maybe create a class in between called like UserTransactionForRpc?

@LukaszRozmej Nonce is not expected to come as part of deposit transactions and we'll get them in, for example, eth_sendTransaction. The Nonce field is only used during serialization for compatibility reasons and it uses the transaction receipt (see https://specs.optimism.io/protocol/deposits.html?search=#the-deposited-transaction-type). In all other scenarios is treated as 0.

I've decided to change the input of eth_sendTransaction in the OptimismRpcModule module to TransactionForRpc to ensure we're overriding the base EthRpcModule. This override ignores the Nonce field (we let it default to 0 in the base Transaction) without changing the base Eth module behavior.

@@ -280,8 +281,12 @@ public ResultWrapper<byte[]> eth_sign(Address addressData, byte[] message)

public virtual Task<ResultWrapper<Hash256>> eth_sendTransaction(TransactionForRpc rpcTx)
{
Transaction tx = rpcTx.ToTransactionWithDefaults(_blockchainBridge.GetChainId());
TxHandlingOptions options = rpcTx.Nonce is null ? TxHandlingOptions.ManagedNonce : TxHandlingOptions.None;
Transaction tx = rpcTx.ToTransaction();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe the parameter here should be LegacyTransactionForRpc?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we do that then we cannot override in OptimismEthRpcModule. We could not override and make the Optimism module use OptimismTransactionForRpc but I'm not sure what method is going to be picked up. Need to double check

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants