purify
C++ Purify implementation with native circuit and BPP support
Loading...
Searching...
No Matches
api.cpp
Go to the documentation of this file.
1// Copyright (c) 2026 Judica, Inc.
2// Distributed under the MIT software license, see the accompanying
3// file COPYING or https://opensource.org/license/mit/.
4
10#include "purify/api.hpp"
11
12#include <algorithm>
13#include <cassert>
14
15#include "purify.h"
16#include "error_bridge.hpp"
17
18namespace purify {
19namespace {
20
21Status status_from_core(purify_error_code code, const char* context) {
22 if (code == PURIFY_ERROR_OK) {
23 return {};
24 }
26}
27
28void clear_core_generated_key(purify_generated_key& generated) noexcept {
29 detail::secure_clear_bytes(generated.secret_key, sizeof(generated.secret_key));
30 std::fill(std::begin(generated.public_key), std::end(generated.public_key), 0);
31}
32
33void clear_core_bip340_key(purify_bip340_key& key) noexcept {
34 detail::secure_clear_bytes(key.secret_key, sizeof(key.secret_key));
35 std::fill(std::begin(key.xonly_public_key), std::end(key.xonly_public_key), 0);
36}
37
38Result<GeneratedKey> generated_key_from_core(const purify_generated_key& generated) {
39 const UInt512 packed_secret = UInt512::from_bytes_be(generated.secret_key, sizeof(generated.secret_key));
40 PURIFY_ASSIGN_OR_RETURN(auto owned_secret, SecretKey::from_packed(packed_secret),
41 "generated_key_from_core:from_packed");
42 const UInt512 public_key = UInt512::from_bytes_be(generated.public_key, sizeof(generated.public_key));
43 return GeneratedKey{std::move(owned_secret), public_key};
44}
45
46Bytes tagged_message(std::string_view prefix, const Bytes& message) {
47 Bytes out;
48 out.reserve(prefix.size() + message.size());
49 out.insert(out.end(), prefix.begin(), prefix.end());
50 out.insert(out.end(), message.begin(), message.end());
51 return out;
52}
53
54void add_expr_slack(NativeBulletproofCircuit::PackedSlackPlan& slack, const Expr& expr) {
55 for (const auto& term : expr.linear()) {
56 switch (term.first.kind) {
58 if (term.first.index < slack.wl.size()) {
59 ++slack.wl[term.first.index];
60 }
61 break;
63 if (term.first.index < slack.wr.size()) {
64 ++slack.wr[term.first.index];
65 }
66 break;
68 if (term.first.index < slack.wo.size()) {
69 ++slack.wo[term.first.index];
70 }
71 break;
73 if (term.first.index < slack.wv.size()) {
74 ++slack.wv[term.first.index];
75 }
76 break;
78 assert(false && "verifier_circuit_template should not pack raw witness symbols");
79 break;
80 }
81 }
82}
83
84NativeBulletproofCircuit::PackedSlackPlan build_template_slack(std::size_t n_gates, std::size_t n_commitments,
85 const Expr& p1x, const Expr& p2x, const Expr& out) {
86 NativeBulletproofCircuit::PackedSlackPlan slack;
87 slack.constraint_slack = 3;
88 slack.wl.assign(n_gates, 0);
89 slack.wr.assign(n_gates, 0);
90 slack.wo.assign(n_gates, 0);
91 slack.wv.assign(n_commitments, 0);
92 add_expr_slack(slack, p1x.split().second);
93 add_expr_slack(slack, p2x.split().second);
94 add_expr_slack(slack, out);
95 if (!slack.wv.empty()) {
96 ++slack.wv[0];
97 }
98 return slack;
99}
100
101} // namespace
102
103Status fill_secure_random(std::span<unsigned char> bytes) noexcept {
104 return status_from_core(purify_fill_secure_random(bytes.data(), bytes.size()),
105 "fill_secure_random:purify_fill_secure_random");
106}
107
109 return random_below(range, fill_secure_random);
110}
111
113 purify_generated_key generated{};
114 const purify_error_code code = purify_generate_key(&generated);
115 if (code != PURIFY_ERROR_OK) {
116 clear_core_generated_key(generated);
117 return unexpected_error(core_api_detail::from_core_error_code(code), "generate_key:purify_generate_key");
118 }
119 Result<GeneratedKey> out = generated_key_from_core(generated);
120 clear_core_generated_key(generated);
121 if (!out.has_value()) {
122 return unexpected_error(out.error(), "generate_key:generated_key_from_core");
123 }
124 return out;
125}
126
128 purify_generated_key generated{};
129 const purify_error_code code = purify_generate_key_from_seed(&generated, seed.data(), seed.size());
130 if (code != PURIFY_ERROR_OK) {
131 clear_core_generated_key(generated);
132 return unexpected_error(core_api_detail::from_core_error_code(code), "generate_key:purify_generate_key_from_seed");
133 }
134 Result<GeneratedKey> out = generated_key_from_core(generated);
135 clear_core_generated_key(generated);
136 if (!out.has_value()) {
137 return unexpected_error(out.error(), "generate_key:generated_key_from_core");
138 }
139 return out;
140}
141
143 PURIFY_ASSIGN_OR_RETURN(auto owned_secret, secret.clone(), "derive_key:clone");
144 return derive_key(std::move(owned_secret));
145}
146
148 std::array<unsigned char, PURIFY_SECRET_KEY_BYTES> secret_bytes = secret.packed().to_bytes_be();
149 std::array<unsigned char, PURIFY_PUBLIC_KEY_BYTES> public_key_bytes{};
150 const purify_error_code code = purify_derive_public_key(public_key_bytes.data(), secret_bytes.data());
151 detail::secure_clear_bytes(secret_bytes.data(), secret_bytes.size());
152 if (code != PURIFY_ERROR_OK) {
153 std::fill(public_key_bytes.begin(), public_key_bytes.end(), 0);
154 return unexpected_error(core_api_detail::from_core_error_code(code), "derive_key:purify_derive_public_key");
155 }
156 const UInt512 public_key = UInt512::from_bytes_be(public_key_bytes.data(), public_key_bytes.size());
157 std::fill(public_key_bytes.begin(), public_key_bytes.end(), 0);
158 return GeneratedKey{std::move(secret), public_key};
159}
160
162 std::array<unsigned char, PURIFY_SECRET_KEY_BYTES> secret_bytes = secret.packed().to_bytes_be();
163 purify_bip340_key key{};
164 const purify_error_code code = purify_derive_bip340_key(&key, secret_bytes.data(), secp_context);
165 detail::secure_clear_bytes(secret_bytes.data(), secret_bytes.size());
166 if (code != PURIFY_ERROR_OK) {
167 clear_core_bip340_key(key);
168 return unexpected_error(core_api_detail::from_core_error_code(code), "derive_bip340_key:purify_derive_bip340_key");
169 }
170 Bip340Key out{};
171 std::copy(std::begin(key.secret_key), std::end(key.secret_key), out.seckey.begin());
172 std::copy(std::begin(key.xonly_public_key), std::end(key.xonly_public_key), out.xonly_pubkey.begin());
173 clear_core_bip340_key(key);
174 return out;
175}
176
177Result<FieldElement> eval(const SecretKey& secret, const Bytes& message) {
178 std::array<unsigned char, PURIFY_SECRET_KEY_BYTES> secret_bytes = secret.packed().to_bytes_be();
179 std::array<unsigned char, PURIFY_FIELD_ELEMENT_BYTES> output_bytes{};
180 const purify_error_code code =
181 purify_eval(output_bytes.data(), secret_bytes.data(), message.data(), message.size());
182 detail::secure_clear_bytes(secret_bytes.data(), secret_bytes.size());
183 if (code != PURIFY_ERROR_OK) {
184 std::fill(output_bytes.begin(), output_bytes.end(), 0);
185 return unexpected_error(core_api_detail::from_core_error_code(code), "eval:purify_eval");
186 }
187 return FieldElement::try_from_bytes32(output_bytes);
188}
189
190Result<std::string> verifier(const Bytes& message, const UInt512& pubkey) {
191 PURIFY_ASSIGN_OR_RETURN(const auto& m1, hash_to_curve(tagged_message("Eval/1/", message), curve1()),
192 "verifier:hash_to_curve_m1");
193 PURIFY_ASSIGN_OR_RETURN(const auto& m2, hash_to_curve(tagged_message("Eval/2/", message), curve2()),
194 "verifier:hash_to_curve_m2");
195 Transcript transcript;
196 PURIFY_ASSIGN_OR_RETURN(const auto& result, circuit_main(transcript, m1, m2), "verifier:circuit_main");
198 PURIFY_RETURN_IF_ERROR(bp.from_transcript(transcript, result.n_bits), "verifier:from_transcript");
199 PURIFY_RETURN_IF_ERROR(bp.add_pubkey_and_out(pubkey, result.p1x, result.p2x, result.out),
200 "verifier:add_pubkey_and_out");
201 return bp.to_string();
202}
203
205 PURIFY_ASSIGN_OR_RETURN(const auto& template_circuit, verifier_circuit_template(message),
206 "verifier_circuit:verifier_circuit_template");
207 return template_circuit.instantiate(pubkey);
208}
209
211 PURIFY_ASSIGN_OR_RETURN(const auto& m1, hash_to_curve(tagged_message("Eval/1/", message), curve1()),
212 "verifier_circuit_template:hash_to_curve_m1");
213 PURIFY_ASSIGN_OR_RETURN(const auto& m2, hash_to_curve(tagged_message("Eval/2/", message), curve2()),
214 "verifier_circuit_template:hash_to_curve_m2");
215 Transcript transcript;
216 PURIFY_ASSIGN_OR_RETURN(const auto& result, circuit_main(transcript, m1, m2),
217 "verifier_circuit_template:circuit_main");
219 PURIFY_RETURN_IF_ERROR(bp.from_transcript(transcript, result.n_bits), "verifier_circuit_template:from_transcript");
220 Expr p1x = result.p1x;
221 Expr p2x = result.p2x;
222 Expr out = result.out;
226
227 NativeBulletproofCircuit base_circuit = bp.native_circuit();
229 auto packed,
230 base_circuit.pack_with_slack(build_template_slack(base_circuit.n_gates, base_circuit.n_commitments, p1x, p2x, out)),
231 "verifier_circuit_template:pack_with_slack");
232 return NativeBulletproofCircuitTemplate::from_parts(std::move(packed), std::move(p1x), std::move(p2x),
233 std::move(out));
234}
235
237 PURIFY_ASSIGN_OR_RETURN(const auto& unpacked, unpack_secret(secret.packed()), "prove_assignment_data:unpack_secret");
238 PURIFY_ASSIGN_OR_RETURN(const auto& m1, hash_to_curve(tagged_message("Eval/1/", message), curve1()),
239 "prove_assignment_data:hash_to_curve_m1");
240 PURIFY_ASSIGN_OR_RETURN(const auto& m2, hash_to_curve(tagged_message("Eval/2/", message), curve2()),
241 "prove_assignment_data:hash_to_curve_m2");
242 PURIFY_ASSIGN_OR_RETURN(const auto& p1, curve1().mul_secret_affine(generator1(), unpacked.first),
243 "prove_assignment_data:mul_secret_affine_p1");
244 PURIFY_ASSIGN_OR_RETURN(const auto& p2, curve2().mul_secret_affine(generator2(), unpacked.second),
245 "prove_assignment_data:mul_secret_affine_p2");
246 PURIFY_ASSIGN_OR_RETURN(const auto& q1, curve1().mul_secret_affine(m1, unpacked.first),
247 "prove_assignment_data:mul_secret_affine_q1");
248 PURIFY_ASSIGN_OR_RETURN(const auto& q2, curve2().mul_secret_affine(m2, unpacked.second),
249 "prove_assignment_data:mul_secret_affine_q2");
250 FieldElement native_out = combine(q1.x, q2.x);
251
252 Transcript transcript;
253 PURIFY_ASSIGN_OR_RETURN(const auto& result, circuit_main(transcript, m1, m2, unpacked.first, unpacked.second),
254 "prove_assignment_data:circuit_main");
255 if (transcript.evaluate(result.p1x) != std::optional<FieldElement>(p1.x)) {
256 return unexpected_error(ErrorCode::InternalMismatch, "prove_assignment_data:p1x_mismatch");
257 }
258 if (transcript.evaluate(result.p2x) != std::optional<FieldElement>(p2.x)) {
259 return unexpected_error(ErrorCode::InternalMismatch, "prove_assignment_data:p2x_mismatch");
260 }
261 if (transcript.evaluate(result.out) != std::optional<FieldElement>(native_out)) {
262 return unexpected_error(ErrorCode::InternalMismatch, "prove_assignment_data:output_mismatch");
263 }
264
265 UInt512 pubkey = pack_public(p1.x.to_uint256(), p2.x.to_uint256());
267 PURIFY_RETURN_IF_ERROR(bp.from_transcript(transcript, result.n_bits), "prove_assignment_data:from_transcript");
268 PURIFY_RETURN_IF_ERROR(bp.add_pubkey_and_out(pubkey, result.p1x, result.p2x, result.out),
269 "prove_assignment_data:add_pubkey_and_out");
270 if (!bp.evaluate(transcript.varmap(), native_out)) {
271 return unexpected_error(ErrorCode::TranscriptCheckFailed, "prove_assignment_data:transcript_check");
272 }
273
274 Result<BulletproofAssignmentData> assignment = bp.assignment_data(transcript.varmap(), native_out);
275 assert(assignment.has_value() && "prove_assignment_data() should materialize a complete assignment");
276 if (!assignment.has_value()) {
277 return unexpected_error(ErrorCode::InternalMismatch, "prove_assignment_data:assignment_data");
278 }
279 return BulletproofWitnessData{pubkey, native_out, std::move(*assignment)};
280}
281
283 PURIFY_ASSIGN_OR_RETURN(const auto& circuit, verifier_circuit(message, witness.public_key),
284 "evaluate_verifier_circuit:verifier_circuit");
285 return circuit.evaluate(witness.assignment);
286}
287
289 PURIFY_ASSIGN_OR_RETURN(const auto& witness, prove_assignment_data(message, secret),
290 "evaluate_verifier_circuit:prove_assignment_data");
291 return evaluate_verifier_circuit(message, witness);
292}
293
294Result<Bytes> prove_assignment(const Bytes& message, const SecretKey& secret) {
295 PURIFY_ASSIGN_OR_RETURN(const auto& witness, prove_assignment_data(message, secret), "prove_assignment:prove_assignment_data");
296 Result<Bytes> serialized = witness.assignment.serialize();
297 assert(serialized.has_value() && "prove_assignment() should serialize a well-formed assignment");
298 if (!serialized.has_value()) {
299 return unexpected_error(ErrorCode::InternalMismatch, "prove_assignment:serialize_assignment");
300 }
301 return serialized;
302}
303
304} // namespace purify
High-level Purify key generation, evaluation, witness generation, and circuit helpers.
Lowering helper that converts a symbolic transcript into native Bulletproof witness and circuit forms...
bool evaluate(const WitnessAssignments &vars, const FieldElement &commitment) const
Evaluates the lowered symbolic constraints with a concrete commitment value.
std::string to_string() const
Renders the lowered circuit in the legacy textual verifier format.
Status from_transcript(const Transcript &transcript, std::size_t n_bits)
Imports a symbolic transcript and pads it to a power-of-two multiplication count.
void replace_expr_v_with_bp_var(Expr &expr)
Rewrites transcript variable names to their eventual Bulletproof wire aliases.
Status add_pubkey_and_out(const UInt512 &pubkey, Expr p1x, Expr p2x, Expr out)
Binds packed public-key coordinates and the output commitment into explicit constraints.
NativeBulletproofCircuit native_circuit() const
Builds the native sparse circuit object from the lowered assignments and constraints.
Result< BulletproofAssignmentData > assignment_data(const WitnessAssignments &vars) const
Materializes the witness columns expected by the native circuit representation.
Purify result carrier that either holds a value or an error.
Definition expected.hpp:64
bool has_value() const noexcept
Definition expected.hpp:170
Symbolic affine expression over indexed variables and field coefficients.
Definition expr.hpp:71
Field element modulo the backend scalar field used by this implementation.
Definition numeric.hpp:815
static Result< FieldElement > try_from_bytes32(const std::array< unsigned char, 32 > &bytes)
Decodes a canonical 32-byte big-endian field element.
Definition numeric.cpp:53
static NativeBulletproofCircuitTemplate from_parts(NativeBulletproofCircuit::PackedWithSlack base_packed, Expr p1x, Expr p2x, Expr out)
Builds a template from a lowered base circuit plus late-bound public expressions.
Move-only packed Purify secret stored in dedicated heap memory.
Definition secret.hpp:52
const UInt512 & packed() const noexcept
Exposes the packed secret for lower-level cryptographic operations.
Definition secret.hpp:93
static Result< SecretKey > from_packed(const UInt512 &packed)
Constructs a validated secret key from packed Purify secret bytes.
Definition secret.hpp:66
Result< SecretKey > clone() const
Creates a second owned copy of this secret key.
Definition secret.hpp:85
Checked span wrapper that guarantees a minimum runtime length.
Definition common.hpp:129
constexpr std::size_t size() const noexcept
Definition common.hpp:153
constexpr const T * data() const noexcept
Definition common.hpp:149
Mutable transcript used to record symbolic multiplication, division, and boolean constraints.
Definition expr.hpp:213
std::optional< FieldElement > evaluate(const Expr &expr) const
Evaluates an expression against the transcript's current witness vector.
Definition expr.cpp:451
const WitnessAssignments & varmap() const
Returns the underlying witness assignment vector.
Definition expr.hpp:234
#define PURIFY_RETURN_IF_ERROR(expr, context)
Evaluates an expected-like expression and returns the wrapped error on failure.
Definition error.hpp:329
#define PURIFY_ASSIGN_OR_RETURN(lhs, expr, context)
Evaluates an expected-like expression, binds the value to lhs, and propagates errors.
Definition error.hpp:338
Bytes tagged_message(std::string_view prefix, const Bytes &message)
Definition c_api.cpp:77
constexpr ErrorCode from_core_error_code(purify_error_code code) noexcept
void secure_clear_bytes(void *data, std::size_t size) noexcept
Definition secret.hpp:22
Definition api.hpp:21
constexpr Unexpected< Error > unexpected_error(ErrorCode code, const char *context=nullptr)
Constructs an unexpected Error value from a machine-readable code.
Definition error.hpp:293
Result< std::string > verifier(const Bytes &message, const UInt512 &pubkey)
Builds the legacy serialized verifier description for a message and public key.
Definition api.cpp:190
Result< NativeBulletproofCircuitTemplate > verifier_circuit_template(const Bytes &message)
Builds a reusable public-key-agnostic verifier-circuit template for a message.
Definition api.cpp:210
Result< BulletproofWitnessData > prove_assignment_data(const Bytes &message, const SecretKey &secret)
Computes the native Purify witness for a message and secret.
Definition api.cpp:236
Result< GeneratedKey > generate_key()
Generates a random Purify keypair using the built-in OS RNG.
Definition api.cpp:112
BigUInt< 8 > UInt512
512-bit unsigned integer used for private and packed public keys.
Definition numeric.hpp:802
Result< NativeBulletproofCircuit > verifier_circuit(const Bytes &message, const UInt512 &pubkey)
Builds the native verifier circuit for a message and public key.
Definition api.cpp:204
Result< Bip340Key > derive_bip340_key(const SecretKey &secret, purify_secp_context *secp_context)
Derives a canonical BIP340 signing keypair from an owned Purify secret.
Definition api.cpp:161
Result< Bytes > prove_assignment(const Bytes &message, const SecretKey &secret)
Serializes the witness assignment produced for a message and secret.
Definition api.cpp:294
Result< GeneratedKey > derive_key(const SecretKey &secret)
Derives the packed public key corresponding to a packed secret.
Definition api.cpp:142
const EllipticCurve & curve1()
Returns the first Purify curve instance.
Definition curve.cpp:256
Result< CircuitMainResult > circuit_main(Transcript &transcript, const JacobianPoint &m1, const JacobianPoint &m2, const std::optional< UInt256 > &z1=std::nullopt, const std::optional< UInt256 > &z2=std::nullopt)
Builds the full symbolic Purify circuit from message points and optional witness scalars.
std::vector< unsigned char > Bytes
Dynamically sized byte string used for messages, serialized witnesses, and proofs.
Definition common.hpp:99
const JacobianPoint & generator1()
Returns the fixed generator for the first curve.
Definition curve.cpp:277
UInt512 pack_public(const UInt256 &x1, const UInt256 &x2)
Packs two x-coordinates into the reference 512-bit public-key encoding.
Definition curve.cpp:339
Result< FieldElement > eval(const SecretKey &secret, const Bytes &message)
Evaluates the Purify PRF for an owned secret key and message.
Definition api.cpp:177
Result< bool > evaluate_verifier_circuit(const Bytes &message, const BulletproofWitnessData &witness)
Evaluates the generated verifier circuit against an explicit witness.
Definition api.cpp:282
Result< std::pair< UInt256, UInt256 > > unpack_secret(const UInt512 &z)
Splits a packed private key into its two per-curve secret scalars.
Definition curve.cpp:321
Result< JacobianPoint > hash_to_curve(const Bytes &data, const EllipticCurve &curve)
Hashes arbitrary data onto the supplied curve by rejection sampling x-coordinates.
Definition curve.cpp:268
Result< UInt512 > random_below(const UInt512 &range, FillRandom &&fill_random)
Samples a uniformly random packed secret below a range using a checked byte-fill source.
Definition api.hpp:132
const EllipticCurve & curve2()
Returns the second Purify curve instance.
Definition curve.cpp:261
const JacobianPoint & generator2()
Returns the fixed generator for the second curve.
Definition curve.cpp:288
FieldElement combine(const FieldElement &x1, const FieldElement &x2)
Applies the Purify curve-combination map to two x-coordinates.
Definition curve.cpp:345
Expected< void, Error > Status
Expected-returning convenience alias for Purify status-only APIs.
Definition error.hpp:102
Status fill_secure_random(std::span< unsigned char > bytes) noexcept
Fills a buffer with operating-system randomness.
Definition api.cpp:103
Public C core for Purify key validation, key derivation, key generation, and evaluation.
purify_error_code purify_generate_key(purify_generated_key *out)
Generates one random Purify keypair.
Definition c_api.cpp:134
purify_error_code purify_fill_secure_random(unsigned char *bytes, size_t bytes_len)
Fills a caller-owned buffer with secure operating-system randomness.
Definition core.c:260
purify_error_code
Machine-readable status code returned by the Purify C core.
Definition purify.h:28
@ PURIFY_ERROR_OK
Definition purify.h:29
purify_error_code purify_generate_key_from_seed(purify_generated_key *out, const unsigned char *seed, size_t seed_len)
Deterministically derives one Purify keypair from seed material.
Definition c_api.cpp:161
purify_error_code purify_eval(unsigned char out_field_element[PURIFY_FIELD_ELEMENT_BYTES], const unsigned char secret_key[PURIFY_SECRET_KEY_BYTES], const unsigned char *message, size_t message_len)
Evaluates the Purify PRF for one packed secret and message.
Definition c_api.cpp:263
purify_error_code purify_derive_public_key(unsigned char out_public_key[PURIFY_PUBLIC_KEY_BYTES], const unsigned char secret_key[PURIFY_SECRET_KEY_BYTES])
Derives the packed public key corresponding to one packed Purify secret.
Definition c_api.cpp:196
purify_error_code purify_derive_bip340_key(purify_bip340_key *out, const unsigned char secret_key[PURIFY_SECRET_KEY_BYTES], purify_secp_context *secp_context)
Derives one canonical BIP340 keypair from one packed Purify secret.
Definition c_api.cpp:217
static BigUInt from_bytes_be(const unsigned char *data, std::size_t size)
Parses a big-endian byte string into the fixed-width integer.
Definition numeric.hpp:235
std::array< unsigned char, Words *8 > to_bytes_be() const
Serializes the value to a fixed-width big-endian byte array.
Definition numeric.hpp:621
Canonical BIP340 keypair derived deterministically from a packed Purify secret.
Definition api.hpp:38
Complete witness bundle for evaluating and proving a Purify instance.
Definition api.hpp:73
BulletproofAssignmentData assignment
Definition api.hpp:76
Derived Purify keypair bundle with an owned packed secret and its matching public key.
Definition api.hpp:24
Native in-memory representation of a Bulletproof-style arithmetic circuit.
Result< PackedWithSlack > pack_with_slack() const
Packs the circuit into one aligned slab with no additional row or constraint slack.
Canonical BIP340 keypair derived from one packed Purify secret.
Definition purify.h:68
Seed/public-key bundle returned by the C core key-generation entry points.
Definition purify.h:62
unsigned char secret_key[PURIFY_SECRET_KEY_BYTES]
Definition purify.h:63
unsigned char public_key[PURIFY_PUBLIC_KEY_BYTES]
Definition purify.h:64