Skip to content

Commit bdad9a5

Browse files
authored
feat: improve message verification utilities (#1198)
1 parent ea6636f commit bdad9a5

File tree

13 files changed

+566
-194
lines changed

13 files changed

+566
-194
lines changed

__mocks__/typedData/v1Nested.json

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
{
2+
"domain": {
3+
"name": "Dappland",
4+
"chainId": "0x534e5f5345504f4c4941",
5+
"version": "1.0.2",
6+
"revision": "1"
7+
},
8+
"message": {
9+
"MessageId": 345,
10+
"From": {
11+
"Name": "Edmund",
12+
"Address": "0x7e00d496e324876bbc8531f2d9a82bf154d1a04a50218ee74cdd372f75a551a"
13+
},
14+
"To": {
15+
"Name": "Alice",
16+
"Address": "0x69b49c2cc8b16e80e86bfc5b0614a59aa8c9b601569c7b80dde04d3f3151b79"
17+
},
18+
"Nft_to_transfer": {
19+
"Collection": "Stupid monkeys",
20+
"Address": "0x69b49c2cc8b16e80e86bfc5b0614a59aa8c9b601569c7b80dde04d3f3151b79",
21+
"Nft_id": 112,
22+
"Negotiated_for": {
23+
"Qty": "18.4569325643",
24+
"Unit": "ETH",
25+
"Token_address": "0x69b49c2cc8b16e80e86bfc5b0614a59aa8c9b601569c7b80dde04d3f3151b79",
26+
"Amount": "0x100243260D270EB00"
27+
}
28+
},
29+
"Comment1": "Monkey with banana, sunglasses,",
30+
"Comment2": "and red hat.",
31+
"Comment3": ""
32+
},
33+
"primaryType": "TransferERC721",
34+
"types": {
35+
"Account1": [
36+
{
37+
"name": "Name",
38+
"type": "string"
39+
},
40+
{
41+
"name": "Address",
42+
"type": "felt"
43+
}
44+
],
45+
"Nft": [
46+
{
47+
"name": "Collection",
48+
"type": "string"
49+
},
50+
{
51+
"name": "Address",
52+
"type": "felt"
53+
},
54+
{
55+
"name": "Nft_id",
56+
"type": "felt"
57+
},
58+
{
59+
"name": "Negotiated_for",
60+
"type": "Transaction"
61+
}
62+
],
63+
"Transaction": [
64+
{
65+
"name": "Qty",
66+
"type": "string"
67+
},
68+
{
69+
"name": "Unit",
70+
"type": "string"
71+
},
72+
{
73+
"name": "Token_address",
74+
"type": "felt"
75+
},
76+
{
77+
"name": "Amount",
78+
"type": "felt"
79+
}
80+
],
81+
"TransferERC721": [
82+
{
83+
"name": "MessageId",
84+
"type": "felt"
85+
},
86+
{
87+
"name": "From",
88+
"type": "Account1"
89+
},
90+
{
91+
"name": "To",
92+
"type": "Account1"
93+
},
94+
{
95+
"name": "Nft_to_transfer",
96+
"type": "Nft"
97+
},
98+
{
99+
"name": "Comment1",
100+
"type": "string"
101+
},
102+
{
103+
"name": "Comment2",
104+
"type": "string"
105+
},
106+
{
107+
"name": "Comment3",
108+
"type": "string"
109+
}
110+
],
111+
"StarknetDomain": [
112+
{
113+
"name": "name",
114+
"type": "string"
115+
},
116+
{
117+
"name": "chainId",
118+
"type": "felt"
119+
},
120+
{
121+
"name": "version",
122+
"type": "string"
123+
}
124+
]
125+
}
126+
}

__tests__/rpcProvider.test.ts

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { getStarkKey, utils } from '@scure/starknet';
2-
1+
import { getStarkKey, Signature, utils } from '@scure/starknet';
2+
import typedDataExample from '../__mocks__/typedData/baseExample.json';
33
import {
44
Account,
55
Block,
@@ -18,7 +18,7 @@ import {
1818
} from '../src';
1919
import { StarknetChainId } from '../src/constants';
2020
import { felt, uint256 } from '../src/utils/calldata/cairo';
21-
import { toHexString } from '../src/utils/num';
21+
import { toBigInt, toHexString } from '../src/utils/num';
2222
import {
2323
compiledC1v2,
2424
compiledC1v2Casm,
@@ -493,3 +493,47 @@ describeIfNotDevnet('waitForBlock', () => {
493493
expect(true).toBe(true); // answer without timeout Error (blocks have to be spaced with 16 minutes maximum : 200 retries * 5000ms)
494494
});
495495
});
496+
497+
describe('EIP712 verification', () => {
498+
const rpcProvider = getTestProvider(false);
499+
const account = getTestAccount(rpcProvider);
500+
501+
test('sign and verify message', async () => {
502+
const signature = await account.signMessage(typedDataExample);
503+
const verifMessageResponse: boolean = await rpcProvider.verifyMessageInStarknet(
504+
typedDataExample,
505+
signature,
506+
account.address
507+
);
508+
expect(verifMessageResponse).toBe(true);
509+
510+
const messageHash = await account.hashMessage(typedDataExample);
511+
const verifMessageResponse2: boolean = await rpcProvider.verifyMessageInStarknet(
512+
messageHash,
513+
signature,
514+
account.address
515+
);
516+
expect(verifMessageResponse2).toBe(true);
517+
});
518+
519+
test('sign and verify EIP712 message fail', async () => {
520+
const signature = await account.signMessage(typedDataExample);
521+
const [r, s] = stark.formatSignature(signature);
522+
523+
// change the signature to make it invalid
524+
const r2 = toBigInt(r) + 123n;
525+
const wrongSignature = new Signature(toBigInt(r2.toString()), toBigInt(s));
526+
if (!wrongSignature) return;
527+
const verifMessageResponse: boolean = await rpcProvider.verifyMessageInStarknet(
528+
typedDataExample,
529+
wrongSignature,
530+
account.address
531+
);
532+
expect(verifMessageResponse).toBe(false);
533+
534+
const wrongAccountAddress = '0x123456789';
535+
await expect(
536+
rpcProvider.verifyMessageInStarknet(typedDataExample, signature, wrongAccountAddress)
537+
).rejects.toThrow();
538+
});
539+
});

__tests__/utils/num.test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,3 +216,14 @@ describe('stringToSha256ToArrayBuff4', () => {
216216
expect(buff).toEqual(new Uint8Array([43, 206, 231, 219]));
217217
});
218218
});
219+
220+
describe('isBigNumberish', () => {
221+
test('determine if value is a BigNumberish', () => {
222+
expect(num.isBigNumberish(234)).toBe(true);
223+
expect(num.isBigNumberish(234n)).toBe(true);
224+
expect(num.isBigNumberish('234')).toBe(true);
225+
expect(num.isBigNumberish('0xea')).toBe(true);
226+
expect(num.isBigNumberish('ea')).toBe(false);
227+
expect(num.isBigNumberish('zero')).toBe(false);
228+
});
229+
});

