Skip to content

Commit

Permalink
Add accout.signRaw function to sign a message without prefix (#7346)
Browse files Browse the repository at this point in the history
* workable code and test

* refine comments and docs

* add parallel test back

* using default value instead of ?

* update changelog

* update changelog

* remove wrong doc

* update package changelog

* Update CHANGELOG.md
  • Loading branch information
blackmoshui authored Nov 4, 2024
1 parent 3283431 commit 4aaf915
Show file tree
Hide file tree
Showing 8 changed files with 239 additions and 4 deletions.
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2772,6 +2772,10 @@ If there are any bugs, improvements, optimizations or any new feature proposal f

### Added

#### web3-eth-accounts

- `hashMessage` now has a new optional param `skipPrefix` with a default value of `false`. A new function `signRaw` was added to sign a message without prefix. (#7346)

#### web3-rpc-providers

- PublicNodeProvider was added (#7322)
- PublicNodeProvider was added (#7322)
1 change: 1 addition & 0 deletions docs/docs/guides/03_wallet/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ The following is a list of [`Accounts`](/libdocs/Accounts) methods in the `web3.
- [recover](/libdocs/Accounts#recover)
- [recoverTransaction](/libdocs/Accounts#recovertransaction)
- [sign](/libdocs/Accounts#sign)
- [signRaw](/libdocs/Accounts#signraw)
- [signTransaction](/libdocs/Accounts#signtransaction)

## Wallets
Expand Down
4 changes: 4 additions & 0 deletions packages/web3-eth-accounts/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,3 +184,7 @@ Documentation:
- Revert `TransactionFactory.registerTransactionType` if there is a version mistatch between `web3-eth` and `web3-eth-accounts` and fix nextjs problem. (#7216)

## [Unreleased]

### Added

- `hashMessage` now has a new optional param `skipPrefix` with a default value of `false`. A new function `signRaw` was added to sign a message without prefix. (#7346)
47 changes: 44 additions & 3 deletions packages/web3-eth-accounts/src/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ export const parseAndValidatePrivateKey = (data: Bytes, ignoreLength?: boolean):
* `"\x19Ethereum Signed Message:\n" + message.length + message` and hashed using keccak256.
*
* @param message - A message to hash, if its HEX it will be UTF8 decoded.
* @param skipPrefix - (default: false) If true, the message will be not prefixed with "\x19Ethereum Signed Message:\n" + message.length
* @returns The hashed message
*
* ```ts
Expand All @@ -154,9 +155,13 @@ export const parseAndValidatePrivateKey = (data: Bytes, ignoreLength?: boolean):
* web3.eth.accounts.hashMessage(web3.utils.utf8ToHex("Hello world")) // Will be hex decoded in hashMessage
*
* > "0x8144a6fa26be252b86456491fbcd43c1de7e022241845ffea1c3df066f7cfede"
*
* web3.eth.accounts.hashMessage("Hello world", true)
*
* > "0xed6c11b0b5b808960df26f5bfc471d04c1995b0ffd2055925ad1be28d6baadfd"
* ```
*/
export const hashMessage = (message: string): string => {
export const hashMessage = (message: string, skipPrefix = false): string => {
const messageHex = isHexStrict(message) ? message : utf8ToHex(message);

const messageBytes = hexToBytes(messageHex);
Expand All @@ -165,7 +170,7 @@ export const hashMessage = (message: string): string => {
fromUtf8(`\x19Ethereum Signed Message:\n${messageBytes.byteLength}`),
);

const ethMessage = uint8ArrayConcat(preamble, messageBytes);
const ethMessage = skipPrefix ? messageBytes : uint8ArrayConcat(preamble, messageBytes);

return sha3Raw(ethMessage); // using keccak in web3-utils.sha3Raw instead of SHA3 (NIST Standard) as both are different
};
Expand Down Expand Up @@ -230,6 +235,42 @@ export const sign = (data: string, privateKey: Bytes): SignResult => {
};
};

/**
* Signs raw data with a given private key without adding the Ethereum-specific prefix.
*
* @param data - The raw data to sign. If it's a hex string, it will be used as-is. Otherwise, it will be UTF-8 encoded.
* @param privateKey - The 32 byte private key to sign with
* @returns The signature Object containing the message, messageHash, signature r, s, v
*
* ```ts
* web3.eth.accounts.signRaw('Some data', '0x4c0883a69102937d6231471b5dbb6204fe5129617082792ae468d01a3f362318')
* > {
* message: 'Some data',
* messageHash: '0x43a26051362b8040b289abe93334a5e3662751aa691185ae9e9a2e1e0c169350',
* v: '0x1b',
* r: '0x93da7e2ddd6b2ff1f5af0c752f052ed0d7d5bff19257db547a69cd9a879b37d4',
* s: '0x334485e42b33815fd2cf8a245a5393b282214060844a9681495df2257140e75c',
* signature: '0x93da7e2ddd6b2ff1f5af0c752f052ed0d7d5bff19257db547a69cd9a879b37d4334485e42b33815fd2cf8a245a5393b282214060844a9681495df2257140e75c1b'
* }
* ```
*/
export const signRaw = (data: string, privateKey: Bytes): SignResult => {
// Hash the message without the Ethereum-specific prefix
const hash = hashMessage(data, true);

// Sign the hash with the private key
const { messageHash, v, r, s, signature } = signMessageWithPrivateKey(hash, privateKey);

return {
message: data,
messageHash,
v,
r,
s,
signature,
};
};

/**
* Signs an Ethereum transaction with a given private key.
*
Expand Down Expand Up @@ -380,7 +421,7 @@ export const recoverTransaction = (rawTransaction: HexString): Address => {
* @param signatureOrV - signature or V
* @param prefixedOrR - prefixed or R
* @param s - S value in signature
* @param prefixed - (default: false) If the last parameter is true, the given message will NOT automatically be prefixed with `"\\x19Ethereum Signed Message:\\n" + message.length + message`, and assumed to be already prefixed.
* @param prefixed - (default: false) If the last parameter is true, the given message will NOT automatically be prefixed with `"\\x19Ethereum Signed Message:\\n" + message.length + message`, and assumed to be already prefixed and hashed.
* @returns The Ethereum address used to sign this data
*
* ```ts
Expand Down
83 changes: 83 additions & 0 deletions packages/web3-eth-accounts/test/fixtures/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,79 @@ export const signatureRecoverData: [string, any][] = [
],
];

export const signatureRecoverWithoutPrefixData: [string, any][] = [
[
'Some long text with integers 1233 and special characters and unicode \u1234 as well.',
{
prefixedOrR: true,
r: '0x66ff35193d5763bbb86428b87cd10451704fa1d00a8831e75cc0eca16701521d',
s: '0x5ec294b63778e854929a53825191222415bf93871d091a137f61d92f2f3d37bb',
address: '0x6E599DA0bfF7A6598AC1224E4985430Bf16458a4',
privateKey: '0xcb89ec4b01771c6c8272f4c0aafba2f8ee0b101afb22273b786939a8af7c1912',
data: 'Some long text with integers 1233 and special characters and unicode \u1234 as well.',
// signature done with personal_sign
signatureOrV:
'0x66ff35193d5763bbb86428b87cd10451704fa1d00a8831e75cc0eca16701521d5ec294b63778e854929a53825191222415bf93871d091a137f61d92f2f3d37bb1b',
},
],
[
'Some data',
{
prefixedOrR: true,
r: '0xbbae52f4cd6776e66e01673228474866cead8ccc9530e0ae06b42d0f5917865f',
s: '0x170e7a9e792288955e884c9b2da7d2c69b69d3b29e24372d1dec1164a7deaec0',
address: '0xEB014f8c8B418Db6b45774c326A0E64C78914dC0',
privateKey: '0xbe6383dad004f233317e46ddb46ad31b16064d14447a95cc1d8c8d4bc61c3728',
data: 'Some data',
// signature done with personal_sign
signatureOrV:
'0xbbae52f4cd6776e66e01673228474866cead8ccc9530e0ae06b42d0f5917865f170e7a9e792288955e884c9b2da7d2c69b69d3b29e24372d1dec1164a7deaec01c',
},
],
[
'Some data!%$$%&@*',
{
prefixedOrR: true,
r: '0x91b3ccd107995becaca361e9f282723176181bb9250e8ebb8a5119f5e0b91978',
s: '0x5e67773c632e036712befe130577d2954b91f7c5fb4999bc94d80d471dfd468b',
address: '0xEB014f8c8B418Db6b45774c326A0E64C78914dC0',
privateKey: '0xbe6383dad004f233317e46ddb46ad31b16064d14447a95cc1d8c8d4bc61c3728',
data: 'Some data!%$$%&@*',
// signature done with personal_sign
signatureOrV:
'0x91b3ccd107995becaca361e9f282723176181bb9250e8ebb8a5119f5e0b919785e67773c632e036712befe130577d2954b91f7c5fb4999bc94d80d471dfd468b1c',
},
],
[
'102',
{
prefixedOrR: true,
r: '0xecbd18fc2919bef2a9371536df0fbabdb09fda9823b15c5ce816ab71d7b5e359',
s: '0x3860327ffde34fe72ae5d6abdcdc91e984f936ea478cfb8b1547383d6e4d6a98',
address: '0xEB014f8c8B418Db6b45774c326A0E64C78914dC0',
privateKey: '0xbe6383dad004f233317e46ddb46ad31b16064d14447a95cc1d8c8d4bc61c3728',
data: '102',
// signature done with personal_sign
signatureOrV:
'0xecbd18fc2919bef2a9371536df0fbabdb09fda9823b15c5ce816ab71d7b5e3593860327ffde34fe72ae5d6abdcdc91e984f936ea478cfb8b1547383d6e4d6a981b',
},
],
[
// testcase for recover(data, V, R, S)
'some data',
{
signatureOrV: '0x1b',
prefixedOrR: '0x48f828a3ed107ce28551a3264d75b18df806d6960c273396dc022baadd0cf26e',
r: '0x48f828a3ed107ce28551a3264d75b18df806d6960c273396dc022baadd0cf26e',
s: '0x373e1b6709512c2dab9dff4066c6b40d32bd747bdb84469023952bc82123e8cc',
address: '0x54BF9ed7F22b64a5D69Beea57cFCd378763bcdc5',
privateKey: '0x03a0021a87dc354855f900fd15c063bcc9c155c33b8f2321ec294e0933ef29d2',
signature:
'0x48f828a3ed107ce28551a3264d75b18df806d6960c273396dc022baadd0cf26e373e1b6709512c2dab9dff4066c6b40d32bd747bdb84469023952bc82123e8cc1b',
},
],
];

export const transactionsTestData: [TxData | AccessListEIP2930TxData | FeeMarketEIP1559TxData][] = [
[
// 'TxLegacy'
Expand Down Expand Up @@ -526,3 +599,13 @@ export const validHashMessageData: [string, string][] = [
['non utf8 string', '0x8862c6a425a83c082216090e4f0e03b64106189e93c29b11d0112e77b477cce2'],
['', '0x5f35dce98ba4fba25530a026ed80b2cecdaa31091ba4958b99b52ea1d068adad'],
];

export const validHashMessageWithoutPrefixData: [string, string][] = [
['🤗', '0x4bf650e97ac50e9e4b4c51deb9e01455c1a9b2f35143bc0a43f1ea5bc9e51856'],
[
'Some long text with integers 1233 and special characters and unicode \u1234 as well.',
'0x6965440cc2890e0f118738d6300a21afb2de316c578dad144aa55c9ea45c0fa7',
],
['non utf8 string', '0x52000fc43fe3aa422eecafff3e0d82205a1409850c4bd2871dfde932de1fec13'],
['', '0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470'],
];
34 changes: 34 additions & 0 deletions packages/web3-eth-accounts/test/integration/account.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
recover,
recoverTransaction,
sign,
signRaw,
signTransaction,
} from '../../src';
import { TransactionFactory } from '../../src/tx/transactionFactory';
Expand All @@ -37,10 +38,12 @@ import {
invalidPrivateKeytoAccountData,
invalidPrivateKeyToAddressData,
signatureRecoverData,
signatureRecoverWithoutPrefixData,
transactionsTestData,
validDecryptData,
validEncryptData,
validHashMessageData,
validHashMessageWithoutPrefixData,
validPrivateKeytoAccountData,
validPrivateKeyToAddressData,
} from '../fixtures/account';
Expand Down Expand Up @@ -128,6 +131,12 @@ describe('accounts', () => {
});
});

describe('Hash Message Without Prefix', () => {
it.each(validHashMessageWithoutPrefixData)('%s', (message, hash) => {
expect(hashMessage(message, true)).toEqual(hash);
});
});

describe('Sign Message', () => {
describe('sign', () => {
it.each(signatureRecoverData)('%s', (data, testObj) => {
Expand All @@ -144,6 +153,31 @@ describe('accounts', () => {
});
});

describe('Sign Raw Message', () => {
describe('signRaw', () => {
it.each(signatureRecoverWithoutPrefixData)('%s', (data, testObj) => {
const result = signRaw(data, testObj.privateKey);
expect(result.signature).toEqual(testObj.signature || testObj.signatureOrV); // makes sure we get signature and not V value
expect(result.r).toEqual(testObj.r);
expect(result.s).toEqual(testObj.s);
});
});

describe('recover', () => {
it.each(signatureRecoverWithoutPrefixData)('%s', (data, testObj) => {
const hashedMessage = hashMessage(data, true); // hash the message first without prefix
const address = recover(
hashedMessage,
testObj.signatureOrV,
testObj.prefixedOrR,
testObj.s,
true, // make sure the prefixed is true since we already hashed the message
);
expect(address).toEqual(testObj.address);
});
});
});

describe('encrypt', () => {
describe('valid cases', () => {
it.each(validEncryptData)('%s', async (input, output) => {
Expand Down
34 changes: 34 additions & 0 deletions packages/web3-eth-accounts/test/unit/account.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
sign,
signTransaction,
privateKeyToPublicKey,
signRaw,
} from '../../src/account';
import {
invalidDecryptData,
Expand All @@ -37,10 +38,12 @@ import {
invalidPrivateKeytoAccountData,
invalidPrivateKeyToAddressData,
signatureRecoverData,
signatureRecoverWithoutPrefixData,
transactionsTestData,
validDecryptData,
validEncryptData,
validHashMessageData,
validHashMessageWithoutPrefixData,
validPrivateKeytoAccountData,
validPrivateKeyToAddressData,
validPrivateKeyToPublicKeyData,
Expand Down Expand Up @@ -143,6 +146,12 @@ describe('accounts', () => {
});
});

describe('Hash Message Without Prefix', () => {
it.each(validHashMessageWithoutPrefixData)('%s', (message, hash) => {
expect(hashMessage(message, true)).toEqual(hash);
});
});

describe('Sign Message', () => {
describe('sign', () => {
it.each(signatureRecoverData)('%s', (data, testObj) => {
Expand All @@ -161,6 +170,31 @@ describe('accounts', () => {
});
});

describe('Sign Raw Message', () => {
describe('signRaw', () => {
it.each(signatureRecoverWithoutPrefixData)('%s', (data, testObj) => {
const result = signRaw(data, testObj.privateKey);
expect(result.signature).toEqual(testObj.signature || testObj.signatureOrV); // makes sure we get signature and not V value
expect(result.r).toEqual(testObj.r);
expect(result.s).toEqual(testObj.s);
});
});

describe('recover', () => {
it.each(signatureRecoverWithoutPrefixData)('%s', (data, testObj) => {
const hashedMessage = hashMessage(data, true); // hash the message first without prefix
const address = recover(
hashedMessage,
testObj.signatureOrV,
testObj.prefixedOrR,
testObj.s,
true, // make sure the prefixed is true since we already hashed the message
);
expect(address).toEqual(testObj.address);
});
});
});

describe('encrypt', () => {
describe('valid cases', () => {
it.each(validEncryptData)('%s', async (input, output) => {
Expand Down
Loading

0 comments on commit 4aaf915

Please # to comment.