Hash with the cipher suite polkadot signs under - #9
Merged
Conversation
seunlanlege
reviewed
Aug 11, 2026
seunlanlege
reviewed
Aug 15, 2026
seunlanlege
approved these changes
Aug 16, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
ApkProof.hashToG1hashed with the proof of possession suite, so a signature from anyone using the basic scheme lands on a different curve point and the pairing returns false with nothing to explain why. Polkadot signs with the basic scheme, which is what this library exists to verify, so that is what it hashes with now. The two prefixes differ only in the last four bytes of the 43 byte string, so the algorithm was never in question, only which constant reaches the hash.This is a behaviour change for a PoP deployment, which would need to pin the previous version.
w3f_blsexposes both,Message::newfor basic andMessage::new_assuming_popfor PoP, so the distinction is easy to reintroduce behind a parameter if a second suite is ever genuinely needed.The suite is pinned by a test in both directions rather than by a comment. The contract's output has to match what
w3f/blsproduces underMessage::new, and it has to differ from the same message hashed under PoP, so a build where the suite never reached the hash cannot pass.Two smaller things picked up along the way.
rust/ffi/build.rslinks CoreFoundation and Security on macOS, without which the cgo archive fails on undefined_CFArrayCreateMutableand_SecTrustEvaluate. Andrust/prover/examples/prove_from_json.rsreads circuit inputs from a json file and writes the proof back out, which is how we drive the prover from a rust project that cannot take a cgo dependency of its own.Building without std
The verifier is pure arkworks with no cgo, so nothing in it actually needs an operating system, but it was never declared
no_stdand so could not be used from a substrate runtime. There is now astdfeature, on by default, forwarded to the arkworks crates, sha2, sha3 and thiserror.One thing genuinely needed replacing.
round_keyscached the Poseidon2 round keys in aOnceLock, and it is called from insidepermutation, so roughly twelve thousand times for a full commitment. Deriving them per call would have been ruinous, so it now usesonce_cell::race::OnceBox, which is the no_std equivalent and keeps the caching. The rest was importingVec,String,format!andvec!from alloc.Checked with
cargo check --no-default-features, awasm32-unknown-unknownbuild, and the existing tests still passing under std. Measured inside wasm on a proof from a live chain, parsing the verifying key and proof takes 6.2ms and the verification itself 12.7ms, which is what makes verifying one of these in a runtime worth doing at all.Proving more than once
Compiling the circuit and generating its keys takes about four minutes, and
prove_from_jsonpays that on every run, so a caller proving repeatedly spends most of its time on setup it already did.rust/prover/examples/prove_serve.rsdoes the setup once, prints{"ready":true}, then answers one json request per line on stdin with one json response per line on stdout. Same inputs and same output asprove_from_json, so it is only the lifetime that differs. Against a live chain a proof came back in under two minutes rather than seven.Testing
cargo test -p gnark-plonk-verifier --test bls_verify, passing.cargo test -p gnark-plonk-verifier --test bls_verify test_full_verify -- --ignored, passing at 550,876 gas, generating a real proof and verifying a real aggregate signature end to end. That is within noise of the 550,864 the PoP path cost before, which is what you would expect when only eleven bytes of the preimage change.Also exercised outside this repo against real signatures and validator keys from a running chain that signs with the basic scheme, end to end through
verify(), and throughprove_servefor several consecutive proofs on one process.