Skip to content

Commit b843695

Browse files
jochem-brouweracolytec3holgerd77
authored
Util: remove ecsign method (instead directly use secp256k1.sign from external ethereum-cryptography package (#3948)
* util/monorepo: remove ecsign (use secp256k1.sign directly from ethereum-cryptography external package) * client: read wasm from util * tx: update auth sign to read from common crypto * devp2p: convert deprecated ecsign to secp256k1.sign * update customCrypto.ecsign return type * fix wasmCrypto test * switch v to number * Revert "switch v to number" This reverts commit 99859e5. * remove old comment * dedupe ecdsaSign * fix test * remove compat version of ecdsaRecover * tx: remove TODO * block/clique: ensure customCrypto and left padding is adhered --------- Co-authored-by: acolytec3 <[email protected]> Co-authored-by: Holger Drewes <[email protected]>
1 parent a5b0dc3 commit b843695

File tree

16 files changed

+140
-214
lines changed

16 files changed

+140
-214
lines changed

packages/block/src/consensus/clique.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,16 @@ import {
66
BIGINT_27,
77
EthereumJSErrorWithoutCode,
88
bigIntToBytes,
9+
bigIntToUnpaddedBytes,
910
bytesToBigInt,
1011
concatBytes,
1112
createAddressFromPublicKey,
1213
createZeroAddress,
1314
ecrecover,
14-
ecsign,
1515
equalsBytes,
16+
setLengthLeft,
1617
} from '@ethereumjs/util'
18+
import { secp256k1 } from 'ethereum-cryptography/secp256k1.js'
1719

1820
import type { CliqueConfig } from '@ethereumjs/common'
1921
import type { BlockHeader } from '../index.ts'
@@ -151,9 +153,13 @@ export function generateCliqueBlockExtraData(
151153

152154
requireClique(header, 'generateCliqueBlockExtraData')
153155

154-
const ecSignFunction = header.common.customCrypto?.ecsign ?? ecsign
156+
const ecSignFunction = header.common.customCrypto?.ecsign ?? secp256k1.sign
155157
const signature = ecSignFunction(cliqueSigHash(header), cliqueSigner)
156-
const signatureB = concatBytes(signature.r, signature.s, bigIntToBytes(signature.v))
158+
const signatureB = concatBytes(
159+
setLengthLeft(bigIntToUnpaddedBytes(signature.r), 32),
160+
setLengthLeft(bigIntToUnpaddedBytes(signature.s), 32),
161+
bigIntToBytes(BigInt(signature.recovery)),
162+
)
157163

158164
const extraDataWithoutSeal = header.extraData.subarray(
159165
0,

packages/client/bin/utils.ts

Lines changed: 28 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,13 @@ import {
1616
} from '@ethereumjs/common'
1717
import {
1818
EthereumJSErrorWithoutCode,
19+
bytesToBigInt,
1920
bytesToHex,
2021
calculateSigRecovery,
2122
concatBytes,
2223
createAddressFromPrivateKey,
2324
createAddressFromString,
2425
ecrecover,
25-
ecsign,
2626
hexToBytes,
2727
parseGethGenesisState,
2828
randomBytes,
@@ -38,7 +38,7 @@ import {
3838
sha256 as wasmSha256,
3939
} from '@polkadot/wasm-crypto'
4040
import { keccak256 } from 'ethereum-cryptography/keccak.js'
41-
import { ecdsaRecover, ecdsaSign } from 'ethereum-cryptography/secp256k1-compat.js'
41+
import { secp256k1 } from 'ethereum-cryptography/secp256k1.js'
4242
import { sha256 } from 'ethereum-cryptography/sha256.js'
4343
import { KZG as microEthKZG } from 'micro-eth-signer/kzg'
4444
import * as verkle from 'micro-eth-signer/verkle'
@@ -626,16 +626,12 @@ function generateAccount(): Account {
626626
return [address, privKey]
627627
}
628628

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

636632
const kzg = new microEthKZG(trustedSetup)
637633
// Initialize WASM crypto if JS crypto is not specified
638-
if (args.useJsCrypto === false) {
634+
if (useJsCrypto === false) {
639635
await waitReadyPolkadotSha256()
640636
cryptoFunctions.keccak256 = keccak256WASM
641637
cryptoFunctions.ecrecover = (
@@ -654,23 +650,12 @@ export async function generateClientConfig(args: ClientOpts) {
654650
).slice(1)
655651
cryptoFunctions.sha256 = wasmSha256
656652
cryptoFunctions.ecsign = (msg: Uint8Array, pk: Uint8Array) => {
657-
if (msg.length < 32) {
658-
// WASM errors with `unreachable` if we try to pass in less than 32 bytes in the message
659-
throw EthereumJSErrorWithoutCode('message length must be 32 bytes or greater')
660-
}
661653
const buf = secp256k1Sign(msg, pk)
662-
const r = buf.slice(0, 32)
663-
const s = buf.slice(32, 64)
664-
const v = BigInt(buf[64])
654+
const r = bytesToBigInt(buf.slice(0, 32))
655+
const s = bytesToBigInt(buf.slice(32, 64))
656+
const recovery = buf[64]
665657

666-
return { r, s, v }
667-
}
668-
cryptoFunctions.ecdsaSign = (hash: Uint8Array, pk: Uint8Array) => {
669-
const sig = secp256k1Sign(hash, pk)
670-
return {
671-
signature: sig.slice(0, 64),
672-
recid: sig[64],
673-
}
658+
return { r, s, recovery }
674659
}
675660
cryptoFunctions.ecdsaRecover = (sig: Uint8Array, recId: number, hash: Uint8Array) => {
676661
return secp256k1Recover(hash, sig, recId)
@@ -679,12 +664,29 @@ export async function generateClientConfig(args: ClientOpts) {
679664
cryptoFunctions.keccak256 = keccak256
680665
cryptoFunctions.ecrecover = ecrecover
681666
cryptoFunctions.sha256 = sha256
682-
cryptoFunctions.ecsign = ecsign
683-
cryptoFunctions.ecdsaSign = ecdsaSign
684-
cryptoFunctions.ecdsaRecover = ecdsaRecover
667+
cryptoFunctions.ecsign = secp256k1.sign
668+
cryptoFunctions.ecdsaRecover = (sig: Uint8Array, recId: number, hash: Uint8Array) => {
669+
// Adapted from @noble/curves docs
670+
const sign = secp256k1.Signature.fromCompact(sig)
671+
const point = sign.addRecoveryBit(recId).recoverPublicKey(hash)
672+
const address = point.toRawBytes(true)
673+
return address
674+
}
685675
}
686676
cryptoFunctions.kzg = kzg
687677
cryptoFunctions.verkle = verkle
678+
return cryptoFunctions
679+
}
680+
681+
export async function generateClientConfig(args: ClientOpts) {
682+
// Give chainId priority over networkId
683+
// Give networkId precedence over network name
684+
const chainName = args.chainId ?? args.networkId ?? args.network ?? Chain.Mainnet
685+
const chain = getPresetChainConfig(chainName)
686+
687+
// `useJsCrypto` defaults to `false` in the CLI defaults
688+
const cryptoFunctions = await getCryptoFunctions(args.useJsCrypto ?? false)
689+
688690
// Configure accounts for mining and prefunding in a local devnet
689691
const accounts: Account[] = []
690692
if (typeof args.unlock === 'string') {

packages/client/test/util/wasmCrypto.spec.ts

Lines changed: 20 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import {
44
bytesToHex,
55
calculateSigRecovery,
66
concatBytes,
7-
ecsign,
87
hexToBytes,
98
randomBytes,
109
setLengthLeft,
@@ -18,9 +17,11 @@ import {
1817
waitReady,
1918
sha256 as wasmSha256,
2019
} from '@polkadot/wasm-crypto'
21-
import { ecdsaRecover, ecdsaSign } from 'ethereum-cryptography/secp256k1-compat.js'
20+
import { secp256k1 } from 'ethereum-cryptography/secp256k1.js'
21+
2222
import { sha256 as jsSha256 } from 'ethereum-cryptography/sha256.js'
2323
import { assert, describe, it } from 'vitest'
24+
import { getCryptoFunctions } from '../../bin/utils.ts'
2425
describe('WASM crypto tests', () => {
2526
it('should compute public key and hash correctly using common.customCrypto functions', async () => {
2627
const wasmecrecover = (
@@ -65,39 +66,36 @@ describe('WASM crypto tests', () => {
6566
})
6667

6768
it('should compute the same signature whether js or WASM signature used', async () => {
68-
const wasmSign = (msg: Uint8Array, pk: Uint8Array) => {
69-
if (msg.length < 32) {
70-
// WASM errors with `unreachable` if we try to pass in less than 32 bytes in the message
71-
throw new Error('message length must be 32 bytes or greater')
72-
}
73-
const buf = secp256k1Sign(msg, pk)
74-
const r = buf.slice(0, 32)
75-
const s = buf.slice(32, 64)
76-
const v = BigInt(buf[64])
77-
78-
return { r, s, v }
79-
}
69+
const crypto = await getCryptoFunctions(true)
70+
const wasmSign = crypto.ecsign!
8071

8172
await waitReady()
8273
const msg = hexToBytes('0x82ff40c0a986c6a5cfad4ddf4c3aa6996f1a7837f9c398e17e5de5cbd5a12b28')
8374
const pk = hexToBytes('0x3c9229289a6125f7fdf1885a77bb12c37a8d3b4962d936f7e3084dece32a3ca1')
84-
const jsSig = ecsign(msg, pk)
75+
const jsSig = secp256k1.sign(msg, pk)
8576
const wasmSig = wasmSign(msg, pk)
8677
assert.deepEqual(wasmSig, jsSig, 'wasm signatures produce same result as js signatures')
87-
assert.throws(
88-
() => wasmSign(randomBytes(31), randomBytes(32)),
89-
'message length must be 32 bytes or greater',
90-
)
9178
})
9279
it('should have the same signature and verification', async () => {
80+
const crypto = await getCryptoFunctions(true)
9381
await waitReady()
9482
const pk = hexToBytes('0xbd3713a6da2c3624fa10bad8a52848b4291e3c9689ab50e0d2761e014d6e4cd7')
9583
const hash = hexToBytes('0x8c6d72155f746a9424b0621d82c5f5d3f6cc82e497b15df1b2ae601c8c14f75c')
96-
const jsSig = ecdsaSign(hash, pk)
84+
const jsSig = secp256k1.sign(hash, pk)
9785
const wasmSig = secp256k1Sign(hash, pk)
98-
assert.equal(bytesToHex(wasmSig.slice(0, 64)), bytesToHex(jsSig.signature))
86+
assert.equal(bytesToHex(wasmSig.slice(0, 64)), bytesToHex(jsSig.toCompactRawBytes()))
9987
const wasmRec = secp256k1Recover(hash, wasmSig.slice(0, 64), wasmSig[64])
100-
const jsRec = ecdsaRecover(jsSig.signature, jsSig.recid, hash)
88+
const jsRec = crypto.ecdsaRecover!(jsSig.toCompactRawBytes(), jsSig.recovery, hash)
89+
assert.equal(bytesToHex(wasmRec), bytesToHex(jsRec))
90+
})
91+
it('should recover the same address', async () => {
92+
const crypto = await getCryptoFunctions(true)
93+
await waitReady()
94+
const pk = hexToBytes('0xbd3713a6da2c3624fa10bad8a52848b4291e3c9689ab50e0d2761e014d6e4cd7')
95+
const hash = hexToBytes('0x8c6d72155f746a9424b0621d82c5f5d3f6cc82e497b15df1b2ae601c8c14f75c')
96+
const jsSig = secp256k1.sign(hash, pk)
97+
const wasmRec = secp256k1Recover(hash, jsSig.toCompactRawBytes(), jsSig.recovery)
98+
const jsRec = crypto.ecdsaRecover!(jsSig.toCompactRawBytes(), jsSig.recovery, hash)
10199
assert.equal(bytesToHex(wasmRec), bytesToHex(jsRec))
102100
})
103101
})

packages/common/src/types.ts

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,5 @@
1-
import type {
2-
BigIntLike,
3-
ECDSASignature,
4-
KZG,
5-
PrefixedHexString,
6-
VerkleCrypto,
7-
} from '@ethereumjs/util'
1+
import type { BigIntLike, KZG, PrefixedHexString, VerkleCrypto } from '@ethereumjs/util'
2+
import type { secp256k1 } from 'ethereum-cryptography/secp256k1.js'
83
import type { ConsensusAlgorithm, ConsensusType, Hardfork } from './enums.ts'
94

105
export interface ChainName {
@@ -95,8 +90,7 @@ export interface CustomCrypto {
9590
msg: Uint8Array,
9691
pk: Uint8Array,
9792
ecSignOpts?: { extraEntropy?: Uint8Array | boolean },
98-
) => ECDSASignature
99-
ecdsaSign?: (msg: Uint8Array, pk: Uint8Array) => { signature: Uint8Array; recid: number }
93+
) => Pick<ReturnType<typeof secp256k1.sign>, 'recovery' | 'r' | 's'>
10094
ecdsaRecover?: (sig: Uint8Array, recId: number, hash: Uint8Array) => Uint8Array
10195
kzg?: KZG
10296
verkle?: VerkleCrypto

packages/common/test/customCrypto.spec.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
1-
import { concatBytes, randomBytes } from '@ethereumjs/util'
1+
import { bytesToBigInt, concatBytes, randomBytes } from '@ethereumjs/util'
2+
import type { secp256k1 } from 'ethereum-cryptography/secp256k1.js'
23
import { assert, describe, it } from 'vitest'
34

45
import { Common, Mainnet, createCustomCommon } from '../src/index.ts'
56

6-
import type { ECDSASignature } from '@ethereumjs/util'
7-
87
describe('[Common]: Custom Crypto', () => {
98
const customKeccak256 = (msg: Uint8Array) => {
109
return concatBytes(msg, new Uint8Array([1]))
@@ -24,11 +23,14 @@ describe('[Common]: Custom Crypto', () => {
2423
return msg
2524
}
2625

27-
const customEcSign = (_msg: Uint8Array, _pk: Uint8Array): ECDSASignature => {
26+
const customEcSign = (
27+
_msg: Uint8Array,
28+
_pk: Uint8Array,
29+
): Pick<ReturnType<typeof secp256k1.sign>, 'recovery' | 'r' | 's'> => {
2830
return {
29-
v: 0n,
30-
r: Uint8Array.from([0, 1, 2, 3]),
31-
s: Uint8Array.from([0, 1, 2, 3]),
31+
recovery: 0,
32+
r: bytesToBigInt(Uint8Array.from([0, 1, 2, 3])),
33+
s: bytesToBigInt(Uint8Array.from([0, 1, 2, 3])),
3234
}
3335
}
3436

@@ -81,6 +83,6 @@ describe('[Common]: Custom Crypto', () => {
8183
ecsign: customEcSign,
8284
}
8385
const c = new Common({ chain: Mainnet, customCrypto })
84-
assert.equal(c.customCrypto.ecsign!(randomBytes(32), randomBytes(32)).v, 0n)
86+
assert.equal(c.customCrypto.ecsign!(randomBytes(32), randomBytes(32)).recovery, 0)
8587
})
8688
})

packages/devp2p/src/dpt/message.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,22 @@
11
import { RLP } from '@ethereumjs/rlp'
22
import {
33
EthereumJSErrorWithoutCode,
4+
bigIntToBytes,
45
bytesToHex,
56
bytesToInt,
67
bytesToUtf8,
78
concatBytes,
89
intToBytes,
10+
setLengthLeft,
911
} from '@ethereumjs/util'
1012
import debugDefault from 'debug'
1113
import { keccak256 } from 'ethereum-cryptography/keccak.js'
12-
import { ecdsaRecover, ecdsaSign } from 'ethereum-cryptography/secp256k1-compat.js'
14+
import { ecdsaRecover } from 'ethereum-cryptography/secp256k1-compat.js'
1315

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

1618
import type { Common } from '@ethereumjs/common'
19+
import { secp256k1 } from 'ethereum-cryptography/secp256k1'
1720
import type { PeerInfo } from '../types.ts'
1821

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

188191
const sighash = (common?.customCrypto.keccak256 ?? keccak256)(typedata)
189-
const sig = (common?.customCrypto.ecdsaSign ?? ecdsaSign)(sighash, privateKey)
190-
const hashdata = concatBytes(sig.signature, Uint8Array.from([sig.recid]), typedata)
192+
const sig = (common?.customCrypto.ecsign ?? secp256k1.sign)(sighash, privateKey)
193+
const hashdata = concatBytes(
194+
setLengthLeft(bigIntToBytes(sig.r), 32),
195+
setLengthLeft(bigIntToBytes(sig.s), 32),
196+
Uint8Array.from([sig.recovery]),
197+
typedata,
198+
)
191199
const hash = (common?.customCrypto.keccak256 ?? keccak256)(hashdata)
192200
return concatBytes(hash, hashdata)
193201
}

packages/devp2p/src/rlpx/ecies.ts

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,24 @@ import * as crypto from 'crypto'
22
import { RLP } from '@ethereumjs/rlp'
33
import {
44
EthereumJSErrorWithoutCode,
5+
bigIntToBytes,
56
bytesToInt,
67
concatBytes,
78
hexToBytes,
89
intToBytes,
10+
setLengthLeft,
911
} from '@ethereumjs/util'
1012
import debugDefault from 'debug'
1113
import { keccak256 } from 'ethereum-cryptography/keccak.js'
1214
import { getRandomBytesSync } from 'ethereum-cryptography/random.js'
13-
import { ecdh, ecdsaRecover, ecdsaSign } from 'ethereum-cryptography/secp256k1-compat.js'
15+
import { ecdh, ecdsaRecover } from 'ethereum-cryptography/secp256k1-compat.js'
1416
import { secp256k1 } from 'ethereum-cryptography/secp256k1.js'
1517

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

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

20-
import type { Common } from '@ethereumjs/common'
22+
import type { Common, CustomCrypto } from '@ethereumjs/common'
2123
type Decipher = crypto.Decipher
2224

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

7981
protected _keccakFunction: (msg: Uint8Array) => Uint8Array
80-
protected _ecdsaSign: (
81-
msg: Uint8Array,
82-
pk: Uint8Array,
83-
) => {
84-
signature: Uint8Array
85-
recid: number
86-
}
82+
protected _ecdsaSign: Required<CustomCrypto>['ecsign']
8783
protected _ecdsaRecover: (
8884
sig: Uint8Array,
8985
recId: number,
@@ -101,7 +97,7 @@ export class ECIES {
10197
this._ephemeralPublicKey = secp256k1.getPublicKey(this._ephemeralPrivateKey, false)
10298

10399
this._keccakFunction = common?.customCrypto.keccak256 ?? keccak256
104-
this._ecdsaSign = common?.customCrypto.ecdsaSign ?? ecdsaSign
100+
this._ecdsaSign = common?.customCrypto.ecsign ?? secp256k1.sign
105101
this._ecdsaRecover = common?.customCrypto.ecdsaRecover ?? ecdsaRecover
106102
}
107103

@@ -198,7 +194,11 @@ export class ECIES {
198194
const x = ecdhX(this._remotePublicKey, this._privateKey)
199195
const sig = this._ecdsaSign(xor(x, this._nonce), this._ephemeralPrivateKey)
200196
const data = [
201-
concatBytes(sig.signature, Uint8Array.from([sig.recid])),
197+
concatBytes(
198+
setLengthLeft(bigIntToBytes(sig.r), 32),
199+
setLengthLeft(bigIntToBytes(sig.s), 32),
200+
Uint8Array.from([sig.recovery]),
201+
),
202202
// this._keccakFunction(pk2id(this._ephemeralPublicKey)),
203203
pk2id(this._publicKey),
204204
this._nonce,
@@ -221,8 +221,9 @@ export class ECIES {
221221
const x = ecdhX(this._remotePublicKey, this._privateKey)
222222
const sig = this._ecdsaSign(xor(x, this._nonce), this._ephemeralPrivateKey)
223223
const data = concatBytes(
224-
sig.signature,
225-
Uint8Array.from([sig.recid]),
224+
bigIntToBytes(sig.r),
225+
bigIntToBytes(sig.s),
226+
Uint8Array.from([sig.recovery]),
226227
this._keccakFunction(pk2id(this._ephemeralPublicKey)),
227228
pk2id(this._publicKey),
228229
this._nonce,

0 commit comments

Comments
 (0)