Konclave: A group vault on Zcash | FROST

Hi everyone,

I’m Daniel Gorgonha, co-founder of DeegaLabs, a software engineering studio in Florianópolis, Brazil, with 10+ years of software development behind me. I got into privacy through practice. In a local circle on privacy and community safety, I built Safe City, an app for anonymous neighborhood alerts here in Florianópolis, with the code public precisely so anyone can audit that there is no tracking. That project pulled me deeper into cryptography and privacy tech.

Last year I attended the Ipê Village conference, two days at the hotel in Jurerê. This year I came back as a volunteer and on the architecture track, presented a valocracy project, and spent nearly the whole month at the workshops. At the Privacy House, during the Raycash workshop on building onchain apps with privacy, I met @michae2xl, the first Zcash ambassador in Brazil and one of the sponsors of the event. I was also there for the session by pacu, Zcash’s developer relations engineer, on contributing effectively to the Zcash ecosystem. That talk, honestly, shaped how I approached this project: instead of building something and then showing up asking for support, start by understanding where the ecosystem actually needs hands.

Since then I’ve been getting closer to the ZcashBrazil community, and along the way I built privacy-focused projects like a shielded BTC collateral protocol on Starknet and Cronos Shield, 2nd place among 191 projects at the Cronos x402 PayTech Hackathon. Konclave was born at the ZecHub Hackathon 3.0, where I competed, and I’ve been working on it full time for the last 2 months. I’m here because I believe the right way to be part of this community is to create real value and to listen, and that’s what I intend to do.

So here it is, and I’d genuinely value your feedback.

What we built

Konclave is a GUI for FROST threshold vaults on Zcash: a group treasury where spend authority is split t-of-n across members by real DKG, the key is never reconstituted (at creation or at signing), and every transaction is shielded. Propose, approve to quorum, sign, broadcast, account. In plain language, no CLI.

We built it because the Foundation’s FROST tooling is excellent but, as ZF itself has noted, wallet integration is the missing piece. “Easy multi-sig tools for shielded addresses” is also a named ZCG funding priority, which is what pointed us here.

We learned a great deal from Zkool, by @hanh, which already does shielded FROST and solved real problems before we did. We are not claiming the first FROST on Zcash. What we did differently was take the whole ceremony into the browser, with nothing to install, and wrap it in a treasury flow: proposal, approval, signing, and a book.

What is proven (verifiable, not claimed)

12 mainnet transactions, each a real FROST ceremony with the key never reconstituted. Quorums proven: 2-of-2, 2-of-3 and 3-of-4.

Each one is a first: the first time a capability was proven on the live network. It is not a log of every send; the vaults have made more transactions than that, and the ones that only repeat something already listed are deliberately left out. A row there is a claim you can check, not evidence of volume.

Among them:

  • an application-driven quorum payment
  • a private payroll: one shielded transaction, N outputs, each payslip in an encrypted memo
  • a send from a vault born of real DKG
  • the full Orchard to Ironwood cycle under NU6.3, including the first Ironwood-pool spend
  • a browser-signed broadcast, two tabs over a blind relay (3022420a…)
  • a ceremony across separate physical machines, over the internet: two people in two places, each browser holding only its own share (aec83baf…, block 3,460,285)
  • a 3-of-4 vault operated by someone other than me (b496fc3c…)
  • and a private payroll on the web path, post-Ironwood: 2 beneficiaries in a single V6 transaction (7c4c1dd5…)

248 orchestrator tests and 189 UI tests, CI on every push, open source (Apache-2.0/MIT).

You do not have to trust any of this: node scripts/verify-proof.mjs reads docs/PROOF.md and checks every txid against public explorers. The script reads the same document the proof page does, precisely so the two cannot drift apart.

Site: konclave.xyz · Code: GitHub - deegalabs/konclave: The vault that decides together. A local-first, browser-native collective Zcash treasury using FROST threshold signatures. Private on the outside, transparent on the inside. · GitHub

No cryptography was reimplemented. In the browser we run the Foundation’s own crates compiled to WebAssembly (frost-rerandomized and reddsa), plus librustzcash. On the local path we use frostd, frost-client and zcash-sign directly.

Honest limits

The chain does not prove where the signers were, for the same structural reason it does not prove the signature was a threshold one: an aggregated FROST signature is indistinguishable from a single-signer one. The cross-machine ceremony rests on the two operators and the ceremony trail, not on the block.

The hosted helper is view-only, not blind. It holds the vault’s UFVK in order to build and broadcast the transaction, so it sees the movements and the memos. It never receives, derives or stores a share, and the relay that carries the ceremony is a blind mailbox. But the distinction matters and we would rather say it.

Quorum policy and proposal expiry are application-enforced, not consensus-enforced, and we say so in the docs.

Some of the proof txids came from trusted-dealer vaults; the DKG-vault sends and the browser broadcasts came from keys born by real DKG.

The desktop app exists and is signed, but has not been validated on each platform. The macOS build is not notarized and Gatekeeper refuses it, so we have disabled the downloads on the site until that is fixed. The vault runs entirely in the browser meanwhile.

