add initial functions

This commit is contained in:
Craig Raw 2025-08-13 13:37:16 +02:00
parent 159bd84b37
commit 3376629644
15 changed files with 391 additions and 148 deletions

View File

@ -20,7 +20,7 @@ jobs:
with:
duckdb_version: main
ci_tools_version: main
extension_name: quack
extension_name: secp256k1
duckdb-stable-build:
name: Build extension binaries
@ -28,7 +28,7 @@ jobs:
with:
duckdb_version: v1.3.2
ci_tools_version: v1.3.2
extension_name: quack
extension_name: secp256k1
code-quality-check:
name: Code Quality Check
@ -36,5 +36,5 @@ jobs:
with:
duckdb_version: v1.3.2
ci_tools_version: main
extension_name: quack
extension_name: secp256k1
format_checks: 'format;tidy'

5
.gitmodules vendored
View File

@ -5,4 +5,7 @@
[submodule "extension-ci-tools"]
path = extension-ci-tools
url = https://github.com/duckdb/extension-ci-tools
branch = main
branch = main
[submodule "secp256k1"]
path = secp256k1
url = git@github.com:bitcoin-core/secp256k1.git

View File

@ -1,28 +1,33 @@
cmake_minimum_required(VERSION 3.5)
# Set extension name here
set(TARGET_NAME quack)
# DuckDB's extension distribution supports vcpkg. As such, dependencies can be added in ./vcpkg.json and then
# used in cmake with find_package. Feel free to remove or replace with other dependencies.
# Note that it should also be removed from vcpkg.json to prevent needlessly installing it..
find_package(OpenSSL REQUIRED)
set(TARGET_NAME secp256k1)
set(EXTENSION_NAME ${TARGET_NAME}_extension)
set(LOADABLE_EXTENSION_NAME ${TARGET_NAME}_loadable_extension)
project(${TARGET_NAME})
include_directories(src/include)
include_directories(secp256k1/include)
set(EXTENSION_SOURCES src/quack_extension.cpp)
# Include secp256k1 source directly to avoid export issues
set(EXTENSION_SOURCES
src/secp256k1_extension.cpp
secp256k1/src/secp256k1.c
secp256k1/src/precomputed_ecmult.c
secp256k1/src/precomputed_ecmult_gen.c
)
# Add necessary preprocessor definitions for secp256k1
add_definitions(-DECMULT_WINDOW_SIZE=15)
add_definitions(-DECMULT_GEN_PRECISION_BITS=4)
add_definitions(-DUSE_FIELD_10X26)
add_definitions(-DUSE_SCALAR_8X32)
add_definitions(-DENABLE_MODULE_ECDH)
build_static_extension(${TARGET_NAME} ${EXTENSION_SOURCES})
build_loadable_extension(${TARGET_NAME} " " ${EXTENSION_SOURCES})
# Link OpenSSL in both the static library as the loadable extension
target_link_libraries(${EXTENSION_NAME} OpenSSL::SSL OpenSSL::Crypto)
target_link_libraries(${LOADABLE_EXTENSION_NAME} OpenSSL::SSL OpenSSL::Crypto)
install(
TARGETS ${EXTENSION_NAME}
EXPORT "${DUCKDB_EXPORT_SET}"

View File

@ -1,7 +1,7 @@
PROJ_DIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
# Configuration of extension
EXT_NAME=quack
EXT_NAME=secp256k1
EXT_CONFIG=${PROJ_DIR}extension_config.cmake
# Include the Makefile from extension-ci-tools

View File

@ -1,10 +1,10 @@
# Quack
# DuckDB secp256k1 Extension
This repository is based on https://github.com/duckdb/extension-template, check it out if you want to build and ship your own DuckDB extension.
---
This extension, Quack, allow you to ... <extension_goal>.
This extension provides secp256k1 cryptographic functions for DuckDB, allowing elliptic curve operations directly in SQL queries.
## Building
@ -26,24 +26,26 @@ The main binaries that will be built are:
```sh
./build/release/duckdb
./build/release/test/unittest
./build/release/extension/quack/quack.duckdb_extension
./build/release/extension/secp256k1/secp256k1.duckdb_extension
```
- `duckdb` is the binary for the duckdb shell with the extension code automatically loaded.
- `unittest` is the test runner of duckdb. Again, the extension is already linked into the binary.
- `quack.duckdb_extension` is the loadable binary as it would be distributed.
- `secp256k1.duckdb_extension` is the loadable binary as it would be distributed.
## Running the extension
To run the extension code, simply start the shell with `./build/release/duckdb`.
Now we can use the features from the extension directly in DuckDB. The template contains a single scalar function `quack()` that takes a string arguments and returns a string:
Now we can use the secp256k1 cryptographic functions directly in DuckDB. The extension provides functions like `secp256k1_ec_pubkey_combine()` for combining elliptic curve public keys:
```
D select quack('Jane') as result;
┌───────────────┐
│ result │
│ varchar │
├───────────────┤
│ Quack Jane 🐥 │
└───────────────┘
D SELECT secp256k1_ec_pubkey_combine(
from_hex('0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'),
from_hex('02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5')
) IS NOT NULL as combined_successfully;
┌─────────────────────┐
│ combined_successfully │
├─────────────────────┤
│ true │
└─────────────────────┘
```
## Running the tests
@ -81,6 +83,6 @@ DuckDB. To specify a specific version, you can pass the version instead.
After running these steps, you can install and load your extension using the regular INSTALL/LOAD commands in DuckDB:
```sql
INSTALL quack
LOAD quack
INSTALL secp256k1
LOAD secp256k1
```

View File

@ -46,15 +46,18 @@ GEN=ninja make
## Running the extension
To run the extension code, simply start the shell with `./build/release/duckdb`. This shell will have the extension pre-loaded.
Now we can use the features from the extension directly in DuckDB. The template contains a single scalar function `quack()` that takes a string arguments and returns a string:
Now we can use the features from the extension directly in DuckDB. This extension contains the secp256k1 cryptographic functions, including `secp256k1_ec_pubkey_combine()` that combines multiple public keys:
```
D select quack('Jane') as result;
┌───────────────┐
│ result │
│ varchar │
├───────────────┤
│ Quack Jane 🐥 │
└───────────────┘
D LOAD 'secp256k1';
D SELECT secp256k1_ec_pubkey_combine(
from_hex('0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'),
from_hex('02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5')
) IS NOT NULL as combined_successfully;
┌─────────────────────┐
│ combined_successfully │
├─────────────────────┤
│ true │
└─────────────────────┘
```
## Running the tests

View File

@ -1,7 +1,7 @@
# This file is included by DuckDB's build system. It specifies which extension to load
# Extension from this repo
duckdb_extension_load(quack
duckdb_extension_load(secp256k1
SOURCE_DIR ${CMAKE_CURRENT_LIST_DIR}
LOAD_TESTS
)

1
secp256k1 Submodule

@ -0,0 +1 @@
Subproject commit e523e4f90e1b1c0fba49cd8a08016e1a8dff9232

View File

@ -4,11 +4,11 @@
namespace duckdb {
class QuackExtension : public Extension {
class Secp256k1Extension : public Extension {
public:
void Load(DuckDB &db) override;
std::string Name() override;
std::string Version() const override;
};
} // namespace duckdb
} // namespace duckdb

View File

@ -1,73 +0,0 @@
#define DUCKDB_EXTENSION_MAIN
#include "quack_extension.hpp"
#include "duckdb.hpp"
#include "duckdb/common/exception.hpp"
#include "duckdb/common/string_util.hpp"
#include "duckdb/function/scalar_function.hpp"
#include "duckdb/main/extension_util.hpp"
#include <duckdb/parser/parsed_data/create_scalar_function_info.hpp>
// OpenSSL linked through vcpkg
#include <openssl/opensslv.h>
namespace duckdb {
inline void QuackScalarFun(DataChunk &args, ExpressionState &state, Vector &result) {
auto &name_vector = args.data[0];
UnaryExecutor::Execute<string_t, string_t>(name_vector, result, args.size(), [&](string_t name) {
return StringVector::AddString(result, "Quack " + name.GetString() + " 🐥");
});
}
inline void QuackOpenSSLVersionScalarFun(DataChunk &args, ExpressionState &state, Vector &result) {
auto &name_vector = args.data[0];
UnaryExecutor::Execute<string_t, string_t>(name_vector, result, args.size(), [&](string_t name) {
return StringVector::AddString(result, "Quack " + name.GetString() + ", my linked OpenSSL version is " +
OPENSSL_VERSION_TEXT);
});
}
static void LoadInternal(DatabaseInstance &instance) {
// Register a scalar function
auto quack_scalar_function = ScalarFunction("quack", {LogicalType::VARCHAR}, LogicalType::VARCHAR, QuackScalarFun);
ExtensionUtil::RegisterFunction(instance, quack_scalar_function);
// Register another scalar function
auto quack_openssl_version_scalar_function = ScalarFunction("quack_openssl_version", {LogicalType::VARCHAR},
LogicalType::VARCHAR, QuackOpenSSLVersionScalarFun);
ExtensionUtil::RegisterFunction(instance, quack_openssl_version_scalar_function);
}
void QuackExtension::Load(DuckDB &db) {
LoadInternal(*db.instance);
}
std::string QuackExtension::Name() {
return "quack";
}
std::string QuackExtension::Version() const {
#ifdef EXT_VERSION_QUACK
return EXT_VERSION_QUACK;
#else
return "";
#endif
}
} // namespace duckdb
extern "C" {
DUCKDB_EXTENSION_API void quack_init(duckdb::DatabaseInstance &db) {
duckdb::DuckDB db_wrapper(db);
db_wrapper.LoadExtension<duckdb::QuackExtension>();
}
DUCKDB_EXTENSION_API const char *quack_version() {
return duckdb::DuckDB::LibraryVersion();
}
}
#ifndef DUCKDB_EXTENSION_MAIN
#error DUCKDB_EXTENSION_MAIN not defined
#endif

231
src/secp256k1_extension.cpp Normal file
View File

@ -0,0 +1,231 @@
#define DUCKDB_EXTENSION_MAIN
#include "secp256k1_extension.hpp"
#include "duckdb.hpp"
#include "duckdb/common/exception.hpp"
#include "duckdb/common/string_util.hpp"
#include "duckdb/function/scalar_function.hpp"
#include "duckdb/main/extension_util.hpp"
#include <duckdb/parser/parsed_data/create_scalar_function_info.hpp>
// secp256k1 library
#include "secp256k1.h"
#include <vector>
#include <memory>
#include <cstring>
namespace duckdb {
// Global secp256k1 context for verification operations
static secp256k1_context* secp256k1_ctx = nullptr;
// Helper function to initialize secp256k1 context
static secp256k1_context* GetSecp256k1Context() {
if (!secp256k1_ctx) {
secp256k1_ctx = secp256k1_context_create(SECP256K1_CONTEXT_NONE);
if (!secp256k1_ctx) {
throw InternalException("Failed to create secp256k1 context");
}
}
return secp256k1_ctx;
}
// Function to combine public keys using secp256k1_ec_pubkey_combine
inline void Secp256k1EcPubkeyCombineScalarFun(DataChunk &args, ExpressionState &state, Vector &result) {
// Get the secp256k1 context
secp256k1_context *ctx = GetSecp256k1Context();
// The function takes a variable number of BLOB arguments (each 33 bytes for compressed pubkeys)
D_ASSERT(args.ColumnCount() >= 1);
// Get number of rows to process
idx_t count = args.size();
// Process each row
for (idx_t i = 0; i < count; i++) {
std::vector<secp256k1_pubkey> parsed_pubkeys;
std::vector<const secp256k1_pubkey*> pubkey_ptrs;
bool all_valid = true;
// Parse all input public keys for this row
for (idx_t col = 0; col < args.ColumnCount(); col++) {
auto &input_vector = args.data[col];
// Check if this column value is NULL
if (FlatVector::IsNull(input_vector, i)) {
all_valid = false;
break;
}
// Get the blob data
auto blob_data = FlatVector::GetData<string_t>(input_vector)[i];
// Validate that the blob is exactly 33 bytes (compressed pubkey format)
if (blob_data.GetSize() != 33) {
all_valid = false;
break;
}
// Parse the public key
secp256k1_pubkey pubkey;
const unsigned char *input_data = reinterpret_cast<const unsigned char*>(blob_data.GetDataUnsafe());
if (secp256k1_ec_pubkey_parse(ctx, &pubkey, input_data, 33) != 1) {
all_valid = false;
break;
}
parsed_pubkeys.push_back(pubkey);
}
if (!all_valid || parsed_pubkeys.empty()) {
// Set result to NULL for this row
FlatVector::SetNull(result, i, true);
continue;
}
// Create array of pointers for secp256k1_ec_pubkey_combine
for (const auto& pk : parsed_pubkeys) {
pubkey_ptrs.push_back(&pk);
}
// Combine the public keys
secp256k1_pubkey combined_pubkey;
if (secp256k1_ec_pubkey_combine(ctx, &combined_pubkey, pubkey_ptrs.data(), pubkey_ptrs.size()) != 1) {
// Set result to NULL for this row if combination failed
FlatVector::SetNull(result, i, true);
continue;
}
// Serialize the combined public key back to compressed format (33 bytes)
unsigned char output[33];
size_t output_len = 33;
if (secp256k1_ec_pubkey_serialize(ctx, output, &output_len, &combined_pubkey, SECP256K1_EC_COMPRESSED) != 1) {
// Set result to NULL for this row if serialization failed
FlatVector::SetNull(result, i, true);
continue;
}
// Create the result blob
string_t result_blob = StringVector::AddStringOrBlob(result, (const char*)output, 33);
FlatVector::GetData<string_t>(result)[i] = result_blob;
}
}
// Function to concatenate a 32-byte blob with a 4-byte integer in little-endian format
inline void CreateOutpointScalarFun(DataChunk &args, ExpressionState &state, Vector &result) {
D_ASSERT(args.ColumnCount() == 2);
auto &blob_vector = args.data[0];
auto &int_vector = args.data[1];
// Get number of rows to process
idx_t count = args.size();
// Process each row
for (idx_t i = 0; i < count; i++) {
// Check if either input is NULL
if (FlatVector::IsNull(blob_vector, i) || FlatVector::IsNull(int_vector, i)) {
FlatVector::SetNull(result, i, true);
continue;
}
// Get the blob data
auto blob_data = FlatVector::GetData<string_t>(blob_vector)[i];
// Validate that the blob is exactly 32 bytes
if (blob_data.GetSize() != 32) {
FlatVector::SetNull(result, i, true);
continue;
}
// Get the integer value
auto int_value = FlatVector::GetData<int32_t>(int_vector)[i];
// Create output buffer (32 bytes + 4 bytes = 36 bytes)
unsigned char output[36];
// Copy the 32-byte blob
memcpy(output, blob_data.GetDataUnsafe(), 32);
// Append the 4-byte integer in little-endian format
output[32] = (unsigned char)(int_value & 0xFF);
output[33] = (unsigned char)((int_value >> 8) & 0xFF);
output[34] = (unsigned char)((int_value >> 16) & 0xFF);
output[35] = (unsigned char)((int_value >> 24) & 0xFF);
// Create the result blob
string_t result_blob = StringVector::AddStringOrBlob(result, (const char*)output, 36);
FlatVector::GetData<string_t>(result)[i] = result_blob;
}
}
static void LoadInternal(DatabaseInstance &instance) {
// Register the secp256k1_ec_pubkey_combine function that accepts variable arguments
ScalarFunctionSet secp256k1_ec_pubkey_combine_function_set("secp256k1_ec_pubkey_combine");
// Add overloads for different numbers of arguments (2-10 public keys)
for (idx_t num_args = 2; num_args <= 10; num_args++) {
vector<LogicalType> arg_types;
for (idx_t i = 0; i < num_args; i++) {
arg_types.push_back(LogicalType::BLOB);
}
secp256k1_ec_pubkey_combine_function_set.AddFunction(ScalarFunction(
arg_types, LogicalType::BLOB, Secp256k1EcPubkeyCombineScalarFun
));
}
ExtensionUtil::RegisterFunction(instance, secp256k1_ec_pubkey_combine_function_set);
// Register the create_outpoint function
auto create_outpoint_function = ScalarFunction("create_outpoint",
{LogicalType::BLOB, LogicalType::INTEGER}, LogicalType::BLOB, CreateOutpointScalarFun);
ExtensionUtil::RegisterFunction(instance, create_outpoint_function);
}
void Secp256k1Extension::Load(DuckDB &db) {
LoadInternal(*db.instance);
}
std::string Secp256k1Extension::Name() {
return "secp256k1";
}
std::string Secp256k1Extension::Version() const {
#ifdef EXT_VERSION_SECP256K1
return EXT_VERSION_SECP256K1;
#else
return "";
#endif
}
} // namespace duckdb
extern "C" {
DUCKDB_EXTENSION_API void secp256k1_init(duckdb::DatabaseInstance &db) {
duckdb::DuckDB db_wrapper(db);
db_wrapper.LoadExtension<duckdb::Secp256k1Extension>();
}
DUCKDB_EXTENSION_API const char *secp256k1_version() {
return duckdb::DuckDB::LibraryVersion();
}
// Cleanup function to destroy the secp256k1 context
DUCKDB_EXTENSION_API void secp256k1_cleanup() {
if (duckdb::secp256k1_ctx) {
secp256k1_context_destroy(duckdb::secp256k1_ctx);
duckdb::secp256k1_ctx = nullptr;
}
}
}
#ifndef DUCKDB_EXTENSION_MAIN
#error DUCKDB_EXTENSION_MAIN not defined
#endif

View File

@ -1,5 +1,5 @@
# Testing this extension
This directory contains all the tests for this extension. The `sql` directory holds tests that are written as [SQLLogicTests](https://duckdb.org/dev/sqllogictest/intro.html). DuckDB aims to have most its tests in this format as SQL statements, so for the quack extension, this should probably be the goal too.
This directory contains all the tests for this extension. The `sql` directory holds tests that are written as [SQLLogicTests](https://duckdb.org/dev/sqllogictest/intro.html). DuckDB aims to have most its tests in this format as SQL statements, so for the secp256k1 extension, this should probably be the goal too.
The root makefile contains targets to build and run all of these tests. To run the SQLLogicTests:
```bash

View File

@ -1,23 +0,0 @@
# name: test/sql/quack.test
# description: test quack extension
# group: [sql]
# Before we load the extension, this will fail
statement error
SELECT quack('Sam');
----
Catalog Error: Scalar Function with name quack does not exist!
# Require statement will ensure this test is run with this extension loaded
require quack
# Confirm the extension works
query I
SELECT quack('Sam');
----
Quack Sam 🐥
query I
SELECT quack_openssl_version('Michael') ILIKE 'Quack Michael, my linked OpenSSL version is OpenSSL%';
----
true

104
test/sql/secp256k1.test Normal file
View File

@ -0,0 +1,104 @@
# name: test/sql/secp256k1.test
# description: test secp256k1 extension
# group: [sql]
# Before we load the extension, this will fail
statement error
SELECT secp256k1_ec_pubkey_combine(from_hex('0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'), from_hex('02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5'));
----
Catalog Error: Scalar Function with name secp256k1_ec_pubkey_combine does not exist!
# Require statement will ensure this test is run with this extension loaded
require secp256k1
# Test combining two valid compressed public keys
# Using secp256k1 generator point (0279be...) and another valid compressed pubkey
query I
SELECT secp256k1_ec_pubkey_combine(
from_hex('0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'),
from_hex('02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5')
) IS NOT NULL;
----
true
# Test combining three public keys
query I
SELECT secp256k1_ec_pubkey_combine(
from_hex('0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'),
from_hex('02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5'),
from_hex('03774ae7f858a9411e5ef4246b70c65aac5649980be5c17891bbec17895da008cb')
) IS NOT NULL;
----
true
# Test with NULL input (should return NULL)
query I
SELECT secp256k1_ec_pubkey_combine(NULL, from_hex('0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'));
----
NULL
# Test with invalid length input (should return NULL)
query I
SELECT secp256k1_ec_pubkey_combine(from_hex('0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f817'), from_hex('0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'));
----
NULL
# Test with invalid public key data (should return NULL)
query I
SELECT secp256k1_ec_pubkey_combine(from_hex('0000000000000000000000000000000000000000000000000000000000000000000'), from_hex('0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'));
----
NULL
# Test create_outpoint function with 32-byte blob and integer
query I
SELECT octet_length(create_outpoint(
from_hex('0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'),
1
));
----
36
# Test create_outpoint with zero integer (little-endian encoding)
query I
SELECT create_outpoint(
from_hex('0000000000000000000000000000000000000000000000000000000000000000'),
0
) = from_hex('000000000000000000000000000000000000000000000000000000000000000000000000');
----
true
# Test create_outpoint with integer 1 (little-endian encoding)
query I
SELECT create_outpoint(
from_hex('0000000000000000000000000000000000000000000000000000000000000000'),
1
) = from_hex('000000000000000000000000000000000000000000000000000000000000000001000000');
----
true
# Test create_outpoint with integer 256 (little-endian encoding)
query I
SELECT create_outpoint(
from_hex('0000000000000000000000000000000000000000000000000000000000000000'),
256
) = from_hex('000000000000000000000000000000000000000000000000000000000000000000010000');
----
true
# Test create_outpoint with NULL blob (should return NULL)
query I
SELECT create_outpoint(NULL, 1);
----
NULL
# Test create_outpoint with NULL integer (should return NULL)
query I
SELECT create_outpoint(from_hex('0000000000000000000000000000000000000000000000000000000000000000'), NULL);
----
NULL
# Test create_outpoint with wrong blob length (should return NULL)
query I
SELECT create_outpoint(from_hex('00000000000000000000000000000000000000000000000000000000000000'), 1);
----
NULL

View File

@ -1,10 +0,0 @@
{
"dependencies": [
"openssl"
],
"vcpkg-configuration": {
"overlay-ports": [
"./extension-ci-tools/vcpkg_ports"
]
}
}