mirror of
https://github.com/sparrowwallet/drongo.git
synced 2024-12-25 09:36:44 +00:00
implement bip322 for p2wpkh and p2tr singlesig addresses
This commit is contained in:
parent
4341973acd
commit
c0555c3fb0
3 changed files with 241 additions and 1 deletions
122
src/main/java/com/sparrowwallet/drongo/crypto/Bip322.java
Normal file
122
src/main/java/com/sparrowwallet/drongo/crypto/Bip322.java
Normal 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));
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
118
src/test/java/com/sparrowwallet/drongo/crypto/Bip322Test.java
Normal file
118
src/test/java/com/sparrowwallet/drongo/crypto/Bip322Test.java
Normal 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);
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue