eth/secp256k1_utility.cpp¶
Namespaces¶
| Name |
|---|
| eth |
Functions¶
| Name | |
|---|---|
| std::optional< Address > | secp256k1_address_from_private_key(const Secp256k1PrivateKey & private_key) |
| std::optional< Address > | secp256k1_recover_address(const Hash256 & message_hash, const codec::ByteBuffer & recoverable_signature) |
| std::optional< codec::ByteBuffer > | secp256k1_sign_recoverable(const Hash256 & message_hash, const Secp256k1PrivateKey & private_key) |
| std::optional< std::string > | DecompressXOnlyPubkey(const std::array< uint8_t, 32 > & contract_x_bytes, bool destination_y_odd) Decompress a 32-byte X-only secp256k1 public key to a 128-char hex destination. |
Functions Documentation¶
function secp256k1_address_from_private_key¶
std::optional< Address > secp256k1_address_from_private_key(
const Secp256k1PrivateKey & private_key
)
function secp256k1_recover_address¶
std::optional< Address > secp256k1_recover_address(
const Hash256 & message_hash,
const codec::ByteBuffer & recoverable_signature
)
function secp256k1_sign_recoverable¶
std::optional< codec::ByteBuffer > secp256k1_sign_recoverable(
const Hash256 & message_hash,
const Secp256k1PrivateKey & private_key
)
function DecompressXOnlyPubkey¶
std::optional< std::string > DecompressXOnlyPubkey(
const std::array< uint8_t, 32 > & contract_x_bytes,
bool destination_y_odd
)
Decompress a 32-byte X-only secp256k1 public key to a 128-char hex destination.
Parameters:
- contract_x_bytes 32 bytes from the ABI-decoded bytes32 parameter (contract byte order: LSB first in hex).
- destination_y_odd Parity of Y from the event (false=even/0x02, true=odd/0x03).
Return: 128-char hex destination string (X+Y concatenated in contract order), or nullopt if X is all-zero or the X+parity combination is not on the curve.
Uses the Y parity from the bridge event's bool parameter to select the correct compressed-key prefix (false = even Y / 0x02, true = odd Y / 0x03). The decompression is deterministic – no parity trial is needed because the event carries the ground-truth parity bit.
Source code¶
// Copyright 2026 Genius Ventures, Inc.
// SPDX-License-Identifier: MIT
#include <eth/secp256k1_utility.hpp>
#include <eth/abi_decoder.hpp>
#include <base/parse_utility.hpp>
#include <secp256k1.h>
#include <secp256k1_recovery.h>
#include <algorithm>
namespace eth {
namespace {
constexpr size_t kCompactSignatureBytes = 64;
constexpr size_t kRecoverableSignatureBytes = 65;
constexpr size_t kUncompressedPublicKeyBytes = 65;
constexpr uint8_t kUncompressedPublicKeyPrefix = 0x04;
// ── X-only decompression constants (Bridge V2) ──────────────────────────────
constexpr size_t kXOnlyKeyBytes = 32;
constexpr size_t kCompressedKeyLen = 33;
constexpr uint8_t kEvenYParityPrefix = 0x02;
constexpr uint8_t kOddYParityPrefix = 0x03;
Address address_from_uncompressed_public_key(
const std::array<uint8_t, kUncompressedPublicKeyBytes>& public_key)
{
codec::ByteBuffer payload(public_key.begin() + 1, public_key.end());
const auto hash = abi::keccak256(payload.data(), payload.size());
Address address{};
std::copy(hash.end() - address.size(), hash.end(), address.begin());
return address;
}
std::optional<Address> address_from_public_key(secp256k1_context* context, const secp256k1_pubkey& public_key)
{
std::array<uint8_t, kUncompressedPublicKeyBytes> public_key_bytes{};
size_t public_key_length = public_key_bytes.size();
if (!secp256k1_ec_pubkey_serialize(
context,
public_key_bytes.data(),
&public_key_length,
&public_key,
SECP256K1_EC_UNCOMPRESSED))
{
return std::nullopt;
}
if (public_key_length != public_key_bytes.size()
|| public_key_bytes.front() != kUncompressedPublicKeyPrefix)
{
return std::nullopt;
}
return address_from_uncompressed_public_key(public_key_bytes);
}
} // namespace
std::optional<Address> secp256k1_address_from_private_key(
const Secp256k1PrivateKey& private_key) noexcept
{
secp256k1_context* context = secp256k1_context_create(SECP256K1_CONTEXT_SIGN);
if (context == nullptr)
{
return std::nullopt;
}
if (!secp256k1_ec_seckey_verify(context, private_key.data()))
{
secp256k1_context_destroy(context);
return std::nullopt;
}
secp256k1_pubkey public_key{};
if (!secp256k1_ec_pubkey_create(context, &public_key, private_key.data()))
{
secp256k1_context_destroy(context);
return std::nullopt;
}
const auto address = address_from_public_key(context, public_key);
secp256k1_context_destroy(context);
return address;
}
std::optional<Address> secp256k1_recover_address(
const Hash256& message_hash,
const codec::ByteBuffer& recoverable_signature) noexcept
{
if (recoverable_signature.size() != kRecoverableSignatureBytes)
{
return std::nullopt;
}
const int recovery_id = static_cast<int>(recoverable_signature[kCompactSignatureBytes]);
if (recovery_id < 0 || recovery_id > 3)
{
return std::nullopt;
}
secp256k1_context* context = secp256k1_context_create(SECP256K1_CONTEXT_VERIFY);
if (context == nullptr)
{
return std::nullopt;
}
secp256k1_ecdsa_recoverable_signature signature{};
if (!secp256k1_ecdsa_recoverable_signature_parse_compact(
context,
&signature,
recoverable_signature.data(),
recovery_id))
{
secp256k1_context_destroy(context);
return std::nullopt;
}
secp256k1_pubkey public_key{};
if (!secp256k1_ecdsa_recover(context, &public_key, &signature, message_hash.data()))
{
secp256k1_context_destroy(context);
return std::nullopt;
}
const auto address = address_from_public_key(context, public_key);
secp256k1_context_destroy(context);
return address;
}
std::optional<codec::ByteBuffer> secp256k1_sign_recoverable(
const Hash256& message_hash,
const Secp256k1PrivateKey& private_key) noexcept
{
secp256k1_context* context = secp256k1_context_create(SECP256K1_CONTEXT_SIGN);
if (context == nullptr)
{
return std::nullopt;
}
if (!secp256k1_ec_seckey_verify(context, private_key.data()))
{
secp256k1_context_destroy(context);
return std::nullopt;
}
secp256k1_ecdsa_recoverable_signature signature{};
if (!secp256k1_ecdsa_sign_recoverable(
context,
&signature,
message_hash.data(),
private_key.data(),
nullptr,
nullptr))
{
secp256k1_context_destroy(context);
return std::nullopt;
}
codec::ByteBuffer signature_bytes(kRecoverableSignatureBytes);
int recovery_id = 0;
secp256k1_ecdsa_recoverable_signature_serialize_compact(
context,
signature_bytes.data(),
&recovery_id,
&signature);
secp256k1_context_destroy(context);
signature_bytes[kCompactSignatureBytes] = static_cast<uint8_t>(recovery_id);
return signature_bytes;
}
std::optional<std::string> DecompressXOnlyPubkey(
const std::array<uint8_t, 32>& contract_x_bytes,
bool destination_y_odd) noexcept
{
// Guard: reject the point-at-infinity (all-zero X) -- invalid as a public key.
const bool all_zero = std::all_of(
contract_x_bytes.begin(), contract_x_bytes.end(),
[](const uint8_t b) { return b == 0; });
if (all_zero)
{
return std::nullopt;
}
// Select the compressed-key prefix from the explicit parity flag carried by
// the event (D-09). false = even Y (0x02), true = odd Y (0x03).
const uint8_t prefix = destination_y_odd ? kOddYParityPrefix : kEvenYParityPrefix;
// libsecp256k1 expects the X coordinate in big-endian (MSB first). Contract
// bytes are stored in reverse order (LSB first in hex), so reverse them.
std::array<uint8_t, kXOnlyKeyBytes> x_bigendian{};
for (size_t i = 0; i < kXOnlyKeyBytes; ++i)
{
x_bigendian[i] = contract_x_bytes[kXOnlyKeyBytes - 1 - i];
}
// Build the 33-byte compressed key: [prefix][X_bigendian].
std::array<uint8_t, kCompressedKeyLen> compressed{};
compressed[0] = prefix;
std::copy(x_bigendian.begin(), x_bigendian.end(), compressed.begin() + 1);
secp256k1_context* context = secp256k1_context_create(SECP256K1_CONTEXT_NONE);
if (context == nullptr)
{
return std::nullopt;
}
secp256k1_pubkey public_key{};
if (!secp256k1_ec_pubkey_parse(
context,
&public_key,
compressed.data(),
compressed.size()))
{
// X + parity combination is not on the curve -- malformed event data.
secp256k1_context_destroy(context);
return std::nullopt;
}
std::array<uint8_t, kUncompressedPublicKeyBytes> uncompressed{};
size_t uncompressed_len = uncompressed.size();
secp256k1_ec_pubkey_serialize(
context,
uncompressed.data(),
&uncompressed_len,
&public_key,
SECP256K1_EC_UNCOMPRESSED);
secp256k1_context_destroy(context);
// uncompressed = [0x04][X_bigendian(32)][Y_bigendian(32)] (65 bytes, indices 0..64).
// Y_bigendian occupies indices 33..64. Reverse to contract byte order so that
// hex_bytes() reproduces the same ordering used by GetAddress():
// contract_y[i] = Y_bigendian[31 - i] = uncompressed[33 + (31 - i)] = uncompressed[64 - i].
std::array<uint8_t, kXOnlyKeyBytes> contract_y{};
for (size_t i = 0; i < kXOnlyKeyBytes; ++i)
{
contract_y[i] = uncompressed[kUncompressedPublicKeyBytes - 1 - i];
}
// Destination = hex_bytes(contract_X, 32) + hex_bytes(contract_Y, 32) = 128 chars.
// hex_bytes() prepends "0x"; GetAddress() returns a plain 128-char hex string with
// no prefix, so strip the leading "0x" from each half before concatenating.
const std::string x_hex = rlp::base::parse::hex_bytes(contract_x_bytes.data(), kXOnlyKeyBytes);
const std::string y_hex = rlp::base::parse::hex_bytes(contract_y.data(), kXOnlyKeyBytes);
const std::string destination = x_hex.substr(rlp::base::parse::kHexCharsPerByte)
+ y_hex.substr(rlp::base::parse::kHexCharsPerByte);
return destination;
}
} // namespace eth
Updated on 2026-06-28 at 18:54:57 -0700