purify
C++ Purify implementation with native circuit and BPP support
Loading...
Searching...
No Matches
accountable_responses.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 "bppp_bridge.h"
10
11namespace purify::accountable {
12namespace {
13
14Result<FieldElement> roster_challenge(
15 const SigningRoster& roster, const ConflictContext& context,
16 std::span<const unsigned char> canonical_body, purify_secp_context* secp_context) {
17 if (context.signing_key != roster.signing_key()) {
18 return unexpected_error(ErrorCode::BindingMismatch, "accountable:response_roster_key");
19 }
20 return signature_challenge(context, canonical_body, secp_context);
21}
22
24 const std::array<unsigned char, 33>& key, const FieldElement& weighted_challenge,
25 const ScalarSignature& response, purify_secp_context* secp_context) {
26 const auto negative_weight = weighted_challenge.negate().to_bytes_be();
27 std::array<unsigned char, 33> key_term{};
28 if (purify_point_scale(secp_context, key.data(), negative_weight.data(), key_term.data()) == 0) {
29 return unexpected_error(ErrorCode::InternalMismatch, "accountable:response_key_term");
30 }
31 const auto scalar = response.serialize();
32 std::array<unsigned char, 33> nonce{};
33 if (purify_bppp_offset_commitment(secp_context, key_term.data(), scalar.data(), nonce.data()) == 0) {
34 return unexpected_error(ErrorCode::RangeViolation, "accountable:response_nonce_at_infinity");
35 }
36 return nonce;
37}
38
39Status check_partial(
40 const SigningRosterMember& member, const FieldElement& challenge,
41 const PartialResponse& partial, purify_secp_context* secp_context) {
42 if ((partial.nonce[0] != 0x02 && partial.nonce[0] != 0x03) ||
43 purify_bip340_validate_xonly_pubkey(secp_context, partial.nonce.data() + 1) == 0) {
44 return unexpected_error(ErrorCode::RangeViolation, "accountable:partial_nonce_encoding");
45 }
46 PURIFY_ASSIGN_OR_RETURN(auto nonce, response_nonce(member.public_key, challenge * member.coefficient,
47 partial.response, secp_context),
48 "accountable:partial_nonce");
49 if (nonce != partial.nonce) {
50 return unexpected_error(ErrorCode::EquationMismatch, "accountable:partial_response_equation");
51 }
52 return {};
53}
54
55} // namespace
56
58 const SigningRoster& roster, std::size_t member_index,
59 const ConflictContext& context, std::span<const unsigned char> canonical_body,
60 const PartialResponse& partial, purify_secp_context* secp_context) {
61 if (member_index >= roster.members().size()) {
62 return unexpected_error(ErrorCode::IndexOutOfRange, "accountable:response_member_index");
63 }
64 PURIFY_ASSIGN_OR_RETURN(auto challenge, roster_challenge(roster, context, canonical_body, secp_context),
65 "accountable:partial_challenge");
66 return check_partial(roster.members()[member_index], challenge, partial, secp_context);
67}
68
70 const SigningRoster& roster, const ConflictContext& context,
71 std::span<const unsigned char> canonical_body,
72 std::span<const PartialResponse> partials, purify_secp_context* secp_context) {
73 if (roster.members().empty()) {
74 return unexpected_error(ErrorCode::EmptyInput, "accountable:empty_response_roster");
75 }
76 if (partials.size() != roster.members().size()) {
77 return unexpected_error(ErrorCode::SizeMismatch, "accountable:response_member_count");
78 }
79 PURIFY_ASSIGN_OR_RETURN(auto challenge, roster_challenge(roster, context, canonical_body, secp_context),
80 "accountable:aggregate_challenge");
81 auto sum = FieldElement::from_u64(0);
82 for (std::size_t i = 0; i < partials.size(); ++i) {
83 PURIFY_RETURN_IF_ERROR(check_partial(roster.members()[i], challenge, partials[i], secp_context),
84 "accountable:aggregate_partial");
85 sum = sum + FieldElement::from_bytes32(partials[i].response.serialize());
86 }
87 PURIFY_ASSIGN_OR_RETURN(auto response, ScalarSignature::deserialize(sum.to_bytes_be()),
88 "accountable:aggregate_response");
89 std::array<unsigned char, 33> key{};
90 key[0] = 0x02;
91 std::copy(roster.signing_key().begin(), roster.signing_key().end(), key.begin() + 1);
92 // The checked partial equations imply this is the sum of their raw nonces.
93 // Reject its identity without a second challenge hash or nonce-point sum.
94 PURIFY_ASSIGN_OR_RETURN(auto nonce, response_nonce(key, challenge, response, secp_context),
95 "accountable:aggregate_nonce");
96 (void)nonce;
97 return response;
98}
99
100} // namespace purify::accountable
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 FieldElement from_u64(std::uint64_t value)
Constructs a field element from an unsigned 64-bit integer.
Definition numeric.cpp:40
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.
Experimental public key aggregation for one ordered coalition roster.
std::span< const SigningRosterMember > members() const
Members in their original order; effective weights include parity.
const std::array< unsigned char, 32 > & signing_key() const
Canonical even-Y aggregate signing key for ConflictContext.
#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
Status verify_partial_response(const SigningRoster &roster, std::size_t member_index, const ConflictContext &context, std::span< const unsigned char > canonical_body, const PartialResponse &partial, purify_secp_context *secp_context)
Check sigma_i*G = R_i + e*b_i*P_i for one ordered roster member.
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< ScalarSignature > aggregate_responses(const SigningRoster &roster, const ConflictContext &context, std::span< const unsigned char > canonical_body, std::span< const PartialResponse > partials, purify_secp_context *secp_context)
Check all ordered partial equations and sum their scalar responses.
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
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.
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.
Public session contribution; the raw full nonce is not normalized.
An ordered individual signing key and its normalized public weight.