diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..f81c6f0c --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +clang-build-release/ +*.DS_Store +build/ \ No newline at end of file diff --git a/README.md b/README.md index e16e663c..1b2d0ac3 100644 --- a/README.md +++ b/README.md @@ -73,4 +73,4 @@ them: ``` $ ./algebra/fft_test --benchmark_filter='BM_*' $ ./circuits/sha/flatsha256_circuit_test --benchmark_filter=BM_ShaZK_fp2_128 -``` +``` \ No newline at end of file diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index dfd21219..22440e81 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -85,6 +85,9 @@ add_subdirectory(circuits/mac) add_subdirectory(circuits/mdoc) add_subdirectory(circuits/sha) +# Hidden PK circuit and tests, not used in production, may be buggy +add_subdirectory(circuits/hidden_pk) + # experiments and tests, not used in production, may be buggy add_subdirectory(circuits/tests/anoncred) add_subdirectory(circuits/tests/sha3) diff --git a/lib/circuits/hidden_pk/CMakeLists.txt b/lib/circuits/hidden_pk/CMakeLists.txt new file mode 100644 index 00000000..109819fc --- /dev/null +++ b/lib/circuits/hidden_pk/CMakeLists.txt @@ -0,0 +1,16 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +proofs_add_test(hidden_pk_test) +target_link_libraries(hidden_pk_test ec sha3 util crypto) diff --git a/lib/circuits/hidden_pk/hidden_pk_circuit.h b/lib/circuits/hidden_pk/hidden_pk_circuit.h new file mode 100644 index 00000000..8eab3ef9 --- /dev/null +++ b/lib/circuits/hidden_pk/hidden_pk_circuit.h @@ -0,0 +1,146 @@ +// Copyright 2026 Google LLC. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef PRIVACY_PROOFS_ZK_LIB_CIRCUITS_HIDDEN_PK_HIDDEN_PK_CIRCUIT_H_ +#define PRIVACY_PROOFS_ZK_LIB_CIRCUITS_HIDDEN_PK_HIDDEN_PK_CIRCUIT_H_ + +// Hidden-PK circuit for secp256k1 ECDSA signatures. +// +// Proves the relation: +// R = { (eth_addr, msg) ; (pk, sig) : +// keccak256(pkx_bytes || pky_bytes)[12:32] == eth_addr +// AND ECDSA_verify_secp256k1(pk, sig, msg) == true } + +#include +#include + +#include "circuits/ecdsa/verify_circuit.h" +#include "circuits/tests/sha3/sha3_circuit.h" +#include "ec/p256k1.h" + +namespace proofs { +template +class HiddenPKCircuit { + public: + using Field = Fp256k1Base; + using EC = P256k1; + using Nat = typename Field::N; + using Elt = typename Field::Elt; + using EltW = typename LogicCircuit::EltW; + using v8 = typename LogicCircuit::v8; + using v256 = typename LogicCircuit::v256; + + static constexpr size_t kBits = EC::kBits; // 256 + static constexpr size_t kKeccakBlocks = 1; // 64-byte input < rate=136 + static constexpr size_t kPKBytes = 32; // bytes per coordinate + + using EcdsaVerc = VerifyCircuit; + using KeccakCircuit = Sha3Circuit; + using KeccakBlockWitness = typename KeccakCircuit::BlockWitness; + + struct Witness { + EltW pk_x; + EltW pk_y; + // Bit decompositions: bit[0] = LSB, bit[255] = MSB. + typename LogicCircuit::template bitvec pk_x_bits; + typename LogicCircuit::template bitvec pk_y_bits; + // Keccak-256 block witnesses (1 block for 64-byte input). + std::vector keccak_bw; + // ECDSA verify witness. + typename EcdsaVerc::Witness ecdsa; + + void input(const LogicCircuit& lc) { + pk_x = lc.eltw_input(); + pk_y = lc.eltw_input(); + pk_x_bits = lc.template vinput(); + pk_y_bits = lc.template vinput(); + keccak_bw.resize(kKeccakBlocks); + for (size_t j = 0; j < kKeccakBlocks; ++j) { + keccak_bw[j].input(lc); + } + ecdsa.input(lc); + } + }; + + explicit HiddenPKCircuit(const LogicCircuit& lc) + : lc_(lc), sha_(lc), verc_(lc, p256k1, n256k1_order) {} + + // Asserts: + // ECDSA_verify(pk, sig, msg) == true + // keccak256(pkx_bytes || pky_bytes)[12:32] == eth_addr + // + // eth_addr must be a 20-element vector of v8 (the Ethereum address). + void assert_hidden_pk(const std::vector& eth_addr, EltW e, + const Witness& w) const { + // 1. Verify ECDSA signature. + verc_.verify_signature3(w.pk_x, w.pk_y, e, w.ecdsa); + + // 2. Verify bit decompositions of pk coordinates. + // Ensures the Keccak input bytes faithfully encode the ECDSA public key. + lc_.assert_eq(w.pk_x, as_scalar_large(w.pk_x_bits)); + lc_.assert_eq(w.pk_y, as_scalar_large(w.pk_y_bits)); + + // 3. Build 64-byte Keccak-256 input: pkx_bytes || pky_bytes (big-endian). + // pk_x_bits[0] = LSB; seed[0] = MSB byte of pkx = bits 255..248. + std::vector seed(64); + for (size_t i = 0; i < 64; ++i) seed[i] = lc_.template vbit<8>(0); + + // Bytes 0-31: pkx in big-endian byte order. + for (size_t i = 0; i < kPKBytes; ++i) { + size_t byte_idx = kPKBytes - 1 - i; // 31, 30, …, 0 + for (size_t b = 0; b < 8; ++b) { + seed[i][b] = w.pk_x_bits[byte_idx * 8 + b]; + } + } + // Bytes 32-63: pky in big-endian byte order. + for (size_t i = 0; i < kPKBytes; ++i) { + size_t byte_idx = kPKBytes - 1 - i; + for (size_t b = 0; b < 8; ++b) { + seed[kPKBytes + i][b] = w.pk_y_bits[byte_idx * 8 + b]; + } + } + + // 4. Assert keccak256(pkx || pky) and check bytes [12..31] == eth_addr. + std::vector hash_out; + sha_.assert_keccak256(seed, hash_out, w.keccak_bw); + + for (size_t i = 0; i < 20; ++i) { + lc_.vassert_eq(hash_out[12 + i], eth_addr[i]); + } + } + + private: + // Convert a bitvec (LSB-first) to a field element. + template + EltW as_scalar_large( + const typename LogicCircuit::template bitvec& v) const { + EltW r = lc_.konst(lc_.f_.zero()); + Elt p = lc_.f_.one(); + Elt two = lc_.f_.two(); + for (size_t i = 0; i < N; ++i) { + EltW vi = lc_.eval(v[i]); + r = lc_.axpy(r, p, vi); + p = lc_.f_.mulf(p, two); + } + return r; + } + + const LogicCircuit& lc_; + mutable KeccakCircuit sha_; + EcdsaVerc verc_; +}; + +} // namespace proofs + +#endif // PRIVACY_PROOFS_ZK_LIB_CIRCUITS_HIDDEN_PK_HIDDEN_PK_CIRCUIT_H_ diff --git a/lib/circuits/hidden_pk/hidden_pk_test.cc b/lib/circuits/hidden_pk/hidden_pk_test.cc new file mode 100644 index 00000000..010cfcbe --- /dev/null +++ b/lib/circuits/hidden_pk/hidden_pk_test.cc @@ -0,0 +1,280 @@ + +// Copyright 2026 Google LLC. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "circuits/hidden_pk/hidden_pk_circuit.h" +#include "circuits/hidden_pk/hidden_pk_witness.h" + +#include +#include +#include +#include + +#include "algebra/crt.h" +#include "algebra/crt_convolution.h" +#include "algebra/reed_solomon.h" +#include "algebra/static_string.h" +#include "arrays/dense.h" +#include "circuits/compiler/circuit_dump.h" +#include "circuits/compiler/compiler.h" +#include "circuits/logic/compiler_backend.h" +#include "circuits/logic/evaluation_backend.h" +#include "circuits/logic/logic.h" +#include "circuits/tests/sha3/sha3_reference.h" +#include "ec/p256k1.h" +#include "random/secure_random_engine.h" +#include "random/transcript.h" +#include "sumcheck/circuit.h" +#include "util/log.h" +#include "util/panic.h" +#include "util/readbuffer.h" +#include "zk/zk_proof.h" +#include "zk/zk_prover.h" +#include "zk/zk_testing.h" +#include "zk/zk_verifier.h" +#include "benchmark/benchmark.h" +#include "gtest/gtest.h" + +namespace proofs { +namespace { + +using Field = Fp256k1Base; +using EC = P256k1; +using Nat = Fp256k1Nat; +using Elt = Field::Elt; + +// Derive the 20-byte Ethereum address for a secp256k1 public key. +// Address = keccak256(pkx_big_endian || pky_big_endian)[12:32]. +std::vector derive_eth_address(Elt pkx_mont, Elt pky_mont) { + const Field& F = p256k1_base; + uint8_t msg[64] = {}; + Nat nx = F.from_montgomery(pkx_mont); + Nat ny = F.from_montgomery(pky_mont); + for (size_t i = 0; i < 32; ++i) { + uint8_t bx = 0, by = 0; + for (int j = 0; j < 8; ++j) { + if (nx.bit(255 - (i * 8 + j))) bx |= (1u << (7 - j)); + if (ny.bit(255 - (i * 8 + j))) by |= (1u << (7 - j)); + } + msg[i] = bx; + msg[32 + i] = by; + } + uint8_t hash[32]; + Sha3Reference::keccak256Hash(msg, 64, hash); + return std::vector(hash + 12, hash + 32); +} + +// Build the compiled circuit once and reuse. +std::unique_ptr> make_circuit() { + using CompilerBackend = CompilerBackend; + using LogicType = Logic; + using CircuitType = HiddenPKCircuit; + + QuadCircuit Q(p256k1_base); + const CompilerBackend cbk(&Q); + const LogicType lc(&cbk, p256k1_base); + CircuitType circuit(lc); + + // Public inputs: eth_addr (20 bytes) then e (1 field element). + std::vector eth_addr(20); + for (size_t i = 0; i < 20; ++i) { + eth_addr[i] = lc.template vinput<8>(); + } + auto e = lc.eltw_input(); + + Q.private_input(); + + typename CircuitType::Witness w; + w.input(lc); + + circuit.assert_hidden_pk(eth_addr, e, w); + return Q.mkcircuit(/*nc=*/1); +} + +// Fill the dense witness vector. +void fill_dense(Dense& W, const HiddenPKWitness& hw, + const Nat& e_nat, bool prover) { + const Field& F = p256k1_base; + DenseFiller filler(W); + + // Position 0: constant 1. + filler.push_back(F.one()); + + // Positions 1..160: eth_addr bits (20 bytes, LSB-first per byte). + auto addr = hw.eth_address_bytes(); + for (size_t i = 0; i < 20; ++i) { + filler.push_back(addr[i], 8, F); + } + + // Position 161: e as a field element. + filler.push_back(F.to_montgomery(e_nat)); + + if (prover) { + hw.fill_witness(filler); + } +} + + +TEST(HiddenPK, CircuitSize) { + set_log_level(INFO); + auto CIRCUIT = make_circuit(); + log(INFO, "HiddenPK circuit: ninputs=%zu npub_in=%zu nl=%zu", + CIRCUIT->ninputs, CIRCUIT->npub_in, CIRCUIT->nl); +} + +TEST(HiddenPK, ZkProverVerifier) { + const Field& F = p256k1_base; + + // Everything in this test is derived from a single sk. + // DISCLAIMER: Fixed k below is for testing only. Never reuse k in production. + Nat sk("0x9FE33A7A06BD0FE6F5208A61991C49B5B4DD12DC42D9903E789F5118F9675030"); + + // pk = sk * G (secp256k1 key derivation) + EC::ECPoint Qpt = p256k1.scalar_multf(p256k1.generator(), sk); + p256k1.normalize(Qpt); + Elt pkx = Qpt.x; + Elt pky = Qpt.y; + + // eth_addr = keccak256(pkx_bytes || pky_bytes)[12:32] + auto addr = derive_eth_address(pkx, pky); + ASSERT_EQ(addr.size(), 20u); + + Nat e_n("0xb94f6f125c79e932d738873f2584e5de7e816ed39e5c26df7ef96a73efacffcd"); + Nat k_n("0x4b688df40bcedbe641ddb16ff0a1842d9c67ea1c3bf63f3e0471baa664531d1a"); + + // R = k * G; r = R.x mod n + EC::ECPoint R_pt = p256k1.scalar_multf(p256k1.generator(), k_n); + p256k1.normalize(R_pt); + Nat r_raw = p256k1_base.from_montgomery(R_pt.x); + Nat r_n = p256k1_scalar.from_montgomery(p256k1_scalar.to_montgomery(r_raw)); + + // s = k^{-1} * (e + r * sk) mod n + Nat k_inv = p256k1_scalar.from_montgomery( + p256k1_scalar.invertf(p256k1_scalar.to_montgomery(k_n))); + auto e_mont = p256k1_scalar.to_montgomery(e_n); + auto r_mont = p256k1_scalar.to_montgomery(r_n); + auto sk_mont = p256k1_scalar.to_montgomery(sk); + auto r_sk = p256k1_scalar.mulf(r_mont, sk_mont); + auto e_rsk = p256k1_scalar.addf(e_mont, r_sk); + auto k_inv_m = p256k1_scalar.to_montgomery(k_inv); + auto s_mont = p256k1_scalar.mulf(k_inv_m, e_rsk); + Nat s_n = p256k1_scalar.from_montgomery(s_mont); + + // Verify the ECDSA equation algebraically: s * k == e + r * sk (mod n). + // This proves that (r, s) was produced by the same sk that generated pk, + // and therefore the address, the public key, and the signature all belong + // to one key pair. + { + auto lhs = p256k1_scalar.mulf(p256k1_scalar.to_montgomery(s_n), + p256k1_scalar.to_montgomery(k_n)); + auto rhs = p256k1_scalar.addf(e_mont, r_sk); + ASSERT_EQ(p256k1_scalar.from_montgomery(lhs), + p256k1_scalar.from_montgomery(rhs)) + << "ECDSA self-consistency failed: s*k != e + r*sk mod n"; + } + + // Compute ZK witnesses. + HiddenPKWitness hw; + ASSERT_TRUE(hw.compute(pkx, pky, e_n, r_n, s_n)) + << "ECDSA witness computation failed"; + + // The witness's address must match addr derived directly from pk. + EXPECT_EQ(hw.eth_address_bytes(), addr); + + // Build circuit and witnesses. + auto CIRCUIT = make_circuit(); + const size_t npub = CIRCUIT->npub_in; + const size_t nin = CIRCUIT->ninputs; + + auto W = std::make_unique>(1, nin); + auto pub = std::make_unique>(1, npub); + + fill_dense(*W, hw, e_n, /*prover=*/true); + fill_dense(*pub, hw, e_n, /*prover=*/false); + + // ZK proof via CRT256 + CrtConvolutionFactory (secp256k1-compatible RS). + using Crt256 = CRT256; + using CrtConvFactory = CrtConvolutionFactory; + using RSFactory = ReedSolomonFactory; + + const CrtConvFactory conv(p256k1_base); + const RSFactory rsf(conv, p256k1_base); + + ZkProof zkpr(*CIRCUIT, kLigeroRate, kLigeroNreq); + Transcript tp((uint8_t*)"hidden_pk_test", 14, kVersion); + SecureRandomEngine rng; + + ZkProver prover(*CIRCUIT, p256k1_base, rsf); + prover.commit(zkpr, *W, tp, rng); + ASSERT_TRUE(prover.prove(zkpr, *W, tp)) << "ZK proof failed"; + + // Verify. + ZkVerifier verifier(*CIRCUIT, rsf, kLigeroRate, + kLigeroNreq, p256k1_base); + Transcript tv((uint8_t*)"hidden_pk_test", 14, kVersion); + verifier.recv_commitment(zkpr, tv); + EXPECT_TRUE(verifier.verify(zkpr, *pub, tv)) << "ZK verification failed"; +} + +void BM_HiddenPKProver(benchmark::State& state) { + set_log_level(LogLevel::ERROR); + + Nat sk("0x9FE33A7A06BD0FE6F5208A61991C49B5B4DD12DC42D9903E789F5118F9675030"); + EC::ECPoint Qpt = p256k1.scalar_multf(p256k1.generator(), sk); + p256k1.normalize(Qpt); + + Nat e_n("0xb94f6f125c79e932d738873f2584e5de7e816ed39e5c26df7ef96a73efacffcd"); + Nat k_n("0x4b688df40bcedbe641ddb16ff0a1842d9c67ea1c3bf63f3e0471baa664531d1a"); + + EC::ECPoint R_pt = p256k1.scalar_multf(p256k1.generator(), k_n); + p256k1.normalize(R_pt); + Nat r_raw = p256k1_base.from_montgomery(R_pt.x); + Nat r_n = p256k1_scalar.from_montgomery(p256k1_scalar.to_montgomery(r_raw)); + + Nat k_inv = p256k1_scalar.from_montgomery( + p256k1_scalar.invertf(p256k1_scalar.to_montgomery(k_n))); + auto s_mont = p256k1_scalar.mulf( + p256k1_scalar.to_montgomery(k_inv), + p256k1_scalar.addf( + p256k1_scalar.to_montgomery(e_n), + p256k1_scalar.mulf(p256k1_scalar.to_montgomery(r_n), + p256k1_scalar.to_montgomery(sk)))); + Nat s_n = p256k1_scalar.from_montgomery(s_mont); + + HiddenPKWitness hw; + hw.compute(Qpt.x, Qpt.y, e_n, r_n, s_n); + + auto CIRCUIT = make_circuit(); + auto W = std::make_unique>(1, CIRCUIT->ninputs); + fill_dense(*W, hw, e_n, true); + + using Crt256 = CRT256; + using CrtConvFactory = CrtConvolutionFactory; + using RSFactory = ReedSolomonFactory; + const CrtConvFactory conv(p256k1_base); + const RSFactory rsf(conv, p256k1_base); + + for (auto s : state) { + ZkProof zkpr(*CIRCUIT, kLigeroRate, kLigeroNreq); + Transcript tp((uint8_t*)"bench", 5, kVersion); + SecureRandomEngine rng; + ZkProver prover(*CIRCUIT, p256k1_base, rsf); + prover.commit(zkpr, *W, tp, rng); + prover.prove(zkpr, *W, tp); + } +} +BENCHMARK(BM_HiddenPKProver); + +} // namespace +} // namespace proofs diff --git a/lib/circuits/hidden_pk/hidden_pk_witness.h b/lib/circuits/hidden_pk/hidden_pk_witness.h new file mode 100644 index 00000000..4facef1e --- /dev/null +++ b/lib/circuits/hidden_pk/hidden_pk_witness.h @@ -0,0 +1,143 @@ +// Copyright 2026 Google LLC. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef PRIVACY_PROOFS_ZK_LIB_CIRCUITS_HIDDEN_PK_HIDDEN_PK_WITNESS_H_ +#define PRIVACY_PROOFS_ZK_LIB_CIRCUITS_HIDDEN_PK_HIDDEN_PK_WITNESS_H_ + +// Witness computation for HiddenPKCircuit. +// +// Given (pkx, pky, e, r, s) in the secp256k1 scalar/base fields, this class +// computes all private witness values needed by the circuit and fills a +// DenseFiller for use with ZkProver. + +#include +#include +#include + +#include "arrays/dense.h" +#include "circuits/ecdsa/verify_witness.h" +#include "circuits/tests/sha3/sha3_reference.h" +#include "circuits/tests/sha3/sha3_witness.h" +#include "ec/p256k1.h" + +namespace proofs { + +class HiddenPKWitness { + public: + using Field = Fp256k1Base; + using EC = P256k1; + using Scalar = Fp256k1Scalar; + using Elt = typename Field::Elt; + using Nat = typename Field::N; + + static constexpr size_t kBits = EC::kBits; + static constexpr size_t kKeccakBlocks = 1; + + // Field elements (Montgomery-encoded). + Elt pkx_; + Elt pky_; + + // Keccak-256 block witness (1 block for 64-byte input). + Sha3Witness::BlockWitness keccak_bw_; + + // ECDSA verify witness. + VerifyWitness3 ecdsa_; + + explicit HiddenPKWitness() : ecdsa_(p256k1_scalar, p256k1) {} + + // Compute all witness values. + // Returns false if the ECDSA witness computation fails (bad signature). + bool compute(const Elt& pkx, const Elt& pky, + const Nat& e, const Nat& r, const Nat& s) { + pkx_ = pkx; + pky_ = pky; + + const Field& F = p256k1_base; + + // 1. Compute ECDSA witnesses. + if (!ecdsa_.compute_witness(pkx, pky, e, r, s)) { + return false; + } + + // 2. Build the 64-byte Keccak message: pkx_bytes || pky_bytes (big-endian). + uint8_t msg[64] = {}; + Nat nx = F.from_montgomery(pkx); + Nat ny = F.from_montgomery(pky); + + for (size_t i = 0; i < 32; ++i) { + uint8_t bx = 0, by = 0; + for (int j = 0; j < 8; ++j) { + if (nx.bit(255 - (i * 8 + j))) bx |= (1u << (7 - j)); + if (ny.bit(255 - (i * 8 + j))) by |= (1u << (7 - j)); + } + msg[i] = bx; + msg[32 + i] = by; + } + + // 3. Compute Keccak-256 block witnesses. + std::vector seed(msg, msg + 64); + std::vector bws; + Sha3Witness::compute_witness_keccak256(seed, bws); + keccak_bw_ = bws[0]; + + return true; + } + + // Return the Ethereum address: last 20 bytes of keccak256(pkx || pky). + std::vector eth_address_bytes() const { + const Field& F = p256k1_base; + uint8_t msg[64] = {}; + Nat nx = F.from_montgomery(pkx_); + Nat ny = F.from_montgomery(pky_); + for (size_t i = 0; i < 32; ++i) { + uint8_t bx = 0, by = 0; + for (int j = 0; j < 8; ++j) { + if (nx.bit(255 - (i * 8 + j))) bx |= (1u << (7 - j)); + if (ny.bit(255 - (i * 8 + j))) by |= (1u << (7 - j)); + } + msg[i] = bx; + msg[32 + i] = by; + } + uint8_t hash[32]; + Sha3Reference::keccak256Hash(msg, 64, hash); + return std::vector(hash + 12, hash + 32); + } + + // Fill a DenseFiller with all private witness values. + void fill_witness(DenseFiller& filler) const { + const Field& F = p256k1_base; + + // pk_x and pk_y as field elements. + filler.push_back(pkx_); + filler.push_back(pky_); + + // Bit decompositions: LSB-first (bit 0 = LSB of the integer). + Nat nx = F.from_montgomery(pkx_); + Nat ny = F.from_montgomery(pky_); + for (size_t i = 0; i < kBits; ++i) + filler.push_back(F.of_scalar(nx.bit(i))); + for (size_t i = 0; i < kBits; ++i) + filler.push_back(F.of_scalar(ny.bit(i))); + + // Keccak-256 block witness bits. + Sha3Witness::fill_witness(filler, keccak_bw_, F); + + // ECDSA verify witnesses. + ecdsa_.fill_witness(filler); + } +}; + +} // namespace proofs + +#endif // PRIVACY_PROOFS_ZK_LIB_CIRCUITS_HIDDEN_PK_HIDDEN_PK_WITNESS_H_ diff --git a/lib/circuits/tests/sha3/sha3_circuit.h b/lib/circuits/tests/sha3/sha3_circuit.h index a5ed59e6..6e784af1 100644 --- a/lib/circuits/tests/sha3/sha3_circuit.h +++ b/lib/circuits/tests/sha3/sha3_circuit.h @@ -322,6 +322,88 @@ class Sha3Circuit { check(bw_idx == bws.size(), "Did not consume all BlockWitnesses"); } + // Identical to assert_shake256 + // except the padding byte is 0x01 instead of 0x1F, and output is fixed at + // 32 bytes so there is no squeeze phase. + // + // For Ethereum address derivation: call with the 64-byte uncompressed public + // key (x || y, no 0x04 prefix) and take out[12..31] as the address. + void assert_keccak256(const std::vector& seed, std::vector& out, + const std::vector& bws) { + constexpr size_t rate = 136; + constexpr size_t outlen = 32; + size_t num_absorb_blocks = (seed.size() + rate) / rate; + // 32-byte output fits in the first rate-sized squeeze, so no extra blocks. + check(bws.size() == num_absorb_blocks, "Incorrect number of BlockWitnesses"); + + out.resize(outlen); + + // Eagerly bind output wires to the final absorbed state (round 23). + // Must happen before the absorb loop so the wire refs are available. + size_t out_ptr = 0; + size_t sx = 0, sy = 0; + for (size_t i = 0; i < outlen; i += 8) { + for (size_t b = 0; b < 8; ++b) { + for (size_t j = 0; j < 8; ++j) { + out[out_ptr][j] = + bws[num_absorb_blocks - 1].a_intermediate[23][sx][sy][b * 8 + j]; + } + ++out_ptr; + } + ++sx; + if (sx == 5) { + ++sy; + sx = 0; + } + } + + // Absorb phase. + std::vector block(200); + for (size_t i = 0; i < 200; ++i) block[i] = lc_.template vbit<8>(0); + size_t bw_idx = 0; + size_t ptr = 0; + + for (size_t i = 0; i < seed.size(); ++i) { + block[ptr++] = seed[i]; + if (ptr == rate) { + v64 A_in[5][5]; + for (int x = 0; x < 5; ++x) { + for (int y = 0; y < 5; ++y) { + if (bw_idx == 0) { + A_in[x][y] = lc_.template vbit<64>(0); + } else { + A_in[x][y] = bws[bw_idx - 1].a_intermediate[23][x][y]; + } + } + } + xorin_block(A_in, block, rate); + keccak_f_1600(A_in, bws[bw_idx++]); + ptr = 0; + for (size_t j = 0; j < 200; ++j) block[j] = lc_.template vbit<8>(0); + } + } + + // Keccak-256 padding: 0x01 at ptr, 0x80 XOR'd at rate-1. + // (SHAKE256 uses 0x1F here; SHA3-256 uses 0x06.) + block[ptr] = lc_.vxor(block[ptr], lc_.template vbit<8>(0x01)); + block[rate - 1] = lc_.vxor(block[rate - 1], lc_.template vbit<8>(0x80)); + + v64 A_in[5][5]; + for (int x = 0; x < 5; ++x) { + for (int y = 0; y < 5; ++y) { + if (bw_idx == 0) { + A_in[x][y] = lc_.template vbit<64>(0); + } else { + A_in[x][y] = bws[bw_idx - 1].a_intermediate[23][x][y]; + } + } + } + xorin_block(A_in, block, rate); + keccak_f_1600(A_in, bws[bw_idx++]); + + check(bw_idx == bws.size(), "Did not consume all BlockWitnesses"); + } + template void sha3_vassert_eq_range(const v64& x, const v64& y) const { auto xx = lc_.as_scalar(lc_.template slice(x)); diff --git a/lib/circuits/tests/sha3/sha3_circuit_test.cc b/lib/circuits/tests/sha3/sha3_circuit_test.cc index 7bc2f433..fd925b57 100644 --- a/lib/circuits/tests/sha3/sha3_circuit_test.cc +++ b/lib/circuits/tests/sha3/sha3_circuit_test.cc @@ -260,6 +260,63 @@ TEST(SHA3_Circuit, AssertShake256) { } } +TEST(SHA3_Circuit, AssertKeccak256EthereumAddress) { + const EvalBackend ebk(F); + const Logic L(&ebk, F); + Sha3Circuit SHAC(L); + + static const uint8_t pubkey[64] = { + 0x2e, 0x30, 0x8d, 0x25, 0xd2, 0x7f, 0x4c, 0x59, 0x5c, 0x4a, 0x75, + 0x43, 0x25, 0x3f, 0x3c, 0x23, 0x50, 0xc9, 0xd3, 0x23, 0xbc, 0xd5, + 0x02, 0x29, 0xc8, 0x37, 0x68, 0x1f, 0x08, 0xda, 0x6a, 0x33, 0xb8, + 0xab, 0x23, 0x8f, 0xae, 0xcd, 0x9c, 0xf2, 0xa7, 0x35, 0x9e, 0x6a, + 0x69, 0x08, 0x97, 0x10, 0x87, 0x6a, 0xb1, 0x36, 0xab, 0x01, 0x7f, + 0x88, 0x50, 0x5c, 0x03, 0x51, 0x6c, 0xc0, 0x99, 0xc6, + }; + // Expected: last 20 bytes of keccak256(pubkey), i.e., digest[12..31]. + static const uint8_t expected_addr[20] = { + 0xb9, 0x0d, 0x65, 0xa6, 0x24, 0x90, 0x9b, 0xc3, 0x6e, 0xee, + 0x6b, 0xff, 0xde, 0xcf, 0x3c, 0x5a, 0xcd, 0x77, 0x74, 0xc0, + }; + + std::vector seed; + for (uint8_t byte : pubkey) seed.push_back(L.vbit8(byte)); + + std::vector bws; + Sha3Witness::compute_witness_keccak256( + std::vector(pubkey, pubkey + 64), bws); + + std::vector::BlockWitness> circuit_bws(bws.size()); + for (size_t k = 0; k < bws.size(); ++k) { + for (size_t round = 0; round < 24; ++round) { + if (sha3_slice_at(round)) { + for (size_t x = 0; x < 5; ++x) { + for (size_t y = 0; y < 5; ++y) { + for (size_t b = 0; b < 64; ++b) { + circuit_bws[k].a_intermediate[round][x][y][b] = + L.bit((bws[k].a_intermediate[round][x][y] >> b) & 1); + } + } + } + } + } + } + + std::vector hash_out; + SHAC.assert_keccak256(seed, hash_out, circuit_bws); + + ASSERT_EQ(hash_out.size(), 32u); + + // Ethereum address is hash_out[12..31]. + for (size_t i = 0; i < 20; ++i) { + uint8_t val = 0; + for (int j = 0; j < 8; ++j) { + if (L.eval(hash_out[12 + i][j]).elt() == F.one()) val |= (1 << j); + } + EXPECT_EQ(val, expected_addr[i]) << "address byte " << i << " mismatch"; + } +} + template std::unique_ptr> make_shake256_circuit(size_t seed_size, size_t out_size, diff --git a/lib/circuits/tests/sha3/sha3_reference.cc b/lib/circuits/tests/sha3/sha3_reference.cc index a87d8e81..604bd57d 100644 --- a/lib/circuits/tests/sha3/sha3_reference.cc +++ b/lib/circuits/tests/sha3/sha3_reference.cc @@ -204,4 +204,41 @@ void Sha3Reference::shake256Hash(const uint8_t* in, size_t inlen, uint8_t* out, shake(136, in, inlen, out, outlen); } +void Sha3Reference::keccak256Hash(const uint8_t* in, size_t inlen, + uint8_t out[32]) { + // Keccak-256 rate = 200 - 2*32 = 136 bytes (same as SHA3-256). + // The only difference from SHA3-256 is the padding byte: 0x01 here vs 0x06. + constexpr size_t rate = 136; + constexpr size_t mdlen = 32; + uint64_t A[5][5] = {}; + uint8_t block[200] = {}; + size_t ptr = 0; + + for (size_t i = 0; i < inlen; ++i) { + block[ptr++] = in[i]; + if (ptr == rate) { + xorin(A, block, rate); + keccak_f_1600(A); + ptr = 0; + for (size_t j = 0; j < rate; ++j) block[j] = 0; + } + } + + // Keccak-256 padding: 0x01 (SHA3 uses 0x06, SHAKE uses 0x1F). + block[ptr] ^= 0x01; + block[rate - 1] ^= 0x80; + xorin(A, block, rate); + keccak_f_1600(A); + + size_t x = 0, y = 0; + for (size_t i = 0; i < mdlen; i += 8) { + wu64le(&out[i], A[x][y]); + ++x; + if (x == 5) { + ++y; + x = 0; + } + } +} + } // namespace proofs diff --git a/lib/circuits/tests/sha3/sha3_reference.h b/lib/circuits/tests/sha3/sha3_reference.h index 9ad0f927..dcd73446 100644 --- a/lib/circuits/tests/sha3/sha3_reference.h +++ b/lib/circuits/tests/sha3/sha3_reference.h @@ -53,6 +53,12 @@ class Sha3Reference { static void shake256Hash(const uint8_t* in, size_t inlen, uint8_t* out, size_t outlen); static void xorin(uint64_t A[5][5], const uint8_t* d, size_t n); + + // Keccak-256 as used by Ethereum. Identical to SHA3-256 except the padding + // byte is 0x01 instead of 0x06. To derive an Ethereum address from a 64-byte + // uncompressed public key (without the 0x04 prefix), hash those 64 bytes with + // this function and take the last 20 bytes of the result. + static void keccak256Hash(const uint8_t* in, size_t inlen, uint8_t out[32]); }; } // namespace proofs diff --git a/lib/circuits/tests/sha3/sha3_reference_test.cc b/lib/circuits/tests/sha3/sha3_reference_test.cc index e802e2c5..1e160718 100644 --- a/lib/circuits/tests/sha3/sha3_reference_test.cc +++ b/lib/circuits/tests/sha3/sha3_reference_test.cc @@ -149,5 +149,74 @@ TEST(Sha3Reference, Shake256Test) { } } +// Known-vector check: keccak256("") and keccak256("abc"). +// These differ from SHA3-256 only in the padding byte (0x01 vs 0x06). +TEST(Sha3Reference, Keccak256KnownVectors) { + struct tv { + const char* msg; + uint8_t hash[32]; + }; + static const tv vecs[] = { + // keccak256("") - used as the canonical Ethereum empty-input vector + {"", + {0xc5, 0xd2, 0x46, 0x01, 0x86, 0xf7, 0x23, 0x3c, 0x92, 0x7e, 0x7d, + 0xb2, 0xdc, 0xc7, 0x03, 0xc0, 0xe5, 0x00, 0xb6, 0x53, 0xca, 0x82, + 0x27, 0x3b, 0x7b, 0xfa, 0xd8, 0x04, 0x5d, 0x85, 0xa4, 0x70}}, + // keccak256("abc") + {"abc", + {0x4e, 0x03, 0x65, 0x7a, 0xea, 0x45, 0xa9, 0x4f, 0xc7, 0xd4, 0x7b, + 0xa8, 0x26, 0xc8, 0xd6, 0x67, 0xc0, 0xd1, 0xe6, 0xe3, 0x3a, 0x64, + 0xa0, 0x36, 0xec, 0x44, 0xf5, 0x8f, 0xa1, 0x2d, 0x6c, 0x45}}, + }; + for (const auto& v : vecs) { + uint8_t out[32]; + Sha3Reference::keccak256Hash(reinterpret_cast(v.msg), + strlen(v.msg), out); + for (size_t i = 0; i < 32; ++i) { + EXPECT_EQ(out[i], v.hash[i]) << "byte " << i << " for input \"" << v.msg + << "\""; + } + } +} + +// Sanity check: derive an Ethereum address from a known public key. +// +// Ethereum address derivation: keccak256(pubkey_x || pubkey_y)[12:32] +// i.e., the last 20 bytes of the 32-byte keccak256 digest. +// +// Public key (64 bytes, uncompressed, no 0x04 prefix): +// 2e308d25d27f4c595c4a7543253f3c23 50c9d323bcd50229c837681f08da6a33 +// b8ab238faecd9cf2a7359e6a69089710 876ab136ab017f88505c03516cc099c6 +// +// Full keccak256 digest: +// 0c2be79367ba0c0b59460475b90d65a624909bc36eee6bffdecf3c5acd7774c0 +// +// Ethereum address = digest[12..31]: +// 0xb90d65a624909bc36eee6bffdecf3c5acd7774c0 +TEST(Sha3Reference, Keccak256EthereumAddress) { + static const uint8_t pubkey[64] = { + 0x2e, 0x30, 0x8d, 0x25, 0xd2, 0x7f, 0x4c, 0x59, 0x5c, 0x4a, 0x75, + 0x43, 0x25, 0x3f, 0x3c, 0x23, 0x50, 0xc9, 0xd3, 0x23, 0xbc, 0xd5, + 0x02, 0x29, 0xc8, 0x37, 0x68, 0x1f, 0x08, 0xda, 0x6a, 0x33, 0xb8, + 0xab, 0x23, 0x8f, 0xae, 0xcd, 0x9c, 0xf2, 0xa7, 0x35, 0x9e, 0x6a, + 0x69, 0x08, 0x97, 0x10, 0x87, 0x6a, 0xb1, 0x36, 0xab, 0x01, 0x7f, + 0x88, 0x50, 0x5c, 0x03, 0x51, 0x6c, 0xc0, 0x99, 0xc6, + }; + // Expected: last 20 bytes of keccak256(pubkey), i.e., digest[12..31]. + static const uint8_t expected_addr[20] = { + 0xb9, 0x0d, 0x65, 0xa6, 0x24, 0x90, 0x9b, 0xc3, 0x6e, 0xee, + 0x6b, 0xff, 0xde, 0xcf, 0x3c, 0x5a, 0xcd, 0x77, 0x74, 0xc0, + }; + + uint8_t hash[32]; + Sha3Reference::keccak256Hash(pubkey, sizeof(pubkey), hash); + + // Ethereum address = last 20 bytes of the 32-byte keccak256 digest. + for (size_t i = 0; i < 20; ++i) { + EXPECT_EQ(hash[12 + i], expected_addr[i]) + << "address byte " << i << " mismatch"; + } +} + } // namespace } // namespace proofs diff --git a/lib/circuits/tests/sha3/sha3_witness.cc b/lib/circuits/tests/sha3/sha3_witness.cc index 98ad7445..ae3f8fce 100644 --- a/lib/circuits/tests/sha3/sha3_witness.cc +++ b/lib/circuits/tests/sha3/sha3_witness.cc @@ -80,4 +80,35 @@ void Sha3Witness::compute_witness_shake256( } } +void Sha3Witness::compute_witness_keccak256( + const std::vector& seed, std::vector& witnesses) { + constexpr size_t rate = 136; + uint64_t A[5][5]; + std::memset(A, 0, sizeof(A)); + + uint8_t block[200] = {0}; + size_t ptr = 0; + + for (size_t i = 0; i < seed.size(); ++i) { + block[ptr++] = seed[i]; + if (ptr == rate) { + Sha3Reference::xorin(A, block, rate); + BlockWitness bw; + compute_witness_block(A, bw); + witnesses.push_back(bw); + ptr = 0; + std::memset(block, 0, rate); + } + } + + // Keccak-256 padding: 0x01 (SHAKE256 uses 0x1F, SHA3-256 uses 0x06). + block[ptr] ^= 0x01; + block[rate - 1] ^= 0x80; + Sha3Reference::xorin(A, block, rate); + BlockWitness bw; + compute_witness_block(A, bw); + witnesses.push_back(bw); + // No squeeze phase: 32-byte output < 136-byte rate, already in final state. +} + } // namespace proofs diff --git a/lib/circuits/tests/sha3/sha3_witness.h b/lib/circuits/tests/sha3/sha3_witness.h index 2946620a..2a7fa246 100644 --- a/lib/circuits/tests/sha3/sha3_witness.h +++ b/lib/circuits/tests/sha3/sha3_witness.h @@ -41,6 +41,12 @@ struct Sha3Witness { size_t outlen, std::vector& witnesses); + // Generate BlockWitnesses for a keccak256 computation (Ethereum's hash). + // Padding byte is 0x01 instead of SHAKE256's 0x1F. Output is always 32 + // bytes so no squeeze-phase witnesses are needed. + static void compute_witness_keccak256(const std::vector& seed, + std::vector& witnesses); + // Fills a Dense array mapping with exactly the bit outputs of the block // witnesses. template