This is beta and under active hardening: the last few weeks have been as much about auditing what already existed as about building, and what we find becomes a public issue before it becomes a release. It is self-custodied software: each member’s key share stays on their own device, and we hold no one’s funds.

Who it’s for

Treasurers who shouldn’t be a single point of failure; cooperatives and community funds that decide together; and NGOs, journalists and activist orgs for whom a transparent multisig is a liability, because it doxes donors, salaries and structure to anyone watching the chain.

What’s next

We’re onboarding a few pilot groups now, and I intend to submit a proposal to ZCG in the next round: evolving Konclave from a proven prototype into a maintained product (cross-device ceremonies, hardening, and real-org onboarding). Before that, I’d rather listen: what would make this genuinely useful to this community? What are we missing?

Thanks for reading. This stands on the Zcash Foundation’s FROST tooling, and we learned a great deal from Zkool along the way.

Credit where it is due: the @zcashfoundation team, and @hanh for Zkool.

Daniel Gorgonha · DeegaLabs

10 Likes

I met Daniel in the south in Brazil while I was at ipecity (a pop-up city / a network state) in April, at an event where Josh also spoke virtually. It was a funny coincidence that he had previously won a hackathon with a project named “Shield,” so he already scored some points with me there lol.

But in all seriousness, he and his wife, both developers, have a great attitude and have remained interested in Zcash ever since. The merit is entirely his. I supported the project with product direction and testing, and I clearly have skin in the game, and I’ll be using the system for Zcash Brazil. Congratulations on the project.

1 Like

Thank you, @michael2xl. Coming from you, that means a great deal, and so does the mentorship behind it.

I opened this thread asking what would make Konclave genuinely useful to this community. The honest answer turned out to be what you did: put it in front of a real group and real money. The Zcash Brazil treasury now running as a 3-of-4 FROST vault on Ironwood, for payroll and proposals, is the first time Konclave has operated a real collective treasury instead of a demo, and that has shaped what I build and how I test it far more than any feature list could.

I want to name the partnership plainly. Having a first group that is patient with the rough edges and clear about what a treasurer actually needs is the reason it keeps getting better, and more robust under the hood. I will keep sharing here what changes and what is still open, honestly.

Grateful to be building on the Zcash Foundation’s FROST tooling, and grateful to have Zcash Brazil as the first hands on it. Thank you for the trust and the partnership.

2 Likes

Konclave update: 27 August to 7 September

Eleven days since this thread opened. 112 pull requests merged, four new mainnet transactions in the proof file, and v0.3.0 tagged. Most of the work went to one question: what does an outsider learn about a vault, and what can they do to it. Below is what changed, by area, with what is still open at the end.

Privacy: the vault id stopped being a key

A vault id used to be enough to read a vault’s books through the hosted helper. Every seated member now holds a per-vault random secret S, minted at the DKG and sealed to members over the encrypted channel, persisted sealed at rest like the share.

  • Private reads (balance, transactions, ceremonies, proposals, ledger, members) require HKDF(S, "read") in a header, never the URL. A leaked id answers 401.
  • The signing room is derived from S rather than the group key, so an id-only outsider can neither compute it nor observe it.
  • A vault export is one opaque blob: metadata, share, S and beneficiaries all under the passphrase. A leaked backup reveals nothing, not even the vault id.
  • The viewing key was still going to the browser in plaintext. It decrypts every payslip the vault has sent, and TLS covers the wire but not the endpoint, where an extension with host permissions or a TLS-terminating proxy reads it. It now goes out sealed to the vault’s registered devices, and the helper refuses to serve it unsealed.

The relay learned less, and can be lied to less

The coordination relay is a mailbox. It is now blind to what a vault pays and to whom: each device derives a persistent comms key from its share, registers it, and the helper hybrid-seals the signing request to the seated devices. The ceremony no longer re-broadcasts the transaction.

Seating in the signing room is authenticated, so an outsider can no longer take a seat or forge room messages, and a sign response that does not verify is rejected instead of killing the send. Both were proven against the live relay rather than in a test.

Governance writes are authenticated

A vote or a member rename from anyone who could reach the helper used to be accepted. A write is now signed by an Ed25519 key derived from the seat’s FROST share, and refused otherwise. It turns on per vault, at the first device that unlocks, so vaults created before it keep working.

Recovery: a backup that rebuilds the vault, not just the seat

The export omitted the viewing key, so a member restoring from their own encrypted backup held real spend authority over money they could not detect. It now carries the viewing key and the wallet birthday, without which a rebuilt wallet scans from the current height, finds nothing, and cannot be rescanned.

open-export.mjs opens a backup with Node’s own crypto: no Konclave, no packages, no network. It reports whether the backup is complete rather than printing what is in it.

Availability

On 27 August the helper served one request at a time, so a five minute send made the whole service indistinguishable from dead, health check included, and it took a vault down. Both the helper and the relay now run a worker pool with a bounded body read. The incident has a postmortem in the repo.

