Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ set(SECP256K1_DISABLE_SHARED ON CACHE BOOL "" FORCE)
set(SECP256K1_ENABLE_MODULE_ECDH OFF CACHE BOOL "" FORCE)
set(SECP256K1_ENABLE_MODULE_RECOVERY ON CACHE BOOL "" FORCE)
set(SECP256K1_ENABLE_MODULE_MUSIG OFF CACHE BOOL "" FORCE)
set(SECP256K1_ENABLE_MODULE_HAZMAT ON CACHE BOOL "" FORCE)
set(SECP256K1_BUILD_BENCHMARK OFF CACHE BOOL "" FORCE)
set(SECP256K1_BUILD_TESTS ${BUILD_TESTS} CACHE BOOL "" FORCE)
set(SECP256K1_BUILD_EXHAUSTIVE_TESTS ${BUILD_TESTS} CACHE BOOL "" FORCE)
Expand Down
6 changes: 6 additions & 0 deletions src/binana.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,12 @@ def gen_binana_h(data, header, depjson):
if discourage:
defines["SUCCESS_OPCODES"].append(f' if (auto e = op_success_check(flags, SCRIPT_VERIFY_{dep}, SCRIPT_VERIFY_DISCOURAGE_{dep}, SCRIPT_ERR_DISCOURAGE_{dep}, serror)) return e; else break;')

if "errors" in b:
for errname, errmsg in b["errors"].items():
defines["SCRIPTERR"].append(f'SCRIPT_ERR_{errname},')
defines["SCRIPTERR_STRING"].append(f'case SCRIPT_ERR_{errname}: return "{errmsg}";')
defines["SCRIPTERR_TEST_NAMES"].append(f'{{ SCRIPT_ERR_{errname}, "{errname}" }},')

header.write("// Automatically generated\n")
header.write("#ifndef BINANA_H\n")
header.write("#define BINANA_H\n\n")
Expand Down
18 changes: 18 additions & 0 deletions src/binana/ec_ops.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"binana": [2026, 6, 0],
"deployment": "EC_OPS",
"scriptverify": true,
"scriptverify_discourage": true,
"opcodes": {
"EC_POINT_ADD": "0xbb",
"EC_POINT_MUL": "0xbc",
"EC_POINT_NEGATE": "0xbd",
"EC_POINT_X_COORD": "0xbe"
},
"errors": {
"EC_POINT_ADD": "Using OP_EC_POINT_ADD to sum invalid points",
"EC_POINT_MUL": "Using OP_EC_POINT_MUL with invalid scalar or point",
"EC_POINT_NEGATE": "Using OP_EC_POINT_NEGATE with invalid point",
"EC_POINT_X_COORD": "Using OP_EC_POINT_X_COORD with invalid point"
}
}
105 changes: 105 additions & 0 deletions src/pubkey.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#include <secp256k1_extrakeys.h>
#include <secp256k1_recovery.h>
#include <secp256k1_schnorrsig.h>
#include <secp256k1_hazmat.h>
#include <span.h>
#include <uint256.h>
#include <util/strencodings.h>
Expand Down Expand Up @@ -184,6 +185,12 @@ int ecdsa_signature_parse_der_lax(secp256k1_ecdsa_signature* sig, const unsigned
return 1;
}

bool parse_ec_point(secp256k1_pubkey *pubkey, const unsigned char *input, size_t inputlen) {
if (inputlen != CPubKey::COMPRESSED_SIZE)
return false;
return secp256k1_ec_pubkey_parse(secp256k1_context_static, pubkey, input, inputlen);
}

