add single call scan_silent_payments function

This commit is contained in:
Craig Raw 2025-09-17 16:38:27 +02:00
parent a05a9cb3de
commit 902ae8cf7e
3 changed files with 464 additions and 0 deletions

View File

@ -152,6 +152,90 @@ SELECT secp256k1_xonly_key_match(
) FROM x_coords; -- Returns: true
```
#### `scan_silent_payments(outputs, keys, label_tweaks)`
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 matches
- `keys` (LIST[BLOB]): Array containing exactly 3 elements: [scan_private_key (32 bytes), spend_public_key (33 bytes), tweak_key (33 bytes)]
- `label_tweaks` (LIST[BLOB]): Array of 33-byte compressed public keys representing label tweak keys for labeled outputs (can be empty)
**Returns:** BOOLEAN (true if any matching output is found, false otherwise)
**Algorithm:**
1. **Tweak Multiplication**: Multiplies the tweak_key by the scan_private_key using `secp256k1_ec_pubkey_tweak_mul(tweak_key, scan_private_key)`
2. **Base Shared Secret**: Computes the base shared secret using `secp256k1_tagged_sha256('BIP0352/SharedSecret', tweaked_key || int_to_big_endian(0))`
3. **Base Output Key**: Creates base output key by combining spend_public_key with the public key derived from the base shared secret
4. **Direct Match Check**: Extracts first 8 bytes of base output key's x-coordinate and checks against the outputs list
5. **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)
6. **Early Return**: Returns true immediately upon finding any match
**Example:**
```sql
-- 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
**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
### Cryptographic Hash Functions
#### `secp256k1_tagged_sha256(tag, message)`

View File