The app

  • A reload asks for the passphrase on the screen you are on, instead of sending you back to the vault list to pick the vault you were already inside.
  • A passkey can open the books, enrolled per device. Sending money still asks for the passphrase.
  • The passphrase can be rotated. It was the one credential with no way to change it.
  • PBKDF2 went from 210,000 iterations to 600,000. The old count is the OWASP figure for SHA-512 and we use SHA-256. Existing vaults keep opening at their own count, which travels with the ciphertext.
  • Text and control edges now meet WCAG AA in both themes, checked by a test that recomputes the ratios from the design tokens rather than trusting an audit’s arithmetic.

Proven on mainnet

The 12 transactions listed when this thread opened are 16, each verifiable with the checker in the repo. The four added in this period each prove something the earlier ones did not:

  • A send whose closing signature was made on an Android phone in a mobile browser, its share sealed on that phone, through the installed PWA.
  • A send that survived a live bogus-response injection: an outsider holding only the public vault id posted a structurally valid but cryptographically bogus signature response into a running ceremony, and the send completed.
  • A send with the relay blind to the payment, attested by the captured room trace rather than by the block, since the block shows an ordinary shielded transaction.
  • A send watched from outside throughout: every private read answered 401 and the public endpoint stayed byte identical before, during and after. A vault mid-payment was indistinguishable from an idle one.

Quorums proven: 2-of-2, 2-of-3, 3-of-4. The helper is still view-only rather than fully blind, which is the limitation named when this thread opened and it has not moved.

Still open

The member list and the remaining private reads still reach the browser in plaintext; sealing covers the viewing key only. A vault created before the read gate cannot fetch its own viewing key, so the export fix does not reach it and its backups still restore the seat and not the vault. Passkey enrolment says nothing when the authenticator has no PRF, because every failure in that path is silent by design. The desktop build has never been validated on real per-platform hardware.

Where I’d like help

  • Migrating an unprotected vault. The read gate lands per vault, and protecting an existing one means re-creating it and moving the funds with a signed send. There is no automatic migration and I do not think there should be, because an automatic one would have to mint the secret somewhere the members are not. If you have taken a threshold setup through a key rotation, I want to know what you did with the old one.
  • The passkey is per device on purpose, and I would like to be wrong. WebAuthn PRF guarantees nothing about its output surviving a passkey sync, and Apple’s own forums have it failing concretely: an iCloud-synced credential returning a different PRF result on the second device ( Security Verification and /822523), and Safari’s cross-device QR flow not supporting the extension at all (/774112). If anyone has measured PRF stability across a synced credential on current OS versions, that changes the design rather than the wording.

These updates will be weekly from here. If you try it, tell me what breaks. It runs on mainnet with real ZEC, and every claim above is a txid or a test you can run.

Repo: GitHub - deegalabs/konclave: The vault that decides together. A local-first, browser-native collective Zcash treasury using FROST threshold signatures. Private on the outside, transparent on the inside. · GitHub · App: https://www.konclave.xyz

2 Likes

In short: four claims in the 7 September update did not hold as written. Two were fixed the same day it went up, one is closed since, and one is still open and says why. Since then the larger change is that a device now checks the payment is the one the quorum approved, not just that it signs what it recomputed, which is proven on mainnet with change. Nineteen verifiable mainnet transactions, up from sixteen.

Everything below is the detail behind those sentences, including what is merged and not yet deployed, and what is still not claimed.

Corrections to the 7 September update, and what has landed since

Four claims in that post did not hold as written. All four are addressed below with their current status, and then what came after.

“Both the helper and the relay now run a worker pool with a bounded body read.”

True of the relay only. The helper read request bodies with no ceiling at all, so one POST could make it buffer without limit, on the service every vault’s balance, proposal and send goes through. The asymmetry had been filed on 23 August, in an issue whose own body reads “the relay and bridge already cap at 2 MiB; the helper does not”, and it was open while the release notes said otherwise.

Fixed in v0.4.0. The ceiling lives in one crate both servers read, and a test refuses a body read that has none. A unit test could not have caught it: the cap worked and its tests passed the whole time. The rule was never “does it work”, it was “does every server call it”.

“The export now carries the viewing key and the wallet birthday.”

True of the export taken from Settings. The backup handed to you the moment a vault is created carried neither, and that is the copy most members keep. It also recorded a blank address, permanently, for every vault made on the web.

The export itself is fixed in v0.4.0. The address backfill is NOT in that release: it merged forty minutes after the tag was cut, so it reached the web app, which ships continuously, and no desktop release carries it yet. That distinction matters more than it looks, and it comes up again below.

And when the copy still comes out incomplete, and it can, what says so is the command-line recovery tool, not a screen. An earlier draft of this correction said the screen reports it. No screen does.

“A leaked id answers 401.”

True per vault, and the post did not say per vault.