__tests__/utils/stark.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,3 +116,12 @@ describe('stark', () => {
116116
expect(stark.v3Details(detailsUndefined)).toEqual(expect.objectContaining(detailsAnything));
117117
});
118118
});
119+
120+
describe('ec full public key', () => {
121+
test('determine if value is a BigNumberish', () => {
122+
const privateKey1 = '0x43b7240d227aa2fb8434350b3321c40ac1b88c7067982549e7609870621b535';
123+
expect(stark.getFullPublicKey(privateKey1)).toBe(
124+
'0x0400b730bd22358612b5a67f8ad52ce80f9e8e893639ade263537e6ef35852e5d3057795f6b090f7c6985ee143f798608a53b3659222c06693c630857a10a92acf'
125+
);
126+
});
127+
});

__tests__/utils/typedData.test.ts

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,17 @@ import exampleEnum from '../../__mocks__/typedData/example_enum.json';
66
import examplePresetTypes from '../../__mocks__/typedData/example_presetTypes.json';
77
import typedDataStructArrayExample from '../../__mocks__/typedData/mail_StructArray.json';
88
import typedDataSessionExample from '../../__mocks__/typedData/session_MerkleTree.json';
9-
import { BigNumberish, StarknetDomain, num } from '../../src';
9+
import v1NestedExample from '../../__mocks__/typedData/v1Nested.json';
10+
import {
11+
Account,
12+
BigNumberish,
13+
StarknetDomain,
14+
num,
15+
stark,
16+
typedData,
17+
type ArraySignatureType,
18+
type Signature,
19+
} from '../../src';
1020
import { PRIME } from '../../src/constants';
1121
import { getSelectorFromName } from '../../src/utils/hash';
1222
import { MerkleTree } from '../../src/utils/merkle';
@@ -346,4 +356,48 @@ describe('typedData', () => {
346356
expect(() => getMessageHash(baseTypes(type), exampleAddress)).toThrow(RegExp(type));
347357
});
348358
});
359+
360+
describe('verifyMessage', () => {
361+
const addr = '0x64b48806902a367c8598f4f95c305e8c1a1acba5f082d294a43793113115691';
362+
const privK = '0x71d7bb07b9a64f6f78ac4c816aff4da9';
363+
const fullPubK = stark.getFullPublicKey(privK);
364+
const myAccount = new Account({ nodeUrl: 'fake' }, addr, privK);
365+
let signedMessage: Signature;
366+
let hashedMessage: string;
367+
let arraySign: ArraySignatureType;
368+
369+
beforeAll(async () => {
370+
signedMessage = await myAccount.signMessage(v1NestedExample);
371+
hashedMessage = await myAccount.hashMessage(v1NestedExample);
372+
arraySign = stark.formatSignature(signedMessage);
373+
});
374+
375+
test('with TypedMessage', () => {
376+
expect(
377+
typedData.verifyMessage(v1NestedExample, signedMessage, fullPubK, myAccount.address)
378+
).toBe(true);
379+
expect(typedData.verifyMessage(v1NestedExample, arraySign, fullPubK, myAccount.address)).toBe(
380+
true
381+
);
382+
});
383+
384+
test('with messageHash', () => {
385+
expect(typedData.verifyMessage(hashedMessage, signedMessage, fullPubK)).toBe(true);
386+
expect(typedData.verifyMessage(hashedMessage, arraySign, fullPubK)).toBe(true);
387+
});
388+
389+
test('failure cases', () => {
390+
expect(() => typedData.verifyMessage('zero', signedMessage, fullPubK)).toThrow(
391+
'message has a wrong format.'
392+
);
393+
394+
expect(() =>
395+
typedData.verifyMessage(v1NestedExample as any, signedMessage, fullPubK)
396+
).toThrow(/^When providing a TypedData .* the accountAddress parameter has to be provided/);
397+
398+
expect(() =>
399+
typedData.verifyMessage(v1NestedExample, signedMessage, fullPubK, 'wrong')
400+
).toThrow('accountAddress shall be a BigNumberish');
401+
});
402+
});
349403
});

