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

Implement EIP2718 and EIP2930 #1048

Merged
merged 32 commits into from
Mar 4, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
3fabbb3
common: add EIP-2718, EIP-2930
jochem-brouwer Jan 15, 2021
76af18f
tx: introduce transactionFactory
jochem-brouwer Mar 1, 2021
1847d8b
tx: add some legacy transaction methods to EIP2930
jochem-brouwer Jan 23, 2021
5f2a1c9
tx: eip2930: add more legacy methods
jochem-brouwer Mar 1, 2021
f9852ca
tx: tests: update tests so that these run
jochem-brouwer Mar 1, 2021
7a35c75
tx: add transactionFactory tests
jochem-brouwer Mar 1, 2021
64cb4c7
tx: add some EIP2930 tests
jochem-brouwer Mar 1, 2021
946e35a
tx: implement BaseTransaction
jochem-brouwer Mar 1, 2021
ac694fb
tx: make txs both support signed/unsigned transactions
jochem-brouwer Mar 1, 2021
2943360
tx: decode EIP2930 transactions from block
jochem-brouwer Jan 27, 2021
2b78645
tx: fix eip2930 hash method
jochem-brouwer Jan 28, 2021
73f4b35
tx: pack both legacy and typed transactions in TxData
jochem-brouwer Mar 1, 2021
25ec7d5
tx: factory: add fromTxData
jochem-brouwer Jan 28, 2021
ae533d2
block: integrate EIP2930 blocks
jochem-brouwer Mar 1, 2021
c5c58d0
tx: add AccessListItem type
jochem-brouwer Jan 29, 2021
cd6a58f
tx: fix EIP2930 v value of 0
jochem-brouwer Jan 30, 2021
bdc0d3b
vm: implement new tx package
jochem-brouwer Feb 20, 2021
0b8643d
tx: add transaction type getter
jochem-brouwer Feb 20, 2021
87955c8
vm: fix VM tests
jochem-brouwer Feb 24, 2021
a638d8b
tx -> EIP-2930: some variable renaming, moved tests to *.spec.ts files
holgerd77 Feb 25, 2021
cc928ee
tx -> EIP-2930: common related improvements and fixes
holgerd77 Feb 25, 2021
2385c6f
tx: fix transaction type of LegacyTransaction
jochem-brouwer Mar 1, 2021
cca3b88
tx: fix transaction type of LegacyTransaction
jochem-brouwer Feb 25, 2021
af6782f
tx, vm -> EIP-2930: smaller fixes and cleanups
holgerd77 Feb 25, 2021
3539369
block/tx: fix `raw` method to return right value
jochem-brouwer Feb 25, 2021
4a7a547
tx -> EIP-2930: set default HF from common to berlin, small fixes
holgerd77 Feb 25, 2021
9ececec
vm: fix state tests for EIP2930
jochem-brouwer Feb 25, 2021
ceeedc6
tx -> EIP-2929: started to separate dedicated legacy and base tests, …
holgerd77 Feb 26, 2021
7d12beb
tx -> EIP-2930: added generic sign(), getSenderAddress(), getSenderPu…
holgerd77 Feb 26, 2021
861d1c0
tx: increase test coverage
jochem-brouwer Feb 26, 2021
be27b71
tx: simplify getMessageToSign
jochem-brouwer Feb 26, 2021
9c6f2f6
monorepo: reset ethereum-tests to develop
jochem-brouwer Mar 1, 2021
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
2 changes: 1 addition & 1 deletion .gitmodules
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
[submodule "ethereum-tests"]
path = packages/ethereum-tests
url = https://github.com/ethereum/tests.git
url = https://github.com/qbzzt/tests.git
branch = develop
4 changes: 4 additions & 0 deletions packages/block/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
(modification: no type change headlines) and this project adheres to
[Semantic Versioning](http://semver.org/spec/v2.0.0.html).

## UNRELEASED

- Integration of [EIP2718](https://eips.ethereum.org/EIPS/eip-2718) (Typed Transactions) and [EIP2930](https://eips.ethereum.org/EIPS/eip-2930) (Access List Transaction), PR [#1048](https://github.com/ethereumjs/ethereumjs-monorepo/pull/1048). It is now possible to create blocks with access list transactions.

## 3.1.0 - 2021-02-22

### Clique/PoA Support
Expand Down
10 changes: 5 additions & 5 deletions packages/block/src/block.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { BaseTrie as Trie } from 'merkle-patricia-tree'
import { BN, rlp, keccak256, KECCAK256_RLP } from 'ethereumjs-util'
import Common from '@ethereumjs/common'
import { Transaction, TxOptions } from '@ethereumjs/tx'
import { TransactionFactory, Transaction, TxOptions } from '@ethereumjs/tx'
import { BlockHeader } from './header'
import { BlockData, BlockOptions, JsonBlock, BlockBuffer, Blockchain } from './types'

Expand Down Expand Up @@ -31,7 +31,7 @@ export class Block {
// parse transactions
const transactions = []
for (const txData of txsData || []) {
const tx = Transaction.fromTxData(txData, {
const tx = TransactionFactory.fromTxData(txData, {
...opts,
// Use header common in case of hardforkByBlockNumber being activated
common: header._common,
Expand Down Expand Up @@ -91,7 +91,7 @@ export class Block {
const transactions = []
for (const txData of txsData || []) {
transactions.push(
Transaction.fromValuesArray(txData, {
TransactionFactory.fromBlockBodyData(txData, {
Copy link
Member

Choose a reason for hiding this comment

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

Was thinking a bit about this breaking part in Block.

So if I get this right the only breaking thing here would be Transaction now being a type in the main constructor, right?

Wouldn't we already back on the compatibility track if we rename this new type Transaction -> TypedTransaction (not sure, or another name, I would think though that this name is justified even if it encompassed the old transaction format, to be discussed) and then LegacyTransaction back to Transaction?

I actually like this in general (we might still want to rename back Transaction -> LegacyTransaction on a general breaking-release-round on all libraries, not sure), since having the type named TypedTransaction more clearly indicates that a function now allows for a typed tx as an input.

Copy link
Member Author

Choose a reason for hiding this comment

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

We can do this, but this still implies we have to use the TransactionFactory in Block, and we also have to use the TypedTransactions as the type for the transactions field in the block body. We can indeed export the LegacyTransaction as a Transaction, this would mean a non-breaking release for Transaction, but due to the type changes on Block they are breaking. (I am not entirely sure if they are completely breaking, since all methods of the old Transaction are available on both EIP2930Transaction and LegacyTransaction, so maybe it is not strictly breaking?)

Copy link
Member

Choose a reason for hiding this comment

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

The TransactionFactory in Block is not a problem. With a new Block release the Transaction dependency is also bumped, so the new Block version for sure works together with the new Transaction version release. So that is no problem.

Compatibility problems only arise on direct API usage, there, the only thing we have is the constructor:

constructor(
    header?: BlockHeader,
    transactions: Transaction[] = [],
    uncleHeaders: BlockHeader[] = [],
    opts: BlockOptions = {}
  ) 

This is the only part which we need to make sure that it still takes in Transaction objects from the current library version, which I think it should when we have a type EIP2930Transaction | Transaction (we should test this though, this would be an application for @cgewecke using the functionality from #1118.

If there is the possibility we should very much do this. This is so so crucial that we remain backwards-compatible (especially if it's just a case about a simple rename).

Copy link
Contributor

@cgewecke cgewecke Mar 4, 2021

Choose a reason for hiding this comment

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

I will rebase #1118 against this and open as draft.

Copy link
Member

Choose a reason for hiding this comment

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

Cool! 😄 Can you do the suggested renaming (see above) also along for test purposes? So this would be needed to see if this will work.

Any opinion if we now might want to merge this in here? I would be still a fan, think this would ease follow-up development. Doesn't mean that we take this out of review. We can just do follow-up fixes on future PRs.

...opts,
// Use header common in case of hardforkByBlockNumber being activated
common: header._common,
Expand Down Expand Up @@ -154,7 +154,7 @@ export class Block {
raw(): BlockBuffer {
return [
this.header.raw(),
this.transactions.map((tx) => tx.raw()),
this.transactions.map((tx) => <Buffer[]>tx.raw()),
this.uncleHeaders.map((uh) => uh.raw()),
]
}
Expand Down Expand Up @@ -223,7 +223,7 @@ export class Block {
const errors: string[] = []

this.transactions.forEach(function (tx, i) {
const errs = tx.validate(true)
const errs = <string[]>tx.validate(true)
if (errs.length > 0) {
errors.push(`errors at tx ${i}: ${errs.join(', ')}`)
}
Expand Down
4 changes: 2 additions & 2 deletions packages/block/src/from-rpc.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Transaction, TxData } from '@ethereumjs/tx'
import { TransactionFactory, Transaction, TxData } from '@ethereumjs/tx'
import { toBuffer, setLengthLeft } from 'ethereumjs-util'
import { Block, BlockOptions } from './index'

Expand Down Expand Up @@ -37,7 +37,7 @@ export default function blockFromRpc(blockParams: any, uncles: any[] = [], optio
const opts = { common: header._common }
for (const _txParams of blockParams.transactions) {
const txParams = normalizeTxParams(_txParams)
const tx = Transaction.fromTxData(txParams as TxData, opts)
const tx = TransactionFactory.fromTxData(txParams as TxData, opts)
transactions.push(tx)
}
}
Expand Down
12 changes: 12 additions & 0 deletions packages/common/src/eips/2718.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "EIP-2718",
"comment": "Typed Transaction Envelope",
"url": "https://eips.ethereum.org/EIPS/eip-2718",
"status": "Draft",
"minimumHardfork": "chainstart",
"gasConfig": {},
"gasPrices": {},
"vm": {},
"pow": {}
}

22 changes: 22 additions & 0 deletions packages/common/src/eips/2930.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"name": "EIP-2929",
"comment": "Optional access lists",
"url": "https://eips.ethereum.org/EIPS/eip-2930",
"status": "Draft",
"minimumHardfork": "istanbul",
"requiredEIPs": [2718, 2929],
"gasConfig": {},
"gasPrices": {
"accessListStorageKeyCost": {
"v": 1900,
"d": "Gas cost per storage key in an Access List transaction"
},
"accessListAddressCost": {
"v": 2400,
"d": "Gas cost per storage key in an Access List transaction"
}
},
"vm": {},
"pow": {}
}

2 changes: 2 additions & 0 deletions packages/common/src/eips/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,7 @@ export const EIPs: eipsType = {
2315: require('./2315.json'),
2537: require('./2537.json'),
2565: require('./2565.json'),
2718: require('./2718.json'),
2929: require('./2929.json'),
2930: require('./2930.json'),
}
2 changes: 1 addition & 1 deletion packages/common/src/hardforks/berlin.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@
"comment": "HF targeted for July 2020 following the Muir Glacier HF",
"url": "https://eips.ethereum.org/EIPS/eip-2070",
"status": "Draft",
"eips": [ 2315, 2565, 2929 ]
"eips": [ 2315, 2565, 2929, 2718, 2930 ]
}
8 changes: 8 additions & 0 deletions packages/common/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,14 @@ export default class Common extends EventEmitter {
`${eip} cannot be activated on hardfork ${this.hardfork()}, minimumHardfork: ${minHF}`
)
}
if (EIPs[eip].requiredEIPs) {
// eslint-disable-next-line prettier/prettier
(<number[]>EIPs[eip].requiredEIPs).forEach((elem: number) => {
if (!(eips.includes(elem) || this.isActivatedEIP(elem))) {
throw new Error(`${eip} requires EIP ${elem}, but is not included in the EIP list`)
}
})
}
}
this._eips = eips
}
Expand Down
15 changes: 14 additions & 1 deletion packages/common/tests/eips.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,22 @@ import Common from '../src/'

tape('[Common]: Initialization / Chain params', function (t: tape.Test) {
t.test('Correct initialization', function (st: tape.Test) {
const eips = [2537, 2929]
let eips = [2537, 2929]
const c = new Common({ chain: 'mainnet', eips })
st.equal(c.eips(), eips, 'should initialize with supported EIP')

eips = [2718, 2929, 2930]
new Common({ chain: 'mainnet', eips, hardfork: 'istanbul' })
st.pass('Should not throw when initializing with a consistent EIP list')

eips = [2930]
const msg =
'should throw when initializing with an EIP with required EIPs not being activated along'
const f = () => {
new Common({ chain: 'mainnet', eips, hardfork: 'istanbul' })
}
st.throws(f, msg)

st.end()
})

Expand Down
2 changes: 1 addition & 1 deletion packages/ethereum-tests
10 changes: 10 additions & 0 deletions packages/tx/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
(modification: no type change headlines) and this project adheres to
[Semantic Versioning](http://semver.org/spec/v2.0.0.html).

## UNRELEASED

- Integration of [EIP2718](https://eips.ethereum.org/EIPS/eip-2718) (Typed Transactions) and [EIP2930](https://eips.ethereum.org/EIPS/eip-2930) (Access List Transaction), PR [#1048](https://github.com/ethereumjs/ethereumjs-monorepo/pull/1048).

This PR integrates the Typed Transactions. In order to produce the right transactions, there is a new class called the `TransactionFactory`. This factory helps to create the correct transaction. When decoding directly from blocks, use the `fromBlockBodyData` method. The PR also refactors the internals of the legacy transaction and the new typed transaction: there is a base transaction class, which both transaction classes extends. It is also possible to import `EIP2930Transaction` (an access list transaction).

### How to migrate

The old `Transaction` class has been renamed to `LegacyTransaction`. This class works just how it used to work.


## 3.0.2 - 2021-02-16

Expand Down
2 changes: 1 addition & 1 deletion packages/tx/karma.conf.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ module.exports = function (config) {
browserNoActivityTimeout: 60000,
frameworks: ['browserify', 'tap'],
// the official transaction's test suite is disabled for now, see https://github.com/ethereumjs/ethereumjs-testing/issues/40
files: ['./test-build/test/api.js'],
files: ['./test-build/test/legacy.spec.js'],
preprocessors: {
'./test-build/**/*.js': ['browserify'],
},
Expand Down
195 changes: 195 additions & 0 deletions packages/tx/src/baseTransaction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
import Common from '@ethereumjs/common'
import {
Address,
BN,
toBuffer,
MAX_INTEGER,
unpadBuffer,
ecsign,
publicToAddress,
} from 'ethereumjs-util'
import { BaseTransactionData, BaseTxOptions, DEFAULT_COMMON, JsonTx } from './types'

export abstract class BaseTransaction<TransactionObject> {
public readonly nonce: BN
public readonly gasLimit: BN
public readonly gasPrice: BN
public readonly to?: Address
public readonly value: BN
public readonly data: Buffer
public readonly common: Common

constructor(txData: BaseTransactionData, txOptions: BaseTxOptions = {}) {
const { nonce, gasLimit, gasPrice, to, value, data } = txData

this.nonce = new BN(toBuffer(nonce))
this.gasPrice = new BN(toBuffer(gasPrice))
this.gasLimit = new BN(toBuffer(gasLimit))
this.to = to ? new Address(toBuffer(to)) : undefined
this.value = new BN(toBuffer(value))
this.data = toBuffer(data)

const validateCannotExceedMaxInteger = {
nonce: this.nonce,
gasPrice: this.gasPrice,
gasLimit: this.gasLimit,
value: this.value,
}

this.validateExceedsMaxInteger(validateCannotExceedMaxInteger)

this.common =
(txOptions.common &&
Object.assign(Object.create(Object.getPrototypeOf(txOptions.common)), txOptions.common)) ??
DEFAULT_COMMON
Copy link
Member

Choose a reason for hiding this comment

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

DEFAULT_COMMON won't work atm for the EIP2930Transaction without the activation of the EIPs. Not sure, do we need to separate the Common objects here and instantiate one with eips: [2718, 2930] or can we use the one with the EIP activations also for the LegacyTransaction? Or will this have side-effects? 🤔

Copy link
Member

Choose a reason for hiding this comment

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

Ah, likely best to directly set to berlin I guess.

Copy link
Member Author

Choose a reason for hiding this comment

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

If we set default common to Berlin, then these EIPs are activated by default right?

Also a side note: I think we should move DEFAULT_COMMON to Common, so we can have package-wide "default" common objects. I also think it would be helpful if we add a copy method to Common.

Copy link
Contributor

Choose a reason for hiding this comment

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

I also think it would be helpful if we add a copy method to Common.

that's a great idea

Copy link
Member

@holgerd77 holgerd77 Feb 26, 2021

Choose a reason for hiding this comment

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

Have been thinking about this a bit, this brings again up the compatibility question. So I guess we can't set to berlin yet, the VM uses the default blocks/txs and this will therefore get in an inconsistent state if the VM and the other libs are still on istanbul.

Might be a bit counterintuitive on first thought, but I guess the technically clean way to do it is to keep default on plain istanbul - even without the EIPs, since this is also a switch on the rule set - and then just let this throw on default when an EIP2930Transaction is instantiated without an explicit common. I think this makes it clear for everyone and should be acceptable if properly communicated (in the constructor docs). And it's just a temporary measure for a few months.

Together with the london implementation the time then might be there for another major release series and we then switch directly to london.

WDYGT? 🤔

DEFAULT_COMMON to Common: that's an interesting idea, we should be careful with this though and make sure it is properly thought through and we don't introduce any redundancy - potential source for inconsistencies here - we already have the DEFAULT_HARDFORK there. We might then also want to do DEFAULT_CHAIN set to mainnet (likely?). Not 100% sure but supportive on first thought, likely some case for a separate issue though.

Copy link
Member Author

Choose a reason for hiding this comment

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

I think I agree here - I don't think it makes sense to default it to Berlin, especially since it is not yet activated on mainnet. It would be very weird to default the VM to currently non-activated new logic.

About creating a DEFAULT_COMMON in Common: this is not in the scope of this PR I'd say, we can open an issue for it and then discuss this. I will open an issue.

Copy link
Member Author

Choose a reason for hiding this comment

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

Issue #1128

Again, it would make sense that we default to Istanbul. It is therefore not possible to create EIP2930Transaction if you don't pass an explicit Common to it with the EIPs activated.

}

protected validateExceedsMaxInteger(validateCannotExceedMaxInteger: { [key: string]: BN }) {
for (const [key, value] of Object.entries(validateCannotExceedMaxInteger)) {
if (value && value.gt(MAX_INTEGER)) {
throw new Error(`${key} cannot exceed MAX_INTEGER, given ${value}`)
}
}
}

/**
* If the tx's `to` is to the creation address
*/
toCreationAddress(): boolean {
return this.to === undefined || this.to.buf.length === 0
}

/**
* Computes a sha3-256 hash of the serialized unsigned tx, which is used to sign the transaction.
*/
rawTxHash(): Buffer {
return this.getMessageToSign()
}

abstract getMessageToSign(): Buffer

/**
* Returns chain ID
*/
getChainId(): number {
return this.common.chainId()
}

/**
* The amount of gas paid for the data in this tx
*/
getDataFee(): BN {
const txDataZero = this.common.param('gasPrices', 'txDataZero')
const txDataNonZero = this.common.param('gasPrices', 'txDataNonZero')

let cost = 0
for (let i = 0; i < this.data.length; i++) {
this.data[i] === 0 ? (cost += txDataZero) : (cost += txDataNonZero)
}
return new BN(cost)
}

/**
* The minimum amount of gas the tx must have (DataFee + TxFee + Creation Fee)
*/
getBaseFee(): BN {
const fee = this.getDataFee().addn(this.common.param('gasPrices', 'tx'))
if (this.common.gteHardfork('homestead') && this.toCreationAddress()) {
fee.iaddn(this.common.param('gasPrices', 'txCreation'))
}
return fee
}

/**
* The up front amount that an account must have for this transaction to be valid
*/
getUpfrontCost(): BN {
return this.gasLimit.mul(this.gasPrice).add(this.value)
}

/**
* Checks if the transaction has the minimum amount of gas required
* (DataFee + TxFee + Creation Fee).
*/
/**
* Checks if the transaction has the minimum amount of gas required
* (DataFee + TxFee + Creation Fee).
*/
validate(): boolean
/* eslint-disable-next-line no-dupe-class-members */
jochem-brouwer marked this conversation as resolved.
Show resolved Hide resolved
validate(stringError: false): boolean
/* eslint-disable-next-line no-dupe-class-members */
validate(stringError: true): string[]
/* eslint-disable-next-line no-dupe-class-members */
validate(stringError: boolean = false): boolean | string[] {
const errors = []

if (this.getBaseFee().gt(this.gasLimit)) {
errors.push(`gasLimit is too low. given ${this.gasLimit}, need at least ${this.getBaseFee()}`)
}

if (this.isSigned() && !this.verifySignature()) {
errors.push('Invalid Signature')
}

return stringError ? errors : errors.length === 0
}

/**
* Returns the encoding of the transaction.
*/
abstract serialize(): Buffer

/**
* Returns an object with the JSON representation of the transaction
*/
abstract toJSON(): JsonTx

abstract isSigned(): boolean

/**
* Determines if the signature is valid
*/
verifySignature(): boolean {
try {
// Main signature verification is done in `getSenderPublicKey()`
const publicKey = this.getSenderPublicKey()
return unpadBuffer(publicKey).length !== 0
} catch (e) {
return false
}
}

/**
* Returns the raw `Buffer[]` (LegacyTransaction) or `Buffer` (typed transaction).
* This is the data which is found in the transactions of the block body.
*/
abstract raw(): Buffer[] | Buffer
abstract hash(): Buffer

abstract getMessageToVerifySignature(): Buffer
/**
* Returns the sender's address
*/
getSenderAddress(): Address {
return new Address(publicToAddress(this.getSenderPublicKey()))
}
abstract getSenderPublicKey(): Buffer

sign(privateKey: Buffer): TransactionObject {
if (privateKey.length !== 32) {
throw new Error('Private key must be 32 bytes in length.')
}

const msgHash = this.getMessageToSign()

// Only `v` is reassigned.
/* eslint-disable-next-line prefer-const */
let { v, r, s } = ecsign(msgHash, privateKey)

return this.processSignature(v, r, s)
}

// Accept the v,r,s values from the `sign` method, and convert this into a TransactionObject
protected abstract processSignature(v: number, r: Buffer, s: Buffer): TransactionObject
}
Loading