Skip to content

Commit

Permalink
refactor: eth-lib removal wip
Browse files Browse the repository at this point in the history
  • Loading branch information
nicolasbrugneaux committed Feb 23, 2024
1 parent 5bb4005 commit be1dd0c
Show file tree
Hide file tree
Showing 9 changed files with 165 additions and 114 deletions.
4 changes: 3 additions & 1 deletion packages/sdk/wallets/wallet-base/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,10 @@
"@celo/base": "^6.0.0",
"@celo/connect": "^5.1.2",
"@celo/utils": "^6.0.0",
"@ethereumjs/rlp": "^5.0.0",
"@ethereumjs/rlp": "^5.0.2",
"@ethereumjs/util": "8.0.5",
"@noble/curves": "^1.3.0",
"@noble/hashes": "^1.3.3",
"@types/debug": "^4.1.5",
"bignumber.js": "^9.0.0",
"debug": "^4.1.1",
Expand Down
71 changes: 48 additions & 23 deletions packages/sdk/wallets/wallet-base/src/signing-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,25 +15,20 @@ import {
} from '@celo/connect/lib/utils/formatter'
import { EIP712TypedData, generateTypedDataHash } from '@celo/utils/lib/sign-typed-data-utils'
import { parseSignatureWithoutPrefix } from '@celo/utils/lib/signatureUtils'
import { publicKeyToAddress } from '@celo/utils/src/address'
// @ts-ignore-next-line
import * as ethUtil from '@ethereumjs/util'
import { secp256k1 } from '@noble/curves/secp256k1'
import { keccak_256 } from '@noble/hashes/sha3'
import { hexToBytes } from '@noble/hashes/utils'
import debugFactory from 'debug'
// @ts-ignore-next-line eth-lib types not found
import { account as Account, bytes as Bytes, RLP } from 'eth-lib'
import { bytes as Bytes, RLP } from 'eth-lib'
// TODO: replace by @ethereumjs/rlp
import Web3 from 'web3' // TODO try to do this without web3 direct
import Accounts from 'web3-eth-accounts'

const {
Address,
ecrecover,
fromRpcSig,
hashPersonalMessage,
pubToAddress,
toBuffer,
toChecksumAddress,
} = ethUtil
const { ecrecover, fromRpcSig, hashPersonalMessage, toBuffer } = ethUtil
const debug = debugFactory('wallet-base:tx:sign')

// Original code taken from
Expand All @@ -57,7 +52,7 @@ export function chainIdTransformationForSigning(chainId: number): number {
return chainId * 2 + 35
}

export function getHashFromEncoded(rlpEncode: string): string {
export function getHashFromEncoded(rlpEncode: string): Hex {
const rlpBytes = hexToBytes(trimLeading0x(rlpEncode))
const hash = Buffer.from(keccak_256(rlpBytes))
return `0x${hash.toString('hex')}`
Expand Down Expand Up @@ -469,13 +464,17 @@ export function recoverTransaction(rawTx: string): [CeloTx, string] {
data: rawValues[8],
chainId,
}
const { r, v, s } = extractSignatureFromDecoded(rawValues)
const signature = Account.encodeSignature([v, r, s])
const { r, v: _v, s } = extractSignatureFromDecoded(rawValues)
let v = parseInt(_v || '0x0', 16)
const signature = new secp256k1.Signature(BigInt(r), BigInt(s)).addRecoveryBit(
v - chainIdTransformationForSigning(chainId)
)
const extraData = recovery < 35 ? [] : [chainId, '0x', '0x']
const signingData = rawValues.slice(0, 9).concat(extraData)
const signingDataHex = RLP.encode(signingData)
const signer = Account.recover(getHashFromEncoded(signingDataHex), signature)
return [celoTx, signer]
const signingDataHash = getHashFromEncoded(signingDataHex)
const publicKey = signature.recoverPublicKey(trimLeading0x(signingDataHash)).toHex(false)
return [celoTx, publicKeyToAddress(publicKey)]
}
}

Expand Down Expand Up @@ -505,7 +504,8 @@ export function getSignerFromTxEIP2718TX(serializedTransaction: string): string
transactionArray,
determineTXType(serializedTransaction)
)
return toChecksumAddress(Address.fromPublicKey(signer).toString())

return publicKeyToAddress(signer.toString('hex'))
}

