diff --git a/src/main/java/com/sparrowwallet/drongo/crypto/Bip322.java b/src/main/java/com/sparrowwallet/drongo/crypto/Bip322.java index 700e158..be14b38 100644 --- a/src/main/java/com/sparrowwallet/drongo/crypto/Bip322.java +++ b/src/main/java/com/sparrowwallet/drongo/crypto/Bip322.java @@ -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; diff --git a/src/test/java/com/sparrowwallet/drongo/crypto/Bip322Test.java b/src/test/java/com/sparrowwallet/drongo/crypto/Bip322Test.java index fabffce..fea4f95 100644 --- a/src/test/java/com/sparrowwallet/drongo/crypto/Bip322Test.java +++ b/src/test/java/com/sparrowwallet/drongo/crypto/Bip322Test.java @@ -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");