Common MPC Pitfalls

Bugs

A searchable list of real-world MPC bugs, mapped to the pitfall taxonomy.

27 documented bugs
27 shown

Category Distribution

    BugPitfallCategoryPrimitivesReferences

    near/mpc

    The NEAR MPC node exposes a threshold key to three different methods on the contract: sign() for arbitrary user-supplied payloads, verify_foreign_transaction() for foreign-chain (Bitcoin, Ethereum) transaction attestation used by bridges, and request_app_private_key() for confidential key derivation (CKD). All three call paths can route to the same set of distributed keys. Before the fix, the contract enforced only that the curve matched the call: any Secp256k1 key could back either sign() or verify_foreign_transaction(). A caller could therefore submit a foreign-chain transaction payload to the generic sign() method, collect a threshold signature, and then replay it to a bridge calling verify_foreign_transaction() against the same key; the bridge would accept the signature as proof that the foreign transaction had been attested.

    Missing Domain Separator Across Signing Contexts
    signature

    sherlock-audit/2025-06-symbiotic-relay

    In Symbiotic Relay’s middleware SDK, KeyBlsBn254.wrap() validates an incoming BN254 G1 point as a validator BLS public key. It rejects the zero point, checks that both coordinates lie in $[0, p)$, then derives $Y$ from $X$ via the curve equation and confirms membership. It then stops, with no subgroup-membership check (source): 1// FILE: middleware-sdk/src/contracts/libraries/keys/KeyBlsBn254.sol 2// sherlock-audit/2025-06-symbiotic-relay (vulnerable) 3function wrap( 4 BN254.G1Point memory keyRaw 5) internal view returns (KEY_BLS_BN254 memory key) { 6 if (keyRaw.X == 0 && keyRaw.Y == 0) { 7 return zeroKey(); 8 } 9 if (keyRaw.X >= BN254.FP_MODULUS || keyRaw.Y >= BN254.FP_MODULUS) { 10 revert KeyBlsBn254_InvalidKey(); 11 } 12 (uint256 beta, uint256 derivedY) = BN254.findYFromX(keyRaw.X); 13 if (mulmod(derivedY, derivedY, BN254.FP_MODULUS) != beta) { 14 revert KeyBlsBn254_InvalidKey(); 15 } 16 if (keyRaw.Y != derivedY && keyRaw.Y != BN254.FP_MODULUS - derivedY) { 17 revert KeyBlsBn254_InvalidKey(); 18 } 19 // MISSING: subgroup check, cofactor*keyRaw should equal point at infinity 20 key = KEY_BLS_BN254(keyRaw); 21} BN254 has cofactor $h > 1$, so the curve contains small-order points outside the prime-order subgroup. An attacker registers such a point as their validator key; every subsequent BLS aggregation that includes it operates outside the subgroup the security proof assumes, and the pairing equation becomes satisfiable for crafted signatures without knowledge of the corresponding private key.

    Curve Points Not Validated
    elliptic-curve signature

    aicis/fresco

    In the SPDZ protocol, parties hold additive shares of a global SPDZ MAC $[\alpha \cdot a]$ on every wire under a single global MAC key $\alpha$. To verify that a reconstructed value $a'$ is correct, each party computes $z_i = a' \cdot \alpha_i - (\alpha \cdot a)_i$, commits to $z_i$, and opens; if the reconstructed $z = \sum z_i \ne 0$, they abort. SPDZ also uses the same commitment scheme in coin-tossing and input-sharing subprotocols.

    Rushing Adversary Copies an Honest Commitment
    commitment mac

    Trust-Machines/wsts

    WSTS (Weighted Schnorr Threshold Signatures), aka WileyProofs, is based on FROST and was vulnerable to threshold-raise attacks. Before PR #88, the per-signer DKG verification in src/v1.rs only checked the Schnorr ID, not the commitment-vector length (source): 1// src/v1.rs — Trust-Machines/wsts (vulnerable, before PR #88) 2if !comm.verify() { 3 bad_ids.push(*i); 4} 5self.group_key += comm.poly[0]; A malicious signer could append commitments to its poly to silently raise the reconstruction threshold. The Trail of Bits length-check fix in Trust-Machines/wsts landed as PR #88 (“Check length of polynomials”). PR #88 added the explicit equality check at every DKG verification site (source):

    Received Sequence Has the Wrong Length
    secret-sharing commitment

    Builder Vault is Blockdaemon’s production MPC threshold-signing platform (powered by the Sepior TSM). Its developer documentation explains that each presignature contains shares of a random signing nonce, and that an MPC node enforces single-use by deleting the presignature in the same transaction in which it consumes its share. The docs additionally warn that backup-and-restore can reintroduce a previously-consumed presignature, turning a routine ops procedure into a key-extraction vector if mishandled. Operators are therefore instructed to delete all presignatures either before taking a database backup or upon restoring.

    Threshold Presignature Reuse (Nonce Reuse)
    signature

    Trail of Bits disclosed a selective-abort vulnerability in an OT-based threshold-ECDSA implementation in the Doerner et al. (DKLS) line. Whether the OT-extension consistency check aborts is itself a function of the sender’s secret choice bits, so a cheating receiver learns “a few bits” per run from the pass/abort signal; because the base OTs are reused, repeating it recovers every secret bit, and in a two-party setting the nonce and the ECDSA signing key.

    Selective-Abort Attacks during OT Extension
    oblivious-transfer signature

    bnb-chain/tss-lib

    Fiat-Shamir hashes need to say which execution context they belong to, and they need an injective encoding of the transcript values. Pre-fix tss-lib was missing both: proof challenges had no caller-supplied session/context tag, and individual inputs were concatenated without recording their lengths. Before v2.0.0, bnb-chain/tss-lib used a shared SHA512_256i helper for proof challenges across Schnorr, MtA, DLN, and commitment proofs. The helper included a block-count prefix, but no caller-supplied session/context tag and no per-input length tag (source).

    Missing Domain Separation When a Hash Function Is Reused
    hash zkp

    bnb-chain/tss-lib

    The Schnorr PoK in bnb-chain/tss-lib lets party $P_i$ prove knowledge of its secret key share $x_i$ by sending $(R = g^k, s = k + c \cdot x_i)$ where $c$ is a Fiat-Shamir challenge. In v1.x the challenge was derived solely from the public key and the commitment (source): 1// FILE: crypto/schnorr/schnorr_proof.go — bnb-chain/tss-lib v1.3.5 (vulnerable) 2 3// NewZKProof constructs a new Schnorr ZK proof of knowledge of the discrete logarithm (GG18Spec Fig. 16) 4func NewZKProof(x *big.Int, X *crypto.ECPoint) (*ZKProof, error) { 5 if x == nil || X == nil || !X.ValidateBasic() { 6 return nil, errors.New("ZKProof constructor received nil or invalid value(s)") 7 } 8 ec := X.Curve() 9 ecParams := ec.Params() 10 q := ecParams.N 11 g := crypto.NewECPointNoCurveCheck(ec, ecParams.Gx, ecParams.Gy) // already on the curve. 12 13 a := common.GetRandomPositiveInt(q) 14 alpha := crypto.ScalarBaseMult(ec, a) 15 16 var c *big.Int 17 { 18 // Challenge includes only public key X and commitment alpha — no session ID, 19 // no party identity, no protocol context. 20 cHash := common.SHA512_256i(X.X(), X.Y(), g.X(), g.Y(), alpha.X(), alpha.Y()) 21 c = common.RejectionSample(q, cHash) 22 } 23 t := new(big.Int).Mul(c, x) 24 t = common.ModInt(q).Add(a, t) 25 26 return &ZKProof{Alpha: alpha, T: t}, nil 27} As described in CVE-2022-47930, the Schnorr proof of knowledge does not utilize a session id, context, or random nonce in the generation of the challenge. This allows a malicious party to replay a proof generated by an honest party. (The CVE record names the IoFinnet fork as the affected product, but the same flaw and PR #256 fix apply to the bnb-chain upstream shown here.) The fix (PR #256) added a Session []byte parameter prepended to every proof challenge via the domain-separating SHA512_256i_TAGGED (source):

    Challenge Hash Missing Prover's Party Identity and Session Identifier
    zkp

    bnb-chain/tss-lib

    Pre-v2.0.0 bnb-chain/tss-lib stored incoming Paillier keys from co-signers with no biprimality or no-small-factor check. A malicious co-signer could publish a structured $N_A = p_1 \cdots p_{16} \cdot q$ and ride the missing validation through MtA into share extraction (see the parent pitfall for the full attack mechanic). The MtA call in ecdsa/signing/round_1.go (source) proceeds with Paillier moduli that keygen never required a proof for: 1// FILE: ecdsa/signing/round_1.go — bnb-chain/tss-lib v1.3.5 (vulnerable) 2// Pre-v2.0.0 keygen accepted each co-signer's Paillier modulus with no 3// biprimality or no-small-factor proof, so MtA runs with moduli that were 4// never proven well-formed. 5cA, pi, err := mta.AliceInit(round.Params().EC(), round.key.PaillierPKs[i], k, 6 round.key.NTildej[j], round.key.H1j[j], round.key.H2j[j]) v2.0.0 (GHSA-5cjx-95fx-68q9) added both CGGMP21 proofs to the DKG phase: each party generates a no-small-factor (facproof) and a Paillier-Blum (modproof) proof of its own modulus, which counterparties verify in a later round before accepting the key (source):

    Smooth or Non-Biprime Paillier Modulus
    paillier homomorphic-encryption zkp

    apache/incubator-milagro-MPC

    Apache Milagro’s MPC library incubator-milagro-MPC implemented GG20-style ECDSA threshold signing and shipped two compounding failures that together produced the most severe variant of the BitForge class: The same missing biprimality and no-small-factor checks on incoming Paillier moduli as the rest of the affected cohort (CVE-2023-33241). The MtA range-proof blinding parameter $\beta$ sized at only 256 bits, rather than the ~2048 bits that match the Paillier modulus. In the standard BitForge attack against a library with the missing checks alone, the attacker crafts a malicious modulus $N_A = p_1 \cdots p_{16} \cdot q$ and harvests $x \bmod p_i$ over ~16 signing sessions. In Milagro, according to Fireblocks' technical report, the undersized $\beta$ collapsed the work budget so far that the victim’s share could be extracted directly from honest signature transcripts of a single co-signing session, without the attacker needing to craft a malicious modulus at all. The malicious-modulus path remained available as a faster amplification.

    Smooth or Non-Biprime Paillier Modulus
    paillier homomorphic-encryption zkp

    BitGo/BitGoJS

    BitGo’s TSS implementation in BitGoJS followed the GG18 key-generation protocol but skipped the biprimality phase entirely. Incoming Paillier moduli from co-signers were accepted with no proof that $N = pq$ for primes $p, q$ of safe size, and no proof of knowledge of the underlying share. Hexens demonstrated a working extraction attack using a chosen-modulus form. The attacker publishes a malicious Paillier public key $(N, V)$ where $N$ has a small smooth factor (e.g. $N = pq$ with $q$ small enough that discrete log in $\mathbb{Z}_q^\ast$ is tractable) and $V$ is an arbitrary quadratic residue. When the victim encrypts their share $x$ under $N$ during signing, the attacker reduces the ciphertext modulo $q$ and computes $C^{p+1} \bmod q$ to isolate $V^x \bmod q$. Brute-force discrete log recovers $x \bmod q$. Repeating across signing sessions and combining residues via CRT reconstructs the full 256-bit share, after which the attacker holds enough material to sign unilaterally under the joint key.

    Smooth or Non-Biprime Paillier Modulus
    paillier homomorphic-encryption zkp

    Lindell’s two-party ECDSA (Lindell, 2017) splits the signing key between a client and a server using Paillier homomorphic encryption, with no oblivious transfer involved. Its security analysis requires that a signatory abort and stop signing the moment a produced signature fails to verify; the abort must be terminal. Fireblocks found that real deployments deviated from this, treating a failed signature as an ordinary, retryable error and continuing to sign with the same key. A party that has compromised its counterparty crafts a malformed Paillier ciphertext so that signature generation succeeds only when the least-significant bit of the honest party’s secret share is zero. Each request then leaks one bit through success-or-abort, and the full key is recovered after a few hundred signatures.

    Selective-Abort Attacks during OT Extension
    signature paillier homomorphic-encryption

    Safeheron/multi-party-ecdsa-cpp

    Safeheron’s multi-party-ecdsa-cpp ran GG18/GG20 key generation without checking the structure of each co-signer’s Paillier modulus $N$, so a non-biprime or smooth $N$ flowed through keygen and into the GG20 signing rounds unchecked. One example of vulnerable code is the Round 3 keygen verifier (pre-fix source): 1// FILE: src/multi-party-ecdsa/gg18/key_gen/round3.cpp 2// Safeheron/multi-party-ecdsa-cpp @ b75d125f (pre-fix, vulnerable) 3ok = bc_message_arr_[pos].pail_proof_.Verify( 4 sign_key.remote_parties_[pos].pail_pub_, 5 sign_key.remote_parties_[pos].index_, 6 bc_message_arr_[pos].dlog_proof_x_.pk_.x(), 7 bc_message_arr_[pos].dlog_proof_x_.pk_.y()); A malicious party could then publish $N = p_1 \cdots p_{16} \cdot q$ with each $p_i \approx 2^{16}$. During GG20 signing, the 16-factor structure opens parallel CRT channels and the small factors keep the MtA range-proof brute force at ~$2^{16}$ per channel. The victim’s encrypted share $x$ leaks $x \bmod p_i$ per session; CRT reconstructs the full share over 16 to ~$10^9$ sessions (Fireblocks technical report, POC).

    Smooth or Non-Biprime Paillier Modulus
    paillier homomorphic-encryption zkp

    data61/MP-SPDZ

    In MP-SPDZ, the concrete synchronization point is Commit_And_Open_, the helper used by the MAC check to commit to local check values and then open them. Before the fix, each thread ran this helper independently. There was no coordinator shared across concurrent MAC checks, so one stalled check did not block another thread using the same global MAC key (source): 1// FILE: Tools/Subroutines.cpp — MP-SPDZ (vulnerable, before 6a42453) 2void Commit_And_Open_(vector<octetStream>& datas, const Player& P) 3{ 4 vector<octetStream> Comm_data(P.num_players()); 5 vector<octetStream> Open_data(P.num_players()); 6 7 Commit(Comm_data[P.my_num()], Open_data[P.my_num()], datas[P.my_num()], 8 P.my_num()); 9 P.Broadcast_Receive(Comm_data); 10 11 P.Broadcast_Receive(Open_data); 12 13 for (int i = 0; i < P.num_players(); i++) 14 { if (i != P.my_num()) 15 { if (!Open(datas[i], Comm_data[i], Open_data[i], i)) 16 { throw invalid_commitment(); } 17 } 18 } 19} The Rushing at SPDZ paper cites commits 6a42453 and b86f29b as the MP-SPDZ fix. The final version passes a shared Coordinator into Commit_And_Open_, waits before the opening phase, validates every opening, and only then calls coordinator.finished() (source):

    SPDZ Multi-Threaded MAC Check
    mac commitment

    bnb-chain/tss-lib

    The audit finding KS-IOF-F-02 pointed out that bnb-chain’s tss-lib applied an ambiguous encoding by using a single dollar-sign delimiter with no per-element length tag.

    Ambiguous Hash Encoding
    hash zkp

    anyswap/FastMulThreshold-DSA

    Multichain’s anyswap/FastMulThreshold-DSA, a fork of bnb-chain/tss-lib, reduced the DLN proof iteration constant from the tss-lib default of 128 down to 1 in commit 4e543437c6, collapsing the soundness margin to a coin flip per attempt (source):

    Insufficient Soundness from Reduced Iteration Count
    zkp

    coinbase/kryptology

    GG20’s joint key-generation procedure (inherited from GG18) assumes the Round 2 P2P delivery of each Shamir share $x_{ij}$ runs over a confidential point-to-point channel. The GG18/GG20 papers assume this private channel abstractly and leave its instantiation to the deployment; Paillier encryption enters only in the signing-phase MtA, never for the keygen shares. The Coinbase library’s GG20 implementation provides no confidentiality of its own and returns the share as a bare struct field (source):

    Unauthenticated or Unencrypted Point-to-Point Channels
    secure-channel paillier homomorphic-encryption

    bnb-chain/tss-lib

    Both failures appear in bnb-chain/tss-lib’s crypto/vss/feldman_vss.go and were disclosed together by Trail of Bits. They were fixed in a single PR #149. Failure 1: zero index mod $q$. Before the fix, Create checked the party index against the integer literal 0 without reducing modulo $q$ first (source): 1// crypto/vss/feldman_vss.go, bnb-chain/tss-lib (vulnerable, pre-PR #149) 2for i := 0; i < num; i++ { 3 if indexes[i].Cmp(big.NewInt(0)) == 0 { 4 return nil, nil, fmt.Errorf("party index should not be 0") 5 } 6 // indexes[i] == q passes the check; evaluatePolynomial(q) ≡ f(0) = secret 7 share := evaluatePolynomial(ec, threshold, poly, indexes[i]) 8 shares[i] = &Share{Threshold: threshold, ID: indexes[i], Share: share} 9} A malicious party submits index $i = q$. The literal-zero check passes, but evaluatePolynomial(q) ≡ evaluatePolynomial(0) = f(0) = s, handing the attacker the shared secret as their share.

    Parties' Shares Not Validated as Non-Zero and Distinct
    secret-sharing

    Chia-Network/bls-signatures

    Chia’s bls-signatures library over BLS12-381 validated incoming G1 public keys via G1Element::CheckValid, which confirmed the on-curve equation but performed no subgroup-membership check. BLS12-381 G1 has cofactor $h > 1$, so the curve contains small-order points outside the prime-order subgroup. A concrete G1 point that satisfies the curve equation but lies outside the subgroup was reported as a witness (source): 1X: 1850443652098619803069679949935703490545934817616361671487073351271435645926537537028144222559542259604367871156773 2Y: 1776970151258755586951871078535415548807448204545244204542330019247278385570277860229537378843413568111354158837149 A non-subgroup public key satisfies the pairing equation for crafted signatures without knowledge of the corresponding private key, giving signature forgery; the same omission also breaks the soundness of any BLS aggregation that combines such a key into the joint public key.

    Curve Points Not Validated
    elliptic-curve signature

    axelarnetwork/tofnd

    Axelar’s tofnd is a Rust daemon implementing GG20 (Gennaro–Goldfeder, 2020), a threshold-ECDSA protocol widely deployed in MPC wallet implementations. Each message is wrapped in a TrafficIn envelope that carries both a transport-level sender identity (from_party_uid) and an inner MsgMeta with a protocol-level sender index (from: usize). As reported in Issue #60, the inner from field is unauthenticated: a malicious party can edit it in the binary payload and send messages on behalf of any other party.

    Unauthenticated or Unencrypted Point-to-Point Channels
    secure-channel

    Kudelski’s audit of ING’s threshold-ECDSA library identified a communication-layer failure in the GG18 resharing protocol. The issue was a design-level mismatch: the resharing mitigation relies on all honest parties seeing the same final confirmation, but that assumption is not realized by sending separate point-to-point messages. ING implemented the standard “forget-and-forgive” mitigation, a final ACK confirmation round before parties delete their old shares; Kudelski noted that this mitigation “might actually make things worse” if a robust broadcast channel is not available, because the final ACK can itself be equivocated. If an application realizes broadcast as $N$ separate point-to-point sends, a malicious sender can equivocate.

    Multicast Masquerading as Broadcast
    broadcast

    Drand is a distributed randomness beacon using DKG and threshold BLS, with a threshold above half the participants under its security model (see the protocol specification). With polynomial degree $t > n/2$, a coalition of at least $n - t + 1$ parties can mount a rogue-key attack: after seeing the honest parties’ constant-term commitment ($A_{i,0} = g^{a_{i,0}}$), the colluding parties choose their own so the group public key becomes an attacker-chosen $Y^\star = g^{x^\star}$.

    Rogue-Key Attacks
    secret-sharing

    bnb-chain/tss-lib

    Standard EdDSA defends against small-subgroup attacks via bit clamping on the single-party secret scalar. The threshold EdDSA path in tss-lib applied no equivalent defense to supplied points received from peers, so as the ZenGo’s Baby Sharks analysis showed, a malicious party could inject an order-8 torsion component into the joint public key so that $1/8$ of signing ceremonies verify, while the other will reject. In the pre-fix tss-lib, the received commitment $R_j$ was constructed straight from peer-supplied coordinates and aggregated into the joint $R$ with no subgroup-membership step (source):

    Curve Points Not Validated
    elliptic-curve signature

    data61/MP-SPDZ

    FKOS15 is a binary MPC-with-preprocessing protocol used by MP-SPDZ’s Tinier backend. Party inputs are masked with preprocessed correlated randomness; the security argument requires that mask to carry the full claimed statistical-security parameter of entropy. In MP-SPDZ pre-fix, Tools/BitVector.h::randomize_blocks produced under-randomized masks for single-bit input types: the loop drove tmp.randomize(G) once per T-sized block, and for a 1-bit T each copied byte carried only one fresh random bit (source): 1// Tools/BitVector.h — data61/MP-SPDZ (vulnerable, pre-99c5efc) 2template<class T> 3inline void BitVector::randomize_blocks(PRNG& G) 4{ 5 T tmp; 6 for (size_t i = 0; i < (nbytes / T::size()); i++) 7 { 8 tmp.randomize(G); // biased for 1-bit T 9 memcpy(bytes + i * T::size(), tmp.get_ptr(), T::size()); 10 } 11} Because masks are read back bit-by-bit but only one bit per byte was randomized, only 1 in every 8 sampled bits was actually random; the other 7 were always 0. The affected values are the party’s own authenticated random inputs (including sacrifice values), so an adversary can predict 7 of every 8 of those bits, far below the intended statistical-security margin.

    Randomness Has Insufficient Entropy
    randomness secret-sharing

    bnb-chain/tss-lib

    GG18/GG20 range proofs instantiate Pedersen commitments under auxiliary bases $h_1, h_2 \in \mathbb{Z}_{\tilde N}^*$ and assume those bases generate the same large subgroup. Two successive bugs hit the tss-lib library on this exact surface. Original keygen broadcast ships bases with no DLN proof at all. Round 2 of ECDSA keygen stored the incoming triple $(\tilde N, h_1, h_2)$ directly, with no validation (source): 1// FILE: ecdsa/keygen/round_2.go 2// bnb-chain/tss-lib @ 6584db7f (pre-PR #89, vulnerable) 3for j, msg := range round.temp.kgRound1Messages { 4 r1msg := msg.Content().(*KGRound1Message) 5 round.save.PaillierPKs[j] = r1msg.UnmarshalPaillierPK() // used in round 4 6 round.save.NTildej[j] = r1msg.UnmarshalNTilde() 7 round.save.H1j[j], round.save.H2j[j] = r1msg.UnmarshalH1(), r1msg.UnmarshalH2() 8 // ... 9} A malicious peer sets $h_2 = 1$ so each subsequent MtA range-proof commitment collapses to $z = h_1^s \bmod \tilde N$, revealing $h_1^s$; the attacker then reconstructs $s$ either by choosing $\tilde N$ as a product of small prime factors so $\phi(\tilde N)$ is smooth and applying Pohlig-Hellman on each factor combined with CRT, or by choosing $\tilde N$ large enough that recovery reduces to an integer logarithm problem.

    Group Elements Not Validated in Discrete-Log Groups
    paillier homomorphic-encryption zkp commitment group

    bnb-chain/tss-lib

    Kudelski Security flagged that pre-fix bnb-chain/tss-lib keygen generated the RSA modulus $\tilde N$ in ecdsa/keygen/round_1.go via Go’s rsa.GenerateMultiPrimeKey, which returns ordinary RSA primes, not safe primes. However, the helper that later derives the DLN bases (common.GetRandomGeneratorOfTheQuadraticResidue) required $\tilde N$ to be a product of safe primes for its output to land in the prime-order QR subgroup (source): 1// FILE: ecdsa/keygen/round_1.go — bnb-chain/tss-lib @ a2c27b4 (vulnerable) 2// 5-7. generate auxiliary RSA primes for ZKPs later on 3go func(ch chan<- *rsa.PrivateKey) { 4 pk, err := rsa.GenerateMultiPrimeKey(rand.Reader, 2, RSAModulusLen) 5 if err != nil { 6 common.Logger.Errorf("RSA generation error: %s", err) 7 ch <- nil 8 } 9 ch <- pk 10}(rsaCh) The fix introduced by PR #68 moved $\tilde N$ generation into a new ecdsa/keygen/prepare.go backed by a GermainSafePrime generator (source):

    Non-Safe-Prime Modulus
    rsa group paillier homomorphic-encryption

    bnb-chain/tss-lib

    The MtA “Bob-with-check” range proof in bnb-chain/tss-lib involves a commitment $u = g^\alpha$ to the prover’s randomness. Pre-fix, the FS hash omitted u (source):

    Challenge Transcript Missing Required Values (Weak Fiat-Shamir)
    zkp