Skip to content

vetkd skill: frontend TypeScript examples are wrong for @dfinity/vetkeys 0.4.0; test_key_1 cycle cost incorrect #156

@marc0olo

Description

@marc0olo

Summary

We built a production-grade vetKeys app from scratch using the ICP skill files as the primary reference — a decentralized family password manager called KeyChain (marc0olo/icp-skill-password-manager). During development, we hit several build-breaking and runtime errors that were directly caused by incorrect information in the vetkd skill. A secondary gap exists in the icp-cli binding-generation reference.

The errors below were verified by fetching the live skill files, not from memory.


1. vetkd skill — Frontend TypeScript API is wrong for @dfinity/vetkeys 0.4.0

The skill pins @dfinity/vetkeys at v0.4.0 in the prerequisites but then documents an API that does not exist in that version. All three errors below occur together when following the skill's TypeScript example.

1a. TransportSecretKey.fromSeed() does not exist

What the skill says:

const seed = crypto.getRandomValues(new Uint8Array(32));
const transportSecretKey = TransportSecretKey.fromSeed(seed);
const transportPublicKey = transportSecretKey.publicKey();

What @dfinity/vetkeys 0.4.0 actually provides:

const transportSecretKey = TransportSecretKey.random();
const transportPublicKey = transportSecretKey.publicKeyBytes();

Both fromSeed() and publicKey() are absent from the 0.4.0 package. Calling them throws a runtime TypeError: TransportSecretKey.fromSeed is not a function.

1b. vetKey.toDerivedKeyMaterial() does not exist

What the skill says:

const aesKeyMaterial = vetKey.toDerivedKeyMaterial();
const aesKey = await crypto.subtle.importKey(
  "raw",
  aesKeyMaterial.data.slice(0, 32), // 256-bit AES key
  { name: "AES-GCM" },
  false,
  ["encrypt", "decrypt"],
);

What 0.4.0 actually provides:

// asDerivedKeyMaterial is async and returns DerivedKeyMaterial directly
const keyMaterial = await vetKey.asDerivedKeyMaterial();

// DerivedKeyMaterial has built-in encrypt/decrypt — no manual importKey needed
const ciphertext = keyMaterial.encryptMessage(plaintext, domainSeparator);
const plaintext  = await keyMaterial.decryptMessage(ciphertext, domainSeparator);

The method was renamed from toDerivedKeyMaterial to asDerivedKeyMaterial, is now async, and the returned DerivedKeyMaterial exposes encryptMessage/decryptMessage directly. There is no .data property. Following the skill results in a runtime crash and a manual AES-GCM wiring that is both unnecessary and incorrect.


2. vetkd skill — test_key_1 cycle cost is wrong on mainnet

What the skill says (table and mistake 8):

test_key_1 | Local + Mainnet | Development & testing | 10_000_000_000

Actual runtime error when following the skill:

vetkd_derive_key request sent with 10_000_000_000 cycles,
but 26_153_846_153 cycles are required.

test_key_1 costs the same ~26B cycles as key_1 on mainnet because the cost is determined by the subnet holding the master key, not the key name. The skill itself explains this correctly in the fees paragraph ("Fees depend on the subnet where the master key resides") but then contradicts itself in the table and in mistake 8.

The skill also correctly says "send more cycles than the current cost so that future subnet size increases do not cause calls to fail; unused cycles are refunded" — but only as a note for canisters that may be blackholed. This guidance should be applied to the code example itself, regardless of key name.

Suggested fix for the Rust example:

// Send 30B cycles — actual cost for both test_key_1 and key_1 is ~26.15B.
// Fees depend on subnet size, not key name. Unused cycles are refunded.
let (response,): (VetKdDeriveKeyResponse,) = ic_cdk::api::call::call_with_payment128(
    Principal::management_canister(),
    "vetkd_derive_key",
    (request,),
    30_000_000_000u128,
)

3. icp-cli binding-generation reference — Result type representation undocumented

The binding-generation reference documents opt T → T | null but does not cover how Result<T, E> Candid types are represented in the generated bindings.

@icp-sdk/bindgen generates discriminated unions:

{ __kind__: "Ok";  Ok:  T }
{ __kind__: "Err"; Err: E }

This is not obvious. The natural patterns ("Ok" in result, result.Ok !== undefined) do not work and silently fail or produce type errors. Without documentation, developers will use these patterns and get no useful error — queries simply return nothing or access is denied.

Suggested addition:

// Result<T, E> is a discriminated union — check __kind__, not property existence
const result = await actor.some_method();
if (result.__kind__ === "Err") {
  console.error(result.Err);
} else {
  console.log(result.Ok);
}

Similarly, Candid variants (e.g. variant { Read; Write; Manage }) become string enums:

import { AccessLevel } from "./bindings/backend";
// Use:     entry.access === AccessLevel.Write
// Not:     "Write" in entry.access  (does not work)

4. internet-identity skill — UserInterrupt not documented

When a user closes the Internet Identity popup without authenticating, authClient.login() calls onError with a UserInterrupt error. This is expected behavior, not a real failure, but the skill does not mention it.

The skill's onError example:

onError: (error) => {
  console.error("Login failed:", error);
  reject(error);
}

As written, this logs/surfaces a UserInterrupt as an error on every popup dismissal.

Suggested addition:

onError: (error) => {
  // UserInterrupt means the user closed the popup — not an error
  if (String(error).includes("UserInterrupt")) return;
  console.error("Login failed:", error);
  reject(error);
},

Reference implementation

All errors above were encountered during the construction of marc0olo/icp-skill-password-manager — a working end-to-end vetKeys app with:

  • Rust backend using ic-stable-structures 0.7, ic-cdk 0.19, raw vetkd_derive_key calls
  • React/TypeScript frontend using @dfinity/vetkeys 0.4.0, @icp-sdk/auth 5.x, @icp-sdk/bindgen 0.3.x
  • Client-side-only encryption: the canister never sees plaintext
  • Shared vault access control (Read / Write / Manage)

The working crypto.ts (source) shows the correct 0.4.0 API in full context.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions