Skip to content

Commit

Permalink
Merge pull request #293 from 0xs34n/feature/merkle-tree
Browse files Browse the repository at this point in the history
  • Loading branch information
janek26 authored Aug 31, 2022
2 parents 5a3fed5 + 1b04224 commit 5c56584
Show file tree
Hide file tree
Showing 14 changed files with 496 additions and 69 deletions.
42 changes: 42 additions & 0 deletions __mocks__/typedDataSessionExample.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"primaryType": "Session",
"types": {
"Policy": [
{ "name": "contractAddress", "type": "felt" },
{ "name": "selector", "type": "selector" }
],
"Session": [
{ "name": "key", "type": "felt" },
{ "name": "expires", "type": "felt" },
{ "name": "root", "type": "merkletree", "contains": "Policy" }
],
"StarkNetDomain": [
{ "name": "name", "type": "felt" },
{ "name": "version", "type": "felt" },
{ "name": "chain_id", "type": "felt" }
]
},
"domain": {
"name": "StarkNet Mail",
"version": "1",
"chain_id": 1
},
"message": {
"key": "0x0000000000000000000000000000000000000000000000000000000000000000",
"expires": "0x0000000000000000000000000000000000000000000000000000000000000000",
"root": [
{
"contractAddress": "0x1",
"selector": "transfer"
},
{
"contractAddress": "0x2",
"selector": "transfer"
},
{
"contractAddress": "0x3",
"selector": "transfer"
}
]
}
}
2 changes: 2 additions & 0 deletions __tests__/utils/__snapshots__/ellipticalCurve.test.ts.snap
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`pedersen() 1`] = `"0x5ed2703dfdb505c587700ce2ebfcab5b3515cd7e6114817e6026ec9d4b364ca"`;

exports[`pedersen() with 0 1`] = `"0x1a5c561f97b52c17a19f34c4499a745cd4e8412e29e4ed5e91e4481c7d53935"`;
5 changes: 5 additions & 0 deletions __tests__/utils/ellipticalCurve.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ test('pedersen()', () => {
expect(own).toMatchSnapshot();
});

test('pedersen() with 0', () => {
const own = pedersen(['0x12773', '0x0']);
expect(own).toMatchSnapshot();
});

