@ethereumjs/block
Version:
Provides Block serialization and help functions
172 lines (152 loc) • 6 kB
text/typescript
import { ConsensusAlgorithm } from '@ethereumjs/common'
import { RLP } from '@ethereumjs/rlp'
import {
Address,
BIGINT_0,
BIGINT_27,
EthereumJSErrorWithoutCode,
bytesToBigInt,
concatBytes,
createAddressFromPublicKey,
createZeroAddress,
ecrecover,
equalsBytes,
} from '@ethereumjs/util'
import { secp256k1 } from '@noble/curves/secp256k1.js'
import type { CliqueConfig } from '@ethereumjs/common'
import type { BlockHeader } from '../index.ts'
// Fixed number of extra-data prefix bytes reserved for signer vanity
export const CLIQUE_EXTRA_VANITY = 32
// Fixed number of extra-data suffix bytes reserved for signer seal
export const CLIQUE_EXTRA_SEAL = 65
// This function is not exported in the index file to keep it internal
export function requireClique(header: BlockHeader, name: string) {
if (header.common.consensusAlgorithm() !== ConsensusAlgorithm.Clique) {
const msg = header['_errorMsg'](
`BlockHeader.${name}() call only supported for clique PoA networks`,
)
throw EthereumJSErrorWithoutCode(msg)
}
}
/**
* PoA clique signature hash without the seal.
*/
export function cliqueSigHash(header: BlockHeader) {
requireClique(header, 'cliqueSigHash')
const raw = header.raw()
raw[12] = header.extraData.subarray(0, header.extraData.length - CLIQUE_EXTRA_SEAL)
return header['keccakFunction'](RLP.encode(raw))
}
/**
* Checks if the block header is an epoch transition
* header (only clique PoA, throws otherwise)
*/
export function cliqueIsEpochTransition(header: BlockHeader): boolean {
requireClique(header, 'cliqueIsEpochTransition')
const epoch = BigInt((header.common.consensusConfig() as CliqueConfig).epoch)
// Epoch transition block if the block number has no
// remainder on the division by the epoch length
return header.number % epoch === BIGINT_0
}
/**
* Returns extra vanity data
* (only clique PoA, throws otherwise)
*/
export function cliqueExtraVanity(header: BlockHeader): Uint8Array {
requireClique(header, 'cliqueExtraVanity')
return header.extraData.subarray(0, CLIQUE_EXTRA_VANITY)
}
/**
* Returns extra seal data
* (only clique PoA, throws otherwise)
*/
export function cliqueExtraSeal(header: BlockHeader): Uint8Array {
requireClique(header, 'cliqueExtraSeal')
return header.extraData.subarray(-CLIQUE_EXTRA_SEAL)
}
/**
* Returns a list of signers
* (only clique PoA, throws otherwise)
*
* This function throws if not called on an epoch
* transition block and should therefore be used
* in conjunction with {@link cliqueIsEpochTransition}
*/
export function cliqueEpochTransitionSigners(header: BlockHeader): Address[] {
requireClique(header, 'cliqueEpochTransitionSigners')
if (!cliqueIsEpochTransition(header)) {
const msg = header['_errorMsg']('Signers are only included in epoch transition blocks (clique)')
throw EthereumJSErrorWithoutCode(msg)
}
const start = CLIQUE_EXTRA_VANITY
const end = header.extraData.length - CLIQUE_EXTRA_SEAL
const signerBytes = header.extraData.subarray(start, end)
const signerList: Uint8Array[] = []
const signerLength = 20
for (let start = 0; start <= signerBytes.length - signerLength; start += signerLength) {
signerList.push(signerBytes.subarray(start, start + signerLength))
}
return signerList.map((buf) => new Address(buf))
}
/**
* Returns the signer address
*/
export function cliqueSigner(header: BlockHeader): Address {
requireClique(header, 'cliqueSigner')
const extraSeal = cliqueExtraSeal(header)
// Reasonable default for default blocks
if (extraSeal.length === 0 || equalsBytes(extraSeal, new Uint8Array(65))) {
return createZeroAddress()
}
const r = extraSeal.subarray(0, 32)
const s = extraSeal.subarray(32, 64)
const v = bytesToBigInt(extraSeal.subarray(64, 65)) + BIGINT_27
const pubKey = ecrecover(cliqueSigHash(header), v, r, s)
return createAddressFromPublicKey(pubKey)
}
/**
* Verifies the signature of the block (last 65 bytes of extraData field)
* (only clique PoA, throws otherwise)
*
* Method throws if signature is invalid
*/
export function cliqueVerifySignature(header: BlockHeader, signerList: Address[]): boolean {
requireClique(header, 'cliqueVerifySignature')
const signerAddress = cliqueSigner(header)
const signerFound = signerList.find((signer) => {
return signer.equals(signerAddress)
})
return !!signerFound
}
/**
* Generates the extraData from a sealed block header
* @param header block header from which to retrieve extraData
* @param cliqueSigner clique signer key used for creating sealed block
* @returns clique seal (i.e. extradata) for the block
*/
export function generateCliqueBlockExtraData(
header: BlockHeader,
cliqueSigner: Uint8Array,
): Uint8Array {
// Ensure extraData is at least length CLIQUE_EXTRA_VANITY + CLIQUE_EXTRA_SEAL
const minExtraDataLength = CLIQUE_EXTRA_VANITY + CLIQUE_EXTRA_SEAL
if (header.extraData.length < minExtraDataLength) {
const remainingLength = minExtraDataLength - header.extraData.length
;(header.extraData as any) = concatBytes(header.extraData, new Uint8Array(remainingLength))
}
requireClique(header, 'generateCliqueBlockExtraData')
const msgHash = cliqueSigHash(header)
// Use custom ecsign if provided, otherwise use secp256k1.sign
const ecSignFunction = header.common.customCrypto?.ecsign ?? secp256k1.sign
// Use noble/curves secp256k1.sign with recovered format (returns 65-byte Uint8Array)
// sigBytes format: [recovery (1 byte) | r (32 bytes) | s (32 bytes)]
const sigBytes = ecSignFunction(msgHash, cliqueSigner, { prehash: false, format: 'recovered' })
// clique format: [r (32 bytes) | s (32 bytes) | recovery (1 byte)]
const cliqueSignature = concatBytes(sigBytes.subarray(1), sigBytes.subarray(0, 1))
const extraDataWithoutSeal = header.extraData.subarray(
0,
header.extraData.length - CLIQUE_EXTRA_SEAL,
)
const extraData = concatBytes(extraDataWithoutSeal, cliqueSignature)
return extraData
}