Network Working Group S. Schlesinger Internet-Draft J. Katz Intended status: Informational Google LLC Expires: 18 April 2027 A. Faz-Hernandez Cloudflare, Inc. 15 October 2026 Anonymous Credit Tokens (ACT) draft-authors-mole-act-latest Abstract This document specifies Anonymous Credit Tokens (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. 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 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-act.html. Status information for this document may be found at https://datatracker.ietf.org/doc/draft-authors-mole-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 18 April 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. 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. Anonymous Credit Tokens 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 [IHAT-ROLLATINI]. 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 ACT configuration (Section 4.1). Shared group methods from [IHAT-ROLLATINI] use the ACT group instance G, which stores ctx_proto and applies 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. 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 [IHAT-ROLLATINI] provides. Sigma protocol: The compact non-interactive Sigma protocol of [SIGMA], instantiated in Section 4.4. 3.1. Prime-Order Group ACT uses the prime-order group interface and the Element and Scalar types of Section 3.1 of [IHAT-ROLLATINI], 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 [IHAT-ROLLATINI]. 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 [IHAT-ROLLATINI], using the 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. 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 [IHAT-ROLLATINI], with the ACT group instance of Section 4.1. Both return tuple[Scalar, Element]; 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"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 [IHAT-ROLLATINI]) 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 [IHAT-ROLLATINI]) 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. 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; 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. 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 [IHAT-ROLLATINI]. ACT uses Ne = 33, Ns = 32, and Nseed = 48. Instantiate every hash with the 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 [IHAT-ROLLATINI]); 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, s1, and s2 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 sum equations 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 [IHAT-ROLLATINI]). 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 [IHAT-ROLLATINI]) 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 [IHAT-ROLLATINI]) 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 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, . [FIAT-SHAMIR] OrrĂ¹, M., "Fiat-Shamir Transformation", Work in Progress, Internet-Draft, draft-irtf-cfrg-fiat-shamir-03, 16 August 2026, . [IHAT-ROLLATINI] Schlesinger, S. and D. I. Mohan, "Rollatini: An Issuer- Hiding Anonymous Token", Work in Progress, Internet-Draft, draft-authors-mole-ihat-rollatini, 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 = 41435476312d503235362d534841323536 suite.L = 4 suite.H1 = 03c16cd46e4807f2e52d460f03b5e8e11bffee91d9c8bd7e1225c384eaf39cd1 ac suite.H2 = 0282697cb8474f49d091da0ac43c72353809a1999fea49a00a37037163279932 7b suite.H3 = 02597d0ba6b076b6c829b7253943ee3c4a796df35cc38bb6bce03e5398b14f02 dd suite.H4 = 02328e1b98965b32bb63af661190c98042ff24bf23c0fb1d5c6c7b38a3006bcd 80 suite.ctx_cred = 4143542d746573742d766563746f72732d636f6e74657874 suite.ctx = d1dda6b4b7a3ea5a1159fdde5652ead672730fed9941ca17f3a41220ca379b9a A.2. Key Pair key.rand = 6b04e6141f669f915de1d7f0ed8b50405bf6fff65b3ef4a63b6610586ddc5e62 b22c98437bb5746647de22933c80f522 key.skM = 6a998a9bd63b4efcd641c1e0e13e4dd6035f1ce748a6aa1461885e0454c928e1 key.pkM = 03513bbb46b2af7f4ed5304505528b96345064e50917e098630286c89383da72 22 A.3. Issuance issue.request.rand = fba90a5829478c7590e20f1a6911d2b4f5a9a50bd4485c309c94fedad95f1f79 0e0bc9994cd3c165d19b78a7e328de6e2c4f77e3aa99f8050f4a0c7d4b52a219 4d33a5ea903a54a02a807cd6cdb7c269c2712669bbe6f5dfb13495eea30b7da9 issue.request.state = 45f6c6354b3a477530da6a339a6647bf1a18738f3e36197861ed0a435279c3ce 6db698dc0ab0a626f2047331981742f4504932ef191ced0db687982bd5813eef 02bc28c7238986ef41811943cfe8ccbfcb3cbe933f8a583f2190dc4187133197 63 issue.request.message = 02bc28c7238986ef41811943cfe8ccbfcb3cbe933f8a583f2190dc4187133197 63a204de1dd76434170a6b1c0a07c3836307c53a17011b1f899f1bd6c58712ac 86a4699524580fe3d4d305a96c18162b692adcecfd2c0ed4dbb04ce4aef93212 1b5c845702018bf287c839f24ee7cd6a3f43494249fd82f78bed467e1d6ab94b 3a issue.response.c = 10 issue.response.rand = 549a65b32ec23bbd0434f3787a143ef286df7fcc1a4afa990c0497780450f987 a98919981c0f9481c4eb6ee73b79569a2ea0fc45c81480ed4451367947c1cc19 ee957b16356a0ae63d6c83fbd9d757e69fa8138ada38975479fd20c98160eb78 issue.response.message = 0219c7ca1c950892496809a235512c49773a31a35520647b92ee8a25c76f9aae 68bed08bbfced4c5b6eee438a45ca29d878db075b304fb58593a93883e7c9030 89000000000000000a229f0198688717aad558b8fb63fb983570067b333cec52 cdebfa26c0ee95cfe98d094fc061ecb10259a0fb576e39907f9ed97438cb414e c162a58a26f51332a5 issue.credential = 45f6c6354b3a477530da6a339a6647bf1a18738f3e36197861ed0a435279c3ce 000000000000000a6db698dc0ab0a626f2047331981742f4504932ef191ced0d b687982bd5813eef0219c7ca1c950892496809a235512c49773a31a35520647b 92ee8a25c76f9aae68bed08bbfced4c5b6eee438a45ca29d878db075b304fb58 593a93883e7c903089 A.4. Spend 1: ordinary spend (s = 3, a = 0) spend1.s = 3 spend1.a = 0 spend1.ctx_spend = 4143542d746573742d766563746f72732d6368616c6c656e67652d6469676573 74 spend1.rand = b429f745895b193f227dbad7fd1798339020f1ebb88b5e0429485e01bc34f6db 93c846f8766d5ec446946ef0d9c6a44873a0ef46a50e37eacf061d40d7440e34 516d54ee586c67dbb8fd0b7e931fb60a1731d3867e50bbaa1a7422f352070618 spend1.state = ee61f8215ebb0ca69e29a0bc91cf184347223292ebc2d3c7b69fb3da1efa92d0 f7e364957859b60aec035e4b0bf64678b09f9d6c5f1c1027a788436ca7cacaa3 0000000000000007000000000000000300000000000000000232345a585cb476 6f98318ac4809ce88f4b1337ef21f0dd4407577c17a4ccc800 spend1.message = 45f6c6354b3a477530da6a339a6647bf1a18738f3e36197861ed0a435279c3ce 00000000000000030000000000000000037462d06d0e1857691e39027fb94ba9 87ee4d626b73957ef2577f4f7f80afdd8f031fd0f1ae300ef6086d3d1badbe53 fdc73af5a3a7c3c7e94d31dfc3e200f1b14c034b324fe0ceaf20d9c14192a366 9dde3854b90cbde819714214408c2173927e0d02e0976db98ca955ed1b86f1e6 f5be405c855fd65b3c6648f26851324d0cd17d5502d9013827da0c05338a3319 760b4d1d6804380d6f45614e70053a7808f4fa66ae02e2f88017a6fa3c10ff1c 9616a4b5db3837b0c9642f43b8ede8af8942e9a8a6bb02143dcd3e4c15cdd475 a94dc06eed156efed163051862ad7016ea1b90d4a646f7848e8dbc81145e73b4 a42094a5383ada620477c12da708f43cbbd6eb0f9cb30a8ddc23a908056971d7 fa2a7397c19466fe33eb3e824863f99e6496651cc669e20adac112c627a2e40b 4a56ce28d75f9d59de1c39e80096a2de5ad3bbe35219ecddb01ff085da60effc fb9f2aa8e3276e55bfb5a080c17f403453b6214d165214eb27467bc9e04cde42 c79c007eebc0c8ec2b4b941766006bc3f950465ac92252b8fa03beced19c03b8 426c1772d50fc06b3a3be87f36639474961892b8e4e71757808bba75bc92afe5 83e8ba2b4aaa2e0b761893248e03d89dae765b410d7c289ed42b934a8652208f 970e64de474d362dc4fd360603b7822d6fbbd5e9eefb4f7b9d7d70795e3650a2 26cbb69d11f65bfc4055462b2645b485e7b943448aadc7dac30e2f9f12ce4868 981ca89cfc8c0a75dfe7e664da65f9b9f8f31d4777d0db35a6ba2420ad5aefda e6273585403b78a1d0132383875488ef3cd6cb20dbcdd81ecb5cbf1349d05840 26634c125f72edbc18fa52fa9b4139794ddafffe668592b623b59af55af2d957 d0c7c8dda658e8847df3366d08c97826dbae3e22674bef611139052c1b9a3c79 0cb957e2b72bc4601f59f8eaa48f7b1574c18bb9c662243695528ace7be880d9 151574c6990893228af3876e4289eee997a5bff757e165c7f2578687b75a2b64 2bdbb6a888b030060573e5db031153e6f13dbd6317bfb51a9f85d90305120ccd ef56e4cab06c5741a5799b1db53f087b5310b037f9caf53716d5d8e5d74b580f 6bb32469a3ef5a5c55a1c1073ec761013409734fdc04f62cefb9f63a7c6e8710 31bc0cc48ee1391c1aec4301764551985d84b488770ca4db2cd2d9fa982df8c8 db892d201048d18585c314913f87761b2bad1c6a9fbc01 spend1.refund.t = 1 spend1.refund.rand = 6e51457124402a73bb25609f1647ee9589b57e96c8bb852d18ed16dc8b9c00f9 79aa90e88908d06ea7ee259a2067e8981aa128ca2c16d17c0b0b145464a18fbb c8c98bdcc8a5541494a93478aece76eb306943b6dcca8781fbc382b4e7801c6d spend1.refund.message = 03d459d1901021a25d58dd90172550e220fe4f0d6991d4bd0c8a1d525912c52b 7ce59c090dede25a7af0262e28c095444730695fa793a549697c9b310980ff2d 980000000000000001cbdc26a6e1426ebd8efb28c38460d07afff64b97d4efb7 1c09357036e670ac6e93553de0af83b16236fc1228fcfe89a63c0d9436f2bcf6 595c70e2b0e747e2dc spend1.credential = ee61f8215ebb0ca69e29a0bc91cf184347223292ebc2d3c7b69fb3da1efa92d0 0000000000000008f7e364957859b60aec035e4b0bf64678b09f9d6c5f1c1027 a788436ca7cacaa303d459d1901021a25d58dd90172550e220fe4f0d6991d4bd 0c8a1d525912c52b7ce59c090dede25a7af0262e28c095444730695fa793a549 697c9b310980ff2d98 A.5. Spend 2: refresh (s = 0, a = 0) spend2.s = 0 spend2.a = 0 spend2.ctx_spend = 4143542d746573742d766563746f72732d6368616c6c656e67652d6469676573 74 spend2.rand = 5df6aa3f75855db9c6eb9130e5fe4c7ea4173f47f405a3457518d227d0b83b1d 342ecfe0498cdb99911dbee6f1c2dd66adf61aba8249bc8b66dbeab6351dbe28 20ec0bec6a0fca3e98ae91f7d3ac00833552cda8198d8a585552c63b1b04a7f9 spend2.state = fd4592710665751a2ea3160ea16fe18ea7986f5af765533d3985625d938456fb 5d634dda9fc8375ac6d2daa51cc50aca954fa420078c204349e1889a77d8b746 00000000000000080000000000000000000000000000000002396aab2696270d 5a81eefd48c992ed28d97873ab3685270a96f5ffee33bee181 spend2.message = ee61f8215ebb0ca69e29a0bc91cf184347223292ebc2d3c7b69fb3da1efa92d0 0000000000000000000000000000000002e68a1d96c0c023dd6d76b420b19a96 d44f9559bb911a3df744b18774111967cb02d848785598ec84ac1bf8c73efaf4 531fc309e0a98bdf232d6c66fbb5c844c0ce02ea09ac5c4013a19262e8479449 232c4b8da8769a34a13e01bf14175454ce2e4f0271058492ebfcf5032a715308 0bf06e4523f1566d331eb662e9009c304e8dd567c77dcde0fe277443f553cf59 bdc8754dbd078ff31a65cad623c3e19a914399bdc1ee32d9991b2c2615094b39 7bc69fd4ad423ed7eed2b72f0e9dc40d7a1410ec7ec45a47f7ba66ab61cf55a4 3bb8e8802355e5c062a20be8512c61bb4ede40586e8a4afc48ee5afb64784e05 fd8539f22b90c6ce32af98cd2d9ebbd42072c3596c7924efd2350c0f8c808347 b15964205c1bb9b2adcbda0dd209afce6733639797eabc41705faa9159f4d177 37ad6d6d7a0ebed0b52660149b06d4bd7d68ed91fb05b92531b18ee526867600 39abead792c71a39e788c067b1d850358e8ece26239daf6e3c5185a57406e895 a09bcda150d32b07f15db27b8a70cc7087f498710f71bdb9a148e7cd31b02ca9 ed019abb611bfeff955b874a1f9a45e3d2ea5044 spend2.refund.t = 0 spend2.refund.rand = 8ab63a8b3af08128540a7dd8c6b84b892a3f0b2d6c3120b8ea53b7f40044860b 33cb6e9930b235d1ac9a7c06261a2b26bba151899bb31a45a2988c425a2637fa 4e1a0d285364f8fa0cf0e0610feda3d4b9980d16bd3da29447ab44b49e3f4713 spend2.refund.message = 02e4ca240712b4248e90bec8ed76f5b5a7008c65ada19c553478dcdc94576686 8e2cbe8e60e995e1443af12dd9b0487fdd1c6ef29d12c12459afeeb16baffc85 3c0000000000000000eab936b5c1e13c20f73a1ecacbc286f2ac7afd4f748844 66435e2c12ed1da13a9a90d61edd767a5be2c6d00796b231311dc58e9e53208c ddbdae9f0db8440f96 spend2.credential = fd4592710665751a2ea3160ea16fe18ea7986f5af765533d3985625d938456fb 00000000000000085d634dda9fc8375ac6d2daa51cc50aca954fa420078c2043 49e1889a77d8b74602e4ca240712b4248e90bec8ed76f5b5a7008c65ada19c55 3478dcdc945766868e2cbe8e60e995e1443af12dd9b0487fdd1c6ef29d12c124 59afeeb16baffc853c A.6. Spend 3: pure top-up (s = 0, a = 2) spend3.s = 0 spend3.a = 2 spend3.ctx_spend = 4143542d746573742d766563746f72732d6368616c6c656e67652d6469676573 74 spend3.rand = cf496508c825bc711aac384f4620152a8d3fc3a2d1943dfcb9cf2eb9fdc410c6 f6dfbef97a289fc6550c72f79daef1e54f5cd0132ead59cf513fedf1fc5a05aa a0255b3f53c1ed12fbb96bf52197b059d609431451b117381a5a06bd4be53ee4 spend3.state = be60dec14d630dcb30a104bf6bc798bc60cf5ca35911d94bab03a27694bc5607 2e8a14c471534ba8155f3ce6b5c35a80fbc01eb46436a5a838627f3812d58267 000000000000000800000000000000000000000000000002020f606f4ab5e448 836d62a5b2ea85f96292dc0912c140aa960a7addd23f4d6d01 spend3.message = fd4592710665751a2ea3160ea16fe18ea7986f5af765533d3985625d938456fb 0000000000000000000000000000000202f4e02b51f9f89f1fab195e19148e57 94e508de92a21d3fdb5b171877a44024bf02a085171bcf8e6bf704798b88161c a627e9d027a5ac2865cc4a0404ac1c0fc45e02b9ab938fea505dc8d1b61bfacc d5332c589877769039c6f5b10602195b9f90dc031d559d967a630462c4670990 674bd15289a1adaa37cac7f4ed83edea793aca1d03f235923a0186e270e8a766 1f8e9610dff7992803c94d8f363e509020ff55451603e6cc28fb65a2cd61ac86 5a38b208dca577799e9a5c60c6ed450a0c86051d15930389c406d1dde685f80c e5e99b59bb7ce8c90bcc89fc4cf20bd9974279cdbcb0ce07d6a7819ed9526b3a fe35fc5dab5ac6867abfa2cc0af0bc9c36c20608e8e8f6b295c8a5eef6a720a8 67f411df1183277d6050ec35d28944b4affc8a4205b2b3c383c28b9d3ba52fdd de3e9b232e63a6c008549947d6bbdc951ac7cbcf6e3fb6296a861c5fbbf546a4 5fc7ea51b2e159d6c359abbe3196ee91f938ec4006c6ab3fd60161096009a07d aaef82be6832bec4e29a6698ea8aa96f0e43fdd81dbb279931cd94e137b364aa b095fe6c91278bd717bd5e6db18d2fcfb332cbbb8edfc1eb247adaa72ba20af9 6189fc4c9a1075dba798ad25692e5b16a5a909dd7f0a31e8f5ccd696b6af26c7 4537e5c258d964539de3e54ff7634cecb65dd498953fab93ef86bda084b76d23 98138cd4023718d34f6df824c70322f1aebd01128e70562291a2ae7fcd376926 d998fe8f6f9b1998f6c358d50c09572e663454bec04b5f6cb5a58e05422d9bb8 142f9bf2d2a7594d4d46784361b647a03b512a3e63a10e99b245f53ae8b620d1 ac1784204ac453c4a7197776a020b6135f242f4d5a11e1ff8191b86703e9aa13 f765ceea505d4850ad7e0ec395545445bc6df1df613d6067b18fa883cf400257 486b93d2b72f1cc1cd08d8fde3370e51c4d79750fa4eef4fb22bbcca8b87665b a24b2c24b51e2736f85c5e24ae913aef5fffedcb6937c21d7f71387d6a1c50e6 9d787fe5e4a6b037dc0e817418c4fac3876d6b33d4cba3396d7a71d2173d3a8c 18c30fe499b1f947ff76348265d1b33b841ecf4f14a04cf49fc6f2ae6d9c6940 c0e867736470424f8fc8eb0fd9887100fa8e870c23260b03cef3f803c2110312 66a9906ddfdf7603b56adddc2437ae25b37b960b35f56ab1f0571092aa484039 2b87292f71ace4d260868e8d4978f73377d7ff3d29b37c spend3.refund.t = 2 spend3.refund.rand = 951a4f39b3225c1929b35820269ac76d585baf0dbb927cd59f35addd001762b8 ca25145e7c4159cb30f4cadac6b2f274a710f8916b01e7aa657125ac036e074c 8dda6de23ecbfa7dba2bb3f490655c25fef2bfc2f3786596045b68563feeb24d spend3.refund.message = 02845cc8aaaec2423bbf14d19685f58a139416dfe5d0fb7ab855cc50dbf063d7 0b918b2bccbfdd4cd8361b8c93d51d7231a77a84acd6f8c46e32df76cf6937de 7e00000000000000020de347e009d1d494a6620e6a32185d753f1a368e85b6be e8e7b2998775d4807a9641533e73c9d52f69453ada2d7d201f3f15c4ec9c9a5e b0a460dbba9d4ce10b spend3.credential = be60dec14d630dcb30a104bf6bc798bc60cf5ca35911d94bab03a27694bc5607 000000000000000a2e8a14c471534ba8155f3ce6b5c35a80fbc01eb46436a5a8 38627f3812d5826702845cc8aaaec2423bbf14d19685f58a139416dfe5d0fb7a b855cc50dbf063d70b918b2bccbfdd4cd8361b8c93d51d7231a77a84acd6f8c4 6e32df76cf6937de7e A.7. Spend 4: spend with top-up (s = 3, a = 2) spend4.s = 3 spend4.a = 2 spend4.ctx_spend = 4143542d746573742d766563746f72732d6368616c6c656e67652d6469676573 74 spend4.rand = a0197dc7f5d2128dceb0e4ac42a172d763b6d625df1d4bfcf61fd5fbfe7e222e 35144cbb058151ff0eba61a90575de984665140d18c49d11df66f7b77bc618b9 60599627a6606376365da00272f43dfe020306d04c39a7dd3c60a26a23830521 spend4.state = a869aa59e7dbd1168f9c4edda1fa4529d5043b9a19101223228b773a941f4c17 969cbf34727198c57a5fc735139b4ce8980fc8d5c41789ebbb52e630ebbfc628 000000000000000700000000000000030000000000000002024f97f8431c0246 608323fed9a160b0fe17c38d71874e0e33fb802f798e95cff3 spend4.message = be60dec14d630dcb30a104bf6bc798bc60cf5ca35911d94bab03a27694bc5607 00000000000000030000000000000002038f7439520ceed497ae6834a6b239b3 9efba07cc708864ca4c9d73496efe88d4103bf1d582e87e40742b91be24fdf68 438e2f72ebf3a22ddacad4fdd73dbc3efaa002f1be336d8979887881554c386c 7cc0951b7f38572d1febd542a1b0145fb374c003359a94092b8d9c5b05e0818a 574f76c84bd9bf7d999d5d2a8e10a4fec59282460317b52c87074a03790419d6 ee5f92f6187c7d20c00341f76f7f43bc7705c0d92502f93967ce41252702b13e 2f53b8e13c5b90a4cb340ad22e0c43e5d4bf45cf82530316f790b968ec4a5444 e2153fb03ddda25a1383ba4160543fd6454352137f1174d0131f716ac762b3b3 aacc3c09d030050c49f5be1c6044997369d3f8f0c9076bad302f62b16181ba19 f96ea24a5491c2c0fbcee71fe4240bf7a0f8105a074b8aa624b55478b2f9ca61 b7c68f7b9f2059ba0f4ddaffd66f8d2bd070236bba498ee68e199a417b7782ca 2a178feba27e92276e0b65ad79f39ce85a6bca0fb16154a326c663da8685ecbb 7267d9833946cd50fb0ae5f71ff77caf55fbce0ab23a1e17fc4e297805f4d20e 5c367fb0329557f7880596b2b134c4629e61bca73a1d3e61f42175f70c2af5d4 6bdd75a72837628858bd0fef3e39e6ab1eb732773a9ca1aa924efccdcbb2311e 6497c635a9fb75607fb94c26dd0786a830bdbb41d202470b2e6fb3e6c562caf6 e28068a54d57c17c1853f7e05e287ae4aafc8992aacaa0ced3a7aa936f6d4a79 a0de667fc068a03c1449359d47924b64c46328f6fb4c4e47a3a2eb41eaf56f0c bf27e613e82a5171e31c069651d6696ab2b09b0c9ed8d9bbc00ff75122aeaf5f 092be2894b2c9da98a689f50496dd6adfc2c0ec79290ed27c2b8c4d75d691112 8922a6e55871e8532a0f5e518eada6ea50e407e2d46253624c059e33d98e1fc0 d1dc9618735a4ab1b86ccc980e54fd37ab24eb72aa2119d9cd3ff3e392c1a589 f360f849e423e8f5881893e38f28b3816df294265d27b0d2ab09bc76ce504f53 d294fd93bf0f94d529cbefbb7145ccf3d7a73a6d52e317d5816a2f4bc35ab529 11037d53c446669dce370ffbba22a44cbbfc549449badf2167c77f1ee2cb30e5 b21b1502c2a65a1f7e416cb48f4d853d0c50ab31643d91c108c9f02160dcd994 dabd5ffba92dbbea3c1e6652a6f0be81996f48e020bf989c0a61254e3c5b8026 91ec348f88aba609de99052b75e6082e5a8a26d681f476 spend4.refund.t = 5 spend4.refund.rand = 752dc7ce9c6fb3e70d8c88a6bb1e1fa40b7956619b9571519feac0eae54c5cfa e117914953d8c1f88b0035512fa223b8a67174046422dffb4a899546001bc936 82a8720ccd9fecd89f8e7b97f5ff29b951e586a5319626dd779b3fe45013dec5 spend4.refund.message = 036cb8c7abe2c428b8e9de4d3e7063bebdeb2bf807dcc77d2b84bcc489c06774 8f7d2ccd560061fba71b7e6477013cb2a69a16bcb6fda9575625050187e4985e 340000000000000005bdd0e6bb456b4f862d2dafa13f8100866d753983bbe280 446ee0ceeb5c7bbacf0ec4de0fbd8a679f158ee2811ce29037e25cf77c65b9bb 9481c2f44924f2ae37 spend4.credential = a869aa59e7dbd1168f9c4edda1fa4529d5043b9a19101223228b773a941f4c17 000000000000000c969cbf34727198c57a5fc735139b4ce8980fc8d5c41789eb bb52e630ebbfc628036cb8c7abe2c428b8e9de4d3e7063bebdeb2bf807dcc77d 2b84bcc489c067748f7d2ccd560061fba71b7e6477013cb2a69a16bcb6fda957 5625050187e4985e34 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