From 0b3b1a5c3f45e1c81cdc15ea565c194ef615f814 Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Wed, 10 Sep 2025 13:10:22 +0200 Subject: [PATCH] align input pubkey retrieval to silent payments reference implementation --- .../silentpayments/SilentPaymentUtils.java | 52 +++++++++++++++++-- 1 file changed, 47 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/sparrowwallet/drongo/silentpayments/SilentPaymentUtils.java b/src/main/java/com/sparrowwallet/drongo/silentpayments/SilentPaymentUtils.java index 3f0fe2a..aa4322a 100644 --- a/src/main/java/com/sparrowwallet/drongo/silentpayments/SilentPaymentUtils.java +++ b/src/main/java/com/sparrowwallet/drongo/silentpayments/SilentPaymentUtils.java @@ -10,13 +10,21 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Map; public class SilentPaymentUtils { private static final Logger log = LoggerFactory.getLogger(SilentPaymentUtils.class); - private static final List SCRIPT_TYPES = List.of(ScriptType.P2TR, ScriptType.P2WPKH, ScriptType.P2SH_P2WPKH, ScriptType.P2PKH); + private static final List SCRIPT_TYPES = List.of(ScriptType.P2TR, ScriptType.P2WPKH, ScriptType.P2SH, ScriptType.P2PKH); + + //Alternative generator point on the secp256k1 curve (x-coordinate) generated from the SHA256 hash of "The scalar for this x is unknown" + private static final byte[] NUMS_H = { + (byte) 0x50, (byte) 0x92, (byte) 0x9b, (byte) 0x74, (byte) 0xc1, (byte) 0xa0, (byte) 0x49, (byte) 0x54, (byte) 0xb7, (byte) 0x8b, (byte) 0x4b, (byte) 0x60, + (byte) 0x35, (byte) 0xe9, (byte) 0x7a, (byte) 0x5e, (byte) 0x07, (byte) 0x8a, (byte) 0x5a, (byte) 0x0f, (byte) 0x28, (byte) 0xec, (byte) 0x96, (byte) 0xd5, + (byte) 0x47, (byte) 0xbf, (byte) 0xee, (byte) 0x9a, (byte) 0xce, (byte) 0x80, (byte) 0x3a, (byte) 0xc0 + }; public static boolean isEligible(Transaction tx, Map spentScriptPubKeys) { if(!containsTaprootOutput(tx)) { @@ -46,21 +54,55 @@ public class SilentPaymentUtils { if(scriptType.isScriptType(scriptPubKey)) { switch(scriptType) { case P2TR: - keys.add(ScriptType.P2TR.getPublicKeyFromScript(scriptPubKey)); + if(input.getWitness() != null && input.getWitness().getPushCount() >= 1) { + List stack = input.getWitness().getPushes(); + if(stack.size() > 1 && stack.getLast().length > 0 && stack.getLast()[0] == 0x50) { //Last item is annex + stack = stack.subList(0, stack.size() - 1); + } + + if(stack.size() > 1) { + // Script path spend + byte[] controlBlock = stack.getLast(); + // Control block is <32 byte internal key> and 0 or more <32 byte hash> + if(controlBlock.length >= 33) { + byte[] internalKey = Arrays.copyOfRange(controlBlock, 1, 33); + if(Arrays.equals(internalKey, NUMS_H)) { + break; + } + } + } + + ECKey pubKey = ScriptType.P2TR.getPublicKeyFromScript(scriptPubKey); + if(pubKey.isCompressed()) { + keys.add(pubKey); + } + } + break; + case P2SH: + Script redeemScript = input.getScriptSig().getFirstNestedScript(); + if(ScriptType.P2WPKH.isScriptType(redeemScript)) { + if(input.getWitness() != null && input.getWitness().getPushCount() == 2) { + byte[] pubKey = input.getWitness().getPushes().getLast(); + if(pubKey != null && pubKey.length == 33) { + keys.add(ECKey.fromPublicOnly(pubKey)); + } + } + } break; case P2WPKH: - case P2SH_P2WPKH: if(input.getWitness() != null && input.getWitness().getPushCount() == 2) { - byte[] pubKey = input.getWitness().getPushes().get(input.getWitness().getPushCount() - 1); + byte[] pubKey = input.getWitness().getPushes().getLast(); if(pubKey != null && pubKey.length == 33) { keys.add(ECKey.fromPublicOnly(pubKey)); } } break; case P2PKH: + byte[] spkHash = ScriptType.P2PKH.getHashFromScript(scriptPubKey); for(ScriptChunk scriptChunk : input.getScriptSig().getChunks()) { - if(scriptChunk.isPubKey() && scriptChunk.getData().length == 33) { + if(scriptChunk.isPubKey() && scriptChunk.getData().length == 33 && Arrays.equals(Utils.sha256hash160(scriptChunk.getData()), spkHash)) { keys.add(scriptChunk.getPubKey()); + break; } } break;