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

Tx: New supports(capability) method #1315

Merged
merged 11 commits into from
Jun 24, 2021
Merged
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
5 changes: 3 additions & 2 deletions packages/block/src/block.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
TxOptions,
FeeMarketEIP1559Transaction,
Transaction,
Capabilities,
} from '@ethereumjs/tx'
import { BlockHeader } from './header'
import { BlockData, BlockOptions, JsonBlock, BlockBuffer, Blockchain } from './types'
Expand Down Expand Up @@ -161,7 +162,7 @@ export class Block {
return [
this.header.raw(),
this.transactions.map((tx) =>
'transactionType' in tx && tx.transactionType > 0 ? tx.serialize() : tx.raw()
tx.supports(Capabilities.EIP2718TypedTransaction) ? tx.serialize() : tx.raw()
) as Buffer[],
this.uncleHeaders.map((uh) => uh.raw()),
]
Expand Down Expand Up @@ -232,7 +233,7 @@ export class Block {
this.transactions.forEach((tx, i) => {
const errs = <string[]>tx.validate(true)
if (this._common.isActivatedEIP(1559)) {
if (tx.transactionType === 2) {
if (tx.supports(Capabilities.EIP1559FeeMarket)) {
tx = tx as FeeMarketEIP1559Transaction
if (tx.maxFeePerGas.lt(this.header.baseFeePerGas!)) {
errs.push('tx unable to pay base fee (EIP-1559 tx)')
Expand Down
55 changes: 54 additions & 1 deletion packages/tx/src/baseTransaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
FeeMarketEIP1559ValuesArray,
FeeMarketEIP1559TxData,
TxValuesArray,
Capabilities,
} from './types'

/**
Expand All @@ -42,6 +43,13 @@ export abstract class BaseTransaction<TransactionObject> {

public readonly common!: Common

/**
* List of tx type defining EIPs,
* e.g. 1559 (fee market) and 2930 (access lists)
* for FeeMarketEIP1559Transaction objects
*/
protected activeCapabilities: number[] = []

/**
* The default chain the tx falls back to if no Common
* is provided and if the chain can't be derived from
Expand Down Expand Up @@ -106,6 +114,26 @@ export abstract class BaseTransaction<TransactionObject> {
return this._type
}

/**
* Checks if a tx type defining capability is active
* on a tx, for example the EIP-1559 fee market mechanism
* or the EIP-2930 access list feature.
*
* Note that this is different from the tx type itself,
* so EIP-2930 access lists can very well be active
* on an EIP-1559 tx for example.
*
* This method can be useful for feature checks if the
* tx type is unknown (e.g. when instantiated with
* the tx factory).
*
* See `Capabilites` in the `types` module for a reference
* on all supported capabilities.
*/
supports(capability: Capabilities) {
return this.activeCapabilities.includes(capability)
}

/**
* Checks if the transaction has the minimum amount of gas required
* (DataFee + TxFee + Creation Fee).
Expand Down Expand Up @@ -240,9 +268,34 @@ export abstract class BaseTransaction<TransactionObject> {
if (privateKey.length !== 32) {
throw new Error('Private key must be 32 bytes in length.')
}

// Hack for the constellation that we have got a legacy tx after spuriousDragon with a non-EIP155 conforming signature
// and want to recreate a signature (where EIP155 should be applied)
// Leaving this hack lets the legacy.spec.ts -> sign(), verifySignature() test fail
// 2021-06-23
let hackApplied = false
if (
this.type === 0 &&
this.common.gteHardfork('spuriousDragon') &&
!this.supports(Capabilities.EIP155ReplayProtection)
) {
this.activeCapabilities.push(Capabilities.EIP155ReplayProtection)
hackApplied = true
}

const msgHash = this.getMessageToSign(true)
const { v, r, s } = ecsign(msgHash, privateKey)
return this._processSignature(v, r, s)
const tx = this._processSignature(v, r, s)

// Hack part 2
if (hackApplied) {
const index = this.activeCapabilities.indexOf(Capabilities.EIP155ReplayProtection)
if (index > -1) {
this.activeCapabilities.splice(index, 1)
}
}

return tx
}

/**
Expand Down
1 change: 1 addition & 0 deletions packages/tx/src/eip1559Transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ export default class FeeMarketEIP1559Transaction extends BaseTransaction<FeeMark
if (!this.common.isActivatedEIP(1559)) {
throw new Error('EIP-1559 not enabled on Common')
}
this.activeCapabilities = this.activeCapabilities.concat([1559, 2718, 2930])

// Populate the access list fields
const accessListData = AccessLists.getAccessListData(accessList ?? [])
Expand Down
1 change: 1 addition & 0 deletions packages/tx/src/eip2930Transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ export default class AccessListEIP2930Transaction extends BaseTransaction<Access
if (!this.common.isActivatedEIP(2930)) {
throw new Error('EIP-2930 not enabled on Common')
}
this.activeCapabilities = this.activeCapabilities.concat([2718, 2930])

// Populate the access list fields
const accessListData = AccessLists.getAccessListData(accessList ?? [])
Expand Down
52 changes: 39 additions & 13 deletions packages/tx/src/legacyTransaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
toBuffer,
unpadBuffer,
} from 'ethereumjs-util'
import { TxOptions, TxData, JsonTx, N_DIV_2, TxValuesArray } from './types'
import { TxOptions, TxData, JsonTx, N_DIV_2, TxValuesArray, Capabilities } from './types'
import { BaseTransaction } from './baseTransaction'
import Common from '@ethereumjs/common'

Expand Down Expand Up @@ -108,6 +108,25 @@ export default class Transaction extends BaseTransaction<Transaction> {

this._validateCannotExceedMaxInteger({ gasPrice: this.gasPrice })

if (this.common.gteHardfork('spuriousDragon')) {
if (!this.isSigned()) {
this.activeCapabilities.push(Capabilities.EIP155ReplayProtection)
} else {
// EIP155 spec:
// If block.number >= 2,675,000 and v = CHAIN_ID * 2 + 35 or v = CHAIN_ID * 2 + 36
// then when computing the hash of a transaction for purposes of signing or recovering
// instead of hashing only the first six elements (i.e. nonce, gasprice, startgas, to, value, data)
// hash nine elements, with v replaced by CHAIN_ID, r = 0 and s = 0.
const v = this.v!
const chainIdDoubled = this.common.chainIdBN().muln(2)

// v and chain ID meet EIP-155 conditions
if (v.eq(chainIdDoubled.addn(35)) || v.eq(chainIdDoubled.addn(36))) {
this.activeCapabilities.push(Capabilities.EIP155ReplayProtection)
}
}
}

const freeze = opts?.freeze ?? true
if (freeze) {
Object.freeze(this)
Expand Down Expand Up @@ -150,11 +169,7 @@ export default class Transaction extends BaseTransaction<Transaction> {
return rlp.encode(this.raw())
}

private _unsignedTxImplementsEIP155() {
return this.common.gteHardfork('spuriousDragon')
}

private _getMessageToSign(withEIP155: boolean) {
private _getMessageToSign() {
const values = [
bnToUnpaddedBuffer(this.nonce),
bnToUnpaddedBuffer(this.gasPrice),
Expand All @@ -164,7 +179,7 @@ export default class Transaction extends BaseTransaction<Transaction> {
this.data,
]

if (withEIP155) {
if (this.supports(Capabilities.EIP155ReplayProtection)) {
values.push(toBuffer(this.common.chainIdBN()))
values.push(unpadBuffer(toBuffer(0)))
values.push(unpadBuffer(toBuffer(0)))
Expand All @@ -191,7 +206,7 @@ export default class Transaction extends BaseTransaction<Transaction> {
getMessageToSign(hashMessage: false): Buffer[]
getMessageToSign(hashMessage?: true): Buffer
getMessageToSign(hashMessage = true) {
const message = this._getMessageToSign(this._unsignedTxImplementsEIP155())
const message = this._getMessageToSign()
if (hashMessage) {
return rlphash(message)
} else {
Expand Down Expand Up @@ -220,8 +235,10 @@ export default class Transaction extends BaseTransaction<Transaction> {
* Computes a sha3-256 hash which can be used to verify the signature
*/
getMessageToVerifySignature() {
const withEIP155 = this._signedTxImplementsEIP155()
const message = this._getMessageToSign(withEIP155)
if (!this.isSigned()) {
throw Error('This transaction is not signed')
}
const message = this._getMessageToSign()
return rlphash(message)
}

Expand All @@ -246,7 +263,7 @@ export default class Transaction extends BaseTransaction<Transaction> {
v!,
bnToUnpaddedBuffer(r!),
bnToUnpaddedBuffer(s!),
this._signedTxImplementsEIP155() ? this.common.chainIdBN() : undefined
this.supports(Capabilities.EIP155ReplayProtection) ? this.common.chainIdBN() : undefined
)
} catch (e) {
throw new Error('Invalid Signature')
Expand All @@ -258,7 +275,7 @@ export default class Transaction extends BaseTransaction<Transaction> {
*/
protected _processSignature(v: number, r: Buffer, s: Buffer) {
const vBN = new BN(v)
if (this._unsignedTxImplementsEIP155()) {
if (this.supports(Capabilities.EIP155ReplayProtection)) {
vBN.iadd(this.common.chainIdBN().muln(2).addn(8))
}

Expand Down Expand Up @@ -338,11 +355,20 @@ export default class Transaction extends BaseTransaction<Transaction> {
return this._getCommon(common, chainIdBN)
}

/**
* @deprecated if you have called this internal method please use `tx.supports(Capabilities.EIP155ReplayProtection)` instead
*/
private _unsignedTxImplementsEIP155() {
return this.common.gteHardfork('spuriousDragon')
}

/**
* @deprecated if you have called this internal method please use `tx.supports(Capabilities.EIP155ReplayProtection)` instead
*/
private _signedTxImplementsEIP155() {
if (!this.isSigned()) {
throw Error('This transaction is not signed')
}

const onEIP155BlockOrLater = this.common.gteHardfork('spuriousDragon')

// EIP155 spec:
Expand Down
30 changes: 30 additions & 0 deletions packages/tx/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,36 @@ import { default as Transaction } from './legacyTransaction'
import { default as AccessListEIP2930Transaction } from './eip2930Transaction'
import { default as FeeMarketEIP1559Transaction } from './eip1559Transaction'

/**
* Can be used in conjunction with the `supports()` function
* to query on tx capabilities
*/
export enum Capabilities {
/**
* Tx supports EIP-155 replay protection
* See: [155](https://eips.ethereum.org/EIPS/eip-155) Replay Attack Protection EIP
*/
EIP155ReplayProtection = 155,

/**
* Tx supports EIP-1559 gas fee market mechansim
* See: [1559](https://eips.ethereum.org/EIPS/eip-1559) Fee Market EIP
*/
EIP1559FeeMarket = 1559,

/**
* Tx is a typed transaction as defined in EIP-2718
* See: [2718](https://eips.ethereum.org/EIPS/eip-2718) Transaction Type EIP
*/
EIP2718TypedTransaction = 2718,

/**
* Tx supports access list generation as defined in EIP-2930
* See: [2930](https://eips.ethereum.org/EIPS/eip-2930) Access Lists EIP
*/
EIP2930AccessLists = 2930,
}

/**
* The options for initializing a Transaction.
*/
Expand Down
36 changes: 36 additions & 0 deletions packages/tx/test/base.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
AccessListEIP2930Transaction,
FeeMarketEIP1559Transaction,
N_DIV_2,
Capabilities,
} from '../src'
import { TxsJsonEntry } from './types'
import { BaseTransaction } from '../src/baseTransaction'
Expand Down Expand Up @@ -41,6 +42,13 @@ tape('[BaseTransaction]', function (t) {
values: Array(6).fill(zero),
txs: legacyTxs,
fixtures: legacyFixtures,
activeCapabilities: [],
notActiveCapabilities: [
Capabilities.EIP1559FeeMarket,
Capabilities.EIP2718TypedTransaction,
Capabilities.EIP2930AccessLists,
9999,
],
},
{
class: AccessListEIP2930Transaction,
Expand All @@ -49,6 +57,8 @@ tape('[BaseTransaction]', function (t) {
values: [Buffer.from([1])].concat(Array(7).fill(zero)),
txs: eip2930Txs,
fixtures: eip2930Fixtures,
activeCapabilities: [Capabilities.EIP2718TypedTransaction, Capabilities.EIP2930AccessLists],
notActiveCapabilities: [Capabilities.EIP1559FeeMarket, 9999],
},
{
class: FeeMarketEIP1559Transaction,
Expand All @@ -57,6 +67,12 @@ tape('[BaseTransaction]', function (t) {
values: [Buffer.from([1])].concat(Array(8).fill(zero)),
txs: eip1559Txs,
fixtures: eip1559Fixtures,
activeCapabilities: [
Capabilities.EIP1559FeeMarket,
Capabilities.EIP2718TypedTransaction,
Capabilities.EIP2930AccessLists,
],
notActiveCapabilities: [9999],
},
]

Expand Down Expand Up @@ -150,6 +166,26 @@ tape('[BaseTransaction]', function (t) {
st.end()
})

t.test('supports()', function (st) {
for (const txType of txTypes) {
txType.txs.forEach(function (tx: any) {
for (const activeCapability of txType.activeCapabilities) {
st.ok(
tx.supports(activeCapability),
`${txType.name}: should recognize all supported capabilities`
)
}
for (const notActiveCapability of txType.notActiveCapabilities) {
st.notOk(
tx.supports(notActiveCapability),
`${txType.name}: should reject non-active existing and not existing capabilities`
)
}
})
}
st.end()
})

t.test('raw()', function (st) {
for (const txType of txTypes) {
txType.txs.forEach(function (tx: any) {
Expand Down
Loading