refactor bip322 implementation for qr and file signing support

This commit is contained in:
Craig Raw 2026-03-05 13:36:47 +02:00
parent 53c999a01b
commit af031d9425
2 changed files with 101 additions and 16 deletions

View File

@ -6,7 +6,6 @@ import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.*;
import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.drongo.psbt.PSBTInput;
import com.sparrowwallet.drongo.psbt.PSBTInputSigner;
import com.sparrowwallet.drongo.psbt.PSBTSignatureException;
import java.nio.charset.StandardCharsets;
@ -22,19 +21,39 @@ public class Bip322 {
ECKey pubKey = ECKey.fromPublicOnly(privKey);
Address address = scriptType.getAddress(pubKey);
Transaction toSpend = getBip322ToSpend(address, message);
Transaction toSign = getBip322ToSign(toSpend);
TransactionOutput utxoOutput = toSpend.getOutputs().get(0);
PSBT psbt = new PSBT(toSign);
PSBTInput psbtInput = psbt.getPsbtInputs().get(0);
psbtInput.setWitnessUtxo(utxoOutput);
psbtInput.setSigHash(SigHash.ALL);
PSBT psbt = getBip322Psbt(scriptType, address, message);
PSBTInput psbtInput = psbt.getPsbtInputs().getFirst();
psbtInput.sign(scriptType.getOutputKey(privKey));
return getBip322SignatureFromPsbt(scriptType, psbt, pubKey);
}
public static PSBT getBip322Psbt(ScriptType scriptType, Address address, String message) {
checkScriptType(scriptType);
Transaction toSpend = getBip322ToSpend(address, message);
Transaction toSign = getBip322ToSign(toSpend);
TransactionOutput utxoOutput = toSpend.getOutputs().getFirst();
PSBT psbt = new PSBT(toSign);
PSBTInput psbtInput = psbt.getPsbtInputs().getFirst();
psbtInput.setWitnessUtxo(utxoOutput);
psbtInput.setSigHash(SigHash.ALL);
return psbt;
}
public static String getBip322SignatureFromPsbt(ScriptType scriptType, PSBT signedPsbt, ECKey pubKey) {
checkScriptType(scriptType);
PSBTInput psbtInput = signedPsbt.getPsbtInputs().getFirst();
TransactionSignature signature = psbtInput.isTaproot() ? psbtInput.getTapKeyPathSignature() : psbtInput.getPartialSignature(pubKey);
if(signature == null) {
throw new IllegalArgumentException("PSBT does not contain a signature");
}
TransactionOutput utxoOutput = psbtInput.getWitnessUtxo();
Transaction finalizeTransaction = new Transaction();
TransactionInput finalizedTxInput = scriptType.addSpendingInput(finalizeTransaction, utxoOutput, pubKey, signature);
@ -74,7 +93,7 @@ public class Bip322 {
}
if(scriptType == ScriptType.P2WPKH) {
signature = witness.getSignatures().get(0);
signature = witness.getSignatures().getFirst();
if(witness.getPushes().size() <= 1) {
throw new SignatureException("BIP322 simple signature for P2WPKH script type does not contain a pubkey.");
}
@ -84,7 +103,7 @@ public class Bip322 {
throw new SignatureException("Provided address does not match pubkey in signature");
}
} else if(scriptType == ScriptType.P2TR) {
signature = witness.getSignatures().get(0);
signature = witness.getSignatures().getFirst();
pubKey = P2TR.getPublicKeyFromScript(address.getOutputScript());
} else {
throw new SignatureException(scriptType + " addresses are not supported");
@ -94,8 +113,8 @@ public class Bip322 {
Transaction toSign = getBip322ToSign(toSpend);
PSBT psbt = new PSBT(toSign);
PSBTInput psbtInput = psbt.getPsbtInputs().get(0);
psbtInput.setWitnessUtxo(toSpend.getOutputs().get(0));
PSBTInput psbtInput = psbt.getPsbtInputs().getFirst();
psbtInput.setWitnessUtxo(toSpend.getOutputs().getFirst());
psbtInput.setSigHash(SigHash.ALL);
if(scriptType == ScriptType.P2TR) {
@ -141,7 +160,7 @@ public class Bip322 {
scriptSigChunks.add(ScriptChunk.fromData(getBip322MessageHash(message)));
Script scriptSig = new Script(scriptSigChunks);
toSpend.addInput(Sha256Hash.ZERO_HASH, 0xFFFFFFFFL, scriptSig, new TransactionWitness(toSpend, Collections.emptyList()));
toSpend.getInputs().get(0).setSequenceNumber(0L);
toSpend.getInputs().getFirst().setSequenceNumber(0L);
toSpend.addOutput(0L, address.getOutputScript());
return toSpend;
@ -154,7 +173,7 @@ public class Bip322 {
TransactionWitness witness = new TransactionWitness(toSign);
toSign.addInput(toSpend.getTxId(), 0L, new Script(new byte[0]), witness);
toSign.getInputs().get(0).setSequenceNumber(0L);
toSign.getInputs().getFirst().setSequenceNumber(0L);
toSign.addOutput(0, new Script(List.of(ScriptChunk.fromOpcode(ScriptOpCodes.OP_RETURN))));
return toSign;

View File

@ -4,6 +4,9 @@ import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.address.InvalidAddressException;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.protocol.SigHash;
import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.drongo.psbt.PSBTInput;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
@ -100,6 +103,69 @@ public class Bip322Test {
Assertions.assertThrows(UnsupportedOperationException.class, () -> Bip322.verifyMessageBip322(ScriptType.P2SH_P2WPKH, address, message1, signature1));
}
@Test
public void getBip322Psbt() {
ECKey privKey = DumpedPrivateKey.fromBase58("L3VFeEujGtevx9w18HD1fhRbCH67Az2dpCymeRE1SoPK6XQtaN2k").getKey();
Address address = ScriptType.P2WPKH.getAddress(privKey);
PSBT psbt = Bip322.getBip322Psbt(ScriptType.P2WPKH, address, "Hello World");
Assertions.assertEquals(1, psbt.getPsbtInputs().size());
PSBTInput psbtInput = psbt.getPsbtInputs().get(0);
Assertions.assertNotNull(psbtInput.getWitnessUtxo());
Assertions.assertEquals(SigHash.ALL, psbtInput.getSigHash());
Assertions.assertEquals(0, psbt.getTransaction().getVersion());
}
@Test
public void getBip322PsbtTaproot() {
ECKey privKey = DumpedPrivateKey.fromBase58("L3VFeEujGtevx9w18HD1fhRbCH67Az2dpCymeRE1SoPK6XQtaN2k").getKey();
Address address = ScriptType.P2TR.getAddress(privKey);
PSBT psbt = Bip322.getBip322Psbt(ScriptType.P2TR, address, "Hello World");
Assertions.assertEquals(1, psbt.getPsbtInputs().size());
PSBTInput psbtInput = psbt.getPsbtInputs().get(0);
Assertions.assertNotNull(psbtInput.getWitnessUtxo());
Assertions.assertEquals(SigHash.ALL, psbtInput.getSigHash());
}
@Test
public void getBip322SignatureFromPsbt() {
ECKey privKey = DumpedPrivateKey.fromBase58("L3VFeEujGtevx9w18HD1fhRbCH67Az2dpCymeRE1SoPK6XQtaN2k").getKey();
ECKey pubKey = ECKey.fromPublicOnly(privKey);
Address address = ScriptType.P2WPKH.getAddress(privKey);
PSBT psbt = Bip322.getBip322Psbt(ScriptType.P2WPKH, address, "Hello World");
psbt.getPsbtInputs().get(0).sign(ScriptType.P2WPKH.getOutputKey(privKey));
String sigFromPsbt = Bip322.getBip322SignatureFromPsbt(ScriptType.P2WPKH, psbt, pubKey);
String sigDirect = Bip322.signMessageBip322(ScriptType.P2WPKH, "Hello World", privKey);
Assertions.assertEquals(sigDirect, sigFromPsbt);
}
@Test
public void getBip322SignatureFromPsbtTaproot() {
ECKey privKey = DumpedPrivateKey.fromBase58("L3VFeEujGtevx9w18HD1fhRbCH67Az2dpCymeRE1SoPK6XQtaN2k").getKey();
ECKey pubKey = ECKey.fromPublicOnly(privKey);
Address address = ScriptType.P2TR.getAddress(privKey);
PSBT psbt = Bip322.getBip322Psbt(ScriptType.P2TR, address, "Hello World");
psbt.getPsbtInputs().get(0).sign(ScriptType.P2TR.getOutputKey(privKey));
String sigFromPsbt = Bip322.getBip322SignatureFromPsbt(ScriptType.P2TR, psbt, pubKey);
String sigDirect = Bip322.signMessageBip322(ScriptType.P2TR, "Hello World", privKey);
Assertions.assertEquals(sigDirect, sigFromPsbt);
}
@Test
public void getBip322SignatureFromUnsignedPsbt() {
ECKey privKey = DumpedPrivateKey.fromBase58("L3VFeEujGtevx9w18HD1fhRbCH67Az2dpCymeRE1SoPK6XQtaN2k").getKey();
ECKey pubKey = ECKey.fromPublicOnly(privKey);
Address address = ScriptType.P2WPKH.getAddress(privKey);
PSBT psbt = Bip322.getBip322Psbt(ScriptType.P2WPKH, address, "Hello World");
Assertions.assertThrows(IllegalArgumentException.class, () -> Bip322.getBip322SignatureFromPsbt(ScriptType.P2WPKH, psbt, pubKey));
}
@Test
public void verifyMessageBip322Multisig() throws SignatureException, InvalidAddressException {
Address address = Address.fromString("bc1ppv609nr0vr25u07u95waq5lucwfm6tde4nydujnu8npg4q75mr5sxq8lt3");