Skip to content

Util: remove ecsign method (instead directly use secp256k1.sign from external ethereum-cryptography package #3948

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

Merged
merged 19 commits into from
Apr 10, 2025

Conversation

jochem-brouwer
Copy link
Member

@jochem-brouwer jochem-brouwer commented Mar 28, 2025

Part of the breaking series to remove methods from util which are almost directly supported from other dedicated external packages (ethereum-cryptography)

TODO:

  • Remove all imports from 'ethereum-cryptography/secp256k1-compat.js'. These are legacy imports and are deprecated. We should use secp256k1 directly instead of this compat module. Partially done, other imports are part of this issue now Remove imports from secp256k1-compat.js #3966
  • CustomCrypto.ecsign type should point to the output of secp256k1.sign with ONLY the fields r, s, recovery
    - [ ] Change tx type v to number (thus also yParity!) (Not workable because chainId could be very large)
  • Fix WASM test in client
  • Dedupe ecsign/ecdsaSign in customCrypto interface

Copy link

codecov bot commented Mar 28, 2025

Codecov Report

Attention: Patch coverage is 84.74576% with 9 lines in your changes missing coverage. Please review.

Project coverage is 79.70%. Comparing base (a5b0dc3) to head (de30338).
Report is 1 commits behind head on master.

Additional details and impacted files

Impacted file tree graph

Flag Coverage Δ
block 83.34% <100.00%> (+0.04%) ⬆️
blockchain 89.33% <ø> (ø)
client 68.41% <55.00%> (?)
common 97.54% <ø> (ø)
devp2p 86.78% <100.00%> (+0.05%) ⬆️
evm 72.98% <ø> (ø)
genesis 99.98% <ø> (ø)
mpt 89.76% <ø> (ø)
rlp 91.43% <ø> (ø)
statemanager 69.16% <ø> (ø)
tx 90.59% <100.00%> (+0.01%) ⬆️
util 82.02% <100.00%> (+0.05%) ⬆️
vm 57.20% <ø> (ø)
wallet 88.55% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@jochem-brouwer
Copy link
Member Author

Our original ecsign method was a wrapper method. If we want to keep the same signature, we have to use ecdsaSign from ethereum-cryptography/secp256k1-compat.js. Need to do a tiny investigation on this.

@jochem-brouwer
Copy link
Member Author

There's a warning in ethereum-cryptography for this compat:

"Warning: use secp256k1 instead. This module is only for users who upgraded from ethereum-cryptography v0.1. It could be removed in the future."

https://github.com/ethereum/js-ethereum-cryptography/tree/main?tab=readme-ov-file#secp256k1-compat-compatibility-layer-with-other-libraries

Will directly do that here, removing all these compat imports.

@jochem-brouwer
Copy link
Member Author

I just did a fresh client run with datadir deleted and pkgs freshly installed to ensure that devp2p works, just imported blocks, so this looks fine :)

acolytec3
acolytec3 previously approved these changes Apr 8, 2025
Copy link
Contributor

@acolytec3 acolytec3 left a comment

Choose a reason for hiding this comment

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

LGTM. A couple of small nits but could be disregarded.

@jochem-brouwer
Copy link
Member Author

jochem-brouwer commented Apr 9, 2025

We should indeed remove ecdsaSign. This is in our current client code (this has changed this PR)

    cryptoFunctions.ecsign = (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 EthereumJSErrorWithoutCode('message length must be 32 bytes or greater')
      }
      const buf = secp256k1Sign(msg, pk)
      const r = bytesToBigInt(buf.slice(0, 32))
      const s = bytesToBigInt(buf.slice(32, 64))
      const recovery = buf[64]

      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 {
        r,
        s,
        recovery,
      }
    }

(They are equivalent)

Note: it was thus previously this wrapper which threw, not the method it seems? But in the new ecdsaSign the length check of hash is not there, which is why the

    assert.throws(
      () => wasmSign(randomBytes(31), randomBytes(32)),
      'message length must be 32 bytes or greater',
    )

Failed

@acolytec3
Copy link
Contributor

We should indeed remove ecdsaSign. This is in our current client code (this has changed this PR)

    cryptoFunctions.ecsign = (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 EthereumJSErrorWithoutCode('message length must be 32 bytes or greater')
      }
      const buf = secp256k1Sign(msg, pk)
      const r = bytesToBigInt(buf.slice(0, 32))
      const s = bytesToBigInt(buf.slice(32, 64))
      const recovery = buf[64]

      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 {
        r,
        s,
        recovery,
      }
    }

(They are equivalent)

Note: it was thus previously this wrapper which threw, not the method it seems? But in the new ecdsaSign the length check of hash is not there, which is why the

    assert.throws(
      () => wasmSign(randomBytes(31), randomBytes(32)),
      'message length must be 32 bytes or greater',
    )

Failed

To be clear, at one point, the wasm version of ecsign did throw when you passed in a message that was less than 32 bytes, but it was an obscure WASM error that didn't tell you what was wrong. This seems to be no longer the case, which is why we can remove that check. I added that check to clarify what the error was.

Copy link
Member

@holgerd77 holgerd77 left a comment

Choose a reason for hiding this comment

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

Not so deep into the topic, so only some loose side-review without any approval decision.

@@ -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

@@ -18,9 +17,11 @@ import {
waitReady,
sha256 as wasmSha256,
} from '@polkadot/wasm-crypto'
import { secp256k1 } from 'ethereum-cryptography/secp256k1'
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.

@@ -18,9 +17,11 @@ import {
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.

@@ -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.

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?

const customEcSign = (
_msg: Uint8Array,
_pk: Uint8Array,
): Pick<ReturnType<typeof secp256k1.sign>, 'recovery' | 'r' | 's'> => {
Copy link
Member

Choose a reason for hiding this comment

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

Maybe also here: ok from TypeScript/Node.js compatibility perspective?

Copy link
Contributor

Choose a reason for hiding this comment

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

The interface gets compiled away so should have zero impact on Nodejs compatibility. So, in this case, this Pick<ReturnType>... is duplicated from here. When you look in the compiled code, there's nothing in dist/esm/types.js

Copy link
Contributor

Choose a reason for hiding this comment

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

have also confirmed that these fancy types do not cause an issue in Node 23 with node --conditions=typescript --experimental-strip-types...

Copy link
Contributor

@acolytec3 acolytec3 left a comment

Choose a reason for hiding this comment

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

LGTM

@acolytec3 acolytec3 merged commit b843695 into master Apr 10, 2025
40 of 41 checks passed
elvhack pushed a commit to elvhack/ethereumjs-monorepo that referenced this pull request Apr 18, 2025
…rom external `ethereum-cryptography` package (ethereumjs#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]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants