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

Introduce EIP-7702 (Set EOA account code) #106

Open
Mdaiki0730 opened this issue Oct 10, 2024 · 13 comments
Open

Introduce EIP-7702 (Set EOA account code) #106

Mdaiki0730 opened this issue Oct 10, 2024 · 13 comments
Assignees
Labels
enhancement New feature or request

Comments

@Mdaiki0730
Copy link
Contributor

Mdaiki0730 commented Oct 10, 2024

Is your request related to a problem? Please describe.
Kaia has an account abstraction in the form of ERC-4337.
We can receive the benefits of AA through this, but if you already have an EOA account, you will need to create a new contract account and transfer assets.

Describe the solution you'd like
Introducing EIP-7702 can solve this problem.
EIP-7702 allows you to treat an EOA like a contract account by setting a contract in the code field of the EOA.
A new SET_CODE_TX_TYPE is going to be defined, which will work as follows:
EIP7702_detail_process

Describe alternatives you've considered

  • EIP-3074
    • EIP-7702 was additionally proposed as an alternative.
  • RIP-7560
    • Because this is a proposal for Rollup, it seems a bit difficult to introduce it to L1 right away.

Additional context
The following changes are expected:

Add Objects
  • Add SetCodeTxType, which implements TxInternalData. This will be treated as the EthTypedTransaction.
  • Define an AuthorizationList([]Authorization) for the authorization tuple.
Changes
  • /api
    • Change the interface of api to correspond to SetCodeTxType
  • /blockchain
    • state_transition
      • Add the main code for adding set code and authority to StateTransition.TransitionDb
    • types/tx_internal_data
      • Add TxTypeSetCode
      • Extende the StateDB interface (for validation)
    • tx_pool
      • There may be changes to validateTx
    • state/statedb
      • Implement ResolveCode in StateDB(also resolveStateObject)
    • vm
      • Add 7702 to eips in VM
      • Changed GetCode to ResolveCode in evm and instructions (If code is requested)
      • Updated interface of StateDB
      • Call enable7702 in jump_table
      • Define gas around EIP7702 in operations_acl
  • /params
    • Define of gas fee for authorization
@Mdaiki0730 Mdaiki0730 added the enhancement New feature or request label Oct 10, 2024
@Mdaiki0730
Copy link
Contributor Author

We have found some additional changes that we need to make.

Points

  • Since EOA does not have a code hash field, how do we retain the EOA after the setcode?
  • A change needs to be made to Kaia's Account RPC API related to setcode.

@blukat29
Copy link
Contributor

blukat29 commented Oct 22, 2024

There is another option.

  1. Allow the EOA have additional "optional RLP" fields

Adding optional RLP fields allows legacy structs keep its RLP encoding while new structs can attach new fields. e.g.

// Added with Magma hardfork for KIP-71 dynamic fee.
BaseFee *big.Int `json:"baseFeePerGas,omitempty" rlp:"optional"`

This way we can keep the existing EOA/SCA separation that has existed for whatever reason. But if we keep the SetCode'd account as EOA type, we may need to modify some conditions such as IsProgramAccount.

The EOA/SCA separation affects how some transaction types behave (e.g. SmartContractExecution can only target an SCA). I personally don't think that the explicit separation (type byte) is better than implicit separation (codehash == 0x0), but they are already in place.

@Mdaiki0730
Copy link
Contributor Author

@kaiachain/core
Regarding how to setcode the EOA, @blukat29 gave me two suggestions for how EOA should retain the code:

  1. Change from EOA to SCA at the time of set code
  2. Define a new AccountType and switch to that when the code is set (such as ExternallyOwnedAccountHasCodeType)

In my opinion, option 1 is ideal.
The reason is that the purpose of introducing EIP7702 is to set code in the existing EOA and make the EOA control programmable, which is consistent with the nature of SCA, which is control by code.

However, I am concerned about the following points regarding the change from EOA to SCA. In some cases, we may need to consider a new type. Could you please let me know your opinions.

I accidentally deleted my comment so I'll restore it. I'm sorry.

@Mdaiki0730
Copy link
Contributor Author

On reflection, changing the Type doesn't seem like a good idea since it affects the address prefix.
Option 3 may be the most realistic approach.
As we make some changes, such as IsProgramAccount, we need to clearly define how we view setcoded EOA.

I also assume, that’s obvious that there are numerous assertions that will become faulty via isContract, like this 5 ? Such checks, if checking for isContract==true, will result fault assertions that account cannot send auxiliary transaction while it can.
https://ethereum-magicians.org/t/eip-7702-set-eoa-account-code/19923/258

This one might not be as big of a deal, because OZ’s isContract function already had some noteable workarounds like contracts in construction, counterfactual addresses, etc and thus was already unreliable.
https://ethereum-magicians.org/t/eip-7702-set-eoa-account-code/19923/260

In Ethereum, there was a debate about whether it was OK to return true for isContract for a set coded EOA, and since this has already been broken, it is considered not to be a big problem.
Can we think of the Kaia ecosystem in the same way?

@blukat29
Copy link
Contributor

blukat29 commented Oct 22, 2024

On reflection, changing the Type doesn't seem like a good idea since it affects the address prefix. Option 3 may be the most realistic approach.

I agree that type change is unrealistic. But let me correct that the account type prefix is NOT on the address, it's inside the StateDB's leaf node. It only affects the Merkle Hash (i.e. header.stateRoot).


The isContract debate is specifically about the OpenZeppelin's way of determining a contract account in Solidity (https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v4.9.6/contracts/utils/Address.sol) that probably use EXTCODE* opcodes. I agree with Ethereum Magician thread that isContract is unsafe anyway so we need not consider this.

But the thing with isProgramAccount is in the EVM implementation

kaia/blockchain/vm/evm.go

Lines 283 to 289 in de62451