src/account/default.ts

Lines changed: 16 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,6 @@ import { parseContract } from '../utils/provider';
5050
import { isString } from '../utils/shortString';
5151
import {
5252
estimateFeeToBounds,
53-
formatSignature,
5453
reduceV2,
5554
toFeeVersion,
5655
toTransactionVersion,
@@ -543,87 +542,37 @@ export class Account extends Provider implements AccountInterface {
543542
return getMessageHash(typedData, this.address);
544543
}
545544

545+
/**
546+
* @deprecated To replace by `myRpcProvider.verifyMessageInStarknet()`
547+
*/
546548
public async verifyMessageHash(
547549
hash: BigNumberish,
548550
signature: Signature,
549551
signatureVerificationFunctionName?: string,
550552
signatureVerificationResponse?: { okResponse: string[]; nokResponse: string[]; error: string[] }
551553
): Promise<boolean> {
552-
// HOTFIX: Accounts should conform to SNIP-6
553-
// (https://github.com/starknet-io/SNIPs/blob/f6998f779ee2157d5e1dea36042b08062093b3c5/SNIPS/snip-6.md?plain=1#L61),
554-
// but they don't always conform. Also, the SNIP doesn't standardize the response if the signature isn't valid.
555-
const knownSigVerificationFName = signatureVerificationFunctionName
556-
? [signatureVerificationFunctionName]
557-
: ['isValidSignature', 'is_valid_signature'];
558-
const knownSignatureResponse = signatureVerificationResponse || {
559-
okResponse: [
560-
// any non-nok response is true
561-
],
562-
nokResponse: [
563-
'0x0', // Devnet
564-
'0x00', // OpenZeppelin 0.7.0 to 0.9.0 invalid signature
565-
],
566-
error: [
567-
'argent/invalid-signature', // ArgentX 0.3.0 to 0.3.1
568-
'is invalid, with respect to the public key', // OpenZeppelin until 0.6.1, Braavos 0.0.11
569-
'INVALID_SIG', // Braavos 1.0.0
570-
],
571-
};
572-
let error: any;
573-
574-
// eslint-disable-next-line no-restricted-syntax
575-
for (const SigVerificationFName of knownSigVerificationFName) {
576-
try {
577-
// eslint-disable-next-line no-await-in-loop
578-
const resp = await this.callContract({
579-
contractAddress: this.address,
580-
entrypoint: SigVerificationFName,
581-
calldata: CallData.compile({
582-
hash: toBigInt(hash).toString(),
583-
signature: formatSignature(signature),
584-
}),
585-
});
586-
// Response NOK Signature
587-
if (knownSignatureResponse.nokResponse.includes(resp[0].toString())) {
588-
return false;
589-
}
590-
// Response OK Signature
591-
// Empty okResponse assume all non-nok responses are valid signatures
592-
// OpenZeppelin 0.7.0 to 0.9.0, ArgentX 0.3.0 to 0.3.1 & Braavos Cairo 0.0.11 to 1.0.0 valid signature
593-
if (
594-
knownSignatureResponse.okResponse.length === 0 ||
595-
knownSignatureResponse.okResponse.includes(resp[0].toString())
596-
) {
597-
return true;
598-
}
599-
throw Error('signatureVerificationResponse Error: response is not part of known responses');
600-
} catch (err) {
601-
// Known NOK Errors
602-
if (
603-
knownSignatureResponse.error.some((errMessage) =>
604-
(err as Error).message.includes(errMessage)
605-
)
606-
) {
607-
return false;
608-
}
609-
// Unknown Error
610-
error = err;
611-
}
612-
}
613-
614-
throw Error(`Signature verification Error: ${error}`);
554+
return this.verifyMessageInStarknet(
555+
hash,
556+
signature,
557+
this.address,
558+
signatureVerificationFunctionName,
559+
signatureVerificationResponse
560+
);
615561
}
616562

563+
/**
564+
* @deprecated To replace by `myRpcProvider.verifyMessageInStarknet()`
565+
*/
617566
public async verifyMessage(
618567
typedData: TypedData,
619568
signature: Signature,
620569
signatureVerificationFunctionName?: string,
621570
signatureVerificationResponse?: { okResponse: string[]; nokResponse: string[]; error: string[] }
622571
): Promise<boolean> {
623-
const hash = await this.hashMessage(typedData);
624-
return this.verifyMessageHash(
625-
hash,
572+
return this.verifyMessageInStarknet(
573+
typedData,
626574
signature,
575+
this.address,
627576
signatureVerificationFunctionName,
628577
signatureVerificationResponse
629578
);

0 commit comments

Comments
 (0)