Re-measured on 15 September against the live coordinator, GET only: of the nine vaults addressable there, four are behind the gate, four are not, and one is a stray testnet vault whose wallet is broken. The four that are not answer a request holding only their id. Every one of them is empty, and all of them are test vaults, mine and one collaborator’s, who knows. No third party’s vault is in that set. That does not make the claim in the post true. It is the reason the answer is a flow to build rather than an incident to run.

Still not fixed, and the reason is worth stating rather than hiding: it cannot be automatic. Making an open vault protected means re-creating it and moving the funds with a signed send, and an automatic version would have to mint the vault’s secret somewhere its members are not. New vaults are protected from creation, including one created on 15 September.

“An outsider can no longer take a seat or forge room messages.”

The first half was true. The second was not, and this is the correction that changed most since.

The signing room is readable and writable by design, and the ceremony’s own messages were not authenticated. That left two ways in. An unproven rejoin could take a seat whose device was offline and then contribute something no share had produced, which made the signature fail to verify and the payment not go out. And the second round of a signature, where one member assembles a package and hands it to the others, was accepted from anyone: each device checked that the package matched the transaction it had approved and that its own seat was listed, and never checked who sent it.

That second one is worse than it sounds. Anyone able to write to the room could assemble a package over the commitments already sitting there, and every device would sign it, spend the single use value it may only use once, and have nothing left when the real member’s package arrived.

Both are closed now. A device refuses a package from a member who does not hold the seat that opens the round. And when the set the coordinator would otherwise pick still needs a seat nobody vouched for, it defers once and chooses after the rest of the round’s contributions have landed, preferring seats that proved they hold their share. It does not always wait, and saying it did would be the same kind of overstatement this post exists to correct.

The part that took longest on the second fix was not the check. It was working out that “the wrong seat” and “a seat I have not learned yet” are different questions. A device learns its peers’ seats from their own announcements, and an announcement can legitimately arrive after the package does. Refusing both cases would have traded a stall for payments that stop silently, which is worse.

What is still true of that sentence, stated plainly so it is not claimed again. The round’s messages are still not themselves signed. Round one and round two contributions carry no signature, and an unproven rejoin can still take a genuinely empty seat, which is deliberate so that a vault whose members run an older build keeps working. What changed is that neither of those is a way to reach the outcome any more: an unvouched seat is not preferred when a real one is on its way, and a package from a tag that does not hold the coordinator’s seat is refused.

Also corrected from that post, and fixed the same day it went up

Creating a proposal and firing a send were unauthenticated. A vault id was enough to fill a vault’s desk with proposals its members then had to read and refuse, and to trigger the broadcast of a payment that had already reached quorum. The funds go where the members decided either way, but the deliberate confirm a send is supposed to require could be fired from outside the room. Both are signed writes now, by the key derived from the seat’s share.

That sentence has the same shape as the one corrected above, so it gets the same treatment: it is true PER VAULT. The gate turns on for a vault when the first device on it registers a write key, which is what lets existing vaults keep working instead of stopping on a flag day. Probed on 8 September: of the three vaults then behind the read gate, one refused an unsigned proposal and two still accepted it. That fraction has not been re-measured for this post, because measuring it means attempting a write against a live vault.


What has landed since

A device now checks that the payment is the one you approved. This is the larger of the two, and it closes a gap that had been open since the beginning.

Since August every device has recomputed the transaction’s own signature hash from the copy of the transaction it holds, and refused to sign if that disagreed with what it was asked to sign. That stops the transaction being swapped underneath a signer, and it holds. What it never did was say anything about whether that transaction is the one the quorum approved. A coordinator assembling a perfectly valid transaction to its own address produces a request that passes every check, and the only thing standing between that and a signature was a human reading a preview and noticing an address they did not recognise.

Each device now compares what the transaction actually pays against what it approved, before it contributes anything.

The hard part is not the comparison, it is change. A payment normally sends money to two places: the recipient, and the vault’s own change. Change goes to an internal address that is not the one the vault publishes for receiving, so a naive check reads it as an unapproved payment and refuses every legitimate send. Getting it wrong in that direction is worse than the hole, because payments stop silently.

Proven on mainnet, deliberately with change: a 2-of-2 vault holding 0.0003 ZEC sent 0.0001, which after the network fee leaves change. Both devices signed, first attempt. Sending the maximum instead would have produced a transaction with no change at all and passed without testing the thing that matters. The chain cannot show the check, and the proof entry says so: what attests it is that the change went to the vault’s own internal receiver, a different address from its published one, and the device recognised it as its own instead of refusing.

On a transaction spending from two internal pools, the browser signed half of it. A vault mid-migration can hold funds in two pools. The part of the app that decides which pieces of such a payment need this device’s signature was answering that question itself instead of asking the one place that knows, and its answer was to take one pool and ignore the rest. The ignored parts were never signed and nothing was raised; the payment failed later, reporting something else.

There is one answer to that question now and every part of the app asks it, and the balance stops offering the sum of both pools as though one payment could spend it. Both of those are live: they are browser code, and the web app ships continuously.

