Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
11 changes: 8 additions & 3 deletions packages/block/src/consensus/clique.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@ import {
BIGINT_27,
EthereumJSErrorWithoutCode,
bigIntToBytes,
bigIntToUnpaddedBytes,
bytesToBigInt,
concatBytes,
createAddressFromPublicKey,
createZeroAddress,
ecrecover,
ecsign,
equalsBytes,
} from '@ethereumjs/util'
import { secp256k1 } from 'ethereum-cryptography/secp256k1.js'

import type { CliqueConfig } from '@ethereumjs/common'
import type { BlockHeader } from '../index.ts'
Expand Down Expand Up @@ -151,9 +152,13 @@ export function generateCliqueBlockExtraData(

requireClique(header, 'generateCliqueBlockExtraData')

const ecSignFunction = header.common.customCrypto?.ecsign ?? ecsign
const ecSignFunction = secp256k1.sign
const signature = ecSignFunction(cliqueSigHash(header), cliqueSigner)
const signatureB = concatBytes(signature.r, signature.s, bigIntToBytes(signature.v))
const signatureB = concatBytes(
bigIntToUnpaddedBytes(signature.r),
bigIntToUnpaddedBytes(signature.s),
bigIntToBytes(BigInt(signature.recovery)),
)

const extraDataWithoutSeal = header.extraData.subarray(
0,
Expand Down
46 changes: 30 additions & 16 deletions packages/client/bin/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,13 @@ import {
} from '@ethereumjs/common'
import {
EthereumJSErrorWithoutCode,
bytesToBigInt,
bytesToHex,
calculateSigRecovery,
concatBytes,
createAddressFromPrivateKey,
createAddressFromString,
ecrecover,
ecsign,
hexToBytes,
parseGethGenesisState,
randomBytes,
Expand All @@ -38,7 +38,8 @@ import {
sha256 as wasmSha256,
} from '@polkadot/wasm-crypto'
import { keccak256 } from 'ethereum-cryptography/keccak.js'
import { ecdsaRecover, ecdsaSign } from 'ethereum-cryptography/secp256k1-compat.js'
import { ecdsaRecover } from 'ethereum-cryptography/secp256k1-compat.js'
Copy link
Member

Choose a reason for hiding this comment

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

So here it is still necessary that we use this deprecated compat module https://github.com/ethereum/js-ethereum-cryptography/blob/main/src/secp256k1-compat.ts ?

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 did not want to tackle this here because I thought it would bloat the PR too much, but yes we can directly do here. It is also part of this issue #3966

import { secp256k1 } from 'ethereum-cryptography/secp256k1.js'
import { sha256 } from 'ethereum-cryptography/sha256.js'
import { KZG as microEthKZG } from 'micro-eth-signer/kzg'
import * as verkle from 'micro-eth-signer/verkle'
Expand Down Expand Up @@ -626,16 +627,12 @@ function generateAccount(): Account {
return [address, privKey]
}

export async function generateClientConfig(args: ClientOpts) {
// Give chainId priority over networkId
// Give networkId precedence over network name
const chainName = args.chainId ?? args.networkId ?? args.network ?? Chain.Mainnet
const chain = getPresetChainConfig(chainName)
export async function getCryptoFunctions(useJsCrypto: boolean): Promise<CustomCrypto> {
const cryptoFunctions: CustomCrypto = {}

const kzg = new microEthKZG(trustedSetup)
// Initialize WASM crypto if JS crypto is not specified
if (args.useJsCrypto === false) {
if (useJsCrypto === false) {
await waitReadyPolkadotSha256()
cryptoFunctions.keccak256 = keccak256WASM
cryptoFunctions.ecrecover = (
Expand All @@ -659,17 +656,22 @@ export async function generateClientConfig(args: ClientOpts) {
throw EthereumJSErrorWithoutCode('message length must be 32 bytes or greater')
}
const buf = secp256k1Sign(msg, pk)
const r = buf.slice(0, 32)
const s = buf.slice(32, 64)
const v = BigInt(buf[64])
const r = bytesToBigInt(buf.slice(0, 32))
const s = bytesToBigInt(buf.slice(32, 64))
const recovery = buf[64]

return { r, s, v }
return { r, s, recovery }
}
cryptoFunctions.ecdsaSign = (hash: Uint8Array, pk: Uint8Array) => {
const sig = secp256k1Sign(hash, pk)
const r = bytesToBigInt(sig.slice(0, 32))
const s = bytesToBigInt(sig.slice(32, 64))
const recovery = Number(sig[64])

return {
signature: sig.slice(0, 64),
recid: sig[64],
r,
s,
recovery,
}
}
cryptoFunctions.ecdsaRecover = (sig: Uint8Array, recId: number, hash: Uint8Array) => {
Expand All @@ -679,12 +681,24 @@ export async function generateClientConfig(args: ClientOpts) {
cryptoFunctions.keccak256 = keccak256
cryptoFunctions.ecrecover = ecrecover
cryptoFunctions.sha256 = sha256
cryptoFunctions.ecsign = ecsign
cryptoFunctions.ecdsaSign = ecdsaSign
cryptoFunctions.ecsign = secp256k1.sign
cryptoFunctions.ecdsaSign = secp256k1.sign
cryptoFunctions.ecdsaRecover = ecdsaRecover
}
cryptoFunctions.kzg = kzg
cryptoFunctions.verkle = verkle
return cryptoFunctions
}

export async function generateClientConfig(args: ClientOpts) {
// Give chainId priority over networkId
// Give networkId precedence over network name
const chainName = args.chainId ?? args.networkId ?? args.network ?? Chain.Mainnet
const chain = getPresetChainConfig(chainName)

// `useJsCrypto` defaults to `false` in the CLI defaults
const cryptoFunctions = await getCryptoFunctions(args.useJsCrypto ?? false)

// Configure accounts for mining and prefunding in a local devnet
const accounts: Account[] = []
if (typeof args.unlock === 'string') {
Expand Down
19 changes: 5 additions & 14 deletions packages/client/test/util/wasmCrypto.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
bytesToHex,
calculateSigRecovery,
concatBytes,
ecsign,
hexToBytes,
randomBytes,
setLengthLeft,
Expand All @@ -18,9 +17,11 @@
waitReady,
sha256 as wasmSha256,
} from '@polkadot/wasm-crypto'
import { secp256k1 } from 'ethereum-cryptography/secp256k1'
Copy link
Member

Choose a reason for hiding this comment

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

Not totally a blocker, but it's a bit semi-beautiful that we have a mixture of these .js and non-.js imports for the ethereum-cryptography imports.

import { ecdsaRecover, ecdsaSign } from 'ethereum-cryptography/secp256k1-compat.js'
Copy link
Member

Choose a reason for hiding this comment

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

Also here, still this compat thing in.

import { sha256 as jsSha256 } from 'ethereum-cryptography/sha256.js'
import { assert, describe, it } from 'vitest'
import { getCryptoFunctions } from '../../bin/utils.ts'
describe('WASM crypto tests', () => {
it('should compute public key and hash correctly using common.customCrypto functions', async () => {
const wasmecrecover = (
Expand Down Expand Up @@ -65,26 +66,16 @@
})

it('should compute the same signature whether js or WASM signature used', async () => {
const wasmSign = (msg: Uint8Array, pk: Uint8Array) => {
if (msg.length < 32) {
// WASM errors with `unreachable` if we try to pass in less than 32 bytes in the message
throw new Error('message length must be 32 bytes or greater')
}
const buf = secp256k1Sign(msg, pk)
const r = buf.slice(0, 32)
const s = buf.slice(32, 64)
const v = BigInt(buf[64])

return { r, s, v }
}
const crypto = await getCryptoFunctions(true)
const wasmSign = crypto.ecdsaSign!

await waitReady()
const msg = hexToBytes('0x82ff40c0a986c6a5cfad4ddf4c3aa6996f1a7837f9c398e17e5de5cbd5a12b28')
const pk = hexToBytes('0x3c9229289a6125f7fdf1885a77bb12c37a8d3b4962d936f7e3084dece32a3ca1')
const jsSig = ecsign(msg, pk)
const jsSig = secp256k1.sign(msg, pk)
const wasmSig = wasmSign(msg, pk)
assert.deepEqual(wasmSig, jsSig, 'wasm signatures produce same result as js signatures')
assert.throws(

Check failure on line 78 in packages/client/test/util/wasmCrypto.spec.ts

View workflow job for this annotation

GitHub Actions / client / test-client

test/util/wasmCrypto.spec.ts > WASM crypto tests > should compute the same signature whether js or WASM signature used

AssertionError: expected [Function] to throw an error - Expected: null + Received: undefined ❯ test/util/wasmCrypto.spec.ts:78:12
() => wasmSign(randomBytes(31), randomBytes(32)),
'message length must be 32 bytes or greater',
)
Expand Down
2 changes: 1 addition & 1 deletion packages/common/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ export interface CustomCrypto {
pk: Uint8Array,
ecSignOpts?: { extraEntropy?: Uint8Array | boolean },
) => ECDSASignature
ecdsaSign?: (msg: Uint8Array, pk: Uint8Array) => { signature: Uint8Array; recid: number }
ecdsaSign?: (msg: Uint8Array, pk: Uint8Array) => { r: bigint; s: bigint; recovery: number }
ecdsaRecover?: (sig: Uint8Array, recId: number, hash: Uint8Array) => Uint8Array
kzg?: KZG
verkle?: VerkleCrypto
Expand Down
10 changes: 5 additions & 5 deletions packages/common/test/customCrypto.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { concatBytes, randomBytes } from '@ethereumjs/util'
import { bytesToBigInt, concatBytes, randomBytes } from '@ethereumjs/util'
import { assert, describe, it } from 'vitest'

import { Common, Mainnet, createCustomCommon } from '../src/index.ts'
Expand Down Expand Up @@ -26,9 +26,9 @@ describe('[Common]: Custom Crypto', () => {

const customEcSign = (_msg: Uint8Array, _pk: Uint8Array): ECDSASignature => {
return {
v: 0n,
r: Uint8Array.from([0, 1, 2, 3]),
s: Uint8Array.from([0, 1, 2, 3]),
recovery: 0,
r: bytesToBigInt(Uint8Array.from([0, 1, 2, 3])),
s: bytesToBigInt(Uint8Array.from([0, 1, 2, 3])),
}
}

Expand Down Expand Up @@ -81,6 +81,6 @@ describe('[Common]: Custom Crypto', () => {
ecsign: customEcSign,
}
const c = new Common({ chain: Mainnet, customCrypto })
assert.equal(c.customCrypto.ecsign!(randomBytes(32), randomBytes(32)).v, 0n)
assert.equal(c.customCrypto.ecsign!(randomBytes(32), randomBytes(32)).recovery, 0)
})
})
14 changes: 11 additions & 3 deletions packages/devp2p/src/dpt/message.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
import { RLP } from '@ethereumjs/rlp'
import {
EthereumJSErrorWithoutCode,
bigIntToBytes,
bytesToHex,
bytesToInt,
bytesToUtf8,
concatBytes,
intToBytes,
setLengthLeft,
} from '@ethereumjs/util'
import debugDefault from 'debug'
import { keccak256 } from 'ethereum-cryptography/keccak.js'
import { ecdsaRecover, ecdsaSign } from 'ethereum-cryptography/secp256k1-compat.js'
import { ecdsaRecover } from 'ethereum-cryptography/secp256k1-compat.js'

import { assertEq, ipToBytes, ipToString, isV4Format, isV6Format, unstrictDecode } from '../util.ts'

import type { Common } from '@ethereumjs/common'
import { secp256k1 } from 'ethereum-cryptography/secp256k1'
import type { PeerInfo } from '../types.ts'

const debug = debugDefault('devp2p:dpt:server')
Expand Down Expand Up @@ -186,8 +189,13 @@ export function encode<T>(typename: string, data: T, privateKey: Uint8Array, com
const typedata = concatBytes(Uint8Array.from([type]), RLP.encode(encodedMsg))

const sighash = (common?.customCrypto.keccak256 ?? keccak256)(typedata)
const sig = (common?.customCrypto.ecdsaSign ?? ecdsaSign)(sighash, privateKey)
const hashdata = concatBytes(sig.signature, Uint8Array.from([sig.recid]), typedata)
const sig = (common?.customCrypto.ecdsaSign ?? secp256k1.sign)(sighash, privateKey)
const hashdata = concatBytes(
setLengthLeft(bigIntToBytes(sig.r), 32),
setLengthLeft(bigIntToBytes(sig.s), 32),
Uint8Array.from([sig.recovery]),
typedata,
)
const hash = (common?.customCrypto.keccak256 ?? keccak256)(hashdata)
return concatBytes(hash, hashdata)
}
Expand Down
27 changes: 14 additions & 13 deletions packages/devp2p/src/rlpx/ecies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,24 @@ import * as crypto from 'crypto'
import { RLP } from '@ethereumjs/rlp'
import {
EthereumJSErrorWithoutCode,
bigIntToBytes,
bytesToInt,
concatBytes,
hexToBytes,
intToBytes,
setLengthLeft,
} from '@ethereumjs/util'
import debugDefault from 'debug'
import { keccak256 } from 'ethereum-cryptography/keccak.js'
import { getRandomBytesSync } from 'ethereum-cryptography/random.js'
import { ecdh, ecdsaRecover, ecdsaSign } from 'ethereum-cryptography/secp256k1-compat.js'
import { ecdh, ecdsaRecover } from 'ethereum-cryptography/secp256k1-compat.js'
import { secp256k1 } from 'ethereum-cryptography/secp256k1.js'

import { assertEq, genPrivateKey, id2pk, pk2id, unstrictDecode, xor, zfill } from '../util.ts'

import { MAC } from './mac.ts'

import type { Common } from '@ethereumjs/common'
import type { Common, CustomCrypto } from '@ethereumjs/common'
type Decipher = crypto.Decipher

const debug = debugDefault('devp2p:rlpx:peer')
Expand Down Expand Up @@ -77,13 +79,7 @@ export class ECIES {
protected _bodySize: number | null = null

protected _keccakFunction: (msg: Uint8Array) => Uint8Array
protected _ecdsaSign: (
msg: Uint8Array,
pk: Uint8Array,
) => {
signature: Uint8Array
recid: number
}
protected _ecdsaSign: Required<CustomCrypto>['ecsign']
Copy link
Member

Choose a reason for hiding this comment

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

Is this ok from a TypeScript/Node.js compatibility perspective?

(if not AND this was not caught by any unit tests: we should really add this to CI in some form, so that similar code adoptions do not slip through in the future)

Copy link
Member Author

Choose a reason for hiding this comment

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

@gabrocheleau could you take a look here? How would we test this?

Copy link
Contributor

Choose a reason for hiding this comment

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

Same as the other place, there really isn't any issue from a Nodejs compatibility perspective because none of these field declarations are in the compiled code. Is that what you're asking about?

protected _ecdsaRecover: (
sig: Uint8Array,
recId: number,
Expand All @@ -101,7 +97,7 @@ export class ECIES {
this._ephemeralPublicKey = secp256k1.getPublicKey(this._ephemeralPrivateKey, false)

this._keccakFunction = common?.customCrypto.keccak256 ?? keccak256
this._ecdsaSign = common?.customCrypto.ecdsaSign ?? ecdsaSign
this._ecdsaSign = common?.customCrypto.ecdsaSign ?? secp256k1.sign
this._ecdsaRecover = common?.customCrypto.ecdsaRecover ?? ecdsaRecover
}

Expand Down Expand Up @@ -198,7 +194,11 @@ export class ECIES {
const x = ecdhX(this._remotePublicKey, this._privateKey)
const sig = this._ecdsaSign(xor(x, this._nonce), this._ephemeralPrivateKey)
const data = [
concatBytes(sig.signature, Uint8Array.from([sig.recid])),
concatBytes(
setLengthLeft(bigIntToBytes(sig.r), 32),
setLengthLeft(bigIntToBytes(sig.s), 32),
Uint8Array.from([sig.recovery]),
),
// this._keccakFunction(pk2id(this._ephemeralPublicKey)),
pk2id(this._publicKey),
this._nonce,
Expand All @@ -221,8 +221,9 @@ export class ECIES {
const x = ecdhX(this._remotePublicKey, this._privateKey)
const sig = this._ecdsaSign(xor(x, this._nonce), this._ephemeralPrivateKey)
const data = concatBytes(
sig.signature,
Uint8Array.from([sig.recid]),
bigIntToBytes(sig.r),
bigIntToBytes(sig.s),
Uint8Array.from([sig.recovery]),
this._keccakFunction(pk2id(this._ephemeralPublicKey)),
pk2id(this._publicKey),
this._nonce,
Expand Down
9 changes: 5 additions & 4 deletions packages/tx/src/capabilities/legacy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ import {
bigIntToUnpaddedBytes,
bytesToHex,
ecrecover,
ecsign,
publicToAddress,
unpadBytes,
} from '@ethereumjs/util'
import { keccak256 } from 'ethereum-cryptography/keccak.js'

import { Capability, TransactionType } from '../types.ts'

import { secp256k1 } from 'ethereum-cryptography/secp256k1'
import type { LegacyTx } from '../legacy/tx.ts'
import type { LegacyTxInterface, Transaction } from '../types.ts'

Expand Down Expand Up @@ -256,9 +256,10 @@ export function sign(
}

const msgHash = tx.getHashedMessageToSign()
const ecSignFunction = tx.common.customCrypto?.ecsign ?? ecsign
const { v, r, s } = ecSignFunction(msgHash, privateKey, { extraEntropy })
const signedTx = tx.addSignature(v, r, s, true)
const ecSignFunction = tx.common.customCrypto?.ecsign ?? secp256k1.sign
const { recovery, r, s } = ecSignFunction(msgHash, privateKey, { extraEntropy })
// TODO: addSignature `v` can likely be converted to type `number` instead `bigint`
const signedTx = tx.addSignature(BigInt(recovery), r, s, true)

// Hack part 2
if (hackApplied) {
Expand Down
2 changes: 1 addition & 1 deletion packages/tx/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ export interface TransactionInterface<T extends TransactionType = TransactionTyp
errorStr(): string

addSignature(
v: bigint,
v: bigint, // TODO: change this to number?
Copy link
Member

Choose a reason for hiding this comment

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

This TODO should be removed.

r: Uint8Array | bigint,
s: Uint8Array | bigint,
convertV?: boolean,
Expand Down
Loading
Loading