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 Merkle Tree #1

Merged
merged 31 commits into from
Jan 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
d6875c1
Add Merkle Tree
Phanco Nov 30, 2023
4719b19
Changed editing, update main.ts scripts
Phanco Nov 30, 2023
09c12e5
Rename package to align with repo name
Phanco Nov 30, 2023
bba45fb
Added more details at README
Phanco Nov 30, 2023
0faa955
Addresses are now sorted, betting naming of `leaf`
Phanco Dec 7, 2023
54d60fe
Update Beddows value to balances.json
Phanco Dec 7, 2023
90672e9
Add unit test
Phanco Dec 8, 2023
1c931af
Update README
Phanco Dec 8, 2023
97b5279
Modulized merkleTree building process, updated test cases
Phanco Jan 2, 2024
c753d7c
Updated README
Phanco Jan 3, 2024
a9037d6
Add set instruction to README
Phanco Jan 3, 2024
3ff9267
Update test cases
Phanco Jan 4, 2024
37e9bfe
Rename merkleTree files, added merkleRoot file
Phanco Jan 4, 2024
e95c789
Improve README
Phanco Jan 4, 2024
7caf394
Merge branch 'main' of github.com:LiskHQ/lisk-token-claim into 115-im…
Phanco Jan 8, 2024
52753b6
Updated README and code according to PR suggestions
Phanco Jan 8, 2024
480430a
Update README
Phanco Jan 8, 2024
0392fc2
Fix unit test
Phanco Jan 8, 2024
2f73a29
Update format
Phanco Jan 9, 2024
4add790
keyPairs are calculated instead of stored, users can custom number of…
Phanco Jan 9, 2024
8f257da
Update comments
Phanco Jan 10, 2024
ade43be
Added append0x function, update tsconfig
Phanco Jan 10, 2024
28d64d5
Update files to snake case
Phanco Jan 10, 2024
d892067
Undo irrelavant changes
Phanco Jan 10, 2024
d3795d9
Update format
Phanco Jan 10, 2024
bedb635
Change example args to flags
Phanco Jan 10, 2024
523510c
Update code according to PR comments
Phanco Jan 11, 2024
044e9c7
Update test file names
Phanco Jan 12, 2024
2a0a7fa
Use oclif logging
Phanco Jan 17, 2024
646fc49
Update formatting and tsconfig
Phanco Jan 17, 2024
cd17364
Update log import, changed fn name build_tree -> buildTree
Phanco Jan 19, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,10 @@ module.exports = {
root: true,
rules: {
'@typescript-eslint/require-await': 'off',
'@typescript-eslint/no-unsafe-assignment': 'off',
'@typescript-eslint/no-unsafe-return': 'off',
'@typescript-eslint/no-unsafe-member-access': 'off',
'@typescript-eslint/no-unsafe-call': 'off',
'@typescript-eslint/no-unsafe-argument': 'off',
},
};
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,11 @@ dist

# Stores VSCode versions used for testing VSCode extensions
.vscode-test

# Example Accounts and MerkleTree are not committed
data/example/accounts.json
data/example/key-pairs.json
data/example/merkle-root.json
data/example/merkle-tree-result.json
data/example/merkle-tree-result-detailed.json
data/example/signatures.json
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ REVISION
*.tsbuildinfo
*.blob
*.lock
*.keep

## jest snapshot
*.snap
Expand Down
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,13 @@
# lisk-token-claim

This library is the monorepo for:

### Tree Builder

Builds Merkle Tree from a snapshot and computes Merkle Root.

## Setup and Installation

```
$ yarn && yarn build
```
Empty file added data/example/.keep
Empty file.
Empty file added data/mainnet/.keep
Empty file.
Empty file added data/testnet/.keep
Empty file.
112 changes: 112 additions & 0 deletions packages/tree-builder/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# Tree Builder

This library builds Merkle Tree from a snapshot and computes Merkle Root.

## Run

```
$ cd packages/tree-builder
$ ./bin/run.js generate-merkle-tree --network=mainnet # Mainnet
$ ./bin/run.js generate-merkle-tree --network=testnet # Testnet
$ ./bin/run.js generate-merkle-tree --network=example # Example, see below
```

## Files

| Name | Description | Generated By |
| -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------- |
| `/data/<network>/accounts.json` | Stores addresses, balances, and multisig details(If any) per account after a snapshot is taken, addresses must be sorted in ascending order. Will be used for MerkleTree computation. | Snapshot |
| `/data/<network>/merkle-tree-result-detailed.json` | Stores MerkleRoot, and leaves for each account. Will be used for examination by 3rd Party or public, also used by Claim Backend API. | `$ ./bin/run.js generate-merkle-tree --network=<network>` |
| `/data/<network>/merkle-tree-result.json` | Stores MerkleRoot, and leaves for each account. A lightweight version of `merkle-tree-result-detailed.json`. Will be used for testing of Claim Contract. | `$ ./bin/run.js generate-merkle-tree --network=<network>` |
| `/data/<network>/merkle-root.json` | Stores MerkleRoot only. Will be used for deployment of Claim Contract. | `$ ./bin/run.js generate-merkle-tree --network=<network>` |