A third piece is merged and NOT live, and this post would rather say so than let it be assumed. The refusal that arrives while the payment is still being composed, instead of after the quorum has approved it, lives in the hosted coordinator, whose binaries were last rebuilt on 24 August. It is in the repository and it is not in production. There is a real difference between those and it is the same difference the first correction above was about.

Vaults holding funds in a single pool, which is every vault in use today, are unaffected by any of this.

The payment screen announced one network fee and refused you with another. It printed an estimate beside the amount and then blocked the proposal using a figure half again as large, so a vault holding exactly the announced fee was told it could not afford a payment that the arithmetic on the same screen said it could. Found by a member hitting it on a real vault, which is the only reason it was found: every test in that area checked the rule, and the rule was right. Nothing compared the rule to the sentence printed next to it.

Two screens told a member something false, and both were found on live vaults by their owners rather than by a test. They are not cosmetic, which is why they are here rather than in the list below.

A vote could go out UNSIGNED and the member was told the server was at fault. Signing a governance write calls into the cryptography module, and on a proposal screen nothing else had loaded it. The call threw, a catch written so that signing could never block a vote swallowed the throw, the vote went out without a signature, and the coordinator refused it with “this vault requires a signed vote”. True, and useless: it names a rule on the server while the cause is an uninitialised module in the browser, and the member had just typed their passphrase. That swallowed catch is the same shape as the one further up this post, and it is worth saying that they were found in the same week by looking for it.

And every refusal to record a vote was reported as “the proposal already changed state, or there is a conflicting vote”. A sentence about consensus, shown for a 401 from a device that simply could not prove it holds its seat. Found while its owner was approving a payment. There was no conflict. On a money screen a wrong reason is worse than no reason, because it sends someone looking for something that does not exist.

Smaller things a member would notice. Every page load printed a deprecation warning to everyone’s console, next to the diagnostics that mean something. Code and WASM files are served under names containing a hash of their contents, so the browser is now told they never change. A vault created before 0.4.0 kept reporting a blank address however many times it was exported. The backup checker did not mention the per-vault secret that lets a restore read the vault, so a backup restoring a device able to sign but not to see anything passed as complete; the same tool could not open a version-1 backup at all, and answered a missing file with a Node stack trace, which is the last thing someone wants when they are running it because their laptop is dead. Two panels broke their own sentences across four lines, and the ceremony drawer called the other signers “member 1” and “member 2” whenever a roster read failed, while the device held the names all along.


Proof

The post above said sixteen mainnet transactions, which was right when it was written. Three have been added since: a send where every governance write was authenticated, a 2-of-3 vault signing with one seat deliberately absent after the seat-poisoning fix landed, and the money gate send described above. Nineteen now.

That middle row was rewritten this week, and how is worth a sentence. It asserted the quorum shape and the absent seat as though the chain showed them. The chain shows neither. The claim turned out to be true, and the operator confirmed it, but true and attested are different things and a proof file trades on the second. The row now splits them: the 2-of-3 shape IS publicly checkable, because the coordinator reports a vault’s threshold and member count to anyone holding its id, with no credential. That a seat was left absent rests on the operator’s word and says so.

node scripts/verify-proof.mjs

It talks only to public block explorers, has no dependencies and no knowledge of Konclave’s internals. It got two changes this week that are about honesty rather than features. It used to return the same failure code for “this transaction is not on the chain” and “an explorer did not answer”, so a flaky network read as a failed proof; those are different answers now. And it prints the count, which it never did, so the number nobody had to go looking for is the number you quote. It also compares that count against what the README and the project docs claim, and warns when they disagree, because they did: the README said eight while the file listed seventeen.

Its stated limits are real and unchanged. On-chain data proves a transaction exists and is mined and, being shielded, reveals nothing about amounts or parties. It does NOT prove the threshold nature of the signature: a FROST-aggregated Orchard signature is meant to be indistinguishable from an ordinary single-signer one, and that indistinguishability is the privacy property. The threshold nature is attested by the build and the ceremony, not by the chain.


Still open

  • The vaults created before the read gate stay readable by their id until they are re-created. Four of the nine on the coordinator, all empty, all test vaults.
  • The ceremony’s round messages are not signed. The two ways that was reachable are closed; the property itself is not claimed.
  • A payment that genuinely needs funds from both internal pools is refused, not supported.
  • Live per-platform hardware validation of the desktop shell has still not happened.
  • The engine binaries the hosted coordinator runs were last rebuilt on 24 August, so a fix merged now in that layer does not reach production until they are rebuilt. That is tracked now rather than assumed, and this week’s fixes were deliberately written to land in layers that do ship.

On how the corrections above were found. Not from a bug report. From pointing audits at the project’s own notes rather than at the code, after statements from those notes reached this thread as fact. The notes said write endpoints were authenticated when two of four were. They said staging was half built when both halves had been answering for two days. They said fifteen verifiable transactions while the file listed sixteen and the checker checked sixteen.