@ -547,6 +547,238 @@ inline void Secp256k1XOnlyKeyMatchScalarFun(DataChunk &args, ExpressionState &st
});
}
// Function to scan for silent payments
inline void ScanSilentPaymentsScalarFun(DataChunk &args, ExpressionState &state, Vector &result) {
D_ASSERT(args.ColumnCount() == 3);
// Get the secp256k1 context
secp256k1_context *ctx = GetSecp256k1Context();
TernaryExecutor::ExecuteWithNulls<list_entry_t, list_entry_t, list_entry_t, bool>(
args.data[0], args.data[1], args.data[2], result, args.size(),
[&](list_entry_t outputs_list, list_entry_t keys_list, list_entry_t label_tweaks_list, ValidityMask &mask,
idx_t idx) {
// Get the child vectors
auto &outputs_child_vector = ListVector::GetEntry(args.data[0]);
auto &keys_child_vector = ListVector::GetEntry(args.data[1]);
auto &label_tweaks_child_vector = ListVector::GetEntry(args.data[2]);
// Validate that keys list has exactly 3 elements
if (keys_list.length != 3) {
mask.SetInvalid(idx);
return false;
}
// Extract the three keys: scan private key, spend public key, and tweak key
idx_t scan_key_idx = keys_list.offset;
idx_t spend_key_idx = keys_list.offset + 1;
idx_t tweak_key_idx = keys_list.offset + 2;
if (FlatVector::IsNull(keys_child_vector, scan_key_idx) ||
FlatVector::IsNull(keys_child_vector, spend_key_idx) ||
FlatVector::IsNull(keys_child_vector, tweak_key_idx)) {
mask.SetInvalid(idx);
return false;
}
auto scan_private_key = FlatVector::GetData<string_t>(keys_child_vector)[scan_key_idx];
auto spend_public_key = FlatVector::GetData<string_t>(keys_child_vector)[spend_key_idx];
auto tweak_key = FlatVector::GetData<string_t>(keys_child_vector)[tweak_key_idx];
// Validate key sizes
if (scan_private_key.GetSize() != 32 || spend_public_key.GetSize() != 33 || tweak_key.GetSize() != 33) {
mask.SetInvalid(idx);
return false;
}
// Parse the spend public key once
secp256k1_pubkey spend_pubkey;
if (secp256k1_ec_pubkey_parse(ctx, &spend_pubkey, (const unsigned char *)spend_public_key.GetData(), 33) !=
1) {
mask.SetInvalid(idx);
return false;
}
// Parse the tweak key
secp256k1_pubkey tweak_pubkey;
if (secp256k1_ec_pubkey_parse(ctx, &tweak_pubkey, (const unsigned char *)tweak_key.GetData(), 33) != 1) {
mask.SetInvalid(idx);
return false;
}
// Implement: secp256k1_ec_pubkey_tweak_mul(tweak_key, SILENT_PAYMENTS_SCAN_PRIVATE_KEY)
if (secp256k1_ec_pubkey_tweak_mul(ctx, &tweak_pubkey, (const unsigned char *)scan_private_key.GetData()) !=
1) {
mask.SetInvalid(idx);
return false;
}
// Serialize the tweaked key
unsigned char tweaked_key_serialized[33];
size_t tweaked_len = 33;
if (secp256k1_ec_pubkey_serialize(ctx, tweaked_key_serialized, &tweaked_len, &tweak_pubkey,
SECP256K1_EC_COMPRESSED) != 1) {
mask.SetInvalid(idx);
return false;
}
// Calculate the base shared secret: secp256k1_tagged_sha256('BIP0352/SharedSecret', tweaked_key ||
// int_to_big_endian(0))
std::string tag = "BIP0352/SharedSecret";
// Concatenate tweaked_key_serialized + int_to_big_endian(0)
unsigned char base_data[37]; // 33 + 4 bytes
memcpy(base_data, tweaked_key_serialized, 33);
// int_to_big_endian(0) = {0, 0, 0, 0}
base_data[33] = 0;
base_data[34] = 0;
base_data[35] = 0;
base_data[36] = 0;
// Compute base shared secret
unsigned char base_shared_secret[32];
if (secp256k1_tagged_sha256(ctx, base_shared_secret, (const unsigned char *)tag.c_str(), tag.size(),
base_data, 37) != 1) {
mask.SetInvalid(idx);
return false;
}
// Create public key from base shared secret
secp256k1_pubkey base_shared_pubkey;
if (secp256k1_ec_pubkey_create(ctx, &base_shared_pubkey, base_shared_secret) != 1) {
mask.SetInvalid(idx);
return false;
}
// Combine with spend public key to get base output key
const secp256k1_pubkey *base_pubkeys[2] = {&spend_pubkey, &base_shared_pubkey};
secp256k1_pubkey base_output_key;
if (secp256k1_ec_pubkey_combine(ctx, &base_output_key, base_pubkeys, 2) != 1) {
mask.SetInvalid(idx);
return false;
}
// Serialize the base output key to compressed format
unsigned char base_compressed[33];
size_t base_len = 33;
if (secp256k1_ec_pubkey_serialize(ctx, base_compressed, &base_len, &base_output_key,
SECP256K1_EC_COMPRESSED) != 1) {
mask.SetInvalid(idx);
return false;
}
// Extract first 8 bytes of base x-coordinate as big-endian int64
int64_t base_prefix = ExtractBigEndianInt64(base_compressed + 1);
// Check direct match against outputs list
for (idx_t j = 0; j < outputs_list.length; j++) {
idx_t output_idx = outputs_list.offset + j;
if (FlatVector::IsNull(outputs_child_vector, output_idx)) {
continue;
}
auto output_int64 = FlatVector::GetData<int64_t>(outputs_child_vector)[output_idx];
if (output_int64 == base_prefix) {
return true;
}
}
// If no label tweaks provided, we're done (no match found)
if (label_tweaks_list.length == 0) {
return false;
}
// Process each label tweak by adding it to the base output key
for (idx_t k = 0; k < label_tweaks_list.length; k++) {
idx_t label_idx = label_tweaks_list.offset + k;
if (FlatVector::IsNull(label_tweaks_child_vector, label_idx)) {
continue;
}
auto label_tweak = FlatVector::GetData<string_t>(label_tweaks_child_vector)[label_idx];
// Validate that the label tweak is exactly 33 bytes (compressed pubkey)
if (label_tweak.GetSize() != 33) {
continue;
}
// Parse the label tweak public key
secp256k1_pubkey label_tweak_pubkey;
if (secp256k1_ec_pubkey_parse(ctx, &label_tweak_pubkey, (const unsigned char *)label_tweak.GetData(),
33) != 1) {
continue;
}
// Combine the base output key with the label tweak key
const secp256k1_pubkey *tweak_pubkeys[2] = {&base_output_key, &label_tweak_pubkey};
secp256k1_pubkey tweaked_output_key;
if (secp256k1_ec_pubkey_combine(ctx, &tweaked_output_key, tweak_pubkeys, 2) != 1) {
continue;
}
// Serialize the tweaked output key to compressed format
unsigned char tweaked_compressed[33];
size_t tweaked_len = 33;
if (secp256k1_ec_pubkey_serialize(ctx, tweaked_compressed, &tweaked_len, &tweaked_output_key,
SECP256K1_EC_COMPRESSED) != 1) {
continue;
}
// Extract first 8 bytes of tweaked x-coordinate as big-endian int64
int64_t tweaked_prefix = ExtractBigEndianInt64(tweaked_compressed + 1);
// Check if this tweaked x value matches any in the outputs list
for (idx_t j = 0; j < outputs_list.length; j++) {
idx_t output_idx = outputs_list.offset + j;
if (FlatVector::IsNull(outputs_child_vector, output_idx)) {
continue;
}
auto output_int64 = FlatVector::GetData<int64_t>(outputs_child_vector)[output_idx];
if (output_int64 == tweaked_prefix) {
return true;
}
}
// Try with negated result of the addition
secp256k1_pubkey negated_tweaked_key = tweaked_output_key;
if (secp256k1_ec_pubkey_negate(ctx, &negated_tweaked_key) != 1) {
continue;
}
// Serialize the negated tweaked key to compressed format
unsigned char negated_tweaked_compressed[33];
size_t negated_tweaked_len = 33;
if (secp256k1_ec_pubkey_serialize(ctx, negated_tweaked_compressed, &negated_tweaked_len,
&negated_tweaked_key, SECP256K1_EC_COMPRESSED) != 1) {
continue;
}
// Extract first 8 bytes of negated tweaked x-coordinate as big-endian int64
int64_t negated_tweaked_prefix = ExtractBigEndianInt64(negated_tweaked_compressed + 1);
// Check if this negated tweaked x value matches any in the outputs list
for (idx_t j = 0; j < outputs_list.length; j++) {
idx_t output_idx = outputs_list.offset + j;
if (FlatVector::IsNull(outputs_child_vector, output_idx)) {
continue;
}
auto output_int64 = FlatVector::GetData<int64_t>(outputs_child_vector)[output_idx];
if (output_int64 == negated_tweaked_prefix) {
return true;
}
}
}
return false;
});
}
static void LoadInternal(DatabaseInstance &instance) {
// Register the secp256k1_ec_pubkey_combine function that accepts an array of blobs
auto secp256k1_ec_pubkey_combine_function =
@ -597,6 +829,14 @@ static void LoadInternal(DatabaseInstance &instance) {
{LogicalType::LIST(LogicalType::BIGINT), LogicalType::BLOB, LogicalType::LIST(LogicalType::BLOB)},
LogicalType::BOOLEAN, Secp256k1XOnlyKeyMatchScalarFun);
ExtensionUtil::RegisterFunction(instance, secp256k1_xonly_key_match_function);
// Register the scan_silent_payments function
auto scan_silent_payments_function =
ScalarFunction("scan_silent_payments",
{LogicalType::LIST(LogicalType::BIGINT), LogicalType::LIST(LogicalType::BLOB),
LogicalType::LIST(LogicalType::BLOB)},
LogicalType::BOOLEAN, ScanSilentPaymentsScalarFun);
ExtensionUtil::RegisterFunction(instance, scan_silent_payments_function);
}
void Secp256k1Extension::Load(DuckDB &db) {

View File

@ -715,3 +715,143 @@ SELECT
FROM comprehensive_test;
----
true
# Test scan_silent_payments function with no label tweaks (empty case)
query I
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 AS (SELECT [8646911284551352320] as list), -- Dummy output that won't match
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),
(SELECT keys_array FROM keys),
CAST([] AS BLOB[]) -- Empty label tweaks
);
----
false
# Test scan_silent_payments function with matching output (functional test)
query I
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),
-- Calculate what the output should be for this specific input
calculated_output AS (
SELECT secp256k1_ec_pubkey_combine([
(SELECT key FROM spend_pub),
secp256k1_ec_pubkey_create(secp256k1_tagged_sha256('BIP0352/SharedSecret',
secp256k1_ec_pubkey_tweak_mul((SELECT key FROM tweak_key), (SELECT key FROM scan_priv)) || int_to_big_endian(0)))
]) as output_key
),
-- Extract first 8 bytes as BIGINT
output_prefix AS (
SELECT hash_prefix_to_int(from_hex(substring(to_hex(output_key), 3, 64)), 0) as prefix
FROM calculated_output
),
outputs AS (SELECT [(SELECT prefix FROM output_prefix)] 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),
(SELECT keys_array FROM keys),
CAST([] AS BLOB[]) -- Empty label tweaks
);
----
true
# Test scan_silent_payments function with no label tweaks (BIP testcase)
query I
WITH
scan_priv AS (SELECT from_hex('0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c') as key),
spend_pub AS (SELECT secp256k1_ec_pubkey_create(from_hex('9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3')) as key),
tweak_key AS (SELECT from_hex('024ac253c216532e961988e2a8ce266a447c894c781e52ef6cee902361db960004') as key),
outputs AS (SELECT [4512552348537027144] as list), -- First 8 bytes of expected output x-coordinate
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),
(SELECT keys_array FROM keys),
CAST([] AS BLOB[]) -- Empty label tweaks
);
----
true
# Test scan_silent_payments function with label tweaks
query I
WITH
scan_priv AS (SELECT from_hex('0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c') as key),
spend_pub AS (SELECT secp256k1_ec_pubkey_create(from_hex('9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3')) as key),
tweak_key AS (SELECT secp256k1_ec_pubkey_create(from_hex('0000000000000000000000000000000000000000000000000000000000000002')) as key),
-- Test with a label tweak key
label_tweak AS (SELECT secp256k1_ec_pubkey_create(from_hex('0000000000000000000000000000000000000000000000000000000000000001')) as key),
outputs AS (SELECT [1234567890] as list), -- Dummy output that won't match
keys AS (SELECT [(SELECT key FROM scan_priv), (SELECT key FROM spend_pub), (SELECT key FROM tweak_key)] as keys_array),
tweaks AS (SELECT [(SELECT key FROM label_tweak)] as tweaks_array)
SELECT scan_silent_payments(
(SELECT list FROM outputs),
(SELECT keys_array FROM keys),
(SELECT tweaks_array FROM tweaks)
);
----
false
# Test scan_silent_payments function with change label tweak (BIP testcase)
query I
WITH
scan_priv AS (SELECT from_hex('11b7a82e06ca2648d5fded2366478078ec4fc9dc1d8ff487518226f229d768fd') as key),
spend_pub AS (SELECT from_hex('03ecd43b9fdad484ff57278b21878b844276ce390622d03dd0cfb4288b7e02a6f5') as key),
tweak_key AS (SELECT from_hex('0314bec14463d6c0181083d607fecfba67bb83f95915f6f247975ec566d5642ee8') as key),
label_tweak AS (SELECT from_hex('03ecd43b9fdad484ff57278b21878b844276ce390622d03dd0cfb4288b7e02a6f5') as key),
outputs AS (SELECT [-4740445252767345406] as list), -- First 8 bytes of expected output x-coordinate
keys AS (SELECT [(SELECT key FROM scan_priv), (SELECT key FROM spend_pub), (SELECT key FROM tweak_key)] as keys_array),
tweaks AS (SELECT [(SELECT key FROM label_tweak)] as tweaks_array)
SELECT scan_silent_payments(
(SELECT list FROM outputs),
(SELECT keys_array FROM keys),
(SELECT tweaks_array FROM tweaks)
);
----
true
# Test scan_silent_payments function with NULL inputs
query I
SELECT scan_silent_payments(NULL, NULL, NULL);
----
NULL
# Test scan_silent_payments function with invalid keys array length
query I
WITH
scan_priv AS (SELECT from_hex('0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1f2c') as key),
spend_pub AS (SELECT secp256k1_ec_pubkey_create(from_hex('9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3')) as key),
outputs AS (SELECT [1234567890] as list),
keys AS (SELECT [(SELECT key FROM scan_priv), (SELECT key FROM spend_pub)] as keys_array) -- Only two keys instead of three
SELECT scan_silent_payments(
(SELECT list FROM outputs),
(SELECT keys_array FROM keys),
CAST([] AS BLOB[])
);
----
NULL
# Test scan_silent_payments function with wrong key sizes
query I
WITH
wrong_scan_priv AS (SELECT from_hex('0f694e068028a717f8af6b9411f9a133dd3565258714cc226594b34db90c1') as key), -- 31 bytes instead of 32
spend_pub AS (SELECT secp256k1_ec_pubkey_create(from_hex('9d6ad855ce3417ef84e836892e5a56392bfba05fa5d97ccea30e266f540e08b3')) as key),
tweak_key AS (SELECT secp256k1_ec_pubkey_create(from_hex('0000000000000000000000000000000000000000000000000000000000000001')) as key),
outputs AS (SELECT [1234567890] as list),
keys AS (SELECT [(SELECT key FROM wrong_scan_priv), (SELECT key FROM spend_pub), (SELECT key FROM tweak_key)] as keys_array)
SELECT scan_silent_payments(
(SELECT list FROM outputs),
(SELECT keys_array FROM keys),
CAST([] AS BLOB[])
);
----
NULL