| Internet-Draft | EC-ACT | October 2026 |
| Schlesinger, et al. | Expires 22 April 2027 | [Page] |
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.¶
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.¶
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. 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.¶
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.¶
This document contains executable Python code to specify much of the protocol.¶
EC-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 [EC-IHAT] provides.¶
The compact non-interactive Sigma protocol of [SIGMA], instantiated in Section 4.4.¶
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.¶
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.¶
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.¶
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]).¶
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"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.¶
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 [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.¶
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 [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.¶
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.¶
EC-ACTv1 is specified against the -03 revisions of [SIGMA] and
[FIAT-SHAMIR]. A later revision that changes the NARG string or its
derivation MUST be adopted under a new ciphersuite identifier, and so a new
ctx_proto; EC-ACTv1-P256-SHA256 then never denotes two transcript
formats.¶
TODO. Revisit once [SIGMA] and [FIAT-SHAMIR] are stable, and
move the pin to -04 when it is published.¶
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).¶
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).¶
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
[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.¶
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 [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.¶
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 [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.¶
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 EC-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 = 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
¶
key.rand =
d069967f203df4d58f4430bf9cb90beb883d542d7c89c551db69dce42570bf4a
e3adacc80db708e282150cea9e505068
key.skM =
34e271d1d4e404dc78a38a6b0ca4b3abc68bc4806b00997edbb901f13846f5f2
key.pkM =
03263eb172073519ff125b897fdc7569e4bd908debe19208b59361e21e8e4cad
78
¶
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
¶
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
¶
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
¶
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
¶
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
¶
TODO acknowledge.¶