trufflesuite/truffle

View on GitHub
packages/hdwallet/src/index.ts

Summary

Maintainability
A
1 hr
Test Coverage
import { keccak256 } from "ethereum-cryptography/keccak";
import { createHmac } from "crypto";
import secp256k1 from "secp256k1";

export type HDKey = {
  privateKey: Buffer;
  publicKey: Uint8Array;
  chainCode: Uint8Array;
};

const HARDENED_OFFSET = 0x80000000 as const;
const MASTER_SECRET = Buffer.from("Bitcoin seed", "utf8");

export function createAccountGeneratorFromSeedAndPath(
  seedBuffer: Uint8Array,
  hdPath: string[]
) {
  const parent = createAccountFromSeed(seedBuffer);
  const path = deriveFromPath(hdPath, parent);
  return (index: number) => {
    return deriveFromIndex(index, path);
  };
}

export const uncompressedPublicKeyToAddress = (
  uncompressedPublicKey: Uint8Array
) => {
  const address = Buffer.from(
    secp256k1.publicKeyConvert(uncompressedPublicKey, false)
  );
  // first byte is discarded
  const hash = keccak256(address.slice(1));
  return hash.slice(-20); // address is the last 20
};

function createAccountFromSeed(seedBuffer: Uint8Array): HDKey {
  const I = createHmac("sha512", MASTER_SECRET).update(seedBuffer).digest();
  const privateKey = I.slice(0, 32);
  const chainCode = I.slice(32);
  const publicKey = makePublicKey(privateKey);

  return {
    privateKey,
    chainCode,
    publicKey
  };
}

function deriveFromPath(fullPath: string[], child: HDKey): HDKey {
  fullPath.forEach(function (c, i) {
    if (i === 0) {
      if (!/^[mM]{1}/.test(c)) {
        throw new Error('Path must start with "m" or "M"');
      }
      return;
    }

    const hardened = c.length > 1 && c[c.length - 1] === "'";
    let childIndex = parseInt(c, 10);
    if (childIndex >= HARDENED_OFFSET) throw new Error("Invalid index");
    if (hardened) childIndex += HARDENED_OFFSET;

    child = deriveChild(
      childIndex,
      hardened,
      child.privateKey,
      child.publicKey,
      child.chainCode
    );
  });
  return child;
}

function deriveFromIndex(index: number, child: HDKey) {
  if (index >= HARDENED_OFFSET) throw new Error("Invalid index");

  return deriveChild(
    index,
    false,
    child.privateKey,
    child.publicKey,
    child.chainCode
  );
}

function makePublicKey(privateKey: Uint8Array) {
  return secp256k1.publicKeyCreate(privateKey);
}

/**
 * A buffer of size 4 that can be reused as long as all changes are consumed
 * within the same event loop.
 */
const SHARED_BUFFER_4 = Buffer.allocUnsafe(4);

function deriveChild(
  index: number,
  isHardened: boolean,
  privateKey: Buffer,
  publicKey: Uint8Array,
  chainCode: Uint8Array
): {
  privateKey: Buffer;
  publicKey: Uint8Array;
  chainCode: Uint8Array;
} {
  const indexBuffer = SHARED_BUFFER_4;
  indexBuffer.writeUInt32BE(index, 0);

  let data: Buffer;
  const privateKeyLength = privateKey.length;

  if (isHardened) {
    // Hardened child

    // privateKeyLength + 1 (BUFFER_ZERO.length) + 4 (indexBuffer.length)
    const dataLength = privateKeyLength + 1 + 4;
    data = Buffer.concat(
      [Buffer.allocUnsafe(1).fill(0), privateKey, indexBuffer],
      dataLength
    );
  } else {
    // Normal child
    data = Buffer.concat([publicKey, indexBuffer], publicKey.length + 4);
  }

  const I = createHmac("sha512", chainCode).update(data).digest();
  const IL = I.slice(0, 32);

  try {
    const privK = Buffer.allocUnsafe(privateKeyLength);
    privateKey.copy(privK, 0, 0, privateKeyLength);
    const newPrivK = secp256k1.privateKeyTweakAdd(privK, IL);
    return {
      privateKey: Buffer.from(newPrivK),
      publicKey: makePublicKey(newPrivK),
      chainCode: I.slice(32)
    };
  } catch {
    return deriveChild(index + 1, isHardened, privateKey, publicKey, chainCode);
  }
}