Skip to content
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

Add accout.signRaw function to sign a message without prefix #7346

Merged
merged 13 commits into from
Nov 4, 2024
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2771,3 +2771,7 @@ If there are any bugs, improvements, optimizations or any new feature proposal f
#### web3-rpc-providers

- PublicNodeProvider was added (#7322)

#### 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)
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
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)
blackmoshui marked this conversation as resolved.
Show resolved Hide resolved
*
* > "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
34 changes: 34 additions & 0 deletions packages/web3-eth-accounts/test/unit/account_dom.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import {
recover,
recoverTransaction,
sign,
signRaw,
signTransaction,
privateKeyToPublicKey,
} from '../../src/account';
Expand All @@ -54,10 +55,12 @@ import {
invalidPrivateKeytoAccountData,
invalidPrivateKeyToAddressData,
signatureRecoverData,
signatureRecoverWithoutPrefixData,
transactionsTestData,
validDecryptData,
validEncryptData,
validHashMessageData,
validHashMessageWithoutPrefixData,
validPrivateKeytoAccountData,
validPrivateKeyToAddressData,
validPrivateKeyToPublicKeyData,
Expand Down Expand Up @@ -158,6 +161,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 @@ -176,6 +185,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