From dfd947cb69ee26de3a8f2fbaf19966c7e2ff7fe1 Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Mon, 18 May 2026 14:55:15 +0200 Subject: [PATCH] add sp support for bip322 message signing --- .../sparrowwallet/drongo/crypto/Bip322.java | 41 ++++++++++++++++++ .../drongo/crypto/Bip322Test.java | 42 +++++++++++++++++++ 2 files changed, 83 insertions(+) diff --git a/src/main/java/com/sparrowwallet/drongo/crypto/Bip322.java b/src/main/java/com/sparrowwallet/drongo/crypto/Bip322.java index ee1394a..f83b222 100644 --- a/src/main/java/com/sparrowwallet/drongo/crypto/Bip322.java +++ b/src/main/java/com/sparrowwallet/drongo/crypto/Bip322.java @@ -1,5 +1,6 @@ package com.sparrowwallet.drongo.crypto; +import com.sparrowwallet.drongo.KeyDerivation; import com.sparrowwallet.drongo.Utils; import com.sparrowwallet.drongo.address.Address; import com.sparrowwallet.drongo.policy.PolicyType; @@ -43,6 +44,46 @@ public class Bip322 { return psbt; } + public static PSBT getBip322PsbtSp(Address address, String message, byte[] silentPaymentsTweak, Map spendDerivations) { + if(silentPaymentsTweak == null) { + throw new IllegalArgumentException("Silent payments tweak is required"); + } + + PSBT psbt = getBip322Psbt(P2TR, address, message); + PSBTInput psbtInput = psbt.getPsbtInputs().getFirst(); + psbtInput.setSilentPaymentsTweak(silentPaymentsTweak); + if(spendDerivations != null && !spendDerivations.isEmpty()) { + psbtInput.getSilentPaymentsSpendDerivations().putAll(spendDerivations); + } + + return psbt; + } + + public static String signMessageBip322Sp(Address address, String message, ECKey spendPrivKey, byte[] silentPaymentsTweak) { + PSBT psbt = getBip322PsbtSp(address, message, silentPaymentsTweak, Collections.emptyMap()); + PSBTInput psbtInput = psbt.getPsbtInputs().getFirst(); + + if(!psbtInput.signSilentPayments(spendPrivKey)) { + throw new IllegalStateException("Failed to sign BIP322 PSBT with silent payments tweak"); + } + + return getBip322SignatureFromPsbtSp(psbt); + } + + public static String getBip322SignatureFromPsbtSp(PSBT signedPsbt) { + PSBTInput psbtInput = signedPsbt.getPsbtInputs().getFirst(); + TransactionSignature signature = psbtInput.getTapKeyPathSignature(); + if(signature == null) { + throw new IllegalArgumentException("PSBT does not contain a taproot keypath signature"); + } + + Transaction finalizeTransaction = new Transaction(); + TransactionWitness witness = new TransactionWitness(finalizeTransaction, signature); + finalizeTransaction.addInput(Sha256Hash.ZERO_HASH, 0, new Script(new byte[0]), witness); + + return Base64.getEncoder().encodeToString(witness.toByteArray()); + } + public static String getBip322SignatureFromPsbt(ScriptType scriptType, PSBT signedPsbt, ECKey pubKey) { checkScriptType(scriptType); diff --git a/src/test/java/com/sparrowwallet/drongo/crypto/Bip322Test.java b/src/test/java/com/sparrowwallet/drongo/crypto/Bip322Test.java index 6182f2a..e7e0296 100644 --- a/src/test/java/com/sparrowwallet/drongo/crypto/Bip322Test.java +++ b/src/test/java/com/sparrowwallet/drongo/crypto/Bip322Test.java @@ -167,6 +167,48 @@ public class Bip322Test { Assertions.assertThrows(IllegalArgumentException.class, () -> Bip322.getBip322SignatureFromPsbt(ScriptType.P2WPKH, psbt, pubKey)); } + @Test + public void signMessageBip322Sp() throws SignatureException { + ECKey spendPrivKey = DumpedPrivateKey.fromBase58("L3VFeEujGtevx9w18HD1fhRbCH67Az2dpCymeRE1SoPK6XQtaN2k").getKey(); + byte[] tweak = Utils.hexToBytes("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"); + + ECKey spendPubKey = ECKey.fromPublicOnly(spendPrivKey); + ECKey tweakPoint = ECKey.fromPublicOnly(ECKey.fromPrivate(tweak)); + ECKey outputKey = spendPubKey.add(tweakPoint, true); + Address address = ScriptType.P2TR.getAddress(PolicyType.SINGLE_SP, outputKey); + + String signature = Bip322.signMessageBip322Sp(address, "Hello World", spendPrivKey, tweak); + Assertions.assertTrue(Bip322.verifyMessageBip322(ScriptType.P2TR, address, "Hello World", signature)); + + //An SP signature for the same key but a different tweak must not verify against the first address + byte[] tweak2 = Utils.hexToBytes("fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210"); + ECKey tweakPoint2 = ECKey.fromPublicOnly(ECKey.fromPrivate(tweak2)); + ECKey outputKey2 = spendPubKey.add(tweakPoint2, true); + Address address2 = ScriptType.P2TR.getAddress(PolicyType.SINGLE_SP, outputKey2); + String signature2 = Bip322.signMessageBip322Sp(address2, "Hello World", spendPrivKey, tweak2); + Assertions.assertTrue(Bip322.verifyMessageBip322(ScriptType.P2TR, address2, "Hello World", signature2)); + Assertions.assertFalse(Bip322.verifyMessageBip322(ScriptType.P2TR, address, "Hello World", signature2)); + } + + @Test + public void getBip322PsbtSp() { + ECKey spendPrivKey = DumpedPrivateKey.fromBase58("L3VFeEujGtevx9w18HD1fhRbCH67Az2dpCymeRE1SoPK6XQtaN2k").getKey(); + byte[] tweak = Utils.hexToBytes("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"); + + ECKey spendPubKey = ECKey.fromPublicOnly(spendPrivKey); + ECKey tweakPoint = ECKey.fromPublicOnly(ECKey.fromPrivate(tweak)); + ECKey outputKey = spendPubKey.add(tweakPoint, true); + Address address = ScriptType.P2TR.getAddress(PolicyType.SINGLE_SP, outputKey); + + PSBT psbt = Bip322.getBip322PsbtSp(address, "Hello World", tweak, java.util.Collections.emptyMap()); + Assertions.assertEquals(1, psbt.getPsbtInputs().size()); + PSBTInput psbtInput = psbt.getPsbtInputs().get(0); + Assertions.assertNotNull(psbtInput.getWitnessUtxo()); + Assertions.assertArrayEquals(tweak, psbtInput.getSilentPaymentsTweak()); + Assertions.assertNull(psbtInput.getTapInternalKey()); + Assertions.assertTrue(psbtInput.getTapDerivedPublicKeys().isEmpty()); + } + @Test public void verifyMessageBip322Multisig() throws SignatureException, InvalidAddressException { Address address = Address.fromString("bc1ppv609nr0vr25u07u95waq5lucwfm6tde4nydujnu8npg4q75mr5sxq8lt3");