Skip to content

Commit

Permalink
feat(indexer): update bitcoin address derivation in multichain indexer
Browse files Browse the repository at this point in the history
  • Loading branch information
cherrybarry committed Nov 7, 2024
1 parent 72e1256 commit 221fc45
Show file tree
Hide file tree
Showing 10 changed files with 312 additions and 83 deletions.
7 changes: 7 additions & 0 deletions apps/backend/migrations/20241106142510_multichain_recreate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { upSQL } from 'knex-migrate-sql-file';

export async function up(knex) {
await upSQL(knex);
}

export function down() {}
51 changes: 51 additions & 0 deletions apps/backend/migrations/20241106142510_multichain_recreate.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
DROP TABLE multichain_accounts;

DROP TABLE multichain_transactions;

CREATE TABLE multichain_accounts (
account_id TEXT NOT NULL,
chain TEXT NOT NULL,
path TEXT NOT NULL,
derived_address TEXT NOT NULL,
public_key TEXT NOT NULL,
block_height NUMERIC(20, 0) NOT NULL,
block_timestamp BIGINT NOT NULL,
PRIMARY KEY (account_id, chain, path)
);

CREATE TABLE multichain_transactions (
id BIGINT GENERATED ALWAYS AS IDENTITY,
receipt_id TEXT NOT NULL,
account_id TEXT NOT NULL,
derived_address TEXT NOT NULL,
public_key TEXT NOT NULL,
chain TEXT NOT NULL,
path TEXT NOT NULL,
derived_transaction TEXT,
block_height NUMERIC(20, 0) NOT NULL,
block_timestamp BIGINT NOT NULL,
PRIMARY KEY (id),
UNIQUE (receipt_id)
);

CREATE INDEX ma_account_id_idx ON multichain_accounts USING btree (account_id);

CREATE INDEX ma_derived_address_idx ON multichain_accounts USING btree (derived_address);

CREATE INDEX ma_block_height_idx ON multichain_accounts USING btree (block_height DESC);

CREATE INDEX ma_block_timestamp_idx ON multichain_accounts USING btree (block_timestamp DESC);

CREATE INDEX mt_receipt_id_idx ON multichain_transactions USING btree (receipt_id);

CREATE INDEX mt_account_id_idx ON multichain_transactions USING btree (account_id);

CREATE INDEX mt_derived_address_idx ON multichain_transactions USING btree (derived_address);

CREATE INDEX mt_derived_transaction_idx ON multichain_transactions USING btree (derived_transaction)
WHERE
derived_transaction IS NOT NULL;

CREATE INDEX mt_block_height_idx ON multichain_transactions USING btree (block_height DESC);