function determineTXType(serializedTransaction: string): TransactionTypes {
Expand Down Expand Up @@ -670,8 +670,8 @@ export function recoverMessageSigner(signingDataHex: string, signedData: string)
const signature = fromRpcSig(signedData)

const publicKey = ecrecover(msgHashBuff, signature.v, signature.r, signature.s)
const address = pubToAddress(publicKey, true)
return ensureLeading0x(address.toString('hex'))
const address = publicKeyToAddress(publicKey.toString('hex'))
return ensureLeading0x(address)
}

export function verifyEIP712TypedDataSigner(
Expand All @@ -696,12 +696,37 @@ export function verifySignatureWithoutPrefix(
}
}

export function decodeSig(sig: any) {
const [v, r, s] = Account.decodeSignature(sig)
function bigintToPaddedHex(n: bigint, length: number): string {
const hex = n.toString(16)
if (hex.length >= length) {
return hex
}
const padded = new Array(length).fill('0').join('') + hex
return padded.slice(-length)
}

export function decodeSig(sig: Hex | ReturnType<typeof secp256k1.sign>, addToV = 0) {
const { recovery, r, s } = typeof sig === 'string' ? secp256k1.Signature.fromCompact(sig) : sig

return {
v: parseInt(v, 16),
r: toBuffer(r) as Buffer,
s: toBuffer(s) as Buffer,
v: recovery! + addToV,
r: Buffer.from(bigintToPaddedHex(r, 64), 'hex'),
s: Buffer.from(bigintToPaddedHex(s, 64), 'hex'),
}
// const [v, r, s] = Account.decodeSignature(sig)

// return {
// v: parseInt(v, 16),
// r: toBuffer(r) as Buffer,
// s: toBuffer(s) as Buffer,
// }
}

export function signTransaction(hash: Hex, privateKey: Hex, addToV = 0) {
const signature = secp256k1.sign(
trimLeading0x(hash),
hexToBytes(trimLeading0x(privateKey)),
{ lowS: true } // canonical:true
)
return decodeSig(signature, addToV)
}
4 changes: 2 additions & 2 deletions packages/sdk/wallets/wallet-ledger/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,11 @@
"@ledgerhq/errors": "^5.50.0",
"@ledgerhq/hw-app-eth": "~5.11.0",
"@ledgerhq/hw-transport": "~5.11.0",
"debug": "^4.1.1",
"eth-lib": "^0.2.8"
"debug": "^4.1.1"
},
"devDependencies": {
"@ledgerhq/hw-transport-node-hid": "^6.27.4",
"@noble/hashes": "^1.3.3",
"web3": "1.10.0"
},
"engines": {
Expand Down
12 changes: 6 additions & 6 deletions packages/sdk/wallets/wallet-ledger/src/ledger-signer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import debugFactory from 'debug'
import { transportErrorFriendlyMessage } from './ledger-utils'
import { AddressValidation } from './ledger-wallet'
import { compareLedgerAppVersions, tokenInfoByAddressAndChainId } from './tokens'
import { ILedger } from './types'

const debug = debugFactory('kit:wallet:ledger')
const CELO_APP_ACCEPTS_CONTRACT_DATA_FROM_VERSION = '1.0.2'
Expand All @@ -15,14 +16,14 @@ const CELO_APP_ACCEPTS_CONTRACT_DATA_FROM_VERSION = '1.0.2'
* Signs the EVM transaction with a Ledger device
*/
export class LedgerSigner implements Signer {
private ledger: any
private ledger: ILedger
private derivationPath: string
private validated: boolean = false
private ledgerAddressValidation: AddressValidation
private appConfiguration: { arbitraryDataEnabled: number; version: string }

constructor(
ledger: any,
ledger: ILedger,
derivationPath: string,
ledgerAddressValidation: AddressValidation,
appConfiguration: { arbitraryDataEnabled: number; version: string } = {
Expand Down Expand Up @@ -57,9 +58,8 @@ export class LedgerSigner implements Signer {
if (rv !== addToV && (rv & addToV) !== rv) {
addToV += 1 // add signature v bit.
}
signature.v = addToV.toString(10)
return {
v: signature.v,
v: addToV,
r: ethUtil.toBuffer(ensureLeading0x(signature.r)) as Buffer,
s: ethUtil.toBuffer(ensureLeading0x(signature.s)) as Buffer,
}
Expand Down Expand Up @@ -88,7 +88,7 @@ export class LedgerSigner implements Signer {
)

return {
v: signature.v,
v: parseInt(signature.v, 16),
r: ethUtil.toBuffer(ensureLeading0x(signature.r)) as Buffer,
s: ethUtil.toBuffer(ensureLeading0x(signature.s)) as Buffer,
}
Expand Down Expand Up @@ -116,7 +116,7 @@ export class LedgerSigner implements Signer {
)

return {
v: parseInt(sig.v, 10),
v: parseInt(sig.v, 16),
r: ethUtil.toBuffer(ensureLeading0x(sig.r)) as Buffer,
s: ethUtil.toBuffer(ensureLeading0x(sig.s)) as Buffer,
}
Expand Down
128 changes: 68 additions & 60 deletions packages/sdk/wallets/wallet-ledger/src/ledger-wallet.test.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
import { ensureLeading0x, normalizeAddressWith0x, trimLeading0x } from '@celo/base/lib/address'
import { CeloTx, EncodedTransaction } from '@celo/connect'
import { CeloTx, EncodedTransaction, Hex } from '@celo/connect'
import { privateKeyToAddress } from '@celo/utils/lib/address'
import { verifySignature } from '@celo/utils/lib/signatureUtils'
import {
chainIdTransformationForSigning,
getHashFromEncoded,
recoverTransaction,
signTransaction,
verifyEIP712TypedDataSigner,
} from '@celo/wallet-base'
import * as ethUtil from '@ethereumjs/util'
import TransportNodeHid from '@ledgerhq/hw-transport-node-hid'
import { keccak_256 } from '@noble/hashes/sha3'
// @ts-ignore-next-line eth-lib types not found
import { account as Account } from 'eth-lib'
import Web3 from 'web3'
import { AddressValidation, LedgerWallet } from './ledger-wallet'
import { ILedger } from './types'

// Update this variable when testing using a physical device
const USE_PHYSICAL_LEDGER = false
Expand All @@ -34,7 +34,7 @@ const ACCOUNT_ADDRESS5 = normalizeAddressWith0x(privateKeyToAddress(PRIVATE_KEY5
const PRIVATE_KEY_NEVER = '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890ffffff'
const ACCOUNT_ADDRESS_NEVER = normalizeAddressWith0x(privateKeyToAddress(PRIVATE_KEY_NEVER))

const ledgerAddresses: { [myKey: string]: { address: string; privateKey: string } } = {
const ledgerAddresses: { [myKey: string]: { address: Hex; privateKey: Hex } } = {
"44'/52752'/0'/0/0": {
address: ACCOUNT_ADDRESS1,
privateKey: PRIVATE_KEY1,
Expand Down Expand Up @@ -100,68 +100,76 @@ const TYPED_DATA = {
}

function mockLedger(wallet: LedgerWallet, mockForceValidation: () => void) {
jest.spyOn<any, any>(wallet, 'generateNewLedger').mockImplementation((_transport: any) => {
return {
getAddress: async (derivationPath: string, forceValidation?: boolean) => {
if (forceValidation) {
mockForceValidation()
}
if (ledgerAddresses[derivationPath]) {
return { address: ledgerAddresses[derivationPath].address, derivationPath }
}
return {}
},
signTransaction: async (derivationPath: string, data: string) => {
if (ledgerAddresses[derivationPath]) {
const hash = getHashFromEncoded(ensureLeading0x(data))
const signature = Account.makeSigner(chainIdTransformationForSigning(CHAIN_ID))(
hash,
ledgerAddresses[derivationPath].privateKey
jest
.spyOn<any, any>(wallet, 'generateNewLedger')
.mockImplementation((_transport: any): ILedger => {
return {
getAddress: async (derivationPath: string, forceValidation?: boolean) => {
if (forceValidation) {
mockForceValidation()
}
if (ledgerAddresses[derivationPath]) {
return { address: ledgerAddresses[derivationPath].address, derivationPath }
}
return {}
},
signTransaction: async (derivationPath: string, data: string) => {
if (ledgerAddresses[derivationPath]) {
const { r, s, v } = signTransaction(
getHashFromEncoded(ensureLeading0x(data)),
ledgerAddresses[derivationPath].privateKey,
chainIdTransformationForSigning(CHAIN_ID)
)
return {
v: v.toString(16),
r: r.toString('hex'),
s: s.toString('hex'),
}
}
throw new Error('Invalid Path')
},
signPersonalMessage: async (derivationPath: string, data: string) => {
if (ledgerAddresses[derivationPath]) {
const dataBuff = ethUtil.toBuffer(ensureLeading0x(data))
const msgHashBuff = ethUtil.hashPersonalMessage(dataBuff)

const trimmedKey = trimLeading0x(ledgerAddresses[derivationPath].privateKey)
const pkBuffer = Buffer.from(trimmedKey, 'hex')
const signature = ethUtil.ecsign(msgHashBuff, pkBuffer)
return {
v: signature.v.toString(16),
r: signature.r.toString('hex'),
s: signature.s.toString('hex'),
}
}
throw new Error('Invalid Path')
},
signEIP712HashedMessage: async (
derivationPath: string,
domainSeparator: Buffer,
structHash: Buffer
) => {
const messageHash = Buffer.from(
keccak_256(Buffer.concat([Buffer.from('1901', 'hex'), domainSeparator, structHash]))
)
const [v, r, s] = Account.decodeSignature(signature)
return { v, r, s }
}
throw new Error('Invalid Path')
},
signPersonalMessage: async (derivationPath: string, data: string) => {
if (ledgerAddresses[derivationPath]) {
const dataBuff = ethUtil.toBuffer(ensureLeading0x(data))
const msgHashBuff = ethUtil.hashPersonalMessage(dataBuff)

const trimmedKey = trimLeading0x(ledgerAddresses[derivationPath].privateKey)
const pkBuffer = Buffer.from(trimmedKey, 'hex')
const signature = ethUtil.ecsign(msgHashBuff, pkBuffer)
const signature = ethUtil.ecsign(messageHash, pkBuffer)
return {
v: signature.v,
v: signature.v.toString(16),
r: signature.r.toString('hex'),
s: signature.s.toString('hex'),
}
}
throw new Error('Invalid Path')
},
signEIP712HashedMessage: async (
derivationPath: string,
domainSeparator: Buffer,
structHash: Buffer
) => {
const messageHash = keccak_256(
Buffer.concat([Buffer.from('1901', 'hex'), domainSeparator, structHash])
) as Buffer

const trimmedKey = trimLeading0x(ledgerAddresses[derivationPath].privateKey)
const pkBuffer = Buffer.from(trimmedKey, 'hex')
const signature = ethUtil.ecsign(messageHash, pkBuffer)
return {
v: signature.v,
r: signature.r.toString('hex'),
s: signature.s.toString('hex'),
}
},
getAppConfiguration: async () => {
return { arbitraryDataEnabled: 1, version: '0.0.0' }
},
}
})
},
getAppConfiguration: async () => {
return { arbitraryDataEnabled: 1, version: '0.0.0' }
},
provideERC20TokenInformation: async (_token) => {
return {}
},
}
})
}

describe('LedgerWallet class', () => {
Expand Down Expand Up @@ -252,8 +260,8 @@ describe('LedgerWallet class', () => {
}
await wallet.init()
if (USE_PHYSICAL_LEDGER) {
knownAddress = wallet.getAccounts()[0]
otherAddress = wallet.getAccounts()[1]
knownAddress = wallet.getAccounts()[0] as Hex
otherAddress = wallet.getAccounts()[1] as Hex
}
}, TEST_TIMEOUT_IN_MS)

Expand Down
18 changes: 18 additions & 0 deletions packages/sdk/wallets/wallet-ledger/src/types.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Hex } from '@celo/connect'

type LedgerSignature = { v: string; r: string; s: string }
export interface ILedger {
getAddress(
derivationPath: string,
forceValidation?: boolean
): Promise<{ address?: Hex; derivationPath?: string }>
signTransaction(derivationPath: string, data: string): Promise<LedgerSignature>
signPersonalMessage(derivationPath: string, data: string): Promise<LedgerSignature>
signEIP712HashedMessage(
derivationPath: string,
domainSeparator: Buffer,
structHash: Buffer
): Promise<LedgerSignature>
getAppConfiguration(): Promise<{ arbitraryDataEnabled: number; version: string }>
provideERC20TokenInformation(TokenInfo): Promise<unknown>
}
Loading

0 comments on commit be1dd0c

Please sign in to comment.