/** Nothing Up My Sleeve (NUMS) point
*
* NUMS_H is a point with an unknown discrete logarithm, constructed by taking the sha256 of 'g'
Expand Down Expand Up @@ -214,6 +221,8 @@ std::vector<CKeyID> XOnlyPubKey::GetKeyIDs() const
return out;
}

secp256k1_context *secp256k1_context_sign = secp256k1_context_create(SECP256K1_CONTEXT_NONE);

CPubKey XOnlyPubKey::GetEvenCorrespondingCPubKey() const
{
unsigned char full_key[CPubKey::COMPRESSED_SIZE] = {0x02};
Expand Down Expand Up @@ -359,6 +368,102 @@ EllSwiftPubKey::EllSwiftPubKey(Span<const std::byte> ellswift) noexcept
std::copy(ellswift.begin(), ellswift.end(), m_pubkey.begin());
}

bool CPubKey::ComputeSum(const CPubKey& other, CPubKey& ret) const {
secp256k1_pubkey pubkey1, pubkey2;
if (!parse_ec_point(&pubkey1, vch, size())) {
return false;
}
if (!parse_ec_point(&pubkey2, other.vch, other.size())) {
return false;
}
const secp256k1_pubkey* pubkeys[2];
pubkeys[0] = &pubkey1;
pubkeys[1] = &pubkey2;
secp256k1_pubkey pubkey_sum;
if (!secp256k1_ec_pubkey_combine(secp256k1_context_static, &pubkey_sum, pubkeys, 2)) {
/* The sum is Infinity. Set to an empty vector */
unsigned char empty[1];
ret.Set(empty, empty);
return true;
}
unsigned char out[COMPRESSED_SIZE];
size_t outlen = COMPRESSED_SIZE;
secp256k1_ec_pubkey_serialize(secp256k1_context_static, out, &outlen, &pubkey_sum, SECP256K1_EC_COMPRESSED);
ret.Set(out, out + outlen);
return true;
}

bool CPubKey::ComputeMul(const valtype& scalar, CPubKey& ret) const {
if (scalar.size() != 32) {
return false;
}
secp256k1_hazmat_scalar factor;
secp256k1_hazmat_scalar_parse(&factor, scalar.data());
if (secp256k1_hazmat_scalar_is_zero(&factor)) {
/* The mul is Infinity. Set to an empty vector */
unsigned char empty[1];
ret.Set(empty, empty);
return true;
}
unsigned char factor_be[32];
std::memcpy(factor_be, factor.data, 32);
std::reverse(factor_be, factor_be + 32);
Comment on lines +409 to +410

@theStack theStack Mar 16, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that the .data member of secp256k1_hazmat_scalar type (and any other types of the hazmat module) contains an internal representation and is thus not meant to be accessed directly by the user. The serialization API function secp256k1_hazmat_scalar_serialize should be used instead (no blaming though, this should definitely be documented better in the API header in bitcoin-core/secp256k1#1635!).

However, in this case it seems you don't need the hazmat API in the first place. You can simply pass scalar.data() to the _ec_pubkey_{create,tweak} functions below, as this is already in the expected form. The following simplification seems to work fine: theStack/bitcoin-inquisition@d4c9c8c

It might still make sense to use a hazmat module consistently through for all operations (if there are notable gains in performance), but I wouldn't recommend bitcoin-core/secp256k1#1635 for that purpose currently, given its PoC state and lack of review.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey, thank you for the feedback.

I used it specifically to satisfy the rule defined in the BIP: "Scalar values greater than or equal to the curve order n are automatically reduced modulo n." by using secp256k1_hazmat_scalar_parse. I tried to find a way to handle this using the standard API, but I couldn't find one.

Is there a recommended approach to perform this reduction modulo n using the standard secp256k1 API without relying on hazmat?

/* Use point G */
if (size() == 0) {
secp256k1_pubkey pubkey;
/* secp256k1_ec_pubkey_create computes G * tweak */
if (!secp256k1_ec_pubkey_create(secp256k1_context_sign, &pubkey, factor_be)) {
return false;
}
unsigned char out[COMPRESSED_SIZE];
size_t outlen = COMPRESSED_SIZE;
secp256k1_ec_pubkey_serialize(secp256k1_context_static, out, &outlen, &pubkey, SECP256K1_EC_COMPRESSED);
ret.Set(out, out + outlen);
return true;
}
secp256k1_pubkey pubkey;
if (!parse_ec_point(&pubkey, vch, size())) {
return false;
}
if (!secp256k1_ec_pubkey_tweak_mul(secp256k1_context_static, &pubkey, factor_be)) {
return false;
}
unsigned char out[COMPRESSED_SIZE];
size_t outlen = COMPRESSED_SIZE;
secp256k1_ec_pubkey_serialize(secp256k1_context_static, out, &outlen, &pubkey, SECP256K1_EC_COMPRESSED);
ret.Set(out, out + outlen);
return true;
}

bool CPubKey::Negate(CPubKey& ret) const {
if (size() == 0) {
/* The negation of Infinity is Infinity. Set to an empty vector */
unsigned char empty[1];
ret.Set(empty, empty);
return true;
}
secp256k1_pubkey pubkey;
if (!parse_ec_point(&pubkey, vch, size()))
return false;
std::ignore = secp256k1_ec_pubkey_negate(secp256k1_context_static, &pubkey);
unsigned char out[COMPRESSED_SIZE];
size_t outlen = COMPRESSED_SIZE;
secp256k1_ec_pubkey_serialize(secp256k1_context_static, out, &outlen, &pubkey, SECP256K1_EC_COMPRESSED);
ret.Set(out, out + outlen);
return true;
}

bool CPubKey::GetX(valtype& ret) const {
secp256k1_pubkey pubkey;
if (!parse_ec_point(&pubkey, vch, size()))
return false;
unsigned char point[COMPRESSED_SIZE];
size_t pointlen = COMPRESSED_SIZE;
secp256k1_ec_pubkey_serialize(secp256k1_context_static, point, &pointlen, &pubkey, SECP256K1_EC_COMPRESSED);
ret.assign(point + 1, point + pointlen);
return true;
}

CPubKey EllSwiftPubKey::Decode() const
{
secp256k1_pubkey pubkey;
Expand Down
13 changes: 13 additions & 0 deletions src/pubkey.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ class CKeyID : public uint160
};

typedef uint256 ChainCode;
typedef std::vector<unsigned char> valtype;

/** An encapsulated public key. */
class CPubKey
Expand Down Expand Up @@ -225,6 +226,18 @@ class CPubKey

//! Derive BIP32 child pubkey.
[[nodiscard]] bool Derive(CPubKey& pubkeyChild, ChainCode &ccChild, unsigned int nChild, const ChainCode& cc) const;

// Compute the sum of this point and another point
bool ComputeSum(const CPubKey& other, CPubKey& ret) const;

// Compute the multiplication of this point by a scalar
bool ComputeMul(const valtype& scalar, CPubKey& ret) const;

// Compute the negation of this point
bool Negate(CPubKey& ret) const;

// Get the x-coordinate of this point
bool GetX(valtype& ret) const;
};

