@ledgerhq/hw-app-btc
Version:
Ledger Hardware Wallet Bitcoin Application API
122 lines (112 loc) • 3.42 kB
text/typescript
import { crypto } from "bitcoinjs-lib";
/**
* This class implements the merkle tree used by Ledger Bitcoin app v2+,
* which is documented at
* https://github.com/LedgerHQ/app-bitcoin-new/blob/master/doc/merkle.md
*/
export class Merkle {
private leaves: Buffer[];
private rootNode: Node;
private leafNodes: Node[];
private h: (buf: Buffer) => Buffer;
constructor(leaves: Buffer[], hasher: (buf: Buffer) => Buffer = crypto.sha256) {
this.leaves = leaves;
this.h = hasher;
const nodes = this.calculateRoot(leaves);
this.rootNode = nodes.root;
this.leafNodes = nodes.leaves;
}
getRoot(): Buffer {
return this.rootNode.hash;
}
size(): number {
return this.leaves.length;
}
getLeaves(): Buffer[] {
return this.leaves;
}
getLeafHash(index: number): Buffer {
return this.leafNodes[index].hash;
}
getProof(index: number): Buffer[] {
if (index >= this.leaves.length) throw Error("Index out of bounds");
return proveNode(this.leafNodes[index]);
}
calculateRoot(leaves: Buffer[]): { root: Node; leaves: Node[] } {
const n = leaves.length;
if (n == 0) {
return {
root: new Node(undefined, undefined, Buffer.alloc(32, 0)),
leaves: [],
};
}
if (n == 1) {
const newNode = new Node(undefined, undefined, leaves[0]);
return { root: newNode, leaves: [newNode] };
}
const leftCount = highestPowerOf2LessThan(n);
const leftBranch = this.calculateRoot(leaves.slice(0, leftCount));
const rightBranch = this.calculateRoot(leaves.slice(leftCount));
const leftChild = leftBranch.root;
const rightChild = rightBranch.root;
const hash = this.hashNode(leftChild.hash, rightChild.hash);
const node = new Node(leftChild, rightChild, hash);
leftChild.parent = node;
rightChild.parent = node;
return { root: node, leaves: leftBranch.leaves.concat(rightBranch.leaves) };
}
hashNode(left: Buffer, right: Buffer): Buffer {
return this.h(Buffer.concat([Buffer.from([1]), left, right]));
}
}
export function hashLeaf(
buf: Buffer,
hashFunction: (buf: Buffer) => Buffer = crypto.sha256,
): Buffer {
return hashConcat(Buffer.from([0]), buf, hashFunction);
}
function hashConcat(bufA: Buffer, bufB: Buffer, hashFunction: (buf: Buffer) => Buffer): Buffer {
return hashFunction(Buffer.concat([bufA, bufB]));
}
class Node {
leftChild?: Node;
rightChild?: Node;
parent?: Node;
hash: Buffer;
constructor(left: Node | undefined, right: Node | undefined, hash: Buffer) {
this.leftChild = left;
this.rightChild = right;
this.hash = hash;
}
isLeaf(): boolean {
return this.leftChild == undefined;
}
}
function proveNode(node: Node): Buffer[] {
if (!node.parent) {
return [];
}
if (node.parent.leftChild == node) {
if (!node.parent.rightChild) {
throw new Error("Expected right child to exist");
}
return [node.parent.rightChild.hash, ...proveNode(node.parent)];
} else {
if (!node.parent.leftChild) {
throw new Error("Expected left child to exist");
}
return [node.parent.leftChild.hash, ...proveNode(node.parent)];
}
}
function highestPowerOf2LessThan(n: number) {
if (n < 2) {
throw Error("Expected n >= 2");
}
if (isPowerOf2(n)) {
return n / 2;
}
return 1 << Math.floor(Math.log2(n));
}
function isPowerOf2(n: number): boolean {
return (n & (n - 1)) == 0;
}