Doing it again this week found three more, which is the part worth reporting. A line saying the coordinator authenticated two kinds of write was true for TWENTY-TWO MINUTES: the correction landed at 18:11 and the fix that authenticated the rest landed at 18:33, so for nine days the notes understated the project’s own security. A comment in the signing code described a live control as groundwork that does not fire yet, which stopped being true when the control started firing. And the proof file asserted a detail about one transaction that nothing attested; it happened to be correct, and the operator confirmed it, but a proof file trades on attestation rather than on being right.

Two of those are now held by tests rather than by attention, because a note that COUNTS something goes stale the moment the thing is counted again and nobody notices: prose has no compiler. The suite now reads the list of authenticated writes, and the list of reads behind the access gate, out of both the notes and the source, and fails when they differ.

Check it yourself

A shared treasury does not leak through the chain. It leaks through
coordination: a link forwarded, an id in a screenshot. That part is ours to
prove, not Zcash’s.

So we stopped claiming it. A vault moving money is now indistinguishable
from an idle one to anyone holding its link, and that is checkable rather
than asserted: scripts/probe-opacity.mjs in the repo runs against a vault
holding nothing but its id, the same position as whoever found the
forwarded link. Every private read must answer 401, and the one
deliberately open route must return the same bytes every time. It exits
non-zero if either fails, including against an unprotected vault, which is
the point.

On a live 2-of-3 mid-payment: every private read refused, and the open
route byte-identical before, during and after the block. 3fa08dce…, block
3,473,869, a V6 transaction with nothing transparent.

What an id does buy is the vault’s shape: quorum, member count, receive
address. Not its contents. No balance, no ledger, no names, no viewing key.

Nineteen mainnet transactions now back this thread, and verify-proof.mjs
checks them.

Konclave builds on the Zcash Foundation’s FROST work and on librustzcash.

1 Like

Security update: three fixes, live today (2026-09-29)

We closed three faults today. The first is the most serious we have recorded: a device could be
made to help sign a payment its owner never approved. The other two could not move funds. We
found all three ourselves, and we have no sign that any of them was used.

What to do

  • Web app: reload it, and accept the update if it offers one. The footer should show
    v0.7.0 or later.

  • Desktop app: update to v0.7.0: Release Konclave v0.7.0 · deegalabs/konclave · GitHub.
    Until you do, sign from the web app.

  • Nothing else. The fixes to the coordinator are already live for every vault.

1. A device could be made to sign a payment other than the one on its screen

Before it signs, each device works out the transaction from its own copy and refuses a request
for a different one. In the second step of a signature it receives a package from the member
whose device opens the round. It checked the label on that package, not the contents, and the
signature is made over the contents. So whoever held that seat, or took it while it stood empty,
could label a package with the approved payment and fill it with another, and an honest device
would add its part while its screen showed the approved one. Turning that into a payment still
took a complete transaction spending the vault’s funds, which a member can build.

Open since the first signature made in a browser. A fix on 2026-08-27 closed the label and left
the contents. Found on 2026-09-29 by a review of our own claims, not by a report. The step that
signs now refuses a package made over anything but the payment the device worked out, and there
is no way to ask for a signature without it (pull request 579).

2. A member could approve a payment in another member’s name

The coordinator checked which seat signed a vote, then recorded the vote under a name taken from
the request. On a vault that needs two approvals, one person could make a payment show as
approved by both, and a payment or payroll could be recorded as proposed by someone else. No
money could move this way: each device still asks its owner to confirm the specific payment. It
mattered only on vaults with signed actions turned on, which on 2026-09-28 were two, both our own
test vaults. Open since 2026-09-06. Three other routes to the same result are known and open
(issues 575, 576 and 577), and in none of them can funds move.

3. The coordinator had no limit on requests or on new vaults

Anyone could ask it for things as fast as they liked, and register new vaults without a ceiling.
It now allows each client address 300 requests every 10 seconds and 5 new vaults an hour. Nothing
could be read and no money could move this way: what was exposed was the service itself.

Details, with what was exposed and for how long:
konclave/ui/CHANGELOG.md at main · deegalabs/konclave · GitHub and
konclave/helper-server/CHANGELOG.md at main · deegalabs/konclave · GitHub

One more fix from the same review (2026-09-30)

Continuing the review of our own claims, we found and fixed a smaller fault today.

Before it signs, each device checks the transaction’s change against the vault’s own change
address, which the device records once and then refuses to change. That is what stops a
compromised coordinator from labelling a stranger’s output as change. Changing your passphrase
erased that record, and the next screen that opened the vault recorded the coordinator’s answer
at that moment instead. So a coordinator compromised at exactly that moment, on as many devices as
the vault needs to sign, could have had a device accept a payment that sends the leftover
elsewhere. It only affected devices where the passphrase was changed after 2026-09-15. We have no
sign that it was used.

Changing the passphrase now replaces only the encryption and keeps everything else the device
recorded (pull request 581).

What to do

Two smaller fixes shipped with it: a password the app generates is now always one its meter rates
strong, and the offline backup checker (scripts/open-export.mjs) no longer reports a correct
backup as incomplete.

1 Like

Before we file our grant application, two things I said earlier in this thread need correcting.