class XOnlyPubKey
Expand Down
105 changes: 105 additions & 0 deletions src/script/interpreter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1377,6 +1377,111 @@ bool EvalScript(std::vector<std::vector<unsigned char> >& stack, const CScript&
break;
}

case OP_EC_POINT_ADD:
{
// OP_EC_POINT_ADD is only available in Tapscript
if (sigversion == SigVersion::BASE || sigversion == SigVersion::WITNESS_V0) return set_error(serror, SCRIPT_ERR_BAD_OPCODE);

assert(execdata.m_validation_weight_left_init);
execdata.m_validation_weight_left -= VALIDATION_WEIGHT_EC_POINT_ADD;
if (execdata.m_validation_weight_left < 0) {
return set_error(serror, SCRIPT_ERR_TAPSCRIPT_VALIDATION_WEIGHT);
}
if (stack.size() < 2) return set_error(serror, SCRIPT_ERR_INVALID_STACK_OPERATION);

const valtype& vch1 = stacktop(-2);
const valtype& vch2 = stacktop(-1);
CPubKey p1(vch1);
CPubKey p2(vch2);

CPubKey pSum;
if (!p1.ComputeSum(p2, pSum)) {
return set_error(serror, SCRIPT_ERR_EC_POINT_ADD);
}

popstack(stack);
popstack(stack);
stack.emplace_back(pSum.begin(), pSum.end());
}
break;