## Merkle Leaf

Each leaf will be encoded as ABI-format, in the following order:

```
LSK_ADDRESS_IN_HEX: bytes20
BALANCE_IN_BEDDOWS: uint64
NUMBER_OF_SIGNATURES: uint32
MANDATORY_KEYS: bytes32[]
OPTIONAL_KEYS: bytes32[]

P.S. If the address is not a multisig address, NUMBER_OF_SIGNATURES would be 0, MANDATORY_KEYS and OPTIONAL_KEYS be []
```

### Params

```
accounts.json:
{
lskAddress: string;
balance: number;
balanceBeddows: number;
numberOfSignatures?: number;
mandatoryKeys?: string[];
optionalKeys?: string[];
}

merkle-tree-result-detailed.json:
{
merkleRoot: string;
leaves: {
lskAddress: string;
address: string;
balance: number;
balanceBeddows: number;
numberOfSignatures: number;
mandatoryKeys: string[];
optionalKeys: string[];
hash: string;
proof: string[];
}[];
}

merkle-tree-result.json:
{
merkleRoot: string;
leaves: {
b32Address: string;
balanceBeddows: number;
mandatoryKeys: string[];
numberOfSignatures: number;
optionalKeys: string[];
proof: string[];
}[];
}
# `address` is a reserved in solidity, hence `b32Address` here

merkle-root.json:
{
merkleRoot: string;
}

# Only used at example
signatures.json:
{
message: string;
sigs: {
pubKey: string
r: string
s: string
}[];
}[];
```

## _Demo/Testing Purpose Only_

```
$ ./bin/run.js example [--amountOfLeaves <value>] [--recipient <value>]

# FLAGS
# --amountOfLeaves=<value> [default: 100] Amount of leaves in the tree
# --recipient=<value> [default: 0x34A1D3fff3958843C43aD80F30b94c510645C316] Destination address at signing stage. Default is the contract address created by default mnemonic in Anvil/Ganache when nonce=0
```

By running the command above, it will, at `data/example` folder:

1. Create `key-pairs.json`, which stores public-private key pairs, along with the corresponding address and path
2. Create `accounts.json` using addresses in `key-pairs.json`, with random LSK balance.
3. Create `merkle-root.json`, `merkle-tree-result.json`, `merkle-tree-result-detailed.json` using the accounts above (Equivalent to `./bin/run.js generate-merkle-tree --network=example`).
4. Sign every leaf using the private keys in `key-pairs.json` and output to `signatures.json`.
15 changes: 8 additions & 7 deletions packages/tree-builder/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,16 +39,16 @@
"plugins": [
"@oclif/plugin-help"
],
"topicSeparator": " ",
"topics": {
"hello": {
"description": "Say hello to the world and others"
}
}
"topicSeparator": " "
},
"dependencies": {
"@liskhq/lisk-cryptography": "4.1.0",
"@oclif/core": "^3",
"@oclif/plugin-help": "^6"
"@oclif/plugin-help": "^6",
"@openzeppelin/merkle-tree": "^1.0.5",
"ethereumjs-util": "^7.1.5",
"ethers": "^6.8.1",
"tweetnacl": "^1.0.3"
},
"devDependencies": {
"@oclif/prettier-config": "^0.2.1",
Expand All @@ -60,6 +60,7 @@
"mocha": "^10",
"oclif": "^3.17.2",
"shx": "^0.3.3",
"sinon": "^17.0.1",
"ts-node": "^10.9.2",
"typescript": "^5"
}
Expand Down
100 changes: 100 additions & 0 deletions packages/tree-builder/src/applications/example/create_accounts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import * as fs from 'fs';
import { address } from '@liskhq/lisk-cryptography';
import { ExampleKey } from '../../interface';

// 1 LSK = 10^8 Beddows
const LSK_MULTIPLIER = 10 ** 8;

// Balances are random between 0 - <RANDOM_RANGE>
const RANDOM_RANGE = 10000;

// Multisig Accounts
// For each account it will use the address of the index as account holder,
// while the "keys" are used from #0 onwards

// First (numberOfAccounts - multiSigs.length) accounts would be regular accounts
// For e.g. numberOfAccounts = 54
// #50: numSig 3 => 3m
// #51: numSig 2 => 1m + 2o
// #52: numSig 5 => 3m + 3o
// #53: numSig 64 => 64m
const multiSigs = [
{
numberOfSignatures: 3,
numberOfMandatoryKeys: 3,
numberOfOptionalKeys: 0,
},
{
numberOfSignatures: 2,
numberOfMandatoryKeys: 1,
numberOfOptionalKeys: 2,
},
{
numberOfSignatures: 5,
numberOfMandatoryKeys: 3,
numberOfOptionalKeys: 3,
},
{
numberOfSignatures: 64,
numberOfMandatoryKeys: 64,
numberOfOptionalKeys: 0,
},
];

const randomBalance = (range: number): number => Number((range * Math.random()).toFixed(8));

