implement bip322 for p2wpkh and p2tr singlesig addresses

This commit is contained in:
Craig Raw 2023-06-30 12:50:43 +02:00
parent 4341973acd
commit c0555c3fb0
3 changed files with 241 additions and 1 deletions

View file

@ -0,0 +1,122 @@
package com.sparrowwallet.drongo.crypto;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.address.Address;
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;
import java.security.SignatureException;
import java.util.*;
import static com.sparrowwallet.drongo.protocol.ScriptType.P2TR;
public class Bip322 {
public static String signMessageBip322(Address address, String message, PSBTInputSigner psbtInputSigner) {
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);
psbtInput.sign(psbtInputSigner);
ECKey pubKey = psbtInputSigner.getPubKey();
TransactionSignature signature = psbtInput.isTaproot() ? psbtInput.getTapKeyPathSignature() : psbtInput.getPartialSignature(pubKey);
Transaction finalizeTransaction = new Transaction();
TransactionInput finalizedTxInput = address.getScriptType().addSpendingInput(finalizeTransaction, utxoOutput, pubKey, signature);
return Base64.getEncoder().encodeToString(finalizedTxInput.getWitness().toByteArray());
}
public static void verifyMessageBip322(Address address, String message, String signatureBase64) throws SignatureException {
byte[] signatureEncoded;
try {
signatureEncoded = Base64.getDecoder().decode(signatureBase64);
} catch(IllegalArgumentException e) {
throw new SignatureException("Could not decode base64 signature", e);
}
TransactionWitness witness = new TransactionWitness(null, signatureEncoded, 0);
TransactionSignature signature;
ECKey pubKey;
if(address.getScriptType() == ScriptType.P2WPKH) {
signature = witness.getSignatures().get(0);
pubKey = ECKey.fromPublicOnly(witness.getPushes().get(1));
if(!address.equals(address.getScriptType().getAddress(pubKey))) {
throw new SignatureException("Provided address does not match pubkey in signature");
}
} else if(address.getScriptType() == ScriptType.P2TR) {
signature = witness.getSignatures().get(0);
pubKey = P2TR.getPublicKeyFromScript(address.getOutputScript());
} else {
throw new IllegalArgumentException(address.getScriptType() + " addresses are not supported");
}
Transaction toSpend = getBip322ToSpend(address, message);
Transaction toSign = getBip322ToSign(toSpend);
PSBT psbt = new PSBT(toSign);
PSBTInput psbtInput = psbt.getPsbtInputs().get(0);
psbtInput.setWitnessUtxo(toSpend.getOutputs().get(0));
psbtInput.setSigHash(SigHash.ALL);
if(address.getScriptType() == ScriptType.P2WPKH) {
psbtInput.getPartialSignatures().put(pubKey, signature);
} else if(address.getScriptType() == ScriptType.P2TR) {
psbtInput.setTapKeyPathSignature(signature);
}
try {
psbt.verifySignatures();
} catch(PSBTSignatureException e) {
throw new SignatureException("Signature did not match for message", e);
}
}
public static Transaction getBip322ToSpend(Address address, String message) {
Transaction toSpend = new Transaction();
toSpend.setVersion(0);
toSpend.setLocktime(0);
List<ScriptChunk> scriptSigChunks = new ArrayList<>();
scriptSigChunks.add(ScriptChunk.fromOpcode(ScriptOpCodes.OP_0));
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.addOutput(0L, address.getOutputScript());
return toSpend;
}
public static Transaction getBip322ToSign(Transaction toSpend) {
Transaction toSign = new Transaction();
toSign.setVersion(0);
toSign.setLocktime(0);
TransactionWitness witness = new TransactionWitness(toSign);
toSign.addInput(toSpend.getTxId(), 0L, new Script(new byte[0]), witness);
toSign.getInputs().get(0).setSequenceNumber(0L);
toSign.addOutput(0, new Script(List.of(ScriptChunk.fromOpcode(ScriptOpCodes.OP_RETURN))));
return toSign;
}
public static byte[] getBip322MessageHash(String message) {
if(message == null) {
throw new IllegalArgumentException("Message cannot be null");
}
return Utils.taggedHash("BIP0322-signed-message", message.getBytes(StandardCharsets.UTF_8));
}
}