case OP_EC_POINT_MUL:
{
// OP_EC_POINT_MUL is only available in Tapscript
if (sigversion == SigVersion::BASE || sigversion == SigVersion::WITNESS_V0) return set_error(serror, SCRIPT_ERR_BAD_OPCODE);

assert(execdata.m_validation_weight_left_init);
execdata.m_validation_weight_left -= VALIDATION_WEIGHT_EC_POINT_MUL;
if (execdata.m_validation_weight_left < 0) {
return set_error(serror, SCRIPT_ERR_TAPSCRIPT_VALIDATION_WEIGHT);
}
if (stack.size() < 2) return set_error(serror, SCRIPT_ERR_INVALID_STACK_OPERATION);

const valtype& scalar = stacktop(-2);
const valtype& vch = stacktop(-1);
CPubKey p(vch);

CPubKey pMul;
if (!p.ComputeMul(scalar, pMul)) {
return set_error(serror, SCRIPT_ERR_EC_POINT_MUL);
}

popstack(stack);
popstack(stack);
stack.emplace_back(pMul.begin(), pMul.end());
}
break;

case OP_EC_POINT_NEGATE:
{
// OP_EC_POINT_NEGATE is only available in Tapscript
if (sigversion == SigVersion::BASE || sigversion == SigVersion::WITNESS_V0) return set_error(serror, SCRIPT_ERR_BAD_OPCODE);

assert(execdata.m_validation_weight_left_init);
execdata.m_validation_weight_left -= VALIDATION_WEIGHT_EC_POINT_NEGATE;
if (execdata.m_validation_weight_left < 0) {
return set_error(serror, SCRIPT_ERR_TAPSCRIPT_VALIDATION_WEIGHT);
}
if (stack.size() < 1) return set_error(serror, SCRIPT_ERR_INVALID_STACK_OPERATION);

const valtype& vch = stacktop(-1);
CPubKey p(vch);

CPubKey pNegate;
if (!p.Negate(pNegate)) {
return set_error(serror, SCRIPT_ERR_EC_POINT_NEGATE);
}

popstack(stack);
stack.emplace_back(pNegate.begin(), pNegate.end());
}
break;

case OP_EC_POINT_X_COORD:
{
// OP_EC_POINT_X_COORD is only available in Tapscript
if (sigversion == SigVersion::BASE || sigversion == SigVersion::WITNESS_V0) return set_error(serror, SCRIPT_ERR_BAD_OPCODE);

assert(execdata.m_validation_weight_left_init);
execdata.m_validation_weight_left -= VALIDATION_WEIGHT_EC_POINT_X_COORD;
if (execdata.m_validation_weight_left < 0) {
return set_error(serror, SCRIPT_ERR_TAPSCRIPT_VALIDATION_WEIGHT);
}
if (stack.size() < 1) return set_error(serror, SCRIPT_ERR_INVALID_STACK_OPERATION);

const valtype& vch = stacktop(-1);
CPubKey p(vch);

valtype x;
if (!p.GetX(x)) {
return set_error(serror, SCRIPT_ERR_EC_POINT_X_COORD);
}

popstack(stack);
stack.emplace_back(x.begin(), x.end());
}
break;

default:
return set_error(serror, SCRIPT_ERR_BAD_OPCODE);
}
Expand Down
10 changes: 10 additions & 0 deletions src/script/script.h
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,16 @@ static constexpr unsigned int ANNEX_TAG = 0x50;
// Validation weight per passing signature (Tapscript only, see BIP 342).
static constexpr int64_t VALIDATION_WEIGHT_PER_SIGOP_PASSED{50};

// Validation weight cost for OP_EC_POINT_ADD (Tapscript only, see BIP ???).
static constexpr int64_t VALIDATION_WEIGHT_EC_POINT_ADD{10};

// Validation weight cost for OP_EC_POINT_MUL (Tapscript only, see BIP ???).
static constexpr int64_t VALIDATION_WEIGHT_EC_POINT_MUL{30};

static constexpr int64_t VALIDATION_WEIGHT_EC_POINT_NEGATE{5};

static constexpr int64_t VALIDATION_WEIGHT_EC_POINT_X_COORD{1};

// How much weight budget is added to the witness size (Tapscript only, see BIP 342).
static constexpr int64_t VALIDATION_WEIGHT_OFFSET{50};

Expand Down
1 change: 1 addition & 0 deletions src/secp256k1/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ ecdsa_example
schnorr_example
ellswift_example
musig_example
hazmat_example
*.exe
*.so
*.a
Expand Down
Loading