CREATE INDEX mt_block_timestamp_idx ON multichain_transactions USING btree (block_timestamp DESC);
5 changes: 4 additions & 1 deletion apps/indexer-multichain/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,11 @@
"lint:check": "tsc --noEmit && eslint ./"
},
"dependencies": {
"@cosmjs/crypto": "0.32.4",
"@cosmjs/encoding": "0.32.4",
"@sentry/node": "7.74.1",
"bs58check": "4.0.0",
"bech32": "2.0.0",
"bitcoinjs-lib": "7.0.0-rc.0",
"elliptic": "6.6.0",
"envalid": "8.0.0",
"js-sha3": "0.9.3",
Expand Down
63 changes: 19 additions & 44 deletions apps/indexer-multichain/src/libs/bitcoin.ts
Original file line number Diff line number Diff line change
@@ -1,59 +1,34 @@
import crypto from 'node:crypto';

import bs58check from 'bs58check';
import hash from 'hash.js';
import * as bitcoin from 'bitcoinjs-lib';

import { Network } from 'nb-types';

import config from '#config';
import {
deriveChildPublicKey,
najPublicKeyStrToUncompressedHexPoint,
} from '#libs/kdf';
import { generateCompressedPublicKey } from '#libs/kdf';

const network = config.network === Network.MAINNET ? 'bitcoin' : 'testnet';
const network =
config.network === Network.MAINNET
? bitcoin.networks.bitcoin
: bitcoin.networks.testnet;

const deriveAddress = async (accountId: string, derivation_path: string) => {
const publicKey = await deriveChildPublicKey(
najPublicKeyStrToUncompressedHexPoint(),
const deriveAddress = async (accountId: string, derivationPath: string) => {
const derivedKey = await generateCompressedPublicKey(
accountId,
derivation_path,
);
const address = await uncompressedHexPointToBtcAddress(publicKey, network);

return { address, publicKey };
};

const uncompressedHexPointToBtcAddress = async (
publicKeyHex: string,
network: string,
) => {
// Step 1: SHA-256 hashing of the public key
const publicKeyBytes = Uint8Array.from(Buffer.from(publicKeyHex, 'hex'));

const sha256HashOutput = await crypto.subtle.digest(
'SHA-256',
publicKeyBytes,
derivationPath,
);

// Step 2: RIPEMD-160 hashing on the result of SHA-256
const ripemd160 = hash
.ripemd160()
.update(Buffer.from(sha256HashOutput))
.digest();
const publicKeyBuffer = Buffer.from(derivedKey, 'hex');

// Step 3: Adding network byte (0x00 for Bitcoin Mainnet)
const network_byte = network === 'bitcoin' ? 0x00 : 0x6f;
const networkByte = Buffer.from([network_byte]);
const networkByteAndRipemd160 = Buffer.concat([
networkByte,
Buffer.from(ripemd160),
]);
// Use P2WPKH (Bech32) address type
const { address } = bitcoin.payments.p2wpkh({
network,
pubkey: publicKeyBuffer,
});

// Step 4: Base58Check encoding
const address = bs58check.encode(networkByteAndRipemd160);
if (!address) {
throw new Error('Failed to generate Bitcoin address');
}

return address;
return { address, publicKey: derivedKey };
};

export default deriveAddress;
33 changes: 33 additions & 0 deletions apps/indexer-multichain/src/libs/cosmos.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { ripemd160, Secp256k1, sha256 } from '@cosmjs/crypto';
import { fromHex } from '@cosmjs/encoding';
import { bech32 } from 'bech32';

import { generateCompressedPublicKey } from './kdf.js';

// implementation is pending becuase prefix is required for cosmos
const deriveAddress = async (
accountId: string,
derivationPath: string,
prefix: string,
) => {
const derivedKeyHex = await generateCompressedPublicKey(
accountId,
derivationPath,
);

const publicKey = fromHex(derivedKeyHex);
const address = pubkeyToAddress(publicKey, prefix);

return { address, publicKey: derivedKeyHex };
};

const pubkeyToAddress = (pubkey: Uint8Array, prefix: string) => {
const pubkeyRaw =
pubkey.length === 33 ? pubkey : Secp256k1.compressPubkey(pubkey);
const sha256Hash = sha256(pubkeyRaw);
const ripemd160Hash = ripemd160(sha256Hash);

return bech32.encode(prefix, bech32.toWords(ripemd160Hash));
};

export default deriveAddress;
8 changes: 4 additions & 4 deletions apps/indexer-multichain/src/libs/ethereum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ import { keccak256 } from 'viem';

import {
deriveChildPublicKey,
najPublicKeyStrToUncompressedHexPoint,
signerUncompressedPublicKeyHex,
} from '#libs/kdf';

const deriveAddress = async (accountId: string, derivation_path: string) => {
const deriveAddress = async (accountId: string, derivationPath: string) => {
const publicKey = await deriveChildPublicKey(
najPublicKeyStrToUncompressedHexPoint(),
signerUncompressedPublicKeyHex,
accountId,
derivation_path,
derivationPath,
);
const address = uncompressedHexPointToEvmAddress(publicKey);

Expand Down
29 changes: 24 additions & 5 deletions apps/indexer-multichain/src/libs/kdf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import config from '#config';

const { ec: EC } = elliptic;
const { sha3_256 } = jsSha3;
const ec = new EC('secp256k1');

export const signer =
config.network === Network.MAINNET
Expand All @@ -22,19 +23,37 @@ export const signer =
'secp256k1:4NfTiv3UsGahebgTaHyD9vF8KYKMBnfd6kh94mK6xv8fGBiJB8TBtFMP5WWXz6B89Ac1fbpzPwAvoyQebemHFwx3',
};

export const najPublicKeyStrToUncompressedHexPoint = () => {
return (
'04' +
Buffer.from(base_decode(signer.publicKey.split(':')[1])).toString('hex')
export const signerUncompressedPublicKeyHex =
'04' +
Buffer.from(base_decode(signer.publicKey.split(':')[1])).toString('hex');

export const generateCompressedPublicKey = async (
signerId: string,
path: string,
): Promise<string> => {
const derivedPublicKeyHex = await deriveChildPublicKey(
signerUncompressedPublicKeyHex,
signerId,
path,
);

const publicKeyBuffer = Buffer.from(derivedPublicKeyHex, 'hex');

// Compress the public key
const compressedPublicKey = ec
.keyFromPublic(publicKeyBuffer)
.getPublic()
.encodeCompressed();

// Return the compressed public key as a hex string
return Buffer.from(compressedPublicKey).toString('hex');
};

export const deriveChildPublicKey = async (
parentUncompressedPublicKeyHex: string,
signerId: string,
path = '',
) => {
const ec = new EC('secp256k1');
const scalarHex = sha3_256(
`near-mpc-recovery v0.1.0 epsilon derivation:${signerId},${path}`,
);
Expand Down
Loading

0 comments on commit 221fc45

Please sign in to comment.