export function createAccounts(numberOfAccounts = 54) {
const keyPairs = JSON.parse(
fs.readFileSync('../../data/example/key-pairs.json', 'utf-8'),
) as ExampleKey[];

// to ensure a deterministic tree construction, the accounts array must be sorted in lexicographical order of their addr entries.
const sortedKeyPairs = [...keyPairs].sort((key1, key2) =>
address
.getAddressFromLisk32Address(key1.address)
.compare(address.getAddressFromLisk32Address(key2.address)),
);

const results: {
lskAddress: string;
balance: number;
balanceBeddows: number;
numberOfSignatures?: number;
mandatoryKeys?: string[];
optionalKeys?: string[];
}[] = [];

// Regular Accounts
for (let index = 0; index < numberOfAccounts - multiSigs.length; index++) {
const account = sortedKeyPairs[index];
const balance = randomBalance(RANDOM_RANGE);
const balanceBeddows = Math.round(balance * LSK_MULTIPLIER);

results.push({
lskAddress: account.address,
balance,
balanceBeddows,
});
}

for (const multiSig of multiSigs) {
const account = sortedKeyPairs[results.length];
const balance = randomBalance(RANDOM_RANGE);
const balanceBeddows = Math.round(balance * LSK_MULTIPLIER);

results.push({
lskAddress: account.address,
balance,
balanceBeddows,
numberOfSignatures: multiSig.numberOfSignatures,
mandatoryKeys: [...Array(multiSig.numberOfMandatoryKeys).keys()].map(
(_, index) => keyPairs[index].publicKey,
),
optionalKeys: [...Array(multiSig.numberOfOptionalKeys).keys()].map(
(_, index) => keyPairs[index + multiSig.numberOfMandatoryKeys].publicKey,
),
});
}

fs.writeFileSync('../../data/example/accounts.json', JSON.stringify(results), 'utf-8');
}
20 changes: 20 additions & 0 deletions packages/tree-builder/src/applications/example/create_key_pairs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import * as fs from 'fs';
import { address, ed } from '@liskhq/lisk-cryptography';
import { ExampleKey } from '../../interface';

const initialPath = "m/44'/134'";

export async function createKeyPairs(amount = 100) {
const keys: ExampleKey[] = [];
for (let i = 0; i < amount; i++) {
const keyPath = `${initialPath}/${i}'`;
const privateKey = await ed.getPrivateKeyFromPhraseAndPath('lisk', keyPath);
keys.push({
address: address.getLisk32AddressFromAddress(address.getAddressFromPrivateKey(privateKey)),
keyPath,
publicKey: ed.getPublicKeyFromPrivateKey(privateKey).toString('hex'),
privateKey: privateKey.toString('hex'),
});
}
fs.writeFileSync('../../data/example/key-pairs.json', JSON.stringify(keys), 'utf-8');
}
78 changes: 78 additions & 0 deletions packages/tree-builder/src/applications/example/sign_accounts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import * as fs from 'fs';
import { AbiCoder, keccak256 } from 'ethers';
import * as tweetnacl from 'tweetnacl';
import { MerkleTree, ExampleKey } from '../../interface';
import { append0x } from '../../utils';

interface SigPair {
pubKey: string;
r: string;
s: string;
}

interface Signature {
message: string;
sigs: SigPair[];
}

const abiCoder = new AbiCoder();
const signMessage = (message: string, key: ExampleKey): string => {
return Buffer.from(
tweetnacl.sign.detached(
Buffer.from(message.substring(2), 'hex'),
Buffer.from(key.privateKey, 'hex'),
),
).toString('hex');
};

const BYTES_9 = '000000000000000000';

export function signAccounts(recipient: string) {
const keys = JSON.parse(
fs.readFileSync('../../data/example/key-pairs.json', 'utf-8'),
) as ExampleKey[];

const merkleTree = JSON.parse(
fs.readFileSync('../../data/example/merkle-tree-result-detailed.json', 'utf-8'),
) as MerkleTree;
const signatures: Signature[] = [];

for (const leaf of merkleTree.leaves) {
const message =
keccak256(abiCoder.encode(['bytes32', 'address'], [leaf.hash, recipient])) + BYTES_9;

const sigs: SigPair[] = [];

// Regular Account
if (leaf.numberOfSignatures === 0) {
const key = keys.find(key => key.address === leaf.lskAddress)!;
const signature = signMessage(message, key);

sigs.push({
pubKey: append0x(key.publicKey),
r: append0x(signature.substring(0, 64)),
s: append0x(signature.substring(64)),
});
} else {
// Multisig Account
// Signing with all keys regardless of the required number of signatures
for (const pubKey of leaf.mandatoryKeys.concat(leaf.optionalKeys)) {
const key = keys.find(key => append0x(key.publicKey) === pubKey)!;
const signature = signMessage(message, key);

sigs.push({
pubKey: append0x(key.publicKey),
r: append0x(signature.substring(0, 64)),
s: append0x(signature.substring(64)),
});
}
}

signatures.push({
message,
sigs,
});
}

fs.writeFileSync('../../data/example/signatures.json', JSON.stringify(signatures), 'utf-8');
}
Loading