Skip to content
This repository was archived by the owner on Mar 5, 2025. It is now read-only.

Commit 4aaf915

Browse files
authored
Add accout.signRaw function to sign a message without prefix (#7346)
* 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
1 parent 3283431 commit 4aaf915

File tree

8 files changed

+239
-4
lines changed

8 files changed

+239
-4
lines changed

CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2772,6 +2772,10 @@ If there are any bugs, improvements, optimizations or any new feature proposal f
27722772

27732773
### Added
27742774

2775+
#### web3-eth-accounts
2776+
2777+
- `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)
2778+
27752779
#### web3-rpc-providers
27762780

2777-
- PublicNodeProvider was added (#7322)
2781+
- PublicNodeProvider was added (#7322)

docs/docs/guides/03_wallet/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ The following is a list of [`Accounts`](/libdocs/Accounts) methods in the `web3.
9090
- [recover](/libdocs/Accounts#recover)
9191
- [recoverTransaction](/libdocs/Accounts#recovertransaction)
9292
- [sign](/libdocs/Accounts#sign)
93+
- [signRaw](/libdocs/Accounts#signraw)
9394
- [signTransaction](/libdocs/Accounts#signtransaction)
9495

9596
## Wallets

packages/web3-eth-accounts/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,3 +184,7 @@ Documentation:
184184
- Revert `TransactionFactory.registerTransactionType` if there is a version mistatch between `web3-eth` and `web3-eth-accounts` and fix nextjs problem. (#7216)
185185

186186
## [Unreleased]
187+
188+
### Added
189+
190+
- `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)

packages/web3-eth-accounts/src/account.ts

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ export const parseAndValidatePrivateKey = (data: Bytes, ignoreLength?: boolean):
144144
* `"\x19Ethereum Signed Message:\n" + message.length + message` and hashed using keccak256.
145145
*
146146
* @param message - A message to hash, if its HEX it will be UTF8 decoded.
147+
* @param skipPrefix - (default: false) If true, the message will be not prefixed with "\x19Ethereum Signed Message:\n" + message.length
147148
* @returns The hashed message
148149
*
149150
* ```ts
@@ -154,9 +155,13 @@ export const parseAndValidatePrivateKey = (data: Bytes, ignoreLength?: boolean):
154155
* web3.eth.accounts.hashMessage(web3.utils.utf8ToHex("Hello world")) // Will be hex decoded in hashMessage
155156
*
156157
* > "0x8144a6fa26be252b86456491fbcd43c1de7e022241845ffea1c3df066f7cfede"
158+
*
159+
* web3.eth.accounts.hashMessage("Hello world", true)
160+
*
161+
* > "0xed6c11b0b5b808960df26f5bfc471d04c1995b0ffd2055925ad1be28d6baadfd"
157162
* ```
158163
*/
159-
export const hashMessage = (message: string): string => {
164+
export const hashMessage = (message: string, skipPrefix = false): string => {
160165
const messageHex = isHexStrict(message) ? message : utf8ToHex(message);
161166

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

168-
const ethMessage = uint8ArrayConcat(preamble, messageBytes);
173+
const ethMessage = skipPrefix ? messageBytes : uint8ArrayConcat(preamble, messageBytes);
169174

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

238+
/**
239+
* Signs raw data with a given private key without adding the Ethereum-specific prefix.
240+
*
241+
* @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.
242+
* @param privateKey - The 32 byte private key to sign with
243+
* @returns The signature Object containing the message, messageHash, signature r, s, v
244+
*
245+
* ```ts
246+
* web3.eth.accounts.signRaw('Some data', '0x4c0883a69102937d6231471b5dbb6204fe5129617082792ae468d01a3f362318')
247+
* > {
248+
* message: 'Some data',
249+
* messageHash: '0x43a26051362b8040b289abe93334a5e3662751aa691185ae9e9a2e1e0c169350',
250+
* v: '0x1b',
251+
* r: '0x93da7e2ddd6b2ff1f5af0c752f052ed0d7d5bff19257db547a69cd9a879b37d4',
252+
* s: '0x334485e42b33815fd2cf8a245a5393b282214060844a9681495df2257140e75c',
253+
* signature: '0x93da7e2ddd6b2ff1f5af0c752f052ed0d7d5bff19257db547a69cd9a879b37d4334485e42b33815fd2cf8a245a5393b282214060844a9681495df2257140e75c1b'
254+
* }
255+
* ```
256+
*/
257+
export const signRaw = (data: string, privateKey: Bytes): SignResult => {
258+
// Hash the message without the Ethereum-specific prefix
259+
const hash = hashMessage(data, true);
260+
261+
// Sign the hash with the private key
262+
const { messageHash, v, r, s, signature } = signMessageWithPrivateKey(hash, privateKey);
263+
264+
return {
265+
message: data,
266+
messageHash,
267+
v,
268+
r,
269+
s,
270+
signature,
271+
};
272+
};
273+
233274
/**
234275
* Signs an Ethereum transaction with a given private key.
235276
*
@@ -380,7 +421,7 @@ export const recoverTransaction = (rawTransaction: HexString): Address => {
380421
* @param signatureOrV - signature or V
381422
* @param prefixedOrR - prefixed or R
382423
* @param s - S value in signature
383-
* @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.
424+
* @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.
384425
* @returns The Ethereum address used to sign this data
385426
*
386427
* ```ts

packages/web3-eth-accounts/test/fixtures/account.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,79 @@ export const signatureRecoverData: [string, any][] = [
164164
],
165165
];
166166

167+
export const signatureRecoverWithoutPrefixData: [string, any][] = [
168+
[
169+
'Some long text with integers 1233 and special characters and unicode \u1234 as well.',
170+
{
171+
prefixedOrR: true,
172+
r: '0x66ff35193d5763bbb86428b87cd10451704fa1d00a8831e75cc0eca16701521d',
173+
s: '0x5ec294b63778e854929a53825191222415bf93871d091a137f61d92f2f3d37bb',
174+
address: '0x6E599DA0bfF7A6598AC1224E4985430Bf16458a4',
175+
privateKey: '0xcb89ec4b01771c6c8272f4c0aafba2f8ee0b101afb22273b786939a8af7c1912',
176+
data: 'Some long text with integers 1233 and special characters and unicode \u1234 as well.',
177+
// signature done with personal_sign
178+
signatureOrV:
179+
'0x66ff35193d5763bbb86428b87cd10451704fa1d00a8831e75cc0eca16701521d5ec294b63778e854929a53825191222415bf93871d091a137f61d92f2f3d37bb1b',
180+
},
181+
],
182+
[
183+
'Some data',
184+
{
185+
prefixedOrR: true,
186+
r: '0xbbae52f4cd6776e66e01673228474866cead8ccc9530e0ae06b42d0f5917865f',
187+
s: '0x170e7a9e792288955e884c9b2da7d2c69b69d3b29e24372d1dec1164a7deaec0',
188+
address: '0xEB014f8c8B418Db6b45774c326A0E64C78914dC0',
189+
privateKey: '0xbe6383dad004f233317e46ddb46ad31b16064d14447a95cc1d8c8d4bc61c3728',
190+
data: 'Some data',
191+
// signature done with personal_sign
192+
signatureOrV:
193+
'0xbbae52f4cd6776e66e01673228474866cead8ccc9530e0ae06b42d0f5917865f170e7a9e792288955e884c9b2da7d2c69b69d3b29e24372d1dec1164a7deaec01c',
194+
},
195+
],
196+
[
197+
'Some data!%$$%&@*',
198+
{
199+
prefixedOrR: true,
200+
r: '0x91b3ccd107995becaca361e9f282723176181bb9250e8ebb8a5119f5e0b91978',
201+
s: '0x5e67773c632e036712befe130577d2954b91f7c5fb4999bc94d80d471dfd468b',
202+
address: '0xEB014f8c8B418Db6b45774c326A0E64C78914dC0',
203+
privateKey: '0xbe6383dad004f233317e46ddb46ad31b16064d14447a95cc1d8c8d4bc61c3728',
204+
data: 'Some data!%$$%&@*',
205+
// signature done with personal_sign
206+
signatureOrV:
207+
'0x91b3ccd107995becaca361e9f282723176181bb9250e8ebb8a5119f5e0b919785e67773c632e036712befe130577d2954b91f7c5fb4999bc94d80d471dfd468b1c',
208+
},
209+
],
210+
[
211+
'102',
212+
{
213+
prefixedOrR: true,
214+
r: '0xecbd18fc2919bef2a9371536df0fbabdb09fda9823b15c5ce816ab71d7b5e359',
215+
s: '0x3860327ffde34fe72ae5d6abdcdc91e984f936ea478cfb8b1547383d6e4d6a98',
216+
address: '0xEB014f8c8B418Db6b45774c326A0E64C78914dC0',
217+
privateKey: '0xbe6383dad004f233317e46ddb46ad31b16064d14447a95cc1d8c8d4bc61c3728',
218+
data: '102',
219+
// signature done with personal_sign
220+
signatureOrV:
221+
'0xecbd18fc2919bef2a9371536df0fbabdb09fda9823b15c5ce816ab71d7b5e3593860327ffde34fe72ae5d6abdcdc91e984f936ea478cfb8b1547383d6e4d6a981b',
222+
},
223+
],
224+
[
225+
// testcase for recover(data, V, R, S)
226+
'some data',
227+
{
228+
signatureOrV: '0x1b',
229+
prefixedOrR: '0x48f828a3ed107ce28551a3264d75b18df806d6960c273396dc022baadd0cf26e',
230+
r: '0x48f828a3ed107ce28551a3264d75b18df806d6960c273396dc022baadd0cf26e',
231+
s: '0x373e1b6709512c2dab9dff4066c6b40d32bd747bdb84469023952bc82123e8cc',
232+
address: '0x54BF9ed7F22b64a5D69Beea57cFCd378763bcdc5',
233+
privateKey: '0x03a0021a87dc354855f900fd15c063bcc9c155c33b8f2321ec294e0933ef29d2',
234+
signature:
235+
'0x48f828a3ed107ce28551a3264d75b18df806d6960c273396dc022baadd0cf26e373e1b6709512c2dab9dff4066c6b40d32bd747bdb84469023952bc82123e8cc1b',
236+
},
237+
],
238+
];
239+
167240
export const transactionsTestData: [TxData | AccessListEIP2930TxData | FeeMarketEIP1559TxData][] = [
168241
[
169242
// 'TxLegacy'
@@ -526,3 +599,13 @@ export const validHashMessageData: [string, string][] = [
526599
['non utf8 string', '0x8862c6a425a83c082216090e4f0e03b64106189e93c29b11d0112e77b477cce2'],
527600
['', '0x5f35dce98ba4fba25530a026ed80b2cecdaa31091ba4958b99b52ea1d068adad'],
528601
];
602+
603+
export const validHashMessageWithoutPrefixData: [string, string][] = [
604+
['🤗', '0x4bf650e97ac50e9e4b4c51deb9e01455c1a9b2f35143bc0a43f1ea5bc9e51856'],
605+
[
606+
'Some long text with integers 1233 and special characters and unicode \u1234 as well.',
607+
'0x6965440cc2890e0f118738d6300a21afb2de316c578dad144aa55c9ea45c0fa7',
608+
],
609+
['non utf8 string', '0x52000fc43fe3aa422eecafff3e0d82205a1409850c4bd2871dfde932de1fec13'],
610+
['', '0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470'],
611+
];

packages/web3-eth-accounts/test/integration/account.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
recover,
2828
recoverTransaction,
2929
sign,
30+
signRaw,
3031
signTransaction,
3132
} from '../../src';
3233
import { TransactionFactory } from '../../src/tx/transactionFactory';
@@ -37,10 +38,12 @@ import {
3738
invalidPrivateKeytoAccountData,
3839
invalidPrivateKeyToAddressData,
3940
signatureRecoverData,
41+
signatureRecoverWithoutPrefixData,
4042
transactionsTestData,
4143
validDecryptData,
4244
validEncryptData,
4345
validHashMessageData,
46+
validHashMessageWithoutPrefixData,
4447
validPrivateKeytoAccountData,
4548
validPrivateKeyToAddressData,
4649
} from '../fixtures/account';
@@ -128,6 +131,12 @@ describe('accounts', () => {
128131
});
129132
});
130133

134+
describe('Hash Message Without Prefix', () => {
135+
it.each(validHashMessageWithoutPrefixData)('%s', (message, hash) => {
136+
expect(hashMessage(message, true)).toEqual(hash);
137+
});
138+
});
139+
131140
describe('Sign Message', () => {
132141
describe('sign', () => {
133142
it.each(signatureRecoverData)('%s', (data, testObj) => {
@@ -144,6 +153,31 @@ describe('accounts', () => {
144153
});
145154
});
146155

156+
describe('Sign Raw Message', () => {
157+
describe('signRaw', () => {
158+
it.each(signatureRecoverWithoutPrefixData)('%s', (data, testObj) => {
159+
const result = signRaw(data, testObj.privateKey);
160+
expect(result.signature).toEqual(testObj.signature || testObj.signatureOrV); // makes sure we get signature and not V value
161+
expect(result.r).toEqual(testObj.r);
162+
expect(result.s).toEqual(testObj.s);
163+
});
164+
});
165+
166+
describe('recover', () => {
167+
it.each(signatureRecoverWithoutPrefixData)('%s', (data, testObj) => {
168+
const hashedMessage = hashMessage(data, true); // hash the message first without prefix
169+
const address = recover(
170+
hashedMessage,
171+
testObj.signatureOrV,
172+
testObj.prefixedOrR,
173+
testObj.s,
174+
true, // make sure the prefixed is true since we already hashed the message
175+
);
176+
expect(address).toEqual(testObj.address);
177+
});
178+
});
179+
});
180+
147181
describe('encrypt', () => {
148182
describe('valid cases', () => {
149183
it.each(validEncryptData)('%s', async (input, output) => {

packages/web3-eth-accounts/test/unit/account.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
sign,
3030
signTransaction,
3131
privateKeyToPublicKey,
32+
signRaw,
3233
} from '../../src/account';
3334
import {
3435
invalidDecryptData,
@@ -37,10 +38,12 @@ import {
3738
invalidPrivateKeytoAccountData,
3839
invalidPrivateKeyToAddressData,
3940
signatureRecoverData,
41+
signatureRecoverWithoutPrefixData,
4042
transactionsTestData,
4143
validDecryptData,
4244
validEncryptData,
4345
validHashMessageData,
46+
validHashMessageWithoutPrefixData,
4447
validPrivateKeytoAccountData,
4548
validPrivateKeyToAddressData,
4649
validPrivateKeyToPublicKeyData,
@@ -143,6 +146,12 @@ describe('accounts', () => {
143146
});
144147
});
145148

149+
describe('Hash Message Without Prefix', () => {
150+
it.each(validHashMessageWithoutPrefixData)('%s', (message, hash) => {
151+
expect(hashMessage(message, true)).toEqual(hash);
152+
});
153+
});
154+
146155
describe('Sign Message', () => {
147156
describe('sign', () => {
148157
it.each(signatureRecoverData)('%s', (data, testObj) => {
@@ -161,6 +170,31 @@ describe('accounts', () => {
161170
});
162171
});
163172

173+
describe('Sign Raw Message', () => {
174+
describe('signRaw', () => {
175+
it.each(signatureRecoverWithoutPrefixData)('%s', (data, testObj) => {
176+
const result = signRaw(data, testObj.privateKey);
177+
expect(result.signature).toEqual(testObj.signature || testObj.signatureOrV); // makes sure we get signature and not V value
178+
expect(result.r).toEqual(testObj.r);
179+
expect(result.s).toEqual(testObj.s);
180+
});
181+
});
182+
183+
describe('recover', () => {
184+
it.each(signatureRecoverWithoutPrefixData)('%s', (data, testObj) => {
185+
const hashedMessage = hashMessage(data, true); // hash the message first without prefix
186+
const address = recover(
187+
hashedMessage,
188+
testObj.signatureOrV,
189+
testObj.prefixedOrR,
190+
testObj.s,
191+
true, // make sure the prefixed is true since we already hashed the message
192+
);
193+
expect(address).toEqual(testObj.address);
194+
});
195+
});
196+
});
197+
164198
describe('encrypt', () => {
165199
describe('valid cases', () => {
166200
it.each(validEncryptData)('%s', async (input, output) => {

0 commit comments

Comments
 (0)