Skip to content

Repair a lost DKLs23 share and refresh all shares - #8

Draft
Jossec101 wants to merge 4 commits into
0xCarbon:mainfrom
Lunar-Rails:upstream/share-repair-refresh
Draft

Jossec101 wants to merge 4 commits into
0xCarbon:mainfrom
Lunar-Rails:upstream/share-repair-refresh

Conversation

@Jossec101

Copy link
Copy Markdown
Contributor

Why

Parties lose shares: a disk dies, a server or enclave gets replaced, a backup turns out to be bad. Today the only fix is a new DKG. That means a new key, new addresses, and moving every coin on chain. If enough parties lose their shares before that happens, the funds are gone.

Repair brings the lost party back under the same key, so nothing changes on chain.

The refresh is needed for two reasons:

  • The rebuilt share has no pairwise OT setup, so it can't sign until a refresh runs.
  • It gives every share new randomness, so any old share that leaked becomes useless.

What

  • Each helper (at least t parties that still have a share) sends the lost party one masked term, to that party only. The masks cancel out, so the party learns its own share and nothing about the helpers' shares.
  • The party adds up the terms and checks the result against its verifying share in the public key package. If they don't match, it stops.
  • Signing refuses a share that has no OT state.
  • A complete refresh across all n parties gives every share new randomness and new OT state. The group key stays the same.
  • C API: tss_repair_helper_term, tss_repair_assemble, tss_refresh_new_dkls, plus Go bindings.
  • secp256k1 only for now. Uses only DKLs23's public API, so DKLs23 needs no change.
  • Tests in Rust, the FFI and Go. Spec pages updated.

Depends on

#7, for chain_code(). Review the last two commits.

CI runs clippy on the latest stable with -D warnings, and that version
rejects uninlined format args, so every pull request fails before its own
changes are even checked.

Inlines the four format! calls it flags, in the tokio example, the FFI
memory hardening test and the protocol tests.
A wallet service hands out a new address for every customer or payment,
and today each one needs every party to run derive_child. The group key
plus the chain code make a BIP-32 xpub, so a server can derive addresses
on its own, and they match the ones the parties later sign for.

Adds chain_code() to the key share handle, in Rust, the C API
(tss_handle_chain_code) and every binding. DKLs23 returns the chain code
made at DKG; FROST Taproot returns the value derive.rs already uses,
through one shared helper. Other suites return ProtocolMismatch. Tests
check child keys against a separate BIP-32 implementation, and the FFI
tests get a shared DKG and signing driver.
Parties lose shares: a disk dies, a server is replaced, a backup is bad.
Today the only fix is a new DKG, which means a new key, new addresses and
moving every coin on chain. Repair brings the lost party back under the
same key.

Each helper sends the lost party one masked term; the masks cancel, so
the party learns its own share and nothing about the helpers'. It checks
the sum against its verifying share and stops on a mismatch. The rebuilt
share has no OT state, so signing refuses it until a complete refresh
across all parties gives every share new randomness and new OT state.
secp256k1 only for now, with C API and Go bindings, and tests in Rust,
the FFI and Go.
Repair and the complete refresh add new functions and a new kind of key
share, a rebuilt one that can't sign yet, so readers of the spec need to
know how they fit with the rest of the API.

Updates the API design, FFI and key management pages: the repair flow,
the hollow share and when it can sign again, the complete refresh, and the
three new C functions.
@n0xena

n0xena commented Oct 16, 2026

Copy link
Copy Markdown
Contributor

Is there any particular reason why this only works with secp256k1 and not with secp256r1/nist p256?

@canesin

canesin commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Placement: the repair protocol belongs in DKLs23, with libtss keeping the handle, C API and bindings. Target DKLs23 0.7.0.

Why:

  • It is a cryptographic protocol, not orchestration. Here it builds Party { .. } field by field, reads zero_share.seeds, and models a hollow share as empty multiplication maps. That couples libtss to DKLs23 internals and limits it to secp256k1.
  • In DKLs23 it becomes curve-generic, sits next to refresh, and is reviewed with the protocol.

Design reviewed; it is sound:

  • Masked Lagrange terms with zero-sum masks from the pairwise zero-share seeds; the target learns only the sum.
  • The assembled share must match the target's verifying share.
  • A complete refresh across all n parties is required before the share can sign.

Required in the DKLs23 version:

  • Target authorization is a hard contract. Repair hands party k's share to whoever is accepted as party k. If the real party k still exists, an impostor gains a second share, lowering the effective threshold by one. Document it as a precondition, and require the refresh before trusting the repaired share.
  • ZeroShare::compute silently skips counterparties without a seed. You guard against this; DKLs23 should return an error instead.

Suggest keeping this as a draft. I will port the protocol into dkls23-core (protocols::repair) based on your design, with you as co-author; this PR then becomes the binding layer.

@Jossec101 : Another option I am thinking of is adding repair support as a new function instead of adding into ZeroShare and than doing another 0.6.x release together with other improvements, since nobody should be using 0.6.x so far (just pushed it) might not be worth it and instead we go straight into 0.7 with the ideal interfaces.

canesin added a commit that referenced this pull request Oct 21, 2026
RefreshSession::new_dkls runs a DKLs23 complete refresh across all n
parties: every share moves by a share of zero, the group key stays the
same and the pairwise OT and zero-share state is regenerated. The refresh
is written once, generic over the curve, and updates the public key
package with DKLs23's PublicKeyPackage::refresh, which verifies every
proof commitment. A failed round ends the session.

Adds tss_refresh_new_dkls to the C API, NewRefreshSessionDKLs and the
Secp256r1ECDSA ciphersuite to the Go binding, tests in Rust, the FFI and
Go, and updates the spec.

Based on the refresh in #8.

Co-authored-by: José A.P <53834183+Jossec101@users.noreply.github.com>
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.

3 participants