Skip to content

Build real ring-VRF proofs behind a ringVrfProofs option - #45

Open
BigTava wants to merge 1 commit into
mainfrom
feat/real-ring-proofs
Open

BigTava wants to merge 1 commit into
mainfrom
feat/real-ring-proofs

Conversation

@BigTava

@BigTava BigTava commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Summary

  • createRingVRFProof can now answer with genuine bandersnatch ring-VRF proofs instead of the sr25519 stand-in, so product flows that submit personhood proofs (registry publishing, alias binding) can be tested end to end.
  • The new ringVrfProofs option makes the server derive the account's RFC-0022 member entropy from its mnemonic, read the ring from the configured People chain, prove with verifiablejs, and validate against the live ring root before answering.
  • The proof context defaults to the product-scoped hash a real host binds, with a raw 32-byte override for contracts that pin a global context, which real hosts deliberately never issue.
  • The derivation is vendored in src/ring-vrf-keys.ts and pinned to host-rust-core's iOS vectors; a MNEMONIC-gated live test builds chain-valid proofs against previewnet in both context forms.

@BigTava
BigTava requested a review from mordamax as a code owner August 24, 2026 13:14
@socket-security

socket-security Bot commented Aug 24, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedverifiablejs@​1.5.076100909570
Addedpolkadot-api@​2.2.2881009498100

View full report

@BigTava
BigTava force-pushed the feat/real-ring-proofs branch from bf9a2f0 to 24dd943 Compare August 24, 2026 13:16
…lows that submit personhood proofs can be tested end to end
@BigTava
BigTava force-pushed the feat/real-ring-proofs branch from 24dd943 to efcfcd1 Compare August 24, 2026 13:43

@mordamax mordamax left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The derivation is right. I reimplemented it independently against @noble/hashes and got c47086f94a7f4c05b7afd9f2339d3fea168f3823b5424ba1f7b31043d8ef60af for index 0 and 8d7f5e1510a7e8d813887e100f5a260ec9de60e68695477b93360ee7e3d16a9f for index 1, both matching the Rust vectors. Nothing in this repo pinned that agreement before, so ring-vrf-keys.ts is worth having either way.

The included slice and the getAlias mismatch are the two I'd want fixed before merge. The rest is smaller, take or leave:

  • ringLocation is dropped, so a people-lite request silently gets a people-full proof and RingNotFound is unreachable
  • errors all become Unknown when the 0.9.1 response type has NotMember and RingNotFound
  • productId isn't normalized before hashing (hosts do trim + NFC + lowercase)
  • the reads aren't pinned to one block and span two clients, so the ring can shift underneath
  • two fresh WS clients plus metadata per proof is slow; could keep them for the server's lifetime
  • verifiablejs could be pinned (one_shot changed arity and the proof encoding lost its compact prefix inside 1.x) and made optional, given it's a 7.5 MB wasm as a hard dep
  • the mnemonic goes over HTTP on every call and the first 12 chars land in the error string; Playwright captures request bodies into trace.zip when tracing is on

Also, the description mentions a MNEMONIC-gated live test against previewnet but I don't see it in the diff, just the derivation vector. Did it get dropped?

Comment thread src/ring-proof.ts
}
const latest = ringRoots[ringRoots.length - 1];

const members = encodeMemberKeys(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Two things here.

The member list isn't sliced to RingKeysStatus.included. host-rust-core reads the status, checks status.included > ring_position before picking a candidate, and truncates:

https://github.com/paritytech/host-rust-core/blob/90668bd6/rust/crates/truapi-server/src/runtime/signing_host/ring_vrf.rs#L124-L131

https://github.com/paritytech/host-rust-core/blob/90668bd6/rust/crates/truapi-server/src/runtime/signing_host/ring_vrf.rs#L190

The reason is spelled out at the top of the statement-allowance path — "the proof is built against the same members the runtime verifies against: the baked-in included prefix of the current ring":

https://github.com/paritytech/host-rust-core/blob/90668bd6/rust/crates/truapi-server/src/runtime/statement_allowance/ring.rs#L1-L6

Any ring with keys appended but not yet included, which is most of the time on a live chain, gives a member set that doesn't hash to the stored root, so validate_with_commitment throws.

Second, line 118: the exponent comes from the PeopleRingExponent constant, but on chain it's per collection — Members.Collections[collection].ring_size, or MembersSubscriber.RingCollectionExponents on the subscriber side. People and people-lite have their own sizes.

container.handleAccountCreateProof((params, { ok }) => {
const [[productId, suffix], _ringLocation, message] = params;

if (config.ringVrfProofs) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

handleAccountGetAlias just above still returns the placeholder blake2(publicKey ++ "context") / blake2(publicKey ++ "alias"), so with ringVrfProofs on the two calls give different answers for the same account — different context and different alias.

Real hosts share context_bytes and the same entropy between account_alias and create_proof, so they agree by construction:

https://github.com/paritytech/host-rust-core/blob/90668bd6/rust/crates/truapi-server/src/runtime/signing_host/ring_vrf.rs#L215-L223

verifiablejs exports alias_in_context(entropy, context), so getAlias could go through the same server path.

Comment thread src/ring-vrf-keys.ts
* Entropy behind the bandersnatch member key the People chain knows a full
* person by, at `//peopl.{tld}//index_bytes(0)`.
*/
export function fullPersonRingVrfEntropy(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think tld is in the wrong slot. The per-network suffix is real — .test on previewnet, .paseo on pnv2 — but it appears in the allowance context, not the key domain:

https://github.com/paritytech/triangle-e2e/blob/1b67ba6/packages/chain-tests/src/lib/individuality-compat.ts#L20-L26

The key domain is peopl.dot on every network:

https://github.com/paritytech/host-rust-core/blob/90668bd6/rust/crates/truapi-server/src/host_logic/product_account.rs#L24

https://github.com/paritytech/polkadot-app-ios-v2/blob/6149722cd/Packages/KeyDerivation/Sources/BuiltInProduct.swift#L16-L17

https://github.com/paritytech/signing-bot/blob/2ecf634/src/core/__tests__/ring-vrf-registry.test.ts#L4

With tld: 'test' you get 02114f37bddc9d5cb0f7bb978710e0e22eba94362cbe3a0a0b9ce991a858f052 instead of c47086f9…, so the README example on line 296 derives someone who doesn't exist.

Comment thread CHANGELOG.md

### Added

- **Real ring-VRF proofs via the `ringVrfProofs` option.** `createRingVRFProof` used to answer with a deterministic stand-in, an sr25519 signature over the message, which no verifier accepts. Product flows that submit a proof, such as registry publishing or alias binding, could only ever be tested up to their failure path. With `ringVrfProofs` configured, the test-host server builds a genuine bandersnatch ring-VRF proof instead. It derives the RFC-0022 personhood member entropy from the account mnemonic at `//peopl.{tld}//index_bytes(0)` off the keyed ring-vrf tree, reads the ring the member sits in from the configured People chain, fetches the ring root from the subscriber chain, proves with `verifiablejs`, and validates against the live ring root before answering. When personhood is minted for that member on the target network, the proof passes network verification.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This doesn't say you need an account that's already a full person on the target network. It's in there as "when personhood is minted for that member", which reads like a bonus rather than the entry condition.

Can we say it plainly — the option is optional, but once you turn it on the identity isn't, and without one every call fails? Plus a pointer to how you get one. Right now someone with a fresh mnemonic has nowhere to go, and the README example (uri: process.env.MNEMONIC) suggests any mnemonic works.

Worth stating the failure mode too: CreateProofErr.Unknown with "Member key … is not in a ring".

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