tinyhumansai/openhuman · error

Ed25519 derivation path must be fully hardened

Error message

Ed25519 derivation path must be fully hardened: ${derivationPath}

What it means

deriveSlip10Ed25519PrivateKey() implements SLIP-0010 for ed25519, whose spec only permits hardened derivation (ed25519's curve math does not support public-key-only child derivation, so non-hardened children cannot be computed). Every path segment after m must end with an apostrophe; the loop checks each segment and throws on the first unhardened one.

Solutions

  1. Append ' (or use h) to every segment: m/44'/501'/0'/0' is the Solana standard this deriver expects
  2. Validate with /^m(\/[0-9]+')+$/ before calling so the user gets a field-level error instead of a throw
  3. Keep per-chain path presets and disable free-text editing for ed25519 chains

Example fix

// before
const path = "m/44'/501'/0'/0"; // last segment unhardened -> throws
// after
const path = "m/44'/501'/0'/0'";
Defensive patterns

Strategy: validation

Validate before calling

function assertFullyHardened(path: string): void {
  const segments = path.split('/').slice(1);
  const ok = segments.length > 0 && segments.every(s => /^[0-9]+'$/.test(s) || /^[0-9]+h$/.test(s));
  if (!ok) throw new Error(`Ed25519 path must be fully hardened: ${path}`);
}

Type guard

function isHardenedEd25519Path(path: string): boolean {
  return /^m(?:\/[0-9]+')+$/.test(path);
}

Prevention

When it happens

Trigger: Passing an EVM-style path with unhardened change/index segments (m/44'/60'/0'/0/0) to the ed25519/Solana deriver; reusing a generic path template across chains; the trailing segment missing its ' (m/44'/501'/0'/0).

Common situations: Multi-chain wallet UI sharing one derivation-path input; defaults copied from Bitcoin/EVM docs; paths round-tripped through code that strips quotes.

Related errors


AI-assisted analysis of tinyhumansai/openhuman@7491200858 (2026-08-17). Data as JSON: /api/errors/e4f1aeb3cf8b2edf. Report an issue: GitHub.

Appendix: source

Thrown at app/src/utils/cryptoKeys.ts:152

function deriveSecp256k1PrivateKey(mnemonic: string, derivationPath: string): Uint8Array {
  const seed = mnemonicToSeedSync(mnemonic);
  const hdkey = HDKey.fromMasterSeed(seed);
  const derived = hdkey.derive(derivationPath);
  if (!derived.privateKey) {
    throw new Error(`Failed to derive private key for path ${derivationPath}`);
  }
  return derived.privateKey;
}

function deriveSlip10Ed25519PrivateKey(seed: Uint8Array, derivationPath: string): Uint8Array {
  let key = hmac(sha512, new TextEncoder().encode('ed25519 seed'), seed);
  let privateKey = key.slice(0, 32);
  let chainCode = key.slice(32);

  for (const segment of derivationPath.split('/').slice(1)) {
    if (!segment.endsWith("'")) {
      throw new Error(`Ed25519 derivation path must be fully hardened: ${derivationPath}`);
    }
    const index = Number.parseInt(segment.slice(0, -1), 10);
    const hardened = (index + 0x80000000) >>> 0;
    const data = new Uint8Array(37);
    data[0] = 0;
    data.set(privateKey, 1);
    data[33] = (hardened >>> 24) & 0xff;
    data[34] = (hardened >>> 16) & 0xff;
    data[35] = (hardened >>> 8) & 0xff;
    data[36] = hardened & 0xff;
    key = hmac(sha512, chainCode, data);
    privateKey = key.slice(0, 32);
    chainCode = key.slice(32);
  }

  return privateKey;
}

View on GitHub (pinned to 7491200858)