use map of scriptpubkeys instead of transaction outputs for tweak computation

This commit is contained in:
Craig Raw 2025-08-21 11:43:19 +02:00
parent a4d86f9ee3
commit da736c8cef
2 changed files with 29 additions and 26 deletions

View file

@ -18,35 +18,35 @@ public class SilentPaymentUtils {
private static final List<ScriptType> SCRIPT_TYPES = List.of(ScriptType.P2TR, ScriptType.P2WPKH, ScriptType.P2SH_P2WPKH, ScriptType.P2PKH); private static final List<ScriptType> SCRIPT_TYPES = List.of(ScriptType.P2TR, ScriptType.P2WPKH, ScriptType.P2SH_P2WPKH, ScriptType.P2PKH);
public static boolean isEligible(Transaction tx, Map<HashIndex, TransactionOutput> spentOutputs) { public static boolean isEligible(Transaction tx, Map<HashIndex, Script> spentScriptPubKeys) {
if(!containsTaprootOutput(tx)) { if(!containsTaprootOutput(tx)) {
return false; return false;
} }
if(getInputPubKeys(tx, spentOutputs).isEmpty()) { if(getInputPubKeys(tx, spentScriptPubKeys).isEmpty()) {
return false; return false;
} }
if(spendsInvalidSegwitOutput(tx, spentOutputs)) { if(spendsInvalidSegwitOutput(tx, spentScriptPubKeys)) {
return false; return false;
} }
return true; return true;
} }
public static List<ECKey> getInputPubKeys(Transaction tx, Map<HashIndex, TransactionOutput> spentOutputs) { public static List<ECKey> getInputPubKeys(Transaction tx, Map<HashIndex, Script> spentScriptPubKeys) {
List<ECKey> keys = new ArrayList<>(); List<ECKey> keys = new ArrayList<>();
for(TransactionInput input : tx.getInputs()) { for(TransactionInput input : tx.getInputs()) {
HashIndex hashIndex = new HashIndex(input.getOutpoint().getHash(), input.getOutpoint().getIndex()); HashIndex hashIndex = new HashIndex(input.getOutpoint().getHash(), input.getOutpoint().getIndex());
TransactionOutput output = spentOutputs.get(hashIndex); Script scriptPubKey = spentScriptPubKeys.get(hashIndex);
if(output == null) { if(scriptPubKey == null) {
throw new IllegalStateException("No output found for input " + input.getOutpoint()); throw new IllegalStateException("No scriptPubKey found for input " + input.getOutpoint().getHash() + ":" + input.getOutpoint().getIndex());
} }
for(ScriptType scriptType : SCRIPT_TYPES) { for(ScriptType scriptType : SCRIPT_TYPES) {
if(scriptType.isScriptType(output.getScript())) { if(scriptType.isScriptType(scriptPubKey)) {
switch(scriptType) { switch(scriptType) {
case P2TR: case P2TR:
keys.add(ScriptType.P2TR.getPublicKeyFromScript(output.getScript())); keys.add(ScriptType.P2TR.getPublicKeyFromScript(scriptPubKey));
break; break;
case P2WPKH: case P2WPKH:
case P2SH_P2WPKH: case P2SH_P2WPKH:
@ -84,10 +84,13 @@ public class SilentPaymentUtils {
return false; return false;
} }
public static boolean spendsInvalidSegwitOutput(Transaction tx, Map<HashIndex, TransactionOutput> spentOutputs) { public static boolean spendsInvalidSegwitOutput(Transaction tx, Map<HashIndex, Script> spentScriptPubKeys) {
for(TransactionInput input : tx.getInputs()) { for(TransactionInput input : tx.getInputs()) {
HashIndex hashIndex = new HashIndex(input.getOutpoint().getHash(), input.getOutpoint().getIndex()); HashIndex hashIndex = new HashIndex(input.getOutpoint().getHash(), input.getOutpoint().getIndex());
Script scriptPubKey = spentOutputs.get(hashIndex).getScript(); Script scriptPubKey = spentScriptPubKeys.get(hashIndex);
if(scriptPubKey == null) {
throw new IllegalStateException("No scriptPubKey found for input " + input.getOutpoint().getHash() + ":" + input.getOutpoint().getIndex());
}
List<ScriptChunk> chunks = scriptPubKey.getChunks(); List<ScriptChunk> chunks = scriptPubKey.getChunks();
if(chunks.size() == 2 && chunks.getFirst().isOpCode() && chunks.get(1).getData() != null if(chunks.size() == 2 && chunks.getFirst().isOpCode() && chunks.get(1).getData() != null
&& chunks.getFirst().getOpcode() >= ScriptOpCodes.OP_2 && chunks.getFirst().getOpcode() <= ScriptOpCodes.OP_16) { && chunks.getFirst().getOpcode() >= ScriptOpCodes.OP_2 && chunks.getFirst().getOpcode() <= ScriptOpCodes.OP_16) {
@ -98,16 +101,16 @@ public class SilentPaymentUtils {
return false; return false;
} }
public static byte[] getTweak(Transaction tx, Map<HashIndex, TransactionOutput> spentOutputs) { public static byte[] getTweak(Transaction tx, Map<HashIndex, Script> spentScriptPubKeys) {
if(tx.getOutputs().stream().noneMatch(output -> ScriptType.P2TR.isScriptType(output.getScript()))) { if(tx.getOutputs().stream().noneMatch(output -> ScriptType.P2TR.isScriptType(output.getScript()))) {
return null; return null;
} }
if(spendsInvalidSegwitOutput(tx, spentOutputs)) { if(spendsInvalidSegwitOutput(tx, spentScriptPubKeys)) {
return null; return null;
} }
List<ECKey> inputKeys = getInputPubKeys(tx, spentOutputs); List<ECKey> inputKeys = getInputPubKeys(tx, spentScriptPubKeys);
if(inputKeys.isEmpty()) { if(inputKeys.isEmpty()) {
return null; return null;
} }

View file

@ -17,15 +17,15 @@ public class SilentPaymentUtilsTest {
transaction.addInput(Sha256Hash.wrap("a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d"), 0, new Script(Utils.hexToBytes("48304602210086783ded73e961037e77d49d9deee4edc2b23136e9728d56e4491c80015c3a63022100fda4c0f21ea18de29edbce57f7134d613e044ee150a89e2e64700de2d4e83d4e2103bd85685d03d111699b15d046319febe77f8de5286e9e512703cdee1bf3be3792"))); transaction.addInput(Sha256Hash.wrap("a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d"), 0, new Script(Utils.hexToBytes("48304602210086783ded73e961037e77d49d9deee4edc2b23136e9728d56e4491c80015c3a63022100fda4c0f21ea18de29edbce57f7134d613e044ee150a89e2e64700de2d4e83d4e2103bd85685d03d111699b15d046319febe77f8de5286e9e512703cdee1bf3be3792")));
transaction.addOutput(1000L, ScriptType.P2TR.getOutputScript(ECKey.fromPublicOnly(Utils.hexToBytes("3e9fce73d4e77a4809908e3c3a2e54ee147b9312dc5044a193d1fc85de46e3c1")))); transaction.addOutput(1000L, ScriptType.P2TR.getOutputScript(ECKey.fromPublicOnly(Utils.hexToBytes("3e9fce73d4e77a4809908e3c3a2e54ee147b9312dc5044a193d1fc85de46e3c1"))));
Map<HashIndex, TransactionOutput> spentOutputs = new HashMap<>(); Map<HashIndex, Script> spentScriptPubKeys = new HashMap<>();
HashIndex hashIndex0 = new HashIndex(transaction.getInputs().getFirst().getOutpoint().getHash(), transaction.getInputs().getFirst().getOutpoint().getIndex()); HashIndex hashIndex0 = new HashIndex(transaction.getInputs().getFirst().getOutpoint().getHash(), transaction.getInputs().getFirst().getOutpoint().getIndex());
TransactionOutput spentOutput0 = new TransactionOutput(new Transaction(), 400L, new Script(Utils.hexToBytes("76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac"))); Script spentScriptPubKey0 = new Script(Utils.hexToBytes("76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac"));
HashIndex hashIndex1 = new HashIndex(transaction.getInputs().getLast().getOutpoint().getHash(), transaction.getInputs().getLast().getOutpoint().getIndex()); HashIndex hashIndex1 = new HashIndex(transaction.getInputs().getLast().getOutpoint().getHash(), transaction.getInputs().getLast().getOutpoint().getIndex());
TransactionOutput spentOutput1 = new TransactionOutput(new Transaction(), 400L, new Script(Utils.hexToBytes("76a914d9317c66f54ff0a152ec50b1d19c25be50c8e15988ac"))); Script spentScriptPubKey1 = new Script(Utils.hexToBytes("76a914d9317c66f54ff0a152ec50b1d19c25be50c8e15988ac"));
spentOutputs.put(hashIndex0, spentOutput0); spentScriptPubKeys.put(hashIndex0, spentScriptPubKey0);
spentOutputs.put(hashIndex1, spentOutput1); spentScriptPubKeys.put(hashIndex1, spentScriptPubKey1);
byte[] tweak = SilentPaymentUtils.getTweak(transaction, spentOutputs); byte[] tweak = SilentPaymentUtils.getTweak(transaction, spentScriptPubKeys);
Assertions.assertNotNull(tweak); Assertions.assertNotNull(tweak);
Assertions.assertEquals("024ac253c216532e961988e2a8ce266a447c894c781e52ef6cee902361db960004", Utils.bytesToHex(tweak)); Assertions.assertEquals("024ac253c216532e961988e2a8ce266a447c894c781e52ef6cee902361db960004", Utils.bytesToHex(tweak));
} }
@ -37,15 +37,15 @@ public class SilentPaymentUtilsTest {
transaction.addInput(Sha256Hash.wrap("f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16"), 0, new Script(Utils.hexToBytes("48304602210086783ded73e961037e77d49d9deee4edc2b23136e9728d56e4491c80015c3a63022100fda4c0f21ea18de29edbce57f7134d613e044ee150a89e2e64700de2d4e83d4e2103bd85685d03d111699b15d046319febe77f8de5286e9e512703cdee1bf3be3792"))); transaction.addInput(Sha256Hash.wrap("f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16"), 0, new Script(Utils.hexToBytes("48304602210086783ded73e961037e77d49d9deee4edc2b23136e9728d56e4491c80015c3a63022100fda4c0f21ea18de29edbce57f7134d613e044ee150a89e2e64700de2d4e83d4e2103bd85685d03d111699b15d046319febe77f8de5286e9e512703cdee1bf3be3792")));
transaction.addOutput(1000L, ScriptType.P2TR.getOutputScript(ECKey.fromPublicOnly(Utils.hexToBytes("3e9fce73d4e77a4809908e3c3a2e54ee147b9312dc5044a193d1fc85de46e3c1")))); transaction.addOutput(1000L, ScriptType.P2TR.getOutputScript(ECKey.fromPublicOnly(Utils.hexToBytes("3e9fce73d4e77a4809908e3c3a2e54ee147b9312dc5044a193d1fc85de46e3c1"))));
Map<HashIndex, TransactionOutput> spentOutputs = new HashMap<>(); Map<HashIndex, Script> spentScriptPubKeys = new HashMap<>();
HashIndex hashIndex0 = new HashIndex(transaction.getInputs().getFirst().getOutpoint().getHash(), transaction.getInputs().getFirst().getOutpoint().getIndex()); HashIndex hashIndex0 = new HashIndex(transaction.getInputs().getFirst().getOutpoint().getHash(), transaction.getInputs().getFirst().getOutpoint().getIndex());
TransactionOutput spentOutput0 = new TransactionOutput(new Transaction(), 400L, new Script(Utils.hexToBytes("76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac"))); Script spentScriptPubKey0 = new Script(Utils.hexToBytes("76a91419c2f3ae0ca3b642bd3e49598b8da89f50c1416188ac"));
HashIndex hashIndex1 = new HashIndex(transaction.getInputs().getLast().getOutpoint().getHash(), transaction.getInputs().getLast().getOutpoint().getIndex()); HashIndex hashIndex1 = new HashIndex(transaction.getInputs().getLast().getOutpoint().getHash(), transaction.getInputs().getLast().getOutpoint().getIndex());
TransactionOutput spentOutput1 = new TransactionOutput(new Transaction(), 400L, new Script(Utils.hexToBytes("76a914d9317c66f54ff0a152ec50b1d19c25be50c8e15988ac"))); Script spentScriptPubKey1 = new Script(Utils.hexToBytes("76a914d9317c66f54ff0a152ec50b1d19c25be50c8e15988ac"));
spentOutputs.put(hashIndex0, spentOutput0); spentScriptPubKeys.put(hashIndex0, spentScriptPubKey0);
spentOutputs.put(hashIndex1, spentOutput1); spentScriptPubKeys.put(hashIndex1, spentScriptPubKey1);
byte[] tweak = SilentPaymentUtils.getTweak(transaction, spentOutputs); byte[] tweak = SilentPaymentUtils.getTweak(transaction, spentScriptPubKeys);
Assertions.assertNotNull(tweak); Assertions.assertNotNull(tweak);
Assertions.assertEquals("024ac253c216532e961988e2a8ce266a447c894c781e52ef6cee902361db960004", Utils.bytesToHex(tweak)); Assertions.assertEquals("024ac253c216532e961988e2a8ce266a447c894c781e52ef6cee902361db960004", Utils.bytesToHex(tweak));
} }