Skip to content

feat: add @bitgo/wasm-dkls-vrf package - #372

Open
davidkaplanbitgo wants to merge 1 commit into
masterfrom
davidkaplan/dkls-vrf-keygen-bindings
Open

feat: add @bitgo/wasm-dkls-vrf package#372
davidkaplanbitgo wants to merge 1 commit into
masterfrom
davidkaplan/dkls-vrf-keygen-bindings

Conversation

@davidkaplanbitgo

Copy link
Copy Markdown
Contributor

New package @bitgo/wasm-dkls-vrf wrapping the VRF keygen and MPC hard derivation protocols from Silence Labs' dkls23-ll, pinned at rev 2079e97f.

Why

Both protocols sit behind the non-default vrf cargo feature, and SL's ci/build-npm-packages.sh does not enable it — so no published @silencelaboratories/dkls-wasm-ll-* version contains VrfKeygenSession, VrfKeyshare or HardDeriveSession. The feature is purely additive (all #[cfg(feature = "vrf")] sites are module decls, re-exports and two error conversions), so this ships as a separate wasm module and the existing signing path stays on SL's published package, untouched.

Signing DKG and DSG are not wrapped here.

API

class purpose
VrfDkgSession 2-round DKG producing a Ristretto VRF key share
VrfKeyshare the key share, fromBytes/toBytes + getters
HardDeriveSession 2-round hard derivation of a DKLS key share from a root + VRF share
DerivedKeyshare derived DKLS key share, public key, chain code

Session classes with toBytes/fromBytes (CONVENTIONS.md 6 and 7), so mid-protocol persistence comes for free and the BitGoJS wrapper can mirror the existing ecdsa-dkls/dkg.ts shape. All getters return Uint8Array, never hex.

Every message is a broadcast carrying its own from_party, so the round handlers take the whole batch the caller has — our own message included — and filter internally where the protocol wants peers only. One uniform JS contract; the round-1-wants-n-1 / round-2-wants-n asymmetry stays buried in Rust.

CBOR (ciborium) rather than bincode: key share bytes have to stay byte-compatible with SL's wasm, and sdk-lib-mpc already decodes DKLS key shares with cbor-x.

Hard derivation cannot live in TypeScript

The tweak combines the root key share's secret scalar with the secret VRF eval output inside handle_msg1. Orchestrating it from the SDK across two wasm modules would mean exporting root.s_i or the secret VRF output into the JS heap. TS only ever drives the rounds.

That makes the CBOR DKLS key share a bidirectional interface with SL's published build:

SL wasm:  KeygenSession -> Keyshare.toBytes()  ->  ours:    HardDeriveSession
ours:     DerivedKeyshare.keyshare             ->  SL wasm: Keyshare.fromBytes() -> SignSession

test/hardDerive.ts runs exactly that loop against dkls-wasm-ll-node@1.2.0 — their keygen, our derivation, their SignSession — and verifies the resulting signature against the derived public key. That was the open risk in the plan (does the rev we pin match the source their npm build came from) and it passes, so their exact commit stops mattering. Re-run on every rev bump: format drift breaks at runtime, not compile time.

Security note

VrfKeyshare.toBytes(), VrfDkgSession.toBytes(), HardDeriveSession.toBytes() and DerivedKeyshare.keyshare are all secret material. Hard-derive session bytes are the worst of the set — they embed the entire root key share. Documented on the methods and in the README. Every persisted blob carries a domain-separation prefix so a round-1 state can't be replayed into round 2, or a VRF state fed to hard derivation.

Tests

  • Rust (15): VRF DKG 2-of-3 and 3-of-3, hard derive 2-of-3, session round-trips between every round, determinism for fixed seeds, and rejections — message counts, duplicate senders, sender set missing own party, participant-set change between rounds, threshold/party-id mismatch between root and VRF shares.
  • Mocha (15): the JS surface plus the cross-package compat test above.

Build

Reuses the wasm-mps machinery verbatim. Registered in build-and-test.yaml, publish.yaml and detect-changes.mjs. Wasm is 632K raw / 248K gzipped — dkg::State and dsg::State strip out as expected, only the Keyshare struct and the Lagrange helper survive.

license: MIT follows the wasm-mps precedent (which statically links two SL crates the same way). Worth one email to whoever owns the SL relationship: dkls23-ll's Cargo.toml says license = "SLL" and their publish script stamps .license="SLL" onto their npm packages, so this is the first case where we relabel something they label SLL. One-line change if the answer is "keep SLL".

CODEOWNERS: @BitGo/wallet-core @BitGo/wallet-core-india @BitGo/hsm.

Wraps the VRF keygen and MPC hard derivation protocols from Silence Labs'
dkls23-ll. Both sit behind the non-default `vrf` cargo feature, which their
npm build script does not enable, so no published dkls-wasm-ll-* version
contains them. Signing DKG and DSG are deliberately not wrapped here and stay
on Silence Labs' published package.

Hard derivation cannot be orchestrated from TypeScript across the two wasm
modules: the tweak combines the root keyshare's secret scalar with the secret
VRF eval output inside handle_msg1, so both have to live in one address space.

That makes the CBOR DKLS keyshare a bidirectional interface with Silence Labs'
published wasm. test/hardDerive.ts runs the full loop against
dkls-wasm-ll-node@1.2.0 - their keygen, our derivation, their SignSession -
and verifies the signature, so format drift shows up in CI rather than at
runtime.
@davidkaplanbitgo
davidkaplanbitgo requested a review from a team as a code owner August 26, 2026 03:07

@OttoAllmendinger OttoAllmendinger left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you add tests similar to https://github.com/BitGo/BitGoWASM/tree/master/packages/wasm-utxo/bundler-test so we know that both ESM and CJS imports work?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants