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
7 changes: 7 additions & 0 deletions electrum/plugins/silent_payments/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Silent Payments Sending and Receiving

Assuming https://github.com/bitcoin-core/secp256k1/pull/1765 is merged

Taking https://github.com/spesmilo/electrum/pull/9900 as inspiration

The index server as https://github.com/cake-tech/blockstream-electrs/tree/cake-update-v1
Empty file.
10 changes: 10 additions & 0 deletions electrum/plugins/silent_payments/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"name": "silent_payments",
"fullname": "Silent Payments",
"description": "BIP 352 Silent Payments support for Electrum, utilizing external index servers for efficient scanning.",
"available_for": ["qt"],
"author": "securitybrahh@empiresec.co",
"license": "MIT",
"version": "1.0.0",
"min_electrum_version": "4.0.0"
}
17 changes: 17 additions & 0 deletions electrum/plugins/silent_payments/qt.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from electrum.plugin import hook
from .silent_payments import SilentPaymentEngine

class Plugin(BasePlugin):
@hook
def before_create_transaction(self, wallet, tx):
# 1. Identify if any output is a Silent Payment type
# 2. Derive the tweaked public key using your math engine
# 3. Replace the output script
for output in tx.outputs():
if output.is_silent_payment_address():
# Derive the tweak using the BIP 352 logic
tweak = SilentPaymentEngine.calculate_tweak(...)
tweaked_pk = SilentPaymentEngine.tweak_pubkey(output.pubkey, tweak)

# Update the transaction output directly
output.scriptpubkey = b'\x51' + len(tweaked_pk).to_bytes(1, 'big') + tweaked_pk
49 changes: 49 additions & 0 deletions electrum/plugins/silent_payments/scanner.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import socket
import json
import threading
from electrum.util import ThreadJob

class SilentPaymentScanner(ThreadJob):
def __init__(self, wallet, db):
self.wallet = wallet
self.db = db
self.server_url = "electrs.cakewallet.com"
self.server_port = 50001
self.running = True

def fetch_tweaks_from_server(self, scan_pubkey: bytes):
"""
Query the index server for tweaks associated with our scan key.
This follows the Cake Wallet Silent Payment indexer protocol.
"""
try:
with socket.create_connection((self.server_url, self.server_port), timeout=10) as sock:
# Construct RPC request
request = {
"method": "sp.get_tweaks",
"params": [scan_pubkey.hex()],
"id": 1,
"jsonrpc": "2.0"
}
sock.sendall(json.dumps(request).encode() + b'\n')
response = sock.recv(4096)
return json.loads(response).get('result', [])
except Exception as e:
print(f"Scanner connection error: {e}")
return []

def run(self):
"""Background execution loop."""
scan_pubkey = self.wallet.get_silent_payment_scan_pubkey()
while self.running:
tweaks = self.fetch_tweaks_from_server(scan_pubkey)
for item in tweaks:
# item: {'txid': ..., 'vout': ..., 'tweak': ...}
self.db.add_mapping(
bytes.fromhex(item['tweak']),
b'', # Logic to map to internal pubkey
item['txid'],
item['vout']
)
# Sleep to avoid excessive network usage
threading.Event().wait(300)
33 changes: 33 additions & 0 deletions electrum/plugins/silent_payments/silent_payments.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from electrum import ecc

class SilentPayment:
CURVE_ORDER = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141

@staticmethod
def derive_shared_secret(input_pubkeys_sum: bytes, scan_privkey: bytes) -> bytes:
"""
Derives shared secret based on BIP 352 using scalar multiplication.
"""
# Logic: shared_secret = input_hash * scan_privkey * G
# Use native secp256k1 bindings provided by Electrum's ecc module
return ecc.ecdh(input_pubkeys_sum, scan_privkey)

@staticmethod
def calculate_tweak(shared_secret: bytes, k: int) -> bytes:
"""
Computes the tweak used to modify the recipient's public key.
"""
# tweak = hash(shared_secret || k)
pass

def derive_spendable_key(scan_privkey: bytes, tweak: bytes, label: bytes = None):
# BIP 352: d = (bspend + tk + hash(bscan || label)) mod n
# The PR #1765 provides the optimized C primitives for these operations.
# You call the native bindings here.
return ecc.silent_payment_derive_key(scan_privkey, tweak, label)

def create_sp_outputs(scan_pubkey, spend_pubkey, input_hash):
# 1. Derive shared secret: scalar = input_hash * scan_privkey
# 2. Derive tweak: tweak = hash(shared_secret)
# 3. Create tweaked spend pubkey: P = G * tweak + spend_pubkey
return tweaked_pubkey
36 changes: 36 additions & 0 deletions electrum/plugins/silent_payments/transaction_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import hashlib
from electrum.transaction import TxOutput

def is_silent_payment_output(output: TxOutput) -> bool:
"""
BIP 352 outputs are P2TR (OP_1 0x20 [32-byte pubkey]).
A standard P2TR scriptPubKey is 34 bytes long (1 byte OP_1, 1 byte PUSHDATA32, 32 bytes x-only pubkey).
"""
return len(output.scriptpubkey) == 34 and output.scriptpubkey.startswith(b'\x51\x20')

def sort_outpoints(outpoints):
"""
Lexicographical sort of outpoints as required by BIP 352.
Outpoint = txid (32 bytes) + vout (4 bytes, little-endian).
"""
# Sort primarily by txid, secondarily by vout index
return sorted(outpoints, key=lambda x: x['txid'] + x['vout'].to_bytes(4, 'little'))

def calculate_integrity_hash(inputs, outputs):
"""
Calculates the integrity hash of the transaction to detect tampering.
hash = sha256(sorted_outpoints || sum_of_input_pubkeys)
"""
# 1. Collect and sort outpoints
outpoints = [{"txid": i.prevout.txid.hex(), "vout": i.prevout.out_idx} for i in inputs]
sorted_ops = sort_outpoints(outpoints)

# 2. Serialize and hash
hasher = hashlib.sha256()
for op in sorted_ops:
hasher.update(bytes.fromhex(op['txid']))
hasher.update(op['vout'].to_bytes(4, 'little'))

# Add input pubkeys sum logic here (requires ecc.point_add)
# ...
return hasher.digest()