if isProgramAccount(evm, caller.Address(), addr, evm.StateDB) {
// Initialise a new contract and set the code that is to be used by the EVM.
// The contract is a scoped environment for this execution context only.
contract := NewContract(caller, to, value, gas)
contract.SetCallCode(&addr, evm.StateDB.GetCodeHash(addr), evm.StateDB.GetCode(addr))
ret, err = run(evm, contract, input)
gas = contract.Gas
that decides whether the EVM will execute the bytecode or not.

@Mdaiki0730
Copy link
Contributor Author

Thank you for correcting me.
In my plan, the EOA after setting is expected to operate as a ProgramAccount.
Once I have thought about how to change it in option 3, I will get back to you.

@Mdaiki0730
Copy link
Contributor Author

Mdaiki0730 commented Oct 24, 2024

Consideration

Treating EOA as a ProgramAccount will affect the following callers of GetProgramAccount:
https://github.com/search?q=repo%3Akaiachain%2Fkaia%20GetProgramAccount&type=code

In this case, the objectives are separated into two:

  1. Calling GetProgramAccount to treat Account as a ProgramAccount
  2. If it is SCA (ProgramAccount), an error is returned

1 is not a big problem because the purpose is only to obtain the ProgramAccount, but in the case of 2, the EOA Type needs to be retained even after it is set, so a correction is required.

New Changes

Taking the above issues into consideration, I propose the following changes.

  • /blockchain
    • types/account
      • external_owned_account
        • Add rlp optional field (StorageRoot, CodeHash, CodeInfo)
        • Add a method to implement ProgramAccount like SCA. Basically, it seems that I just need to implement the getter and setter, but I'm a little worried about how to implement EOA's EncodeRLPExt.
          • func (sca *SmartContractAccount) GetStorageRoot() common.ExtHash {
            return sca.storageRoot
            }
            func (sca *SmartContractAccount) GetCodeHash() []byte {
            return sca.codeHash
            }
            func (sca *SmartContractAccount) GetCodeFormat() params.CodeFormat {
            return sca.codeInfo.GetCodeFormat()
            }
            func (sca *SmartContractAccount) GetVmVersion() params.VmVersion {
            return sca.codeInfo.GetVmVersion()
            }
            func (sca *SmartContractAccount) SetStorageRoot(h common.ExtHash) {
            sca.storageRoot = h
            }
            func (sca *SmartContractAccount) SetCodeHash(h []byte) {
            sca.codeHash = h
            }
            func (sca *SmartContractAccount) SetCodeInfo(ci params.CodeInfo) {
            sca.codeInfo = ci
            }
          • func (sca *SmartContractAccount) EncodeRLPExt(w io.Writer) error {
            if sca.storageRoot.IsZeroExtended() {
            return rlp.Encode(w, sca.toSerializable())
            } else {
            return rlp.Encode(w, sca.toSerializableExt())
            }
            }
          • func (sca *SmartContractAccount) toSerializable() *smartContractAccountSerializable {
            return &smartContractAccountSerializable{
            CommonSerializable: sca.AccountCommon.toSerializable(),
            StorageRoot: sca.storageRoot.Unextend(),
            CodeHash: sca.codeHash,
            CodeInfo: sca.codeInfo,
            }
            }
            func (sca *SmartContractAccount) toSerializableExt() *smartContractAccountSerializableExt {
            return &smartContractAccountSerializableExt{
            CommonSerializable: sca.AccountCommon.toSerializable(),
            StorageRoot: sca.storageRoot,
            CodeHash: sca.codeHash,
            CodeInfo: sca.codeInfo,
            }
            }
            func (sca *SmartContractAccount) fromSerializable(o *smartContractAccountSerializable) {
            sca.AccountCommon.fromSerializable(o.CommonSerializable)
            sca.storageRoot = o.StorageRoot.ExtendZero()
            sca.codeHash = o.CodeHash
            sca.codeInfo = o.CodeInfo
            }
            func (sca *SmartContractAccount) fromSerializableExt(o *smartContractAccountSerializableExt) {
            sca.AccountCommon.fromSerializable(o.CommonSerializable)
            sca.storageRoot = o.StorageRoot
            sca.codeHash = o.CodeHash
            sca.codeInfo = o.CodeInfo
            }
            func (sca *SmartContractAccount) EncodeRLP(w io.Writer) error {
            return rlp.Encode(w, sca.toSerializable())
            }
            func (sca *SmartContractAccount) EncodeRLPExt(w io.Writer) error {
            if sca.storageRoot.IsZeroExtended() {
            return rlp.Encode(w, sca.toSerializable())
            } else {
            return rlp.Encode(w, sca.toSerializableExt())
            }
            }
            func (sca *SmartContractAccount) DecodeRLP(s *rlp.Stream) error {
            savedStream, err := s.Raw()
            if err != nil {
            return err
            }
            // s.Raw() has consumed the stream. Refill with original data.
            s.Reset(bytes.NewReader(savedStream), 0)
            // Try decode into smartContractAccountSerializableExt
            serializedExt := &smartContractAccountSerializableExt{
            CommonSerializable: newAccountCommonSerializable(),
            }
            if err := s.Decode(serializedExt); err == nil {
            sca.fromSerializableExt(serializedExt)
            return nil
            }
            // s.Decode() may have consumed the stream. Refill with original data.
            s.Reset(bytes.NewReader(savedStream), 0)
            // Retry with smartContractAccountSerializable
            serialized := &smartContractAccountSerializable{
            CommonSerializable: newAccountCommonSerializable(),
            }
            if err := s.Decode(serialized); err == nil {
            sca.fromSerializable(serialized)
            return nil
            } else {
            return err
            }
            }
            func (sca *SmartContractAccount) MarshalJSON() ([]byte, error) {
            return json.Marshal(&smartContractAccountSerializableJSON{
            Nonce: sca.nonce,
            Balance: (*hexutil.Big)(sca.balance),
            HumanReadable: sca.humanReadable,
            Key: accountkey.NewAccountKeySerializerWithAccountKey(sca.key),
            StorageRoot: sca.storageRoot.Unextend(), // Unextend for API compatibility
            CodeHash: sca.codeHash,
            CodeFormat: sca.codeInfo.GetCodeFormat(),
            VmVersion: sca.codeInfo.GetVmVersion(),
            })
            }
            func (sca *SmartContractAccount) UnmarshalJSON(b []byte) error {
            serialized := &smartContractAccountSerializableJSON{}
            if err := json.Unmarshal(b, serialized); err != nil {
            return err
            }
            sca.nonce = serialized.Nonce
            sca.balance = (*big.Int)(serialized.Balance)
            sca.humanReadable = serialized.HumanReadable
            sca.key = serialized.Key.GetKey()
            sca.storageRoot = serialized.StorageRoot.ExtendZero() // API inputs should contain merkle hash
            sca.codeHash = serialized.CodeHash
            sca.codeInfo = params.NewCodeInfo(serialized.CodeFormat, serialized.VmVersion)
            return nil
            }
    • types
      • Change the SCA decision in the following method: (IsProgramAccount == true) to (AccountType == SCA)
    • state_transition

@shiki-tak
Copy link
Contributor

@Mdaiki0730 @blukat29
I added a CodeHash field to the EOA, and as a test, prepared an EOA with CodeHash in genesis and started the chain.
After that, when I checked the account information, the AccountType was 2.

> kaia.getAccount("9fdda5cee9e2482eb93716b98310068da9914e7d")
{
  accType: 2,
  account: {
    balance: "0x446c3b15f9926687d2c40534fdb564000000000000",
    codeHash: "I2hB6mVLDxjoPpNLoPabSrIV8Lb/vu4oh5fOZ8ia6iU=",
    humanReadable: false,
    key: {
      key: {},
      keyType: 3
    },
    nonce: 0,
    storageRoot: "0x01f9e62d19297f681ea2208de1fec6c67ed86a26e55dbf4b27a1b0881060507b"
  }
}

This is because SetCode is called at the following point based on the genesis data.

https://github.com/kaiachain/kaia/blob/feat/eip-7702/blockchain/genesis.go#L313

The current implementation plan assumes that SetCode will be called only when tx is called.

  • Consider an implementation that assumes that the genesis EOA will include Code
  • Do not consider adding such an EOA to genesis (SetCode will only be called through tx)

Any other opinions?

@blukat29
Copy link
Contributor

blukat29 commented Nov 1, 2024

If we limit the discussion into Genesis only, The GenesisAlloc with nonempty Code is considered a contract account. There is no way to specify "EOA with code" in the Genesis. I think that happened here, so the accType=2 (SCA).

If we only think about Mainnet, "Do not consider adding such an EOA to genesis (SetCode will only be called through tx)" should suffice. But for the sake of easier testing, we may have a option that "allows genesis EOA to have Code".

e.g. (any better suggestions are welcome)

type GenesisAccount struct {
	Code       []byte                      `json:"code,omitempty"`
	Storage    map[common.Hash]common.Hash `json:"storage,omitempty"`
	Balance    *big.Int                    `json:"balance" gencodec:"required"`
	Nonce      uint64                      `json:"nonce,omitempty"`
    ForceEOA bool                         `json:"forceEoa,omitempty"` // for tests
	PrivateKey []byte                      `json:"secretKey,omitempty"` // for tests
}

@shiki-tak
Copy link
Contributor

If I am allowed to add fields to the GenesisAccount struct for testing purposes, I think your suggestion is so good.

I'll try adding codeHash[]byte directly rather than your suggestion of ForceEOA bool.

@Mdaiki0730
If SetCode is only called through tx, I don't think there is any need to implement anything additional for SetCode. What do you think?

@blukat29
Copy link
Contributor

blukat29 commented Nov 1, 2024

@shiki-tak Absolutely! there is already a "for tests" field after all.

@Mdaiki0730
Copy link
Contributor Author

Mdaiki0730 commented Nov 1, 2024

@shiki-tak

If SetCode is only called through tx, I don't think there is any need to implement anything additional for SetCode. What do you think?

When SetCode is sent from SetCodeTx to EOA, if the account does not exist in the state, an object will be created as SCA.
To avoid this, how about creating a separate SetCode for EOA?

@shiki-tak
Copy link
Contributor

@Mdaiki0730 Would that end up being something I should include in my current work?

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

No branches or pull requests

3 participants