My first post said the twelve transactions listed then were all FROST ceremonies with the key never reconstituted. Six of what are now nineteen came from vaults a trusted dealer split in July, as docs/PROOF.md says.

It also said the desktop app is signed. Its installers carry no code signature yet (issue 606).

Hi @conradoplg. I’m Daniel Gorgonha, and I write the code of Konclave, the group vault this thread is about. We haven’t talked before, so let me start with a thank you: Konclave is built on the Foundation’s FROST work, and the design we are writing follows the direction you described for DKG in ZIP 312.

We are preparing a ZCG application, and there are two points where your view would help us get that design right, whenever you have time.

Creating the viewing key on a device. Today our coordinator creates each vault’s viewing key: it runs the Foundation’s signing tool with the vault’s group key, so nk and rivk come from a random sk, through the constructor you suggested keeping for now (frost tools issue 591). The coordinator then keeps the viewing key. We would like one member’s device to create sk instead and seal it to the other members, so the coordinator never holds the viewing key. Spending stays with the group key from the DKG either way. Is there anything you would do differently in moving that step onto a device?

Quantum recoverability. We read the “Usage with FROST” section of ZIP 2005. Every participant keeps qsk, and the ZIP notes that a quantum adversary may be able to steal the funds with qsk alone. Our reading is that, for a threshold vault, recovery becomes one of n. Our vaults have no quantum recovery today, since orchard main has no qsk derivation yet. Is that the Foundation’s reading too, or is the key generation work for ZIP 312 (zips issue 1051) heading somewhere that keeps the threshold?

No rush, and if another place suits you better, we are happy to continue there. Thank you.

How is this intended to be deployed in terms of network exposure?

Hi @hanh, thanks for the question. If I read it right, you are asking what is reachable from the internet and who gets to see what, so here is how it works today.

For now we host everything, so a group does not need to run any server. A member’s browser only makes outbound HTTPS requests: to our web host, a relay and a coordinator, plus a price lookup if the member turns it on. Members never connect to each other, and nothing on their side accepts connections from the network. The code of both servers is open, though running your own is not a documented path yet.

The relay is a mailbox the devices use for the DKG and signing rounds. It keeps messages in memory only and limits their size and rate, per sender and per IP. Signing requests reach it sealed to the members’ devices, so it cannot see who is paid or how much. It does see IP addresses, timing and some metadata, such as who gets ready to sign.

The coordinator is where most of the exposure is. It scans, builds and proves for every vault, so it keeps each vault’s viewing key, along with proposals and member names. Its API is public, a vault’s private reads need a key its members hold, and it uses a public lightwalletd (zec.rocks) to sync and broadcast. It holds no share and cannot move funds on its own. If it were compromised, though, every vault’s history could be read, and until approvals are bound to each proposal’s content (issue 567), it could change where an approved payment goes.

Our current thinking: rather than relying only on making that server harder to break into, we would like it to end up holding nothing worth taking. That means scanning and proving on the members’ devices, so the viewing key can leave it, and then sealing names and proposals. One trade off we already see is that each member’s IP would then reach a light client server, instead of only ours.

That is our reasoning so far. Since Zkool runs FROST with no coordination server at all, I would really value your view: does this way of thinking make sense to you, or would you approach the exposure differently? If there is something we are not seeing, we would much rather hear it now.

@hanh, three things I should have said in my reply above.

The key that gates a vault’s private reads exists only for vaults created since 30 August, which today are all the vaults on our coordinator.

For privacy, the coordinator is the larger exposure. For the funds, it is the web host: every member loads the app from it, so a compromised host could serve all of them a modified app at once, and the threshold would not stop that. We will publish a hash of every file in each release, with a script anyone can run against what the site serves (issue 605). That makes a modified app detectable, not impossible, and a version served to one member only would be harder to catch.

On running your own: I wrote that it is not a documented path, but our docs do describe a coordinator of your choosing in the desktop app. Its security policy refuses any server but ours, so that choice most likely fails today (issue 612). In practice, every vault created in the web app uses our servers.

Yes, avoiding a central server in charge of the ceremony was a big reason behind the design of zkool/frost.
My concerns are:

  • the coordinator can be offline and lock the funds. They could be recovered by it would require some extra work,
  • the coordinator knows the txs of the participants, correct? A malicious one could use this to their advantage.
  • there are others, but these are the main ones.
1 Like

Thanks, both concerns are fair.

Yes, our coordinator can lock the funds by going offline: it builds, proves and broadcasts every payment. The funds stay recoverable as long as one member has a backup, which carries their share, the vault’s viewing key and the height to scan from; without any backup, the viewing key exists only on our side. Even with backups, there is no tool yet to rebuild the vault and spend without us (open work in our recovery doc). The risk grows with every vault that depends on it, and our hosted service has already failed more than once, the worst time for about seven hours on 26 August.

And yes, it knows the vault’s transactions: it holds the viewing key and builds each payment, so it sees amounts, recipients and memos, though not the members’ own wallets. Until our open issues are fixed, a malicious one could also change an approved payment, burn funds as fee or alter a memo (issue 567, issue 610).

Moving scanning, building and proving to the devices takes the viewing key off the coordinator and spreads the load. It does not remove the outage risk, since proposals, votes and the signing relay would still depend on us, and we do not have an answer for that part yet. Today a group trusts us to stay up, to keep what we read to ourselves and to build only what was approved; it never trusts us with a share of its spending key.

You mentioned there are others. Whenever you have time, we would like to hear them too.

The fact that there are malleability, denial of service, privacy, and spoofing attacks are worrying. Also, the keys are in the browser and cannot be exported. It’s not a safe place to store important data.

Generally speaking, my concern is that this is proposed as a high security solution for managing shared treasury, which can be a lot of money. But I know it is a work in progress.

Thank you, that is a fair reading, and I agree with the main point.

We do not offer it as ready for a lot of money yet. Our security notes say so plainly: “Do not custody significant funds with it yet” (security notes). The attacks you list are the ones we disclosed above, and each has an open issue we work through in public.

One correction: the keys can be exported. Each member can save an encrypted backup of their share, and a short script in our repository opens it with nothing but Node, without Konclave. What does not exist yet is importing that backup into other FROST tools (issue 214).

On the browser as a home for the share, you are right: a browser can clear site data, and iOS Safari often will not promise to keep it. The app asks for persistent storage and warns when it is refused, and the plan is a native app that keeps the share in the operating system’s keychain.

Thank you for reading this so closely.

v0.10.0 is live on the web: the device now checks the payment it signs

This answers part of what @hanh raised and what I wrote back in the posts above, and it corrects one sentence of mine.

The correction first. In the 29 September post I wrote that no money could move through the vote fault because each device still asks its owner to confirm the specific payment. That confirmation was the signing screen, and the screen showed what the coordinator reported about the transaction. The device did not check it against what it was about to sign (issue 610). So the protection that sentence leaned on was weaker than I said. We have no report of a payment that went wrong.

What v0.10.0 changes. Before a device adds its part of a signature, it now reads each payment, the address it names, its memo and the network fee from the parts of the transaction its signature covers. It refuses if an address, an amount or a memo differs from the approved payment as the coordinator serves it, or if the fee is above the network’s standard fee for that size. The signing screen lists every payment, the change returning to the vault and the fee, as the device read them. Before, it showed only the first recipient, so a payroll showed one beneficiary of several.

The order matters: you approve the proposal, then at signing the device reads the transaction again and decides on its own. It does not stop to show you that reading first.

In my 2 October reply to @hanh I said a malicious coordinator could change an approved payment, burn funds as fee or alter a memo. Here is what is left of that list. A changed address, amount or memo is now refused by the device. A fee above the standard fee is refused, but the coordinator still chooses the size of the transaction, and each extra action can add 0.00005 ZEC, paid to the network. What stays open is that the approved payment the device compares against is still fetched from the coordinator, and a vote is tied to the proposal, not to its exact content (issue 567). The coordinator holds no share and cannot move funds on its own, but until issue 567 is fixed, a payment the quorum signs rests on the coordinator not changing a proposal after it was approved. This does not change what I wrote on 2 October: do not custody significant funds with it yet.

Also fixed: some of the coordinator’s checks had defects. Of the routes I listed on 29 September (issues 575, 576 and 577), 576 is fixed and 575 is narrowed, not closed; 577 is still open. The details of what was fixed follow after the next release. This does not mean the coordinator hardening is finished. The same post said funds cannot move through any of those routes. I no longer rely on that either, for the same reason.

Two more limits, both listed under Known limits in ui/CHANGELOG.md on GitHub:

  • The device learned the vault’s own receivers from the coordinator once, and keeps the first answer. A coordinator that was already compromised at that moment could have named an address of its own.
  • The /net page signs without this check. Sign from the vault’s desk.

What to do. Web app: if it says a new version is available, tap Update. If it does not, close every Konclave tab and open the app again, because reloading one tab keeps the version you had. The version line at the bottom of Settings should read v0.10.0. Desktop app: the published version is still v0.8.0, so until the 0.10.0 installers are out, sign from the web app.

If a device refuses, it says why in plain words and will not sign that payment. If the transaction differs from what was approved, do not sign it, and tell the other members. If it only says it could not load what was approved, reload the page first and look again.

What is live: the web app (www.konclave.xyz) runs v0.10.0, released at commit 10ccbb3 and since updated only with documentation fixes, and the coordinator runs b62a7f0, as of 4 October.

On-chain example. We signed a one-line payroll from a 2-of-3 vault on the production path: 1c53fe50c3179754b622b3a96efb43bd3ab0073b38c32bbccbec6d752c361ff4, block 3,506,220. The signing panel showed the output and the fee. The chain shows an ordinary shielded transaction and cannot show the check; you can confirm it exists at that block. It is now in our proof table, which lists twenty, and scripts/verify-proof.mjs checks it.

Konclave builds on the Zcash Foundation’s FROST tools and on librustzcash.

1 Like