Network Working Group S. Schlesinger Internet-Draft J. Katz Intended status: Informational Google LLC Expires: 3 May 2027 A. Faz-Hernandez Cloudflare, Inc. 30 October 2026 EC-ACT: Anonymous Credit Tokens draft-authors-mole-ec-act-latest Abstract This document specifies EC-ACT, a keyed-verification anonymous credential scheme with a hidden credit balance. A Client spends a public amount and receives a fresh Credential for the remaining balance and a return amount chosen by the issuer. The issuer verifies each spend without learning the balance or linking it to issuance or to other spends. EC-ACT uses a pairing-free BBS-style signature and proofs over linear relations. This document defines its cryptographic operations, encodings, and security requirements. The MOLE protocols use EC-ACT as a Credential scheme. About This Document This note is to be removed before publishing as an RFC. The latest revision of this draft can be found at https://moderation- of-unlinkable-endorsements.github.io/internet-drafts/draft-authors- mole-ec-act.html. Status information for this document may be found at https://datatracker.ietf.org/doc/draft-authors-mole-ec-act/. Source for this draft and an issue tracker can be found at https://github.com/Moderation-of-unLinkable-Endorsements/internet- drafts. Status of This Memo This Internet-Draft is submitted in full conformance with the provisions of BCP 78 and BCP 79. Internet-Drafts are working documents of the Internet Engineering Task Force (IETF). Note that other groups may also distribute working documents as Internet-Drafts. The list of current Internet- Drafts is at https://datatracker.ietf.org/drafts/current/. Internet-Drafts are draft documents valid for a maximum of six months and may be updated, replaced, or obsoleted by other documents at any time. It is inappropriate to use Internet-Drafts as reference material or to cite them other than as "work in progress." This Internet-Draft will expire on 3 May 2027. Copyright Notice Copyright (c) 2026 IETF Trust and the persons identified as the document authors. All rights reserved. This document is subject to BCP 78 and the IETF Trust's Legal Provisions Relating to IETF Documents (https://trustee.ietf.org/ license-info) in effect on the date of publication of this document. Please review these documents carefully, as they describe your rights and restrictions with respect to this document. Code Components extracted from this document must include Revised BSD License text as described in Section 4.e of the Trust Legal Provisions and are provided without warranty as described in the Revised BSD License. Table of Contents 1. Introduction 2. Conventions and Definitions 3. Preliminaries 3.1. Prime-Order Group 3.2. Errors 3.3. Deriving Scalars 3.4. Key Generation 4. The Credential Scheme 4.1. Configuration 4.1.1. Generators 4.1.2. Key Generation 4.2. Credential Context 4.3. Amounts 4.4. Zero-Knowledge Proofs 4.4.1. Tags 4.4.2. Prover Nonces 4.4.3. Proving and Verifying 4.5. Issuance 4.5.1. Signing Exponent 4.5.2. Issuance Request 4.5.3. Issuance Response 4.5.4. Issuance Finalization 4.6. Spending 4.6.1. The Spend Relation 4.6.2. Spend Proof Generation 4.6.3. Spend Verification 4.6.4. Refund Issuance 4.6.5. Refund Finalization 4.7. Encodings 5. Ciphersuites 5.1. EC-ACT(P-256, SHA-256) 5.2. Randomness 6. Security Considerations 7. IANA Considerations 8. References 8.1. Normative References 8.2. Informative References Appendix A. Test Vectors A.1. Ciphersuite A.2. Key Pair A.3. Issuance A.4. Spend 1: ordinary spend (s = 3, a = 0) A.5. Spend 2: refresh (s = 0, a = 0) A.6. Spend 3: pure top-up (s = 0, a = 2) A.7. Spend 4: spend with top-up (s = 3, a = 2) Acknowledgments Authors' Addresses 1. Introduction Applications such as MOLE require an updateable state managed by a client but authenticated by a server, with stringent requirements on unlinkability of updates and tests. EC-ACT provides such functionality: a Client holds a balance that is authenticated via signature issued by a Moderator, and can be told to update the balance in a verifiable manner. 2. Conventions and Definitions The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all capitals, as shown here. The capitalized terms Client, Moderator, and Credential are used as defined in [ARCH]; a lowercase credential is the generic cryptographic notion. The message-encoding conventions, Python notation, and helpers I2OSP, U16Prefixed, random, and Seed are those of Section 2 of [EC-IHAT]. The algorithms use bytes for byte strings, int for integers, and Sequence and NamedTuple from Python's typing module. Record fields appear in the same order as their tuple representation. The group G, protocol context ctx_proto, seed length Nseed, generators, and balance width L are globals fixed by the EC-ACT configuration (Section 4.1). Shared group methods from [EC-IHAT] use the EC-ACT group instance G, which stores ctx_proto and applies EC- ACT domain separation. Both schemes use Nseed = 48. Public byte- string inputs retain the length bounds specified by the operation that uses them. 3. Preliminaries This document contains executable Python code to specify much of the protocol. EC-ACT uses three three dependencies: Group: A prime-order group implementing the interface in Section 3.1. Section 5 gives concrete instances. Hash: A cryptographic hash function, used by the HashToGroup, HashToScalar, and derivation algorithms that the group of [EC-IHAT] provides. Sigma protocol: The compact non-interactive Sigma protocol of [SIGMA], instantiated in Section 4.4. 3.1. Prime-Order Group EC-ACT uses the prime-order group interface and the Element and Scalar types of Section 3.1 of [EC-IHAT], instantiated as in Section 5. The prime p = G.Order() is the group order and the scalar-field modulus. The Python interface provides G.scalar(x) for an integer x in [0, G.Order()), s.isZero() for a scalar, and A.isIdentity() for an element. Arithmetic combines values of the same type: integer constants are converted with G.scalar before scalar arithmetic. The negative of an element A is G.Identity() - A. 3.2. Errors DeserializeError, VerifyError, DeriveError, and ValueError have the meanings given in Section 3.2 of [EC-IHAT]. EC-ACT additionally uses AmountError when a balance or amount is outside the range admitted by Section 4.3: class AmountError(ValueError): """A credit amount is outside its permitted range.""" An implementation that raises an error MUST abort the affected protocol run. 3.3. Deriving Scalars Use G.DeriveScalars(rand: bytes, info: bytes, count: int) -> list[Scalar] from Section 4.2 of [EC-IHAT], using the EC-ACT group instance initialized with ctx_proto. It rejects rand whose length is not Nseed with ValueError, and raises DeriveError in the negligible event that a derived scalar is zero. EC-ACT applies the following requirements to this shared algorithm. rand MUST be Nseed bytes of output of random and MUST NOT be used for more than one derivation. An algorithm derives all of its scalars with one call, under an info string that names it. See Section 5.2. The hash-to-scalar method reduces an Ns + 16-byte string modulo p. For a uniformly random string of this length, the resulting scalar has statistical distance at most 2^-128 from uniform over the nonzero scalars, conditioned on the result being nonzero. The 16-byte margin follows the hash-to-field method for a 128-bit security target (Section 5 of [HASH2CURVE]). Section 6 describes the random-oracle analysis needed to apply this sampling bound to the derived scalar and nonce vectors. 3.4. Key Generation Use G.DeriveKeyPair(seed, info) and G.GenerateKeyPair() from Section 4.4 of [EC-IHAT], with the EC-ACT group instance of Section 4.1. Both return tuple[Scalar, Element]; EC-ACT names the returned keys (skM, pkM) because the Moderator is the issuer. The Moderator publishes G.SerializeElement(pkM) in its configuration ([PROTOCOLS]). 4. The Credential Scheme An Anonymous Credit Token is a keyed-verification anonymous credential [KVAC] over a privately verifiable, pairing-free BBS-style signature [BBS] [TZ23], whose hidden state is a _balance_ c: a nonnegative integer number of credits. An Moderator issues a Credential with an initial balance of its choosing, and the Client later _spends_ from it. A spend reveals a public amount s and proves, in zero knowledge, that the Credential holds at least s credits. It also reveals a nullifier, which the Moderator checks against its record of accepted spends to enforce single use. In the same exchange the Moderator issues a _refund_: a message containing a signature, a public return amount t, and a proof of correct signing. The Client combines this message with its saved spend state to finalize a fresh Credential with balance c - s + t. Every spend declares a public _top-up allowance_ a. When no top-up is allowed, the spend sets a = 0. The Moderator chooses t with 0 <= t <= s + a, so the balance increases exactly when t > s, by at most a. With a = 0, the return amount is at most s. A presentation hides its Credential's balance beyond the public amounts and the predicates they establish. It is intended to be unlinkable to the issuance or refund that produced that Credential and to other presentations, subject to the assumptions and anonymity- set requirements of Section 6. The scheme is a two-party protocol between a Client and a Moderator. The Moderator holds a key pair (skM, pkM) and is both the issuer and the verifier: Credentials are not publicly verifiable, and only their issuer can check a spend. Each of the two flows, issuance and spending, is a single request/response exchange followed by a Client- local finalization. For spending, both parties know s, the spend amount, a, the maximum refund, and a _spend context_ ctx_spend that binds the proof to the Moderator's challenge (Section 4.6). [PROTOCOLS] defines this context as the SHA-256 digest of the encoded challenge. Client(pkM, ctx_cred) Moderator(skM, ctx_cred) ------------------------------------------------------------------ state, request = IssueRequest() request --------> response = IssueResponse(skM, ctx_cred, c, request) response <-------- credential = FinalizeIssue(pkM, ctx_cred, state, response) Spending inputs at both parties: s, a, ctx_spend state, proof = ProveSpend(credential, ctx_cred, s, a, ctx_spend) proof (includes nullifier k) --------> VerifySpend(skM, ctx_cred, ctx_spend, proof) refund = IssueRefund(skM, ctx_cred, proof, t) Record proof.k atomically, failing if present refund <-------- credential = FinalizeRefund(pkM, ctx_cred, state, refund) Figure 1: Credential issuance and spending overview The Moderator chooses the initial balance c and the return amount t according to its policy; both are outside the scope of this document, as is the nullifier store the Moderator keeps to reject a second spend of the same Credential. [PROTOCOLS] specifies both, together with the carriage of the four messages. 4.1. Configuration A ciphersuite (Section 5) is identified by an ASCII byte string identifier, and both parties MUST agree on it before running the protocol. The Credential scheme is specified for the P-256 ciphersuite of Section 5; its protocol context is def CreateCredentialProtocolContext(identifier: bytes) -> bytes: return b"EC-ACTv1-" + identifier Throughout this section and wherever the algorithms of this section are invoked, ctx_proto denotes this value, and HashToGroup, HashToScalar, G.DeriveScalars, G.DeriveNonces, and the key generation of Section 3.4 are parameterized by it. The scheme has one further parameter, the _balance width_ L. Balances and amounts are integers in [0, 2^L). L MUST satisfy 1 <= L <= MAX_BIT_LENGTH, where MAX_BIT_LENGTH is fixed by the ciphersuite. The size of a spend proof and the cost of producing and verifying it grow linearly in L, so a deployment SHOULD choose the smallest L that accommodates its largest balance. The Moderator publishes L together with its public key ([PROTOCOLS]); a Client MUST use the published value. L is fixed for the lifetime of a key and credential context. Section 6 shows by induction over issuances and refunds that every balance the Moderator has signed lies below 2^L, assuming one value of L per key and context. A Moderator that changes L MUST also change the credential context or the key. Deployments are RECOMMENDED to include L in the credential context, for instance by appending I2OSP(L, 1) to it, so that changing L changes the context. 4.1.1. Generators The scheme uses the group generator B = G.Generator() and four further elements H1, H2, H3, H4, fixed by the ciphersuite: def CreateGenerators() -> tuple[Element, Element, Element, Element]: return ( G.HashToGroup(b"GenH1"), G.HashToGroup(b"GenH2"), G.HashToGroup(b"GenH3"), G.HashToGroup(b"GenH4"), ) H1 commits to the balance, H2 to the nullifier, H3 is the blinding base, and H4 binds the credential context (Section 4.2). The discrete logarithm of any of these elements with respect to any other, or to B, MUST NOT be known to any party; deriving them by hashing fixed labels ensures this. The five elements are pairwise distinct except with negligible probability; an implementation MAY verify this once when instantiating a ciphersuite. 4.1.2. Key Generation A Moderator holds a key pair (skM, pkM), generated with G.GenerateKeyPair of Section 3.4 under the protocol context of this section, or derived from a seed with G.DeriveKeyPair. The Moderator publishes SerializeElement(pkM) in its configuration ([PROTOCOLS]). The signing key skM is also the verification key: VerifySpend requires it. 4.2. Credential Context Every Credential is bound at issuance to a _credential context_ ctx_cred, an opaque byte string of at most 2^16 - 1 bytes chosen by the Moderator and agreed with the Client out of band; [PROTOCOLS] says how. It restricts _when_, or under which policy, a Credential may be spent, so that a Moderator can for instance expire all Credentials of an epoch at once without rotating its key. The context is bound as a signed attribute. It is mapped to a scalar and carried under H4: def CreateContextScalar(ctx_cred: bytes) -> Scalar: return G.HashToScalar( U16Prefixed(ctx_cred) + b"CredentialContext" ) No message of this document carries the context. Each party supplies it to every algorithm of this section that computes or checks a signature, so a spend verifies only under the context the Credential was issued under. A Client keeps its own copy of the context for as long as it holds the Credential. A Credential presented under a context other than the one it was issued under fails verification; the mismatch is not otherwise signalled. The set of Clients sharing a credential context MUST be coarse; see Section 6. 4.3. Amounts Balances and amounts are nonnegative integers below 2^L. On the wire they are uint64 values; in the algebra they enter as scalars. G.scalar(x) denotes the Scalar whose integer value is x. The spend range proof shows that a hidden value lies in [0, m] for a public bound m < 2^L. It decomposes the value into bits with the weights returned by RangeWeights(m): the powers of two below the leading power of m, followed by a top weight chosen so that all weights sum to m. Every weight is positive, so a sum of weighted bits lies in [0, m], and every integer in [0, m] is such a sum. Bits(x, weights) returns the bits of x for these weights, least significant first, as scalars each equal to 0 or 1: def RangeWeights(m: int) -> list[int]: if not 0 <= m < 2**L: raise AmountError n = m.bit_length() if n == 0: return [] return [2**j for j in range(n - 1)] + [m - 2 ** (n - 1) + 1] def Bits(x: int, weights: Sequence[int]) -> list[Scalar]: n = len(weights) if n == 0: return [] top = (x >> (n - 1)) & 1 rest = x - top * weights[n - 1] return [G.scalar((rest >> j) & 1) for j in range(n - 1)] + [ G.scalar(top) ] For x in [0, m] with m >= 1, x < 2^n where n is the bit length of m, so top is set exactly when x >= 2^(n-1), which is at least the top weight; rest then lies in [0, 2^(n-1)). When m is a power of two minus one, the weights are the powers of two and Bits is the binary decomposition. When m = 0, there are no weights and the value is zero. An implementation MUST compute Bits in constant time with respect to x, which is derived from the Client's hidden balance. The number of weights depends only on m, which is public. A party that receives an amount MUST check that it is below 2^L before using it, and MUST raise an AmountError otherwise. The spend range proof bounds a difference of amounts. The bounds on each amount are needed to interpret this difference over the integers (Section 6). The comparisons s <= c and c + a < 2^L in ProveSpend, the bound 2^L - 1 - (s + a) in ProveSpend and VerifySpend, t <= s + a in IssueRefund, and t <= s + a and v1 + t < 2^L in FinalizeRefund are between integers. Each sum can reach 2^(L+1) - 2, which does not fit in a uint64 when L = 64, and an implementation MUST compute them without overflow. 4.4. Zero-Knowledge Proofs Each proof of this document is a compact NARG string (Section 5.5 of [SIGMA]) for a linear relation declared where it is used, in the notation of Section 3.4 of [SIGMA]. In a Relation block, the reserved name G denotes the generator B at element index zero. Compact proofs carry a single challenge scalar in place of all per- equation commitments, reducing the size of the spend message. The ciphersuite is sigma-proofs_Shake128_P256 of Section 8 of [SIGMA]. Its group is the group of Section 5 with the same element and scalar encodings, so statement elements are encoded identically in both documents. 4.4.1. Tags Every proof is bound to a _tag_ (Section 5.1 of [SIGMA]) built by Tag from a label naming the operation and a possibly empty list of _bindings_: public byte strings to which the proof must be bound that do not appear in the relation. def Tag(label: bytes, bindings: Sequence[bytes]) -> bytes: tag = ctx_proto + b"-" + label + b"-CMPT-with-" + SIGMA_SUITE for binding in bindings: tag += U16Prefixed(binding) return tag SIGMA_SUITE is the ciphersuite identifier "sigma- proofs_Shake128_P256". The labels are a fixed set, and each binding is length-prefixed, so distinct inputs yield distinct tags (Section 5.1 of [FIAT-SHAMIR]). Bindings are arbitrary byte strings, not the US-ASCII text that section recommends; the length prefixes keep the tag unambiguous. 4.4.2. Prover Nonces ProveCompact draws one nonce per witness scalar from its rng, in scalar-index order. The rng MUST return, on its i-th call, the value that ProverNonces.random_scalar computes below. The nonces are derived together with G.DeriveNonces (Section 4.3 of [EC-IHAT]) from the witness, the session, the relation, and fresh randomness; see "Derived prover nonces" in Section 6. class ProverNonces: def __init__( self, witness: Sequence[Scalar], session_id: bytes, instance: bytes, ) -> None: self.nonces = G.DeriveNonces( b"".join(G.SerializeScalar(w) for w in witness), b"nonce", session_id + I2OSP(len(instance), 4) + instance, random(Nseed), len(witness), ) self.count = 0 def random_scalar(self) -> int: i = self.count self.count = i + 1 return int(self.nonces[i]) witness is the prover's whole witness in scalar-index order; session_id is the 32-byte DeriveSessionID(tag) of Section 5.1 of [FIAT-SHAMIR]; and instance is the SerializeLinearRelation of the relation (Section 3.6 of [SIGMA]). This rule binds only the prover; a verifier following [SIGMA] accepts these proofs unchanged. The derivation is deterministic in the witness, the tag, the relation, and rand: replaying the bytes of random reproduces a proof byte for byte, which the test vectors of Appendix A rely on, and a random source that repeats all of rand reproduces the same proof. A retried operation draws fresh rand and produces a different proof. 4.4.3. Proving and Verifying A relation is proven and verified as follows, where relation is the declared relation compiled as in Section 3.4 of [SIGMA], and DeriveSessionID, SerializeLinearRelation, ProveCompact, and VerifyCompact are those of [FIAT-SHAMIR] and [SIGMA]. def Prove( tag: bytes, relation: LinearRelation, witness: Sequence[Scalar] ) -> bytes: nonces = ProverNonces( witness, DeriveSessionID(tag), SerializeLinearRelation(relation), ) return ProveCompact(tag, relation, witness, nonces) def Verify( tag: bytes, relation: LinearRelation, proof: bytes ) -> bool: return VerifyCompact(tag, relation, proof) 4.5. Issuance Issuance produces a signature on the attributes (c, k, r, ctx), whose group-element encoding is B + c * H1 + k * H2 + r * H3 + ctx * H4. Here c is the balance chosen by the Moderator, ctx is the context scalar, and k and r are chosen by the Client and hidden from the Moderator: IssueRequest -> IssueResponse -> FinalizeIssue IssueRequest and FinalizeIssue are run by the Client, and IssueResponse by the Moderator. The Python records are: class ClientIssuanceState(NamedTuple): k: Scalar r: Scalar K: Element class IssueRequestMessage(NamedTuple): K: Element pok: bytes class IssueResponseMessage(NamedTuple): A: Element e: Scalar c: int pok: bytes class Credential(NamedTuple): k: Scalar c: int r: Scalar A: Element e: Scalar 4.5.1. Signing Exponent IssueResponse here and IssueRefund (Section 4.6.4) choose the exponent e of the signature (A, e). It is derived with G.DeriveNonces (Section 4.3 of [EC-IHAT]) from the signing key, the group-element encoding of the signed attributes, and fresh randomness. Repeating these inputs reproduces the same signature. Changing the signed encoding changes the derivation input even when the random source repeats (Section 6). def SigningExponent( skM: Scalar, label: bytes, X_A: Element ) -> Scalar: instance = U16Prefixed(label) + U16Prefixed( G.SerializeElement(X_A) ) (e,) = G.DeriveNonces( G.SerializeScalar(skM), b"e", instance, random(Nseed), 1 ) if (e + skM).isZero(): raise DeriveError return e label is IssueResponse or Refund, the tag label of the accompanying proof, and X_A is the group-element encoding being signed. The abort when e + skM is zero has probability about 1/p; a caller that retries draws fresh randomness. 4.5.2. Issuance Request The Client commits to a fresh nullifier k and blinding factor r, and proves that it knows the opening: Relation Commitment(H2, H3, K): Witness: k, r Equations: K = k * H2 + r * H3 def IssueRequest() -> ( tuple[ClientIssuanceState, IssueRequestMessage] ): rand = random(Nseed) (k, r) = G.DeriveScalars(rand, b"IssueRequest", 2) K = k * H2 + r * H3 tag = Tag(b"IssueRequest", []) pok = Prove(tag, CommitmentRelation(K), [k, r]) return ClientIssuanceState(k, r, K), IssueRequestMessage(K, pok) The nullifier k is revealed when the Credential is spent, and the Moderator uses it to reject a second spend. A Client MUST NOT reuse it across Credentials, and MUST NOT finalize one state against two responses: the two Credentials would share k, and at most one of them can be spent (Section 6). 4.5.3. Issuance Response The Moderator checks the request, chooses the balance c, and signs the encoding X_A = B + c * H1 + ctx * H4 + K as A = X_A / (e + skM). It proves that A is such a signature under pkM without revealing skM, with the witness x = e + skM and X_G = x * B, which the Client computes as e * B + pkM: Relation Signature(A, X_A, X_G): Witness: x Equations: X_A = x * A X_G = x * G def IssueResponse( skM: Scalar, ctx_cred: bytes, c: int, request: IssueRequestMessage, ) -> IssueResponseMessage: K, pok = request if not 0 <= c < 2**L: raise AmountError tag = Tag(b"IssueRequest", []) if not Verify(tag, CommitmentRelation(K), pok): raise VerifyError ctx = CreateContextScalar(ctx_cred) X_A = B + G.scalar(c) * H1 + ctx * H4 + K e = SigningExponent(skM, b"IssueResponse", X_A) x = e + skM A = G.ScalarInverse(x) * X_A X_G = G.ScalarMultGen(x) tag = Tag(b"IssueResponse", []) pok = Prove(tag, SignatureRelation(A, X_A, X_G), [x]) return IssueResponseMessage(A, e, c, pok) The check c < 2^L bounds every issued balance; the soundness of spending relies on it (Section 6). 4.5.4. Issuance Finalization The Client checks the response and assembles the Credential. def FinalizeIssue( pkM: Element, ctx_cred: bytes, state: ClientIssuanceState, response: IssueResponseMessage, ) -> Credential: k, r, K = state A, e, c, pok = response if not 0 <= c < 2**L: raise AmountError ctx = CreateContextScalar(ctx_cred) X_A = B + G.scalar(c) * H1 + ctx * H4 + K X_G = G.ScalarMultGen(e) + pkM tag = Tag(b"IssueResponse", []) if not Verify(tag, SignatureRelation(A, X_A, X_G), pok): raise VerifyError return Credential(k, c, r, A, e) A Credential is the tuple (k, c, r, A, e); its encoding, for storage, is given in Section 4.7. It is never sent. The Client also retains ctx_cred, which is not part of the Credential but is needed to spend it. 4.6. Spending Spending consumes a Credential with balance c and produces one with balance c - s + t. The amount s and the top-up allowance a are chosen by the Client to match the Moderator's challenge ([PROTOCOLS]); the return amount t, with 0 <= t <= s + a, is chosen by the Moderator. With s = a = 0 a spend changes nothing but the nullifier and refreshes the Credential. Spending consists of four algorithms: ProveSpend -> VerifySpend -> IssueRefund -> FinalizeRefund ProveSpend and FinalizeRefund are run by the Client; VerifySpend and IssueRefund by the Moderator, in that order and only if VerifySpend succeeds. A spend is bound to a _spend context_ ctx_spend, an opaque byte string of at most 2^16 - 1 bytes supplied by the verifier and known to the Client. The spend proof uses Tag(b"Spend", [ctx_spend]), with ctx_spend as its sole binding (Section 4.4.1). [PROTOCOLS] sets it to the SHA-256 digest of the challenge that triggered the presentation. The top-up allowance is bound by the relation: a proof produced for one value of a does not verify under another, so a Moderator authorizes a top-up by verifying the proof with the value it accepts, and a Moderator that offers none MUST reject a spend with a != 0. The Python records for spending are: class ClientSpendState(NamedTuple): kstar: Scalar r_star: Scalar v1: int s: int a: int K_prime: Element class SpendMessage(NamedTuple): k: Scalar s: int a: int A_prime: Element B_bar: Element K_n: Element Com1: Sequence[Element] Com_c: Element | None pok: bytes class RefundMessage(NamedTuple): A: Element e: Scalar t: int pok: bytes An absent Com_c is None. 4.6.1. The Spend Relation A spend shows that the _remainder_ v1 = c - s of the hidden balance c satisfies 0 <= v1 <= m, where m = 2^L - 1 - (s + a) The lower bound shows that the Credential covers the spend, and the upper bound that the _topped-up balance_ c + a = s + v1 + a is below 2^L, so the refund may return up to s + a without the balance exceeding 2^L - 1. A single range proof over the bits of v1, weighted by w = RangeWeights(m) (Section 4.3), shows both. A spend with s + a >= 2^L cannot satisfy them and is rejected with an AmountError. The number n = len(w) of bit commitments is the bit length of m and is fixed by the public amounts. A _refresh_, with s = a = 0, needs no range proof: the remainder is the balance c, whose range follows from issuance and prior refunds (Section 6). It commits to c in a single commitment Com_c instead. The relation therefore has two shapes, selected by whether s + a is zero; a group marked with a condition below is present exactly when it holds. Relation Spend(A_prime, B_bar, A_bar, H1_prime, K_n, s, a, w[0], ..., w[n-1], (s + a > 0) Com1[0], ..., Com1[n-1], (s + a > 0) Com_c): (s + a = 0) Witness: e, r2, r3, c, r, kstar, rn, b1[0], ..., b1[n-1], s1[0], ..., s1[n-1], u1[0], ..., u1[n-1], (s + a > 0) rc (s + a = 0) Equations: A_bar = -e * A_prime + r2 * B_bar H1_prime = r3 * B_bar - c * H1 - r * H3 K_n = kstar * H2 + rn * H3 for j in 0, ..., n-1: (s + a > 0) Com1[j] = b1[j] * H1 + s1[j] * H3 Com1[j] = b1[j] * Com1[j] + u1[j] * H3 K_n + s * H1 + sum_j w[j] * Com1[j] = c * H1 + kstar * H2 + rn * H3 + sum_j w[j] * s1[j] * H3 (s + a > 0) Com_c = c * H1 + rc * H3 (s + a = 0) The first equation states that A_prime is a rerandomization of a signature under skM, since the verifier computes A_bar = skM * A_prime. The second opens the signed encoding to the hidden balance c and blinding factor r, relative to the public H1_prime, which fixes the nullifier k and the context. The third opens K_n to the next nullifier and so shows that K_n has no H1 component, which would otherwise raise the balance the refund signs. Each pair of bit equations is the Bit relation of Section 3.4 of [SIGMA], with u1[j] = (1 - b1[j]) * s1[j] for an honest prover. The sum equation ties the committed bits to the balance: given the third equation, its left-hand side opens under H1 to s + v1 with v1 = sum_j w[j] * b1[j], so c = s + v1 with 0 <= v1 <= m. Its left-hand side is s * H1 + K_prime, with K_prime the commitment the refund signs (Section 4.6.4); including K_n keeps it from being the identity element when s = 0 and m = 0, which [SIGMA] would reject. The weights are public scalars of the relation, computed by both parties from s and a. In a refresh, Com_c opens to c directly. The witness scalars are supplied in the order listed. 4.6.2. Spend Proof Generation The Client rerandomizes its signature, commits to the next nullifier and to the remainder, and proves the spend relation. def ProveSpend( credential: Credential, ctx_cred: bytes, s: int, a: int, ctx_spend: bytes, ) -> tuple[ClientSpendState, SpendMessage]: k, c, r, A, e = credential if not (0 <= s < 2**L and 0 <= a < 2**L): raise AmountError if s > c or c + a >= 2**L: raise AmountError v1 = c - s w = RangeWeights(2**L - 1 - (s + a)) if s + a > 0 else [] n = len(w) ctx = CreateContextScalar(ctx_cred) # Four fixed scalars, then one for each bit of the remainder or # one for the commitment of a refresh. count = 4 + (n if s + a > 0 else 1) derived = G.DeriveScalars(random(Nseed), b"ProveSpend", count) (r1, r2, kstar, rn) = derived[:4] # Rerandomize the signature. B_msg = B + G.scalar(c) * H1 + k * H2 + r * H3 + ctx * H4 A_prime = (r1 * r2) * A B_bar = r1 * B_msg r3 = G.ScalarInverse(r1) A_bar = r2 * B_bar - e * A_prime # Commit to the next Credential's nullifier. K_n = kstar * H2 + rn * H3 Com1: list[Element] = [] Com_c: Element | None = None witness = [e, r2, r3, G.scalar(c), r, kstar, rn] # Commit to the remainder: bitwise when it must be shown to be in # range, in one commitment when it is the balance of a refresh. if s + a > 0: b1 = Bits(v1, w) s1 = derived[4:] Com1 = [b1[j] * H1 + s1[j] * H3 for j in range(n)] u1 = [(G.scalar(1) - b1[j]) * s1[j] for j in range(n)] r_star = rn for j in range(n): r_star = r_star + G.scalar(w[j]) * s1[j] witness += b1 + s1 + u1 else: rc = derived[4] Com_c = G.scalar(c) * H1 + rc * H3 r_star = rn + rc witness += [rc] H1_prime = B + k * H2 + ctx * H4 relation = SpendRelation( A_prime, B_bar, A_bar, H1_prime, K_n, s, a, Com1, Com_c ) pok = Prove(Tag(b"Spend", [ctx_spend]), relation, witness) # The commitment the refund will sign, opened by the new # Credential's secrets; the Moderator recomputes it from `proof`. K_prime = G.scalar(v1) * H1 + kstar * H2 + r_star * H3 state = ClientSpendState(kstar, r_star, v1, s, a, K_prime) proof = SpendMessage( k, s, a, A_prime, B_bar, K_n, Com1, Com_c, pok ) return state, proof A_bar is not sent: the Moderator computes it as skM * A_prime. The values kstar and r_star are the nullifier and blinding factor of the Credential the refund will produce. They open K_prime, which equals K_n + V1 with V1 the balance commitment of BalanceCommitment below; the Moderator recomputes it from proof and signs it in IssueRefund, and the Client keeps it in state to check the refund. ProveSpend consumes the Credential: an implementation MUST NOT allow a second call on the same Credential value. The Client MUST treat the Credential as spent, and MUST have stored state durably, no later than the moment proof becomes observable outside the Client; the refund is unusable without the state, and a second proof from the same Credential reveals the same nullifier (Section 6). state holds everything FinalizeRefund needs. [PROTOCOLS] additionally requires retaining proof for refund recovery. 4.6.3. Spend Verification The Moderator checks the spend proof under its own key and contexts. def VerifySpend( skM: Scalar, ctx_cred: bytes, ctx_spend: bytes, proof: SpendMessage, ) -> None: k, s, a, A_prime, B_bar, K_n, Com1, Com_c, pok = proof if not (0 <= s < 2**L and 0 <= a < 2**L): raise AmountError if s + a >= 2**L: raise AmountError ctx = CreateContextScalar(ctx_cred) A_bar = skM * A_prime H1_prime = B + k * H2 + ctx * H4 relation = SpendRelation( A_prime, B_bar, A_bar, H1_prime, K_n, s, a, Com1, Com_c ) if not Verify(Tag(b"Spend", [ctx_spend]), relation, pok): raise VerifyError The shape of proof is fixed by s and a (Section 4.7): Com_c is present exactly when s + a = 0, and otherwise Com1 holds one commitment per weight of RangeWeights(2^L - 1 - (s + a)); a message of any other shape is rejected at deserialization, and SpendRelation raises a VerifyError on one. The amount checks of Section 4.3 precede verification (Section 6). VerifySpend does not check whether k has been seen before, nor whether the Moderator is willing to grant the top-up a. The Moderator MUST do both. It MUST record k atomically, failing if k is already recorded, and MUST NOT accept the spend or release a refund unless k was recorded ([PROTOCOLS]). 4.6.4. Refund Issuance After a successful VerifySpend, the Moderator signs the remainder plus its chosen return amount, proving the Signature relation under the Refund tag. def BalanceCommitment(proof: SpendMessage) -> Element: k, s, a, A_prime, B_bar, K_n, Com1, Com_c, pok = proof if s + a > 0: w = RangeWeights(2**L - 1 - (s + a)) if len(Com1) != len(w): raise VerifyError V1 = G.Identity() for j in range(len(w)): V1 = V1 + G.scalar(w[j]) * Com1[j] else: if Com_c is None: raise VerifyError V1 = Com_c return K_n + V1 def IssueRefund( skM: Scalar, ctx_cred: bytes, proof: SpendMessage, t: int ) -> RefundMessage: k, s, a, A_prime, B_bar, K_n, Com1, Com_c, pok = proof if not 0 <= t < 2**L or t > s + a: raise AmountError ctx = CreateContextScalar(ctx_cred) K_prime = BalanceCommitment(proof) X_A = B + K_prime + G.scalar(t) * H1 + ctx * H4 e = SigningExponent(skM, b"Refund", X_A) x = e + skM A = G.ScalarInverse(x) * X_A X_G = G.ScalarMultGen(x) tag = Tag(b"Refund", []) pok = Prove(tag, SignatureRelation(A, X_A, X_G), [x]) return RefundMessage(A, e, t, pok) The comparison t <= s + a is between integers (Section 4.3). This bound keeps the new balance below 2^L without a range proof over the refund (Section 6). The Moderator places the new balance anywhere in [c - s, c + a] without learning where in that interval it falls. 4.6.5. Refund Finalization The Client checks the refund and assembles its new Credential. def FinalizeRefund( pkM: Element, ctx_cred: bytes, state: ClientSpendState, refund: RefundMessage, ) -> Credential: kstar, r_star, v1, s, a, K_prime = state A, e, t, pok = refund if not 0 <= t < 2**L or t > s + a or v1 + t >= 2**L: raise AmountError ctx = CreateContextScalar(ctx_cred) X_A = B + K_prime + G.scalar(t) * H1 + ctx * H4 X_G = G.ScalarMultGen(e) + pkM tag = Tag(b"Refund", []) if not Verify(tag, SignatureRelation(A, X_A, X_G), pok): raise VerifyError return Credential(kstar, v1 + t, r_star, A, e) A refund issued for a different spend signs a different K_prime and fails verification. FinalizeRefund consumes state: a Client MUST NOT finalize one spend state against two refunds, since the two Credentials would share the nullifier kstar. The Client MUST NOT spend the consumed Credential again, whether or not the refund arrives. A Client that has sent a spend proof and not received a valid refund keeps state and MAY ask the Moderator for the refund again; [PROTOCOLS] describes how, and the Moderator recognizes the request by the recorded nullifier k. The Moderator can answer with the refund it issued, which requires storing it with the nullifier, or run IssueRefund again. The latter produces a second signature under a different exponent; this is harmless, because every refund of one spend carries the nullifier kstar, so at most one of them can ever be spent (Section 6). 4.7. Encodings This section gives the encoding of the four messages exchanged and of the Credential the Client stores. Element and Scalar are the fixed- length encodings of SerializeElement and SerializeScalar, of Ne and Ns bytes. A recipient MUST deserialize every received Element and Scalar and MUST raise a DeserializeError if deserialization fails, which in particular rejects the identity element. Amounts are uint64 values, checked against 2^L as Section 4.3 requires. Every pok field is a compact NARG string (Section 4.4) of (Nw + 1) * Ns bytes, where Nw is the number of witness scalars of its relation: 2 for the request, 1 for the response and the refund, and for the spend Nw = 7 + (3 * n if s + a > 0 else 1), where n = len(RangeWeights(2^L - 1 - (s + a))) A recipient MUST reject a message whose pok has any other length, and MUST reject a SpendMessage whose shape does not match its s and a. A recipient of a SpendMessage MUST raise an AmountError if s + a >= 2^L, before computing n. Rejecting the identity element at deserialization keeps every relation of this document valid (Section 3.5 of [SIGMA]). The Client opens issuance with its commitment: struct { Element K; opaque pok[3 * Ns]; } IssueRequestMessage; The Moderator answers with the signature and the balance it chose: struct { Element A; Scalar e; uint64 c; opaque pok[2 * Ns]; } IssueResponseMessage; The Client spends with a proof whose shape follows its two amounts, as above. The select clause distinguishes a refresh from other spends: struct { Scalar k; uint64 s; uint64 a; Element A_prime; Element B_bar; Element K_n; select (s + a) { case 0: Element Com_c; default: Element Com1[n * Ne]; }; opaque pok[(Nw + 1) * Ns]; } SpendMessage; The Moderator answers a valid spend with the refund: struct { Element A; Scalar e; uint64 t; opaque pok[2 * Ns]; } RefundMessage; Neither context appears in these messages; both are inputs held by each party (Section 4.2, Section 4.6). The Credential is held by the Client and never sent: struct { Scalar k; uint64 c; Scalar r; Element A; Scalar e; } Credential; With Ne = 33 and Ns = 32, the request is 129 bytes and the response and refund are 137 bytes each. The spend message is 468 bytes for a refresh (s = a = 0) and otherwise 129 * n + 403 bytes, at most 129 * L + 403, with n = L whenever s + a < 2^(L-1). The spend message dominates and is linear in L; Section 4.1 asks deployments to keep L small for this reason. 5. Ciphersuites A ciphersuite fixes the group, hash functions, encodings, and domain separation tags. Both parties MUST agree on the ciphersuite as specified in Section 4.1. ctx_proto is as computed in Section 4.1. The seed length, that of the random input to every derivation, is Nseed = 48 bytes. EC-ACTv1 is specified against the -03 revisions of [SIGMA] and [FIAT-SHAMIR]. A later revision that changes the NARG string or its derivation MUST be adopted under a new ciphersuite identifier, and so a new ctx_proto; EC-ACTv1-P256-SHA256 then never denotes two transcript formats. *TODO.* Revisit once [SIGMA] and [FIAT-SHAMIR] are stable, and move the pin to -04 when it is published. 5.1. EC-ACT(P-256, SHA-256) This ciphersuite uses P-256 (secp256r1) for the group and SHA-256 for the hash function. The value of the ciphersuite identifier is b"P256-SHA256". Use the P-256 group, SHA-256 hash, hash-to-curve and hash-to-scalar algorithms, and canonical encodings of Section 7.1 of [EC-IHAT]. EC- ACT uses Ne = 33, Ns = 32, and Nseed = 48. Instantiate every hash with the EC-ACT ctx_proto of Section 4.1, including the explicit DSTs of G.DeriveScalars, G.ExpandScalars, G.DeriveNonces, and G.DeriveKeyPair. For this ciphersuite MAX_BIT_LENGTH = 64, which allows amounts to be carried as uint64 values and keeps 2^(L+1) far below the group order (Section 6). 5.2. Randomness Every random value in this document is drawn with random and consumed in inputs of Nseed bytes, by G.DeriveScalars (Section 3.3), by G.DeriveKeyPair (Section 3.4) for a key, or, for the values listed below, by G.DeriveNonces (Section 4.3 of [EC-IHAT]); no scalar is sampled directly. Implementations MUST draw with a cryptographically secure random number generator and MUST NOT reuse randomness across derivations. Randomness is as sensitive as the values derived from it, and the constant-time requirement of Section 6 covers both. The following values MUST be derived with G.DeriveNonces, with the inputs stated where they are used; drawing them directly is not conformant: * the Moderator's signing exponent e in IssueResponse and IssueRefund, through SigningExponent (Section 4.5.1); * every nonce of a ProveCompact prover of the Credential scheme, through ProverNonces (Section 4.4.2). The Client's k, r, r1, r2, kstar, rn, rc, and s1 are derived with G.DeriveScalars from randomness alone; a repetition among them harms only that Client. The signing exponent and the prover nonces are also derived from a secret, because a repeated signing exponent, or a prover nonce repeated under a different challenge, can compromise skM or reveal a witness scalar (Section 6). 6. Security Considerations The Credential scheme is a keyed-verification anonymous credential [KVAC] over the pairing-free BBS-style signature of [BBS] as analysed by [TZ23], with the balance, the nullifier, the blinding factor, and the context scalar as the signed attributes. Because the Moderator is both issuer and verifier and there is no pairing, the scheme is an algebraic MAC in the sense of [KVAC], of the shape introduced as MAC_BB by [BBDT16]. Assumptions: Credit conservation rests on the q-SDH assumption in G, for the unforgeability of the signature [TZ23]; on no party knowing a nontrivial linear relation among B, H1, H2, H3, and H4, which the hash-to-curve derivation of Section 4.1.1 provides in the random oracle model; and on the random oracle model for HashToGroup, HashToScalar, and the Fiat-Shamir transform of [FIAT-SHAMIR], with the special soundness of the relations of Section 4.4. The reduction is expected to follow that of [TZ23] in the algebraic group model with a programmable random oracle: the keyed-verification oracle is answered from the adversary's algebraic representations, since skM * B, skM * H_i, and skM * A_j are all computable by the reduction, and the derived signing exponents are programmed. [BBDT16] covers the keyed-verification setting for the underlying MAC. *TODO.* Write out the credit-conservation reduction and the statistical unlinkability argument for the idealized scheme, together with bounds for the concrete scalar and nonce derivations in the random oracle model. Analyze unlinkability against quantum random-oracle queries. Credit conservation: Fix a Moderator key, a credential context, and a balance width L. For any sequence of presentations the Moderator accepts, with amounts s_i and return amounts t_i, and any set of issuances with balances c_j, sum_i s_i <= sum_j c_j + sum_i t_i except with negligible probability, for an adversary controlling every Client, provided the nullifier store of [PROTOCOLS] rejects a repeated nullifier. The right-hand side is under the Moderator's control. The property rests on the following: * an accepted spend proof yields, by special soundness, a signature under skM on (c, k, r, ctx); * every such signature was produced by IssueResponse or IssueRefund, whose signing oracles are constrained by the proofs they verify, or q-SDH is broken; * the balance constraints of Section 4.6.1 hold over the integers ("Amount validation" below), so the refund Credential has balance exactly c - s + t; and * a recorded nullifier is never counted twice. Unlinkability: The intended property is that a spend hides which issuance or refund produced its Credential, and the balance beyond the public amounts and predicates, from a Moderator that sees every message and holds skM. The idealized argument uses independent uniform blinding scalars and prover nonces: the commitments K, K_n, and the bit commitments are hiding [Pedersen91], the pair (A_prime, B_bar) rerandomizes the signature [TZ23], and the proofs are statistically zero-knowledge in the random oracle model (Section 7.5 of [SIGMA]). This idealized argument does not rely on the hardness of discrete logarithms. For the specified hash-derived scalars and nonces, we are assuming the security of Hash based pseudorandom generators in addition. The proof strategy replaces the derived vectors by independent random scalars, with a loss accounting for scalar-reduction bias, collisions, and queries to secret derivation inputs (Section 4.2 of [EC-IHAT]). Concrete bounds for this replacement and its composition with the proofs remain to be established. The public amounts s, a, and t, and the configuration, determine which Credentials could have produced a presentation; [PROTOCOLS] constrains these amounts, and [ARCH] states the anonymity-set requirements. Quantum adversaries: A quantum computer capable of solving discrete logarithms in G can recover skM from pkM and forge tokens. The unlinkability of recorded transcripts under the specified hash- derived randomness requires an analysis in the quantum random oracle model. This analysis remains open for this construction. Amount validation: The spend relation constrains c, s, a, and v1 only modulo p: the bit equations show that v1 = sum_j w[j] * b1[j] lies in [0, m] as an integer, since every weight is positive and the weights sum to m = 2^L - 1 - (s + a), while c = s + v1 holds in the scalar field. For this to hold over the integers, s and a must be small, and they are small only if the Moderator checks them, so VerifySpend MUST raise AmountError on either being >= 2^L, or on s + a >= 2^L, before verifying the proof. With those checks, s + v1 + a <= 2^L - 1 < p, which MAX_BIT_LENGTH = 64 guarantees, so the relation cannot wrap: the balance c equals s + v1 as an integer and c + a < 2^L. A refresh has no range proof and relies on c < 2^L by induction: IssueResponse rejects c >= 2^L, and a refund produces v1 + t <= c + a, which is c for a refresh and was proved below 2^L otherwise. The same reasoning requires t <= s + a in IssueRefund, which keeps every balance below 2^L without a range proof over the refund. Randomness reuse: A repeated nonce in the IssueResponse or Refund proof reveals skM, and a repeated signing exponent e under one context lets a Client holding two Credentials with that exponent forge Credentials at any balance below 2^L, each with a fresh nullifier, which the nullifier store cannot detect. Both values are therefore derived with G.DeriveNonces (Section 4.3 of [EC-IHAT]) from the key and the operation (Section 4.5.1, Section 4.4.2), so that a rolled back or snapshotted random source reproduces an earlier response instead of yielding a second one. An implementation that draws either value directly MUST treat every repetition as a compromise of skM. Identity elements: Deserialization rejects the identity element (Section 4.7), and the verifier of [SIGMA] rejects an instance that contains one. The check is soundness-critical for A_prime: were it the identity, A_bar would be too, the first spend equation would hold with r2 = 0, and the second would let a prover present B_bar as a blinding of any message of its choosing, at any balance, without holding a signature. An implementation whose group library accepts an encoding of the identity must perform the rejection itself. Nullifier store: The Moderator MUST check the nullifier of a spend against a permanent record and add it to the record atomically, so that a spend is either fully processed or not at all. Losing the record re-admits every Credential spent while it was in effect; the record covers at least the lifetime of the credential context it was recorded under ([PROTOCOLS]). Single use of Credentials and states: A Client MUST treat a Credential as spent, and MUST have stored the returned state durably, no later than the moment the spend proof becomes observable outside the Client, and MUST NOT run ProveSpend on it again, whether or not the refund arrives. An implementation MUST have ProveSpend consume the Credential value, FinalizeIssue consume the issuance state, and FinalizeRefund consume the spend state, so that no process can use any of them twice; a Credential or state restored from a backup after use is the wallet's responsibility. A second spend of the same Credential presents the same nullifier, which the Moderator rejects, and links the two presentations to each other; one issuance state finalized against two responses, or one spend state against two refunds, yields Credentials that share a nullifier, of which at most one can ever be spent. A Client that loses the spend state after the Moderator has recorded the nullifier loses the balance. Constant time: Bits, every operation on the witness of the spend relation, and every operation on the Client's randomness and the scalars derived from it MUST be implemented in constant time with respect to those secrets (Section 7.6 of [SIGMA]). The bit equations are linear in the bits, so nothing is selected by a bit's value. On the Moderator, skM * A_prime in VerifySpend and the inversion of e + skM and the multiplications by x in IssueResponse and IssueRefund operate on the signing key with inputs the Client chooses, and MUST be constant time with respect to skM; A_bar then enters the verification through a group element rather than a scalar, which Section 7.6 of [SIGMA] addresses for keyed-verification credentials. Derived prover nonces: ProverNonces derives the nonces of a proof together with G.DeriveNonces (Section 4.3 of [EC-IHAT]) from the witness, the session, the relation, and Nseed bytes of fresh randomness. Its security analysis must establish that the nonce vector is indistinguishable from fresh uniform randomness to parties without the witness, as required by Section 8.4.2 of [FIAT-SHAMIR]. This requires bounds on oracle queries and collisions, as well as the accumulated scalar-reduction bias of Section 3.3. If the random source fails independently of the witness, the argument additionally requires sufficient entropy in the witness that is unknown to the adversary. Verification key secrecy: VerifySpend requires skM, so a Credential can be verified only by the Moderator that issued it. A Moderator that shares skM with another party lets that party issue Credentials in its name. Context granularity: The credential context is visible at every spend through the fact that the proof verifies under it, and so partitions Clients into the set sharing its value. It MUST be coarse: every Client issued under a given context MUST derive the byte-identical ctx_cred. The spend context does not partition Clients, since it binds a single presentation to a single challenge. Balance width: L is public and identical for all Clients of a Moderator, and fixed for the lifetime of a key and context (Section 4.1). A Moderator that used different values of L for different Clients would partition them by the length of their spend proofs, and one that changed L under a key and context with outstanding Credentials would invalidate the balance invariant of the conservation argument. System requirements: The requirements on deployments are those of "Nullifier store", "Context granularity", and "Single use of Credentials and states" above, consistent configuration across Clients ([ARCH]), and the randomness requirements of Section 5.2. 7. IANA Considerations This document has no IANA actions. The EC-ACT credential type is registered by [PROTOCOLS]. 8. References 8.1. Normative References [ARCH] Schlesinger, S., Jackson, D., and T. Meunier, "Moderation of unLinkable Endorsements (MoLE) Architecture", Work in Progress, Internet-Draft, draft-jms-mole-architecture-00, 6 July 2026, . [EC-IHAT] Schlesinger, S. and D. I. Mohan, "EC-IHAT: An Issuer- Hiding Anonymous Token", Work in Progress, Internet-Draft, draft-authors-mole-ec-ihat, 2026, . [FIAT-SHAMIR] OrrĂ¹, M., "Fiat-Shamir Transformation", Work in Progress, Internet-Draft, draft-irtf-cfrg-fiat-shamir-03, 16 August 2026, . [PROTOCOLS] Schlesinger, S., Jackson, D., and T. Meunier, "MoLE Protocols", Work in Progress, Internet-Draft, draft-jms- mole-protocols-00, 6 July 2026, . [RFC2119] Bradner, S., "Key words for use in RFCs to Indicate Requirement Levels", BCP 14, RFC 2119, DOI 10.17487/RFC2119, March 1997, . [RFC8174] Leiba, B., "Ambiguity of Uppercase vs Lowercase in RFC 2119 Key Words", BCP 14, RFC 8174, DOI 10.17487/RFC8174, May 2017, . [SIGMA] OrrĂ¹, M. and C. Yun, "Sigma Proofs for Linear Relations", Work in Progress, Internet-Draft, draft-irtf-cfrg-sigma- protocols-03, 16 August 2026, . 8.2. Informative References [BBDT16] Barki, A., Brunet, S., Desmoulins, N., and J. Traore, "Improved Algebraic MACs and Practical Keyed-Verification Anonymous Credentials", SAC 2016, 2016, . [BBS] Boneh, D., Boyen, X., and H. Shacham, "Short Group Signatures", CRYPTO 2004, 2004, . [HASH2CURVE] Faz-Hernandez, A., Scott, S., Sullivan, N., Wahby, R. S., and C. A. Wood, "Hashing to Elliptic Curves", RFC 9380, DOI 10.17487/RFC9380, August 2023, . [KVAC] Chase, M., Meiklejohn, S., and G. Zaverucha, "Algebraic MACs and Keyed-Verification Anonymous Credentials", CCS 2014, 2014, . [Pedersen91] Pedersen, T. P., "Non-Interactive and Information- Theoretic Secure Verifiable Secret Sharing", CRYPTO 1991, 1991, . [TZ23] Tessaro, S. and C. Zhu, "Revisiting BBS Signatures", EUROCRYPT 2023, 2023, . Appendix A. Test Vectors The vectors below cover the Credential scheme under the P256-SHA256 ciphersuite at L = 4: the generators of Section 4.1.1, CreateContextScalar, one key pair, one issuance, and a chain of four spends with their refunds, one for each pair of zero and nonzero amounts s and a (Section 4.6.1). Byte strings are in hexadecimal, wrapped at 64 digits with the continuation lines indented; amounts and L are decimal. Every algorithm is a deterministic function of the bytes it draws from random. Each rand entry is the concatenation of every random call the algorithm makes, in the order made, including those of SigningExponent and ProverNonces; an implementation replays a vector by serving those bytes in place of random. The order is: for G.GenerateKeyPair, the key seed; for IssueRequest, the Nseed bytes of k and r, then the prover's Nseed bytes; for IssueResponse and IssueRefund, the Nseed bytes of e, then the prover's Nseed bytes; for ProveSpend, the Nseed bytes of its scalars, then the prover's Nseed bytes. The state entries are the ClientIssuanceState (k, r, K) as SerializeScalar(k) || SerializeScalar(r) || SerializeElement(K), and the ClientSpendState (kstar, r_star, v1, s, a, K_prime) as SerializeScalar(kstar) || SerializeScalar(r_star) || I2OSP(v1, 8) || I2OSP(s, 8) || I2OSP(a, 8) || SerializeElement(K_prime). Every message and credential entry is the encoding of Section 4.7, and the spend context of every spend is ctx_spend. A.1. Ciphersuite suite.identifier = 503235362d534841323536 suite.ctx_proto = 45432d41435476312d503235362d534841323536 suite.L = 4 suite.H1 = 021a3022391ada95c2e90ec5452609f54b6c72db02587405e64e23e1fb38e3a8 dd suite.H2 = 02db70cf75af61816a299e607b0cb664d682416a82f62a34515849d42645b13c 4a suite.H3 = 02eb51113045f6834d61ab89b9c1b29e136d07f37bbb7e8ad02a871bee85708a 62 suite.H4 = 035f47490268b4aff204f8dff672cf54b36e0d3c012617d480d1f7470734e844 c7 suite.ctx_cred = 45432d4143542d746573742d766563746f72732d636f6e74657874 suite.ctx = d685e534e574bcedc28a422bcc0f68267c59cf633771c1ad1d2a76a18f404346 A.2. Key Pair key.rand = d069967f203df4d58f4430bf9cb90beb883d542d7c89c551db69dce42570bf4a e3adacc80db708e282150cea9e505068 key.skM = 34e271d1d4e404dc78a38a6b0ca4b3abc68bc4806b00997edbb901f13846f5f2 key.pkM = 03263eb172073519ff125b897fdc7569e4bd908debe19208b59361e21e8e4cad 78 A.3. Issuance issue.request.rand = 90cc52ee9f2addb59f11528b75bfbf1c4cdf583ab8c00ab56193c6e78e451c38 cf21ac4d582578e7effb4a5af7bda867c14b1781c22924b02eb07e7a927fb859 e07b66ca27390f209cb05c52cf856e2020b6062f2b487698b71352f7e29bb10d issue.request.state = 10fc7a30fa9fd08188d3e32f3c41e9939a037700c010220403fdf14771559d46 645beb9f699d06abbe0346165bdebcf816b43b8b81c81010058d38878710906b 028c4625c08d866d63ac5fe067cadabf1e6432c11735dabd8127c0fe5fa42ea6 70 issue.request.message = 028c4625c08d866d63ac5fe067cadabf1e6432c11735dabd8127c0fe5fa42ea6 706708266fdc5bcd931f51be4ccb9b8cbb9944e9b89e52c2b980e83fe2c128f8 6f296f482c0ad3e5e389ab1a2f50be2ff4dd2dfa3ae79d2ab7c8d579fe7c34a7 ab69586198e6a59c857c1fc7db3ffbc8f74e3b448226431186283eae617f4521 57 issue.response.c = 10 issue.response.rand = 84e42dfed8e44d65a2832ab18f705cf12dc961978c29d8baf4fe0113def1e4f7 d73a60863711e7ea31afc5a8d59d9a6b6f61ebce002cdc2d70122518f2092ba9 368e21e0538b2a242f17b497a4814c6fb4e97ccf81025b1c46821c40c455dc24 issue.response.message = 03971ccde028dee2ef52509320cdfc5f924404f78f5f761d77541eda9490a4cf 99756a4528f1acc3cca034306dd3b1898c4676bb78083492b67c4466d2951951 08000000000000000ae96a894b413d3b7505f9f4cc24641980ae58a9d2de237b 0d911e69856535b051719ee0ad06fa8d00303e873ae6b545d5d38b8a8a4ae938 8115a30988534d0e37 issue.credential = 10fc7a30fa9fd08188d3e32f3c41e9939a037700c010220403fdf14771559d46 000000000000000a645beb9f699d06abbe0346165bdebcf816b43b8b81c81010 058d38878710906b03971ccde028dee2ef52509320cdfc5f924404f78f5f761d 77541eda9490a4cf99756a4528f1acc3cca034306dd3b1898c4676bb78083492 b67c4466d295195108 A.4. Spend 1: ordinary spend (s = 3, a = 0) spend1.s = 3 spend1.a = 0 spend1.ctx_spend = 45432d4143542d746573742d766563746f72732d6368616c6c656e67652d6469 67657374 spend1.rand = a343c4b867f1de824eb47951ed28c6d393fb2b9e31cab746571fabd63e7a79ea e967796ab6f8b28ea5166350134dd2137fa525deec68d8126f7a3da4505122d1 a4bcb891b5fe184da4342a2b18c7a4c3466e334aedc6307f90951384e07c5e75 spend1.state = ca281b89a42946e503c819b3ed6d820b4946ed075c0ebd41e37ff7260bad4fdf 27f5bd121fcecb0785a455d46d36612fd193e06d5648369edcdca6a6376704cd 00000000000000070000000000000003000000000000000003f8c8bf93aacc88 ae2c498eadc68269d46b0e2e0314adc4bd6df38724bead6f02 spend1.message = 10fc7a30fa9fd08188d3e32f3c41e9939a037700c010220403fdf14771559d46 0000000000000003000000000000000003e347ebc46950d96074e2033c46715f fceb842c81395590069acaa188f413c69903589508c33878a080df7b65d6f345 f9d6fdf241f73fc593f5de7c61286d7acf9b03b117a3e919906a670ee02d6825 b0a8a4e54456148b87a66a8c9fd53fa6faadb50208fbedbb6e37f02474acfa6f 1fe051b6b15b6d31ff4865791eab72c10e7479ca03431c5a476b7070a48862a7 4365b8a2c334f61141260a664a60fbaa6b382a2ed6036fccf8dbb72e8ed2865b 475737e767d4aad1844fecb4ce8dd58ecd81d79bb02402448ed5928af211ecfa 5868159713a2560e6e2bafee5fd48f7ecd5fd974e69a4f6c243ad036550dab1e 5a5ebd6a3790f2918f6d59d2314f34e0f7452e66dd7aefb161f87018749558d1 c294573c794c785a147de8705b95a61269065e236c813ed6e87c31af5aba8cd8 a71cb95674cb95d728c4789047bd4a66111a93ce9760203863bf0db78e0030c0 d64a9ec43ec3353d734bf3e7e42caeae6237f24254c778f0bb5a2669c678803c 70b775d00fc4ac01d145eb975815e7e934f1307041dbcce38cdaefee312780ab 7f61f7d2341f7b1aabcaab8e31c3d20424271c4b31ee5093685ff17dadf3bbb3 9ae219c770bea16537e89b9a7cb75d588ce229d04f80345c19c6a480f9553d92 fb6e0adef3926ce503c33df08ed5dcade21700c3f0a5d33782a0b145cd2716e8 e2332d9325fce4ebe1edb78e10d443e2be9771d842991da98910461d7cfbe7ab f0259a1923b2a2c29f66c369949ed4b2171cfc500f1a3e995a7b87bfe9443891 11a02ded19578826af776da483da19f31ce2ad566c34c1bf6d793cf41b152b99 3a6504d56ef53a73eb2a81a1551a6263b476c86bd181678922cbf7261ac4f04c 88b87c0d76b2cf9a4721250ce5ccb368ecd7ca20733151a9d59da0ce7bfb581e ebe9500dba0f5d8b1470e8be96f0d93d70f4743e35d566b12c7af686e26c6620 44ab3b8bb7f0fabc28617f6e0e292f709b37edd1d5c9e712494b7c9779997c70 624da9b9c9c86e129763826b1ccad68f1f17abb38e2804911986b1198ead1e7f bd0dd5ace2bbdc30d3e6dccee83711a278771a1063d21b60d7f8cdfa006a8faa 8d29a7bb281b0199b8c0d669b875828df17bc9c60f89589c8a518f2478104777 ebd6e96d2ed12be6af631bcb47efacf6855e7030dc9f847b40f364872d327c7a b242517f421d11a1d23acebfba61439c7dff4dff3b4d49 spend1.refund.t = 1 spend1.refund.rand = a29c40ee250bbc7f94fa1dfe1cd289fcc98b20049e52d83dbb5daf8de1f65d87 ea834ad411474fd77bd30ccebbda1e35261b6484a89d8ea28e306c7ebad73a3a 88a079153525b9bc4cf611118e7cd7fb40ed6636a3c6a6835009b24e8917ad93 spend1.refund.message = 03c5bdf7e351668081bdff1e550f95b07de28686632e223c829745839228f1b0 2f64c6ad4903faab1d30471713b00d14c8d6f74f3c892b6a7d9b320459d6b6bb 8c0000000000000001a0225cd9effadc5cd70ef74852d3257a27e1c75af96a67 21ff3fdc6271676f55d42d59544614da779fa9096cef8e27c9182ffd8694ec51 61a0324dc3ce7e0a42 spend1.credential = ca281b89a42946e503c819b3ed6d820b4946ed075c0ebd41e37ff7260bad4fdf 000000000000000827f5bd121fcecb0785a455d46d36612fd193e06d5648369e dcdca6a6376704cd03c5bdf7e351668081bdff1e550f95b07de28686632e223c 829745839228f1b02f64c6ad4903faab1d30471713b00d14c8d6f74f3c892b6a 7d9b320459d6b6bb8c A.5. Spend 2: refresh (s = 0, a = 0) spend2.s = 0 spend2.a = 0 spend2.ctx_spend = 45432d4143542d746573742d766563746f72732d6368616c6c656e67652d6469 67657374 spend2.rand = 03590adbeaa38bf6bdfc6c77e94f7d8852569c457344b438fddd0f9c396531bb 2ab3a2d573e969ae7dfdd74291f5bcfba48c3799336404b50f08b0965e98f856 3613284307e32f57d0e64cead84d346b71dc0ed9d134b2b206170df388389775 spend2.state = e2c5d5f6303fcd004ed0934169501daacfbef52e18badcb9e288b3329948e266 fe1588e420eb910b3db621ad75227d26aa18bbbec572884943b869010c574570 000000000000000800000000000000000000000000000000039097f13bb706da 42b83612e50e0dcb3b421326d6d93a52aa1ae13b6200f2afae spend2.message = ca281b89a42946e503c819b3ed6d820b4946ed075c0ebd41e37ff7260bad4fdf 0000000000000000000000000000000002af0791abafae532be3892ad2bb0d14 d8d4d6db82fb5e2732376fa962eced6c2102df52a12b26e00dc3e2c713438521 d783b65acad55ff9c7b63623aa5adcf00d5502089619e75542ff84944bdacda8 1fad238b286b38872a4818e9982df54b6202430316300ace17b546c4d319e104 1e14e1590e25f113b2fcbe4a2198a337653cd0af866e9537e9e1cdf957faf381 fc4718e29716d525753c21db484d9011040050df71e1bad74375a99c4bed4e9e e9cbda7d0681e5b5a7cb93e45f76442478fc1b240eaa8d5b5cae759cb0d03be7 ddaedbcc38697578ebc5b9539ec7fa4baa1b9e802db0a0dc2f8113bc4cd7946d 09f1281d5d405bf5f80cbff7a015ba5b258325165be62fc93643159803706e0a f64fc7eeb84f8e923b78d773ecf40f3a247b4707c9c6f3053f9d4c88490a72ee 8dd881aa5dc8f1ec7fe4b0a175db56e6a21d47e374bc81d1f71f9e762661772a 8ce52a6204920673ab459ad71d3f2a34177f20a5eb6fb1ae0f082c9a5e079cb7 00117118d2df6f08bbbc6c72dbc7739b5ef12af4392b64b0d1b8a6bcbf8edc10 f79b60fb97a0a77d10c2a7da08201d34af150294 spend2.refund.t = 0 spend2.refund.rand = 4af336412b38462f4341cc2c59890106574ac3337b88da1d48eb24a1e1cdfdb0 b0e8a47e9b37128c3058fa3d7c0772c1b81984214e8b1b925d56d19f3db0e760 3cbb462710dd437c47105c8c6eaf697eaa9882766c4eff6c503c804c143745a8 spend2.refund.message = 03ff6be15e4232edb30ef5578ee38559366ac33f06b8b90e795d3bf716c2102c df4bd6980945347f79711707ef8eefaaee85eacc898d130507ac1a8e32d7505a 8b0000000000000000582d8a5ba13e7fcce6e04a53612da436a06e925e97e219 ebd82be05e52b3131ae6553f623363e8ef02d9b9a2ea376ac74fa1b36c9d4bbb 8d9f01fb59c157020f spend2.credential = e2c5d5f6303fcd004ed0934169501daacfbef52e18badcb9e288b3329948e266 0000000000000008fe1588e420eb910b3db621ad75227d26aa18bbbec5728849 43b869010c57457003ff6be15e4232edb30ef5578ee38559366ac33f06b8b90e 795d3bf716c2102cdf4bd6980945347f79711707ef8eefaaee85eacc898d1305 07ac1a8e32d7505a8b A.6. Spend 3: pure top-up (s = 0, a = 2) spend3.s = 0 spend3.a = 2 spend3.ctx_spend = 45432d4143542d746573742d766563746f72732d6368616c6c656e67652d6469 67657374 spend3.rand = 24a32412b20712265bf9bd28b5d37200bdf1022dbc6243c71c8b0828cf40ef25 e03cf72bfcbe0e6b44b2d2bb199535aef568d662d65390a853e86f7868fe6dd6 bb3dee7e42a3c728a9185b7a246cac0bce2248632756ad88792481d1e942a60a spend3.state = 6b702de8c79573d19bae26457ce27017a2224b834e296eae4f0ce77673e443e2 70da003305191208879f280802f4ca421f63aec33a5446726ad4ab1993adfb35 0000000000000008000000000000000000000000000000020226b521f937bcd1 7b81cfdd877055077c4ed2788e9826351809692a61e9932fbc spend3.message = e2c5d5f6303fcd004ed0934169501daacfbef52e18badcb9e288b3329948e266 000000000000000000000000000000020286e279a9fa15d1a8850b35acada6fd 8bb66087d2a615ec02ae2308a590e957be021a50ddcc47d1b8e85a854ed385b0 9f783ea06b8d0098949c35211984a860977c0295842de3f0a9c00fdcc4389734 393df738a0e7f6ec08e65a8ed2580beebbf5a902f4d6bb38ba26f2d39fa4a845 383b1be2d8c2e2c9f59704e25352a882297c16cb02d41551f6f980addca30acf 704304280da75e47b21e444f300099e6926785b5520325625b86cb98543f70a6 5b240025e4a1912873c0a5a7239da2a8be1c8f756b7c03c932dea8cc13c6cf79 d87bc62b80f0ea1e099d4522edd7b16c6a65a08b89a780f9d66d55a9a577f51f 6738273ef3fbe2ccd917394bf1b7c2af0eeb7254c1c46120e3cc77cabff58a56 cfbb092e0eaa91ed78b3d6f7550b1e26281480fedede516f77d9a9b56b546627 cfbabb504a4ed5285dfe64be8d443671ede57f1b8e08acae8e4a173540fc14a7 a98aaf2d2b3f327ca8d52f349284311a108e6f157542f04fabbd9f9892266501 630c18868ed028469e9a8c307113efe709fa9a12e98c78a80634cf3e51fec896 12dfdb3878263da9b2ef4ee87e081fa522fb7c32a0b3a64a4499fe77b9d6e162 ef44ec14915b2a7674f2d374fddec708bfc23a89f4d0c600700ab7c8aca1a844 541ad72d6498f06aacc8587da53663b069e175b49f683cc491679858e7a39fce a6f6a3e421db93744e1595d42b1312bb6d6be48d44a3762a59f448cb9681eb8a 6df3d1085ccbbf9c1ffe541cc8c2eabd5b5d2d63383564ffc59b15212e01b548 5809c25d564810e2f3bbb76d71357576632bb3b147db29038dc5193380d5d646 0e8149dd0d19b44d65f8b1c76efd896f089640607666ee8698ab261da99949a5 7f963cfa368633a33dd2cafe012e3cee4588b9ad0ae5715c23743fb7b4b11813 541cb63fa58fcd8d125efad3536ecb7f895a83b77a2dc52c6f36a1def7a86834 080f7412cfbaceca985421911a234fa004fdf89b98743de6c5d804cb31a01536 310d55f9e9ff433b887ae62c291604030d8774675ec065ca0c6df5c9d7579737 56a6dbe61cf3ee929ac92482c2bf450b0092502668e5febb4919c9bb225edbe2 5655c72140f79d3bebb653580e3e74a7c75f0e9984d2a28179ea14cc155a4ae6 d8d0f8164cc34b7a541eefdf941e909294b2c779ae8c24707e3ca622532753ec cc6eec224e3824e7cb95dd884dbee608d7aef28fe86109 spend3.refund.t = 2 spend3.refund.rand = 91389375873e00ce89289d026a010ed01427848a7ccf88f16edd7013072dfc65 2017448bba631f9f0975ac017a8e5bebae89b3a36bd2a5710a32012af07d2457 39c1aac9a875d43148101a6fc3f6ed92ddbceaf6d47c233eeeb153406405b579 spend3.refund.message = 028eb7785090e04bd06f03fe909618b2e2c0a6cd4e0aa7a3f20ae41ad84fc9e5 8b991560e17771ba4439ee3ac5684e61bc08c8dd3ea2e4739aad158cf9727337 3f0000000000000002562ae12c4158aa88bae62147a3bd3364e62bddb9e5cc45 5585387a2d9ed9451a399bf1451e583dc1e1882b371adf106016c386267cfc19 7674a4252d0f0401bc spend3.credential = 6b702de8c79573d19bae26457ce27017a2224b834e296eae4f0ce77673e443e2 000000000000000a70da003305191208879f280802f4ca421f63aec33a544672 6ad4ab1993adfb35028eb7785090e04bd06f03fe909618b2e2c0a6cd4e0aa7a3 f20ae41ad84fc9e58b991560e17771ba4439ee3ac5684e61bc08c8dd3ea2e473 9aad158cf97273373f A.7. Spend 4: spend with top-up (s = 3, a = 2) spend4.s = 3 spend4.a = 2 spend4.ctx_spend = 45432d4143542d746573742d766563746f72732d6368616c6c656e67652d6469 67657374 spend4.rand = 4d1b8c9d578f0b4bcc0de3b557693579cddf27a975168f9cee97d9b83ecf0bd2 efb0406a9e5eb1a7aab1db0b09e8d3b8bffceaff6e6136f97000c3e496b1a2c2 ce820e4dff38c6c020ce794d93ca508a237b75ee6e2b54873ca80d9e915fa255 spend4.state = ba2cd13a8b2e9aaf59bc2334a375fd9b66c9494591053370786ef3732ba7fb93 0535b57b758d30a850589bd1f682d8bc63db1a80976a7d9ccc95a8b900cdde7a 0000000000000007000000000000000300000000000000020344be4bd9b4dac8 589e9d36498ce79c0e91990edfa625d3feb4a9b394fcac9002 spend4.message = 6b702de8c79573d19bae26457ce27017a2224b834e296eae4f0ce77673e443e2 0000000000000003000000000000000203376f8d281c2741e9ebe8e172af5b26 9ed6231737b566f572f4030abcd7d3f79202095af656b0913713805f5dda62f4 a4e91c78bc23affe077b3c0558d5b984e8f80226a4f630ea7db3ce072ce8adb0 23ebb49fffb821d5e4ddcf6bf5439043b734c0025660136119f3e7a30193b3bd cea91942af6e08bf116793255da24677e82c6c630301b2fa79e7288810c4f48a 69e610af3febe9b5dfd68d71aaf846f0ed8bc3a933022bf3de971fba0d19a955 4d7d9ede4e6f3883bcd505a49f7976ba211fbbb7d17a034924ab1b22c67e23f3 a8752ba95133f9315cd28233e7a0dd3e4b884b63ab6a4a68183548e2bdb374d7 40a4e48ee490605e8e634067782f62a4852a5205dc8ef6b54ad054fd99b5830f bf095ddd2227f670da4c4a4531eec1dab8197d6e5f3c7c0a4cbbf1ad10ee1e58 e8d8fc7582a5a5821c0f695b03791b36c9115c869b601ac6a93610fb375aa09f 2c0086e5b6e15fee782bcc1b0e399dce7a4f447e978dfaf8e0cc1f0491d66c0b 9ef4c86b0faa6700892a338d2685d534fc4a722ccb03701095dc3ddfa3b4a452 e9736037055499e1a70b0def55f86685a7227f883a2388e711639042c2c41f88 e694f3b429eecf6dc86fa4211406eb7d84715b07371defff565f0405aa1cd97c 561ee1db3bf4a2937c4e3171db862f8eb418bf7116f9b662ae59dff6c6d3f816 59b178725bfe43470e85d6daeb3eb73a2c0e784c13b71804179f524b9ed78d0d 56bbfce5674b85f2949c84d411a7edbdd7e1890901c06e996f988c255d74a876 68b60e594b6d5f3a5a47edcc1bb2bf256fd83fe949f5fd95a4bd64391d6126e3 c18a85fda5287ed937d1a1dad39a73400f3c5f87c3dd2a6df764c7e394bd3995 4e87b91bec790466900e658fe2f76d58413c06590003e1b48f36e7c8fc570236 9fbd660537888b21b65742777040404b45680d1d7d8a2f2923be07234dff626d 9ae2815831a89a4e3ba2f19da409527d9ab3abf7d2d6b5aada511774db5b9acc 5ccaba3e8cc930dc1b545e1934f9ca8178c12188a3c3dc285f5a5894815f9940 76af4a2d7903b5fad0c182b36bc124284f93fda7e3f0a8f29772f5033eff0680 559eba60cbfbd50fa76557956c4853ee9d30688e2ac376683193ee488bccb43b 64c7ba87c11276aaced1deeba9682971fd52a18c76cf10ba703f3134d79ebc95 b124483f59fd68519812f4abad7481c168cb8443de94b1 spend4.refund.t = 5 spend4.refund.rand = 322efd511af210cc6ad1e36ae1ecb099cb69e5bff36b940636e4027b623f2471 898b77c29145b2ceba5d1b20e87cac314ea10cfb902d280ed020163dc301219e a1f437f9f71a35dceb6770d090d2c30a41de21f86a485a6381a43f50167d4582 spend4.refund.message = 02b67226fef87f1f374cb9554faece12c083ee1d92ac1e4c8d7252d05cf150e5 0deb0ff6cd0918dfbdf5576fa52cac658a4b81b2d14f7abe7ab4d9bd981afd74 530000000000000005a489963fa647f132a55799807190de75be9b10a58a369a 992131ff9390337a3e46a9d2eff83f96abe347b6bcecaee752488a3440198185 4620ef4fca3d4caa5f spend4.credential = ba2cd13a8b2e9aaf59bc2334a375fd9b66c9494591053370786ef3732ba7fb93 000000000000000c0535b57b758d30a850589bd1f682d8bc63db1a80976a7d9c cc95a8b900cdde7a02b67226fef87f1f374cb9554faece12c083ee1d92ac1e4c 8d7252d05cf150e50deb0ff6cd0918dfbdf5576fa52cac658a4b81b2d14f7abe 7ab4d9bd981afd7453 Acknowledgments TODO acknowledge. Authors' Addresses Samuel Schlesinger Google LLC Email: sgschlesinger@gmail.com Jonathan Katz Google LLC Email: jkcrypto@google.com Armando Faz-Hernandez Cloudflare, Inc. Email: armfazh@cloudflare.com