A DuckDB extension that provides secp256k1 elliptic curve cryptography functions and Bitcoin-related utilities. All cryptographic operations use the libsecp256k1 library.
- CMake 3.21 or higher
- C++ compiler with C++17 support
- Git
- Clone the repository:
git clone https://github.com/your-repo/duckdb-secp256k1-extension.git
cd duckdb-secp256k1-extension
- Initialize and update submodules:
git submodule update --init --recursive
- Build the extension:
make
- Run tests:
make test
The compiled extension will be available at build/release/extension/secp256k1/secp256k1.duckdb_extension
.
LOAD 'path/to/secp256k1.duckdb_extension';
Creates a public key from a 32-byte secret key.
Parameters:
secret_key
(BLOB): 32-byte secret key
Returns: BLOB (33-byte compressed public key)
Example:
SELECT secp256k1_ec_pubkey_create(from_hex('0000000000000000000000000000000000000000000000000000000000000001'));
Combines multiple public keys using elliptic curve addition.
Parameters:
public_keys
(LIST[BLOB]): Array of 33-byte compressed public keys
Returns: BLOB (33-byte compressed combined public key)
Example:
SELECT secp256k1_ec_pubkey_combine([
from_hex('0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'),
from_hex('02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5')
]);
Tweaks a public key by scalar multiplication.
Parameters:
public_key
(BLOB): 33-byte compressed public keytweak
(BLOB): 32-byte scalar value
Returns: BLOB (33-byte compressed tweaked public key)
Example:
SELECT secp256k1_ec_pubkey_tweak_mul(
from_hex('0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'),
from_hex('0000000000000000000000000000000000000000000000000000000000000002')
);
Checks if a target compressed public key can be found either directly by matching its x-coordinate's first 8 bytes (big-endian) against a list of BIGINT prefixes, or by combining the target with compressed public keys from a second list and comparing the result. This function is useful for Bitcoin Silent Payments and other protocols that need to scan for key matches with elliptic curve operations.
Parameters:
xonly_prefixes
(LIST[BIGINT]): Array of 64-bit integers representing the first 8 bytes (big-endian) of x-only public keys to search intarget_compressed
(BLOB): 33-byte target compressed public keycompressed_keys
(LIST[BLOB]): Array of 33-byte compressed public keys to combine with target
Returns: BOOLEAN (true if match found, false otherwise)
Behavior:
- Extracts first 8 bytes (big-endian) of
target_compressed
key's x-coordinate as a BIGINT prefix - First checks for direct match of this prefix against the
xonly_prefixes
list - If no direct match, iterates through
compressed_keys
list:- For each compressed key, combines it with
target_compressed
using elliptic curve addition - Extracts x-coordinate of result and converts first 8 bytes to BIGINT prefix
- Compares this prefix to all values in
xonly_prefixes
list - Also tries negated version of compressed key (covers both possible y-coordinates)
- Returns true on first match found
- For each compressed key, combines it with
- Returns false if no matches found after checking all combinations
Example:
-- Direct match test using BIGINT prefixes
WITH test_data AS (
SELECT
from_hex('79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798') as x_only_key,
from_hex('0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798') as compressed_key
)
SELECT secp256k1_xonly_key_match(
[hash_prefix_to_int(x_only_key, 0)], -- BIGINT[] with first 8 bytes of key as big-endian int
compressed_key, -- Target compressed key
[] -- Empty compressed keys
) FROM test_data; -- Returns: true
-- Key combination test (G + 2G = 3G scenario)
WITH test_keys AS (
SELECT
secp256k1_ec_pubkey_create(from_hex('0000000000000000000000000000000000000000000000000000000000000001')) as g_point,
secp256k1_ec_pubkey_create(from_hex('0000000000000000000000000000000000000000000000000000000000000002')) as double_g,
secp256k1_ec_pubkey_create(from_hex('0000000000000000000000000000000000000000000000000000000000000003')) as triple_g
),
x_coords AS (
SELECT
from_hex(substring(to_hex(g_point), 3, 64)) as g_x,
from_hex(substring(to_hex(triple_g), 3, 64)) as triple_g_x,
double_g
FROM test_keys
)
SELECT secp256k1_xonly_key_match(
[hash_prefix_to_int(triple_g_x, 0)], -- BIGINT[] with first 8 bytes of 3G x-coordinate
g_point, -- Target: G point as compressed key
[double_g] -- Compressed keys: 2G (G + 2G = 3G should match)
) FROM x_coords; -- Returns: true
Efficiently scans for Bitcoin Silent Payments (BIP 352) by combining multiple cryptographic operations into a single function. This function implements the core silent payments scanning algorithm, avoiding the serialization/deserialization overhead that would occur when using individual functions.
Parameters:
outputs
(LIST[BIGINT]): Array of 64-bit integers representing the first 8 bytes (big-endian) of output x-coordinates to scan for matcheskeys
(LIST[BLOB]): Array containing exactly 3 elements: [scan_private_key (32 bytes), spend_public_key (33 or 64 bytes), tweak_key (33 or 64 bytes)]label_tweaks
(LIST[BLOB]): Array of compressed public keys (33 bytes) or raw secp256k1_pubkey structs (64 bytes) representing label tweak keys for labeled outputs (can be empty)
If public keys are provided as 64 bytes, they must be in little endian format with the x-value first, followed by the y value.
Returns: BOOLEAN (true if any matching output is found, false otherwise)
Algorithm:
- Tweak Multiplication: Multiplies the tweak_key by the scan_private_key using
secp256k1_ec_pubkey_tweak_mul(tweak_key, scan_private_key)
- Base Shared Secret: Computes the base shared secret using
secp256k1_tagged_sha256('BIP0352/SharedSecret', tweaked_key || int_to_big_endian(0))
- Base Output Key: Creates base output key by combining spend_public_key with the public key derived from the base shared secret
- Direct Match Check: Extracts first 8 bytes of base output key's x-coordinate and checks against the outputs list
- Label Tweak Processing: For each label tweak key:
- Combines the base output key with the label tweak key using elliptic curve addition
- Checks if the combined result matches any output in the list
- Also checks the negated version of the combined result (covers both possible y-coordinates)
- Early Return: Returns true immediately upon finding any match
Example:
-- Basic silent payments scanning without label tweaks
WITH
scan_priv AS (SELECT from_hex('0000000000000000000000000000000000000000000000000000000000000001') as key),
spend_pub AS (SELECT secp256k1_ec_pubkey_create(from_hex('0000000000000000000000000000000000000000000000000000000000000002')) as key),
tweak_key AS (SELECT secp256k1_ec_pubkey_create(from_hex('0000000000000000000000000000000000000000000000000000000000000003')) as key),
-- Outputs to scan (first 8 bytes of x-coordinates as BIGINT)
outputs_to_scan AS (SELECT [1234567890123456789, 9876543210987654321] as list),
keys AS (SELECT [(SELECT key FROM scan_priv), (SELECT key FROM spend_pub), (SELECT key FROM tweak_key)] as keys_array)
SELECT scan_silent_payments(
(SELECT list FROM outputs_to_scan),
(SELECT keys_array FROM keys),
CAST([] AS BLOB[]) -- No label tweaks
); -- Returns: true/false depending on whether any output matches
-- Silent payments scanning with label tweaks
WITH
scan_priv AS (SELECT from_hex('1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef') as key),
spend_pub AS (SELECT secp256k1_ec_pubkey_create(from_hex('fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321')) as key),
tweak_key AS (SELECT secp256k1_ec_pubkey_create(from_hex('abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890')) as key),
-- Label tweak keys for labeled outputs
label_tweaks AS (SELECT [
secp256k1_ec_pubkey_create(from_hex('1111111111111111111111111111111111111111111111111111111111111111')),
secp256k1_ec_pubkey_create(from_hex('2222222222222222222222222222222222222222222222222222222222222222'))
] as tweaks),
outputs_to_scan AS (SELECT [hash_prefix_to_int(from_hex('abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890'), 0)] as list),
keys AS (SELECT [(SELECT key FROM scan_priv), (SELECT key FROM spend_pub), (SELECT key FROM tweak_key)] as keys_array)
SELECT scan_silent_payments(
(SELECT list FROM outputs_to_scan),
(SELECT keys_array FROM keys),
(SELECT tweaks FROM label_tweaks)
); -- Returns: true if any output (base or labeled) matches
-- Equivalent to this complex multi-function SQL (but much more efficient):
-- SELECT secp256k1_xonly_key_match(
-- outputs,
-- secp256k1_ec_pubkey_combine([
-- spend_public_key,
-- secp256k1_ec_pubkey_create(secp256k1_tagged_sha256('BIP0352/SharedSecret',
-- secp256k1_ec_pubkey_tweak_mul(tweak_key, scan_private_key) || int_to_big_endian(0)))
-- ]),
-- label_tweak_keys
-- );
Performance Benefits:
- Reduced Overhead: Eliminates serialization/deserialization between individual secp256k1 function calls
- Memory Efficiency: Operates on internal secp256k1 data structures without intermediate conversions
- Atomic Operation: Single function call handles entire scanning workflow
- Early Termination: Returns immediately upon finding first match, avoiding unnecessary computations
- Flexible Key Formats: Accepts both 33-byte compressed keys and 64-byte raw secp256k1_pubkey structs for maximum efficiency
Key Format Support:
- 33-byte compressed keys: Parsed using
secp256k1_ec_pubkey_parse()
- 64-byte raw structs: Cast directly to
secp256k1_pubkey
without parsing overhead for maximum performance - Automatic detection: Function automatically detects format based on byte length
Use Cases:
- Bitcoin Silent Payments wallet scanning (BIP 352)
- Batch checking of multiple output candidates
- Efficient labeled silent payments processing
- High-performance blockchain analysis requiring multiple key operations
Computes a tagged SHA256 hash as defined in BIP 340.
Parameters:
tag
(VARCHAR): Tag stringmessage
(BLOB or VARCHAR): Message to hash
Returns: BLOB (32-byte hash)
Example:
SELECT secp256k1_tagged_sha256('BIP0340/challenge', 'test message');
Creates a Bitcoin outpoint by concatenating a transaction ID with a transaction output index.
Parameters:
txid
(BLOB): 32-byte transaction IDvout
(INTEGER): Transaction output index (32-bit)
Returns: BLOB (36-byte outpoint: reversed txid + little-endian vout)
Example:
SELECT create_outpoint(
from_hex('1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef'),
0
);
Finds the lexicographically smallest outpoint from a list.
Parameters:
outpoints
(LIST[BLOB]): Array of 36-byte outpoints
Returns: BLOB (36-byte outpoint with smallest lexicographic value)
Example:
SELECT min_outpoint([
from_hex('1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef00000000'),
from_hex('fedcba0987654321fedcba0987654321fedcba0987654321fedcba098765432100000000')
]);
Extracts 8 bytes from a blob starting at an offset and converts to a signed 64-bit integer (big-endian).
Parameters:
blob
(BLOB): Input binary dataoffset
(UINTEGER): Byte offset to start reading from
Returns: BIGINT (64-bit signed integer)
Example:
-- Read from beginning of a 32-byte hash
SELECT hash_prefix_to_int(from_hex('FFFFFFFFFFFFFFFF000000000000000000000000000000000000000000000000'), 0);
-- Returns: -1
-- Read from byte offset 8
SELECT hash_prefix_to_int(from_hex('00000000000000000000000000000001000000000000000000000000000000'), 8);
-- Returns: 1
Converts a 32-bit integer to a 4-byte big-endian blob.
Parameters:
value
(INTEGER): 32-bit signed integer
Returns: BLOB (4-byte big-endian representation)
Example:
SELECT int_to_big_endian(1234567890);
-- Returns: \x499602d2
SELECT int_to_big_endian(-1);
-- Returns: \xffffffff
All functions return NULL
when:
- Input parameters are
NULL
- Input data has incorrect size/format
- Cryptographic operations fail (e.g., invalid keys, point at infinity)
- Buffer boundaries are exceeded
- This extension uses the libsecp256k1 library for cryptographic operations
- All functions perform input validation to prevent buffer overflows
- Memory access is handled safely through DuckDB's execution framework
- No private key material is logged or exposed in error messages
- libsecp256k1: Optimized C library for secp256k1 elliptic curve operations
- DuckDB 1.3.2 or higher
This project is licensed under the MIT License - see the LICENSE file for details.
- Fork the repository
- Create a feature branch
- Make your changes
- Add tests for new functionality
- Ensure all tests pass
- Submit a pull request
-- Generate a random 32-byte private key (in practice, use secure randomness)
WITH private_key AS (
SELECT from_hex('e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855') as privkey
),
public_key AS (
SELECT secp256k1_ec_pubkey_create(privkey) as pubkey
FROM private_key
)
SELECT pubkey FROM public_key;
-- Create multiple outpoints and find the minimum
WITH outpoints AS (
SELECT create_outpoint(from_hex('1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef'), 0) as out1,
create_outpoint(from_hex('fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321'), 1) as out2
)
SELECT min_outpoint([out1, out2]) as min_outpoint FROM outpoints;
-- Example of scanning for Silent Payments keys using BIGINT prefixes
-- This demonstrates the key matching functionality used in Bitcoin Silent Payments (BIP 352)
WITH wallet_keys AS (
-- Simulated wallet's known x-only public keys (32 bytes each)
SELECT [
from_hex('1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef'),
from_hex('fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321')
] as known_keys
),
key_prefixes AS (
-- Convert x-only keys to BIGINT prefixes (first 8 bytes, big-endian)
SELECT [
hash_prefix_to_int(known_keys[1], 0),
hash_prefix_to_int(known_keys[2], 0)
] as known_prefixes
FROM wallet_keys
),
transaction_inputs AS (
-- Compressed public keys from transaction inputs (33 bytes each)
SELECT [
secp256k1_ec_pubkey_create(from_hex('1111111111111111111111111111111111111111111111111111111111111111')),
secp256k1_ec_pubkey_create(from_hex('2222222222222222222222222222222222222222222222222222222222222222'))
] as input_keys
),
scan_target AS (
-- Target compressed key we're checking for (e.g., from a transaction output)
SELECT secp256k1_ec_pubkey_create(from_hex('abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890')) as target_key
)
SELECT secp256k1_xonly_key_match(
(SELECT known_prefixes FROM key_prefixes), -- BIGINT[] array of key prefixes
(SELECT target_key FROM scan_target), -- Target compressed key (BLOB)
(SELECT input_keys FROM transaction_inputs) -- Compressed input keys for combination
) as key_belongs_to_wallet;