| Internet-Draft | ACT | October 2026 |
| Schlesinger, et al. | Expires 22 April 2027 | [Page] |
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.¶
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.¶
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.¶
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.¶
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.¶
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.¶
This document contains executable Python code to specify much of the protocol.¶
ACT uses three three dependencies:¶
A prime-order group implementing the interface in Section 3.1. Section 5 gives concrete instances.¶
A cryptographic hash function, used by the HashToGroup, HashToScalar,
and derivation algorithms that the group of [IHAT-ROLLATINI] provides.¶
The compact non-interactive Sigma protocol of [SIGMA], instantiated in Section 4.4.¶
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.¶
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.¶
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.¶
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]).¶
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)
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.¶
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.¶
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.¶
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.¶
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.¶
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.¶
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.¶
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.¶
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)
¶
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
¶
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.¶
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).¶
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).¶
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.¶
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.¶
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.¶
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.¶
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]).¶
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.¶
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).¶
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.¶
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.¶
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).¶
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, 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).¶
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].¶
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.¶
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.¶
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.¶
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.¶
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.¶
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.¶
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.¶
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]).¶
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.¶
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.¶
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.¶
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.¶
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.¶
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.¶
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.¶
This document has no IANA actions. The ACT credential type is registered by [PROTOCOLS].¶
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.¶
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
¶
key.rand =
6b04e6141f669f915de1d7f0ed8b50405bf6fff65b3ef4a63b6610586ddc5e62
b22c98437bb5746647de22933c80f522
key.skM =
6a998a9bd63b4efcd641c1e0e13e4dd6035f1ce748a6aa1461885e0454c928e1
key.pkM =
03513bbb46b2af7f4ed5304505528b96345064e50917e098630286c89383da72
22
¶
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
¶
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
¶
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
¶
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
¶
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
¶
TODO acknowledge.¶