View file

@ -103,7 +103,7 @@ public class TransactionSignature {
}
public static TransactionSignature decodeFromBitcoin(byte[] bytes, boolean requireCanonicalEncoding) throws SignatureDecodeException {
if(bytes.length == 64) {
if(bytes.length == 64 || bytes.length == 65) {
return decodeFromBitcoin(Type.SCHNORR, bytes, requireCanonicalEncoding);
}

View file

@ -0,0 +1,118 @@
package com.sparrowwallet.drongo.crypto;
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.Sha256Hash;
import com.sparrowwallet.drongo.protocol.SigHash;
import com.sparrowwallet.drongo.protocol.TransactionSignature;
import com.sparrowwallet.drongo.psbt.PSBTInputSigner;
import org.junit.Assert;
import org.junit.Test;
import java.security.SignatureException;
public class Bip322Test {
@Test
public void getBip322TaggedHash() {
byte[] empty = Bip322.getBip322MessageHash("");
Assert.assertArrayEquals(Utils.hexToBytes("c90c269c4f8fcbe6880f72a721ddfbf1914268a794cbb21cfafee13770ae19f1"), empty);
byte[] hello = Bip322.getBip322MessageHash("Hello World");
Assert.assertArrayEquals(Utils.hexToBytes("f0eb03b1a75ac6d9847f55c624a99169b5dccba2a31f5b23bea77ba270de0a7a"), hello);
}
@Test
public void signMessageBip322() {
ECKey privKey = DumpedPrivateKey.fromBase58("L3VFeEujGtevx9w18HD1fhRbCH67Az2dpCymeRE1SoPK6XQtaN2k").getKey();
Address address = ScriptType.P2WPKH.getAddress(privKey);
Assert.assertEquals("bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l", address.toString());
String signature = Bip322.signMessageBip322(address, "", new PSBTInputSigner() {
@Override
public TransactionSignature sign(Sha256Hash hash, SigHash sigHash, TransactionSignature.Type signatureType) {
return privKey.sign(hash, sigHash, signatureType);
}
@Override
public ECKey getPubKey() {
return ECKey.fromPublicOnly(privKey);
}
});
Assert.assertEquals("AkcwRAIgM2gBAQqvZX15ZiysmKmQpDrG83avLIT492QBzLnQIxYCIBaTpOaD20qRlEylyxFSeEA2ba9YOixpX8z46TSDtS40ASECx/EgAxlkQpQ9hYjgGu6EBCPMVPwVIVJqO4XCsMvViHI=", signature);
String signature2 = Bip322.signMessageBip322(address, "Hello World", new PSBTInputSigner() {
@Override
public TransactionSignature sign(Sha256Hash hash, SigHash sigHash, TransactionSignature.Type signatureType) {
return privKey.sign(hash, sigHash, signatureType);
}
@Override
public ECKey getPubKey() {
return ECKey.fromPublicOnly(privKey);
}
});
Assert.assertEquals("AkcwRAIgZRfIY3p7/DoVTty6YZbWS71bc5Vct9p9Fia83eRmw2QCICK/ENGfwLtptFluMGs2KsqoNSk89pO7F29zJLUx9a/sASECx/EgAxlkQpQ9hYjgGu6EBCPMVPwVIVJqO4XCsMvViHI=", signature2);
}
@Test(expected = SignatureException.class)
public void verifyMessageBip322Fail() throws InvalidAddressException, SignatureException {
Address address = Address.fromString("bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l");
String message1 = "";
String signature2 = "AkcwRAIgZRfIY3p7/DoVTty6YZbWS71bc5Vct9p9Fia83eRmw2QCICK/ENGfwLtptFluMGs2KsqoNSk89pO7F29zJLUx9a/sASECx/EgAxlkQpQ9hYjgGu6EBCPMVPwVIVJqO4XCsMvViHI=";
Bip322.verifyMessageBip322(address, message1, signature2);
}
@Test
public void verifyMessageBip322() throws InvalidAddressException, SignatureException {
Address address = Address.fromString("bc1q9vza2e8x573nczrlzms0wvx3gsqjx7vavgkx0l");
String message1 = "";
String signature1 = "AkcwRAIgM2gBAQqvZX15ZiysmKmQpDrG83avLIT492QBzLnQIxYCIBaTpOaD20qRlEylyxFSeEA2ba9YOixpX8z46TSDtS40ASECx/EgAxlkQpQ9hYjgGu6EBCPMVPwVIVJqO4XCsMvViHI=";
Bip322.verifyMessageBip322(address, message1, signature1);
String message2 = "Hello World";
String signature2 = "AkcwRAIgZRfIY3p7/DoVTty6YZbWS71bc5Vct9p9Fia83eRmw2QCICK/ENGfwLtptFluMGs2KsqoNSk89pO7F29zJLUx9a/sASECx/EgAxlkQpQ9hYjgGu6EBCPMVPwVIVJqO4XCsMvViHI=";
Bip322.verifyMessageBip322(address, message2, signature2);
String signature3 = "AkgwRQIhAOzyynlqt93lOKJr+wmmxIens//zPzl9tqIOua93wO6MAiBi5n5EyAcPScOjf1lAqIUIQtr3zKNeavYabHyR8eGhowEhAsfxIAMZZEKUPYWI4BruhAQjzFT8FSFSajuFwrDL1Yhy";
Bip322.verifyMessageBip322(address, message2, signature3);
}
@Test
public void signMessageBip322Taproot() {
ECKey privKey = DumpedPrivateKey.fromBase58("L3VFeEujGtevx9w18HD1fhRbCH67Az2dpCymeRE1SoPK6XQtaN2k").getKey();
Address address = ScriptType.P2TR.getAddress(privKey);
Assert.assertEquals("bc1ppv609nr0vr25u07u95waq5lucwfm6tde4nydujnu8npg4q75mr5sxq8lt3", address.toString());
String signature = Bip322.signMessageBip322(address, "Hello World", new PSBTInputSigner() {
@Override
public TransactionSignature sign(Sha256Hash hash, SigHash sigHash, TransactionSignature.Type signatureType) {
return address.getScriptType().getOutputKey(privKey).sign(hash, sigHash, signatureType);
}
@Override
public ECKey getPubKey() {
return ECKey.fromPublicOnly(privKey);
}
});
Assert.assertEquals("AUHd69PrJQEv+oKTfZ8l+WROBHuy9HKrbFCJu7U1iK2iiEy1vMU5EfMtjc+VSHM7aU0SDbak5IUZRVno2P5mjSafAQ==", signature);
}
@Test
public void verifyMessageBip322Taproot() throws SignatureException {
ECKey privKey = DumpedPrivateKey.fromBase58("L3VFeEujGtevx9w18HD1fhRbCH67Az2dpCymeRE1SoPK6XQtaN2k").getKey();
Address address = ScriptType.P2TR.getAddress(privKey);
Assert.assertEquals("bc1ppv609nr0vr25u07u95waq5lucwfm6tde4nydujnu8npg4q75mr5sxq8lt3", address.toString());
String message1 = "Hello World";
String signature1 = "AUHd69PrJQEv+oKTfZ8l+WROBHuy9HKrbFCJu7U1iK2iiEy1vMU5EfMtjc+VSHM7aU0SDbak5IUZRVno2P5mjSafAQ==";
Bip322.verifyMessageBip322(address, message1, signature1);
}
}