test('computeHashOnElements()', () => {
const array = ['1', '2', '3', '4'];
expect(computeHashOnElements(array)).toBe(
Expand Down
146 changes: 146 additions & 0 deletions __tests__/utils/merkle.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import { MerkleTree, proofMerklePath } from '../../src/utils/merkle';

describe('MerkleTree class', () => {
describe('generate roots', () => {
test('should generate valid root for 1 elements', async () => {
const leaves = ['0x1'];
const tree = new MerkleTree(leaves);

const manualMerkle = leaves[0];

expect(tree.root).toBe(manualMerkle);
});
test('should generate valid root for 2 elements', async () => {
const leaves = ['0x1', '0x2'];
const tree = new MerkleTree(leaves);

const manualMerkle = MerkleTree.hash(leaves[0], leaves[1]);

expect(tree.root).toBe(manualMerkle);
});
test('should generate valid root for 4 elements', async () => {
const leaves = ['0x1', '0x2', '0x3', '0x4'];
const tree = new MerkleTree(leaves);

const manualMerkle = MerkleTree.hash(
MerkleTree.hash(leaves[0], leaves[1]),
MerkleTree.hash(leaves[2], leaves[3])
);

expect(tree.root).toBe(manualMerkle);
});
test('should generate valid root for 6 elements', async () => {
const leaves = ['0x1', '0x2', '0x3', '0x4', '0x5', '0x6'];
const tree = new MerkleTree(leaves);

const manualMerkle = MerkleTree.hash(
MerkleTree.hash(
MerkleTree.hash(leaves[0], leaves[1]),
MerkleTree.hash(leaves[2], leaves[3])
),
MerkleTree.hash(leaves[4], leaves[5])
);

expect(tree.root).toBe(manualMerkle);
});
test('should generate valid root for 7 elements', async () => {
const leaves = ['0x1', '0x2', '0x3', '0x4', '0x5', '0x6', '0x7'];
const tree = new MerkleTree(leaves);

const manualMerkle = MerkleTree.hash(
MerkleTree.hash(
MerkleTree.hash(leaves[0], leaves[1]),
MerkleTree.hash(leaves[2], leaves[3])
),
MerkleTree.hash(MerkleTree.hash(leaves[4], leaves[5]), leaves[6])
);

expect(tree.root).toBe(manualMerkle);
});
});
describe('generate proofs', () => {
let tree: MerkleTree;
beforeAll(() => {
const leaves = ['0x1', '0x2', '0x3', '0x4', '0x5', '0x6', '0x7'];
tree = new MerkleTree(leaves);
});
test('should return proof path for valid child', async () => {
const proof = tree.getProof('0x3');

const manualProof = [
'0x4',
MerkleTree.hash('0x1', '0x2'),
MerkleTree.hash(MerkleTree.hash('0x5', '0x6'), '0x7'),
];

expect(proof).toEqual(manualProof);
});
test('should return proof path for valid child', async () => {
const proof = tree.getProof('0x7');

const manualProof = [
'0x0', // proofs should always be as long as the tree is deep
MerkleTree.hash('0x5', '0x6'),
MerkleTree.hash(MerkleTree.hash('0x1', '0x2'), MerkleTree.hash('0x3', '0x4')),
];

expect(proof).toEqual(manualProof);
});
test('should throw for invalid child', () => {
expect(() => tree.getProof('0x8')).toThrow('leaf not found');
});
});
describe('verify proofs', () => {
let tree: MerkleTree;
beforeAll(() => {
const leaves = ['0x1', '0x2', '0x3', '0x4', '0x5', '0x6', '0x7'];
tree = new MerkleTree(leaves);
});

test('should return true for valid manual proof', async () => {
const manualProof = [
MerkleTree.hash('0x5', '0x6'),
MerkleTree.hash(MerkleTree.hash('0x1', '0x2'), MerkleTree.hash('0x3', '0x4')),
];
const leaf = '0x7';
const { root } = tree;

expect(proofMerklePath(root, leaf, manualProof)).toBe(true);
});
test('should return true for valid proof', async () => {
const proof = tree.getProof('0x3');
const leaf = '0x3';
const { root } = tree;

expect(proofMerklePath(root, leaf, proof)).toBe(true);
});
test('should return false for invalid proof (root)', async () => {
const proof = tree.getProof('0x3');
const leaf = '0x3';
const root = '0x4';

expect(proofMerklePath(root, leaf, proof)).toBe(false);
});
test('should return false for invalid proof (proof[0])', async () => {
const proof = tree.getProof('0x3');
const leaf = '0x3';
const { root } = tree;
proof[0] = '0x7';
expect(proofMerklePath(root, leaf, proof)).toBe(false);
});
test('should return false for invalid proof (proof[1])', async () => {
const proof = tree.getProof('0x3');
const leaf = '0x3';
const { root } = tree;
proof[1] = '0x4';
expect(proofMerklePath(root, leaf, proof)).toBe(false);
});
test('should return false for invalid proof (proof[2])', async () => {
const proof = tree.getProof('0x3');
const leaf = '0x3';
const { root } = tree;
proof[2] = '0x4';
expect(proofMerklePath(root, leaf, proof)).toBe(false);
});
});
});
116 changes: 107 additions & 9 deletions __tests__/utils/typedData.test.ts
Original file line number Diff line number Diff line change
@@ -1,46 +1,134 @@
import typedDataExample from '../../__mocks__/typedDataExample.json';
import typedDataSessionExample from '../../__mocks__/typedDataSessionExample.json';
import typedDataStructArrayExample from '../../__mocks__/typedDataStructArrayExample.json';
import { number } from '../../src';
import { getSelectorFromName } from '../../src/utils/hash';
import { MerkleTree } from '../../src/utils/merkle';
import { BigNumberish } from '../../src/utils/number';
import { encodeType, getMessageHash, getStructHash, getTypeHash } from '../../src/utils/typedData';
import {
StarkNetDomain,
encodeType,
encodeValue,
getMessageHash,
getStructHash,
getTypeHash,
} from '../../src/utils/typedData';

describe('typedData', () => {
test('should get right type encoding', () => {
const typeEncoding = encodeType(typedDataExample, 'Mail');
const typeEncoding = encodeType(typedDataExample.types, 'Mail');
expect(typeEncoding).toMatchInlineSnapshot(
`"Mail(from:Person,to:Person,contents:felt)Person(name:felt,wallet:felt)"`
);
const typeEncodingStructArr = encodeType(typedDataStructArrayExample, 'Mail');
const typeEncodingStructArr = encodeType(typedDataStructArrayExample.types, 'Mail');
expect(typeEncodingStructArr).toMatchInlineSnapshot(
`"Mail(from:Person,to:Person,posts_len:felt,posts:Post*)Person(name:felt,wallet:felt)Post(title:felt,content:felt)"`
);
});

test('should get right type hash', () => {
const typeHashDomain = getTypeHash(typedDataExample, 'StarkNetDomain');
const typeHashDomain = getTypeHash(typedDataExample.types, 'StarkNetDomain');
expect(typeHashDomain).toMatchInlineSnapshot(
`"0x1bfc207425a47a5dfa1a50a4f5241203f50624ca5fdf5e18755765416b8e288"`
);
const typeHashPerson = getTypeHash(typedDataExample, 'Person');
const typeHashPerson = getTypeHash(typedDataExample.types, 'Person');
expect(typeHashPerson).toMatchInlineSnapshot(
`"0x2896dbe4b96a67110f454c01e5336edc5bbc3635537efd690f122f4809cc855"`
);
const typeHashMail = getTypeHash(typedDataExample, 'Mail');
const typeHashMail = getTypeHash(typedDataExample.types, 'Mail');
expect(typeHashMail).toMatchInlineSnapshot(
`"0x13d89452df9512bf750f539ba3001b945576243288137ddb6c788457d4b2f79"`
);
const typeHashPost = getTypeHash(typedDataStructArrayExample, 'Post');
const typeHashPost = getTypeHash(typedDataStructArrayExample.types, 'Post');
expect(typeHashPost).toMatchInlineSnapshot(
`"0x1d71e69bf476486b43cdcfaf5a85c00bb2d954c042b281040e513080388356d"`
);
const typeHashMailWithStructArray = getTypeHash(typedDataStructArrayExample, 'Mail');
const typeHashMailWithStructArray = getTypeHash(typedDataStructArrayExample.types, 'Mail');
expect(typeHashMailWithStructArray).toMatchInlineSnapshot(
`"0x873b878e35e258fc99e3085d5aaad3a81a0c821f189c08b30def2cde55ff27"`
);
const selectorTypeHash = getTypeHash({}, 'selector');
expect(selectorTypeHash).toMatchInlineSnapshot(
`"0x1d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470"`
);
});

test('should transform type selector', () => {
const selector = 'transfer';
const selectorHash = getSelectorFromName(selector);
const rawSelectorValueHash = encodeValue({}, 'felt', selectorHash);
const selectorValueHash = encodeValue({}, 'selector', selector);
expect(selectorValueHash).toEqual(rawSelectorValueHash);
expect(selectorValueHash).toMatchInlineSnapshot(`
Array [
"felt",
"0x83afd3f4caedc6eebf44246fe54e38c95e3179a5ec9ea81740eca5b482d12e",
]
`);
});

test('should transform merkle tree', () => {
const tree = new MerkleTree(['0x1', '0x2', '0x3']);
const [, merkleTreeHash] = encodeValue({}, 'merkletree', tree.leaves);
expect(merkleTreeHash).toBe(tree.root);
expect(merkleTreeHash).toMatchInlineSnapshot(
`"0x551b4adb6c35d49c686a00b9192da9332b18c9b262507cad0ece37f3b6918d2"`
);
});

test('should transform merkle tree with custom types', () => {
const leaves = [
{
contractAddress: '0x1',
selector: 'transfer',
},
{
contractAddress: '0x2',
selector: 'transfer',
},
{
contractAddress: '0x3',
selector: 'transfer',
},
];
const hashedLeaves = leaves.map(
(leaf) =>
encodeValue(
{
Policy: [
{ name: 'contractAddress', type: 'felt' },
{ name: 'selector', type: 'selector' },
],
},
'Policy',
leaf
)[1]
);
const tree = new MerkleTree(hashedLeaves);
const [, merkleTreeHash] = encodeValue(
{
Parent: [{ name: 'root', type: 'merkletree', contains: 'Policy' }],
Policy: [
{ name: 'contractAddress', type: 'felt' },
{ name: 'selector', type: 'selector' },
],
},
'merkletree',
leaves,
{ key: 'root', parent: 'Parent' }
);
expect(merkleTreeHash).toBe(tree.root);
expect(merkleTreeHash).toMatchInlineSnapshot(
`"0x75c4f467f4527a5348f3e302007228a6b0057fc4c015f981ffb5b3ace475727"`
);
});

test('should get right hash for StarkNetDomain', () => {
const hash = getStructHash(typedDataExample, 'StarkNetDomain', typedDataExample.domain as any);
const hash = getStructHash(
typedDataExample.types,
'StarkNetDomain',
typedDataExample.domain as StarkNetDomain
);
expect(hash).toMatchInlineSnapshot(
`"0x54833b121883a3e3aebff48ec08a962f5742e5f7b973469c1f8f4f55d470b07"`
);
Expand Down Expand Up @@ -122,4 +210,14 @@ describe('typedData', () => {
`"0x70338fb11b8f70b68b261de8a322bcb004bd85e88ac47d9147982c7f5ac66fd"`
);
});

test('should transform session message correctly', () => {
const hash = getMessageHash(
typedDataSessionExample,
'0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826'
);
expect(hash).toMatchInlineSnapshot(
`"0x1ad0330a62a4a94eae5ea1a7ad96388179d2e4d33e6f909d17421d315110653"`
);
});
});
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export * as json from './utils/json';
export * as number from './utils/number';
export * as transaction from './utils/transaction';
export * as stark from './utils/stark';
export * as merkle from './utils/merkle';
export * as ec from './utils/ellipticCurve';
export * as uint256 from './utils/uint256';
export * as shortString from './utils/shortString';
Expand Down
4 changes: 2 additions & 2 deletions src/signer/default.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Abi, Invocation, InvocationsSignerDetails, KeyPair, Signature } from '../types';
import { getStarkKey, sign } from '../utils/ellipticCurve';
import { genKeyPair, getStarkKey, sign } from '../utils/ellipticCurve';
import { calculcateTransactionHash, getSelectorFromName } from '../utils/hash';
import { fromCallsToExecuteCalldataWithNonce } from '../utils/transaction';
import { TypedData, getMessageHash } from '../utils/typedData';
Expand All @@ -8,7 +8,7 @@ import { SignerInterface } from './interface';
export class Signer implements SignerInterface {
protected keyPair: KeyPair;

constructor(keyPair: KeyPair) {
constructor(keyPair: KeyPair = genKeyPair()) {
this.keyPair = keyPair;
}

Expand Down
Loading

0 comments on commit 5c56584

Please sign in to comment.