diff --git a/electrum/plugins/silent_payments/README.md b/electrum/plugins/silent_payments/README.md new file mode 100644 index 000000000000..5013cfb19817 --- /dev/null +++ b/electrum/plugins/silent_payments/README.md @@ -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 \ No newline at end of file diff --git a/electrum/plugins/silent_payments/__init__.py b/electrum/plugins/silent_payments/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/electrum/plugins/silent_payments/manifest.json b/electrum/plugins/silent_payments/manifest.json new file mode 100644 index 000000000000..fe46d568177a --- /dev/null +++ b/electrum/plugins/silent_payments/manifest.json @@ -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" +} \ No newline at end of file diff --git a/electrum/plugins/silent_payments/qt.py b/electrum/plugins/silent_payments/qt.py new file mode 100644 index 000000000000..3c59a32d1aa7 --- /dev/null +++ b/electrum/plugins/silent_payments/qt.py @@ -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 \ No newline at end of file diff --git a/electrum/plugins/silent_payments/scanner.py b/electrum/plugins/silent_payments/scanner.py new file mode 100644 index 000000000000..12fb09c8074f --- /dev/null +++ b/electrum/plugins/silent_payments/scanner.py @@ -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) \ No newline at end of file diff --git a/electrum/plugins/silent_payments/silent_payments.py b/electrum/plugins/silent_payments/silent_payments.py new file mode 100644 index 000000000000..db030f7f91db --- /dev/null +++ b/electrum/plugins/silent_payments/silent_payments.py @@ -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 \ No newline at end of file diff --git a/electrum/plugins/silent_payments/transaction_utils.py b/electrum/plugins/silent_payments/transaction_utils.py new file mode 100644 index 000000000000..d65576061066 --- /dev/null +++ b/electrum/plugins/silent_payments/transaction_utils.py @@ -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() \ No newline at end of file