purify
C++ Purify implementation with native circuit and BPP support
Loading...
Searching...
No Matches
accountable.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
6
7#include <algorithm>
8
9#include "purify/curve.hpp"
10#include "bppp_bridge.h"
11
12namespace purify::accountable {
13namespace {
14
15std::array<unsigned char, 8> encode_length(std::size_t size) {
16 std::array<unsigned char, 8> out{};
17 const auto value = static_cast<std::uint64_t>(size);
18 for (std::size_t i = 0; i < out.size(); ++i) {
19 out[out.size() - 1 - i] = static_cast<unsigned char>(value >> (8 * i));
20 }
21 return out;
22}
23
24Status validate_context(const ConflictContext& context, purify_secp_context* secp_context) {
25 PURIFY_RETURN_IF_ERROR(require_secp_context(secp_context, "accountable:context"),
26 "accountable:validate_context");
27 if (context.slot.empty()) {
28 return unexpected_error(ErrorCode::EmptyInput, "accountable:empty_slot");
29 }
30 if (!size_fits_u64(context.slot.size())) {
31 return unexpected_error(ErrorCode::RangeViolation, "accountable:slot_length");
32 }
33 if (purify_bip340_validate_xonly_pubkey(secp_context, context.signing_key.data()) == 0) {
34 return unexpected_error(ErrorCode::RangeViolation, "accountable:signing_key");
35 }
36 return {};
37}
38
39} // namespace
40
41Result<ScalarSignature> ScalarSignature::deserialize(std::span<const unsigned char> bytes) {
42 if (bytes.size() != 32) {
43 return unexpected_error(ErrorCode::InvalidFixedSize, "accountable:scalar_size");
44 }
45 std::array<unsigned char, 32> encoded{};
46 std::copy(bytes.begin(), bytes.end(), encoded.begin());
48 "accountable:scalar_encoding");
49 return ScalarSignature(scalar);
50}
51
53 const ConflictContext& context, purify_secp_context* secp_context) {
54 PURIFY_RETURN_IF_ERROR(validate_context(context, secp_context), "accountable:nonce_input");
55 static const TaggedHash hash("Purify/Accountable/Nonce/v1");
56 const auto slot_length = encode_length(context.slot.size());
57 return hash.digest_many<4>({context.signing_key, context.host_commitment,
58 slot_length, context.slot});
59}
60
62 const ConflictContext& context, std::span<const unsigned char> canonical_body,
63 purify_secp_context* secp_context) {
64 PURIFY_RETURN_IF_ERROR(validate_context(context, secp_context), "accountable:challenge");
65 if (!size_fits_u64(canonical_body.size())) {
66 return unexpected_error(ErrorCode::RangeViolation, "accountable:body_length");
67 }
68 static const TaggedHash hash("Purify/Accountable/Challenge/v1");
69 const auto slot_length = encode_length(context.slot.size());
70 const auto body_length = encode_length(canonical_body.size());
71 const auto digest = hash.digest_many<6>({context.signing_key, context.host_commitment,
72 slot_length, context.slot, body_length, canonical_body});
73 purify_scalar reduced;
74 purify_scalar_set_b32(&reduced, digest.data(), nullptr);
75 const auto challenge = purify::detail::FieldElementAccess::from_raw(reduced);
76 if (challenge.is_zero()) {
77 return unexpected_error(ErrorCode::RangeViolation, "accountable:zero_challenge");
78 }
79 return challenge;
80}
81
83 const ConflictContext& context, std::span<const unsigned char> canonical_body,
84 const ScalarSignature& signature, purify_secp_context* secp_context) {
85 PURIFY_ASSIGN_OR_RETURN(auto challenge, signature_challenge(context, canonical_body, secp_context),
86 "accountable:reconstruct_nonce");
87 std::array<unsigned char, 33> key{};
88 key[0] = 0x02;
89 std::copy(context.signing_key.begin(), context.signing_key.end(), key.begin() + 1);
90 const auto negative_challenge = challenge.negate().to_bytes_be();
91 std::array<unsigned char, 33> negative_key_term{};
92 if (purify_point_scale(secp_context, key.data(), negative_challenge.data(),
93 negative_key_term.data()) == 0) {
94 return unexpected_error(ErrorCode::InternalMismatch, "accountable:public_key_term");
95 }
96 const auto scalar = signature.serialize();
97 std::array<unsigned char, 33> nonce{};
98 if (purify_bppp_offset_commitment(secp_context, negative_key_term.data(), scalar.data(),
99 nonce.data()) == 0) {
100 return unexpected_error(ErrorCode::RangeViolation, "accountable:nonce_at_infinity");
101 }
102 return nonce;
103}
104
106 const ConflictContext& context,
107 std::span<const unsigned char> first_body, const ScalarSignature& first_signature,
108 std::span<const unsigned char> second_body, const ScalarSignature& second_signature,
109 purify_secp_context* secp_context) {
110 PURIFY_ASSIGN_OR_RETURN(auto first_challenge, signature_challenge(context, first_body, secp_context),
111 "accountable:recover_first_challenge");
112 PURIFY_ASSIGN_OR_RETURN(auto second_challenge, signature_challenge(context, second_body, secp_context),
113 "accountable:recover_second_challenge");
114 const auto difference = first_challenge - second_challenge;
115 if (difference.is_zero()) {
116 return unexpected_error(ErrorCode::BindingMismatch, "accountable:equal_challenges");
117 }
118
119 const auto first_scalar = FieldElement::from_bytes32(first_signature.serialize());
120 const auto second_scalar = FieldElement::from_bytes32(second_signature.serialize());
121 const auto recovered = (first_scalar - second_scalar) * difference.inverse();
122 if (recovered.is_zero()) {
123 return unexpected_error(ErrorCode::EquationMismatch, "accountable:recovered_zero");
124 }
125 const auto scalar = recovered.to_bytes_be();
126 static constexpr std::array<unsigned char, 33> generator{
127 0x02, 0x79, 0xbe, 0x66, 0x7e, 0xf9, 0xdc, 0xbb, 0xac, 0x55, 0xa0,
128 0x62, 0x95, 0xce, 0x87, 0x0b, 0x07, 0x02, 0x9b, 0xfc, 0xdb, 0x2d,
129 0xce, 0x28, 0xd9, 0x59, 0xf2, 0x81, 0x5b, 0x16, 0xf8, 0x17, 0x98,
130 };
131 std::array<unsigned char, 33> point{};
132 if (purify_point_scale(secp_context, generator.data(), scalar.data(), point.data()) == 0) {
133 return unexpected_error(ErrorCode::InternalMismatch, "accountable:recovered_point");
134 }
135 // Compare the full point: normalizing a recovered -q would accept a pair
136 // whose reconstructed nonces differ under this context's even-Y Q.
137 if (point[0] != 0x02 || !std::equal(context.signing_key.begin(), context.signing_key.end(), point.begin() + 1)) {
138 return unexpected_error(ErrorCode::EquationMismatch, "accountable:recovered_key");
139 }
140 // Exact qG=Q already implies R_first=R_second. Check the common raw nonce
141 // is nonzero without repeating either challenge hash or point operation.
142 if ((first_scalar - first_challenge * recovered).is_zero()) {
143 return unexpected_error(ErrorCode::RangeViolation, "accountable:common_nonce_at_infinity");
144 }
145 return scalar;
146}
147
148} // namespace purify::accountable
Experimental public transcripts for scalar-only accountable signatures.
int purify_point_scale(purify_secp_context *context, const unsigned char point33[33], const unsigned char scalar32[32], unsigned char out33[33])
int purify_bppp_offset_commitment(purify_secp_context *context, const unsigned char commitment33[33], const unsigned char scalar32[32], unsigned char commitment_out33[33])
C ABI bridging Purify C++ code to secp256k1-zkp BPPP functionality.
static FieldElement from_bytes32(const std::array< unsigned char, 32 > &bytes)
Decodes a 32-byte big-endian field element.
Definition numeric.cpp:63
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
Reusable BIP340-style tagged SHA-256 helper.
Definition curve.hpp:111
std::array< unsigned char, 32 > digest_many(const std::array< std::span< const unsigned char >, Segments > &segments) const
Definition curve.hpp:123
Canonical 32-byte scalar response; carries no nonce or proof.
static Result< ScalarSignature > deserialize(std::span< const unsigned char > bytes)
Parse exactly 32 big-endian bytes below the group order, including zero.
std::array< unsigned char, 32 > serialize() const
Return the complete scalar-only wire representation.
Elliptic-curve helpers, fixed parameters, and hash-to-curve utilities for Purify.
#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
Result< std::array< unsigned char, 33 > > reconstruct_nonce(const ConflictContext &context, std::span< const unsigned char > canonical_body, const ScalarSignature &signature, purify_secp_context *secp_context)
Reconstruct the full nonce point as sigma*G - e*Q, rejecting infinity.
Result< std::array< unsigned char, 32 > > nonce_evaluation_input(const ConflictContext &context, purify_secp_context *secp_context)
Derive the candidate deterministic Purify input from the registered slot.
Result< FieldElement > signature_challenge(const ConflictContext &context, std::span< const unsigned char > canonical_body, purify_secp_context *secp_context)
Compute the versioned Q/U/T/body challenge, without a nonce point.
Result< std::array< unsigned char, 32 > > recover_same_slot_key(const ConflictContext &context, std::span< const unsigned char > first_body, const ScalarSignature &first_signature, std::span< const unsigned char > second_body, const ScalarSignature &second_signature, purify_secp_context *secp_context)
Recover the exact signing scalar exposed by two same-slot responses.
Status require_secp_context(const purify_secp_context *context, const char *error_context)
Definition common.hpp:56
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
bool size_fits_u64(std::size_t value) noexcept
Definition common.hpp:83
Expected< T, Error > Result
Expected-returning convenience alias for Purify value-producing APIs.
Definition error.hpp:99
Expected< void, Error > Status
Expected-returning convenience alias for Purify status-only APIs.
Definition error.hpp:102
int purify_bip340_validate_xonly_pubkey(purify_secp_context *context, const unsigned char xonly_pubkey32[32])
Returns nonzero when the x-only public key encoding parses successfully.
void purify_scalar_set_b32(purify_scalar *out, const unsigned char input32[32], int *overflow)
Parses a big-endian 32-byte scalar.
struct purify_secp_context purify_secp_context
Caller-owned reusable secp256k1 context handle shared across Purify C and C++ APIs.
Public context shared by conflicting statements in one slashable slot.
std::array< unsigned char, 32 > host_commitment
std::span< const unsigned char > slot
std::array< unsigned char, 32 > signing_key
static FieldElement from_raw(const purify_scalar &raw) noexcept
Definition numeric.hpp:925
Opaque scalar storage compatible with secp256k1-zkp internal scalar storage.
Definition secp_bridge.h:22
int PURIFY_UINT_FN is_zero(const uint64_t value[PURIFY_UINT_WORDS])
Definition uint_impl.h:38