Internet-Draft EC-ACT October 2026
Schlesinger, et al. Expires 22 April 2027 [Page]
Workgroup:
Network Working Group
Internet-Draft:
draft-authors-mole-ec-act-latest
Published:
Intended Status:
Informational
Expires:
Authors:
S. Schlesinger
Google LLC
J. Katz
Google LLC
A. Faz-Hernandez
Cloudflare, Inc.

EC-ACT: Anonymous Credit Tokens

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 22 April 2027.

▲

Table of Contents

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.

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.

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, , <https://datatracker.ietf.org/doc/html/draft-jms-mole-architecture-00>.
[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, , <https://moderation-of-unlinkable-endorsements.github.io/internet-drafts/draft-authors-mole-ec-ihat.html>.
[FIAT-SHAMIR]
Orrù, M., "Fiat-Shamir Transformation", Work in Progress, Internet-Draft, draft-irtf-cfrg-fiat-shamir-03, , <https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-fiat-shamir-03>.
[PROTOCOLS]
Schlesinger, S., Jackson, D., and T. Meunier, "MoLE Protocols", Work in Progress, Internet-Draft, draft-jms-mole-protocols-00, , <https://datatracker.ietf.org/doc/html/draft-jms-mole-protocols-00>.
[RFC2119]
Bradner, S., "Key words for use in RFCs to Indicate Requirement Levels", BCP 14, RFC 2119, DOI 10.17487/RFC2119, , <https://www.rfc-editor.org/rfc/rfc2119>.
[RFC8174]
Leiba, B., "Ambiguity of Uppercase vs Lowercase in RFC 2119 Key Words", BCP 14, RFC 8174, DOI 10.17487/RFC8174, , <https://www.rfc-editor.org/rfc/rfc8174>.
[SIGMA]
Orrù, M. and C. Yun, "Sigma Proofs for Linear Relations", Work in Progress, Internet-Draft, draft-irtf-cfrg-sigma-protocols-03, , <https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-sigma-protocols-03>.

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, , <https://crypto.orange-labs.fr/acg/publication/infoPublication.php?id=225>.
[BBS]
Boneh, D., Boyen, X., and H. Shacham, "Short Group Signatures", CRYPTO 2004, , <https://crypto.stanford.edu/~dabo/pubs/papers/groupsigs.pdf>.
[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, , <https://www.rfc-editor.org/rfc/rfc9380>.
[KVAC]
Chase, M., Meiklejohn, S., and G. Zaverucha, "Algebraic MACs and Keyed-Verification Anonymous Credentials", CCS 2014, , <https://eprint.iacr.org/2013/516>.
[Pedersen91]
Pedersen, T. P., "Non-Interactive and Information-Theoretic Secure Verifiable Secret Sharing", CRYPTO 1991, , <https://doi.org/10.1007/3-540-46766-1_9>.
[TZ23]
Tessaro, S. and C. Zhu, "Revisiting BBS Signatures", EUROCRYPT 2023, , <https://eprint.iacr.org/2023/275>.

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
Jonathan Katz
Google LLC
Armando Faz-Hernandez
Cloudflare, Inc.