support taproot single key keypath spends

This commit is contained in:
Craig Raw 2021-07-14 15:12:18 +02:00
parent f1ce2ec939
commit c71979966b
11 changed files with 368 additions and 55 deletions

View file

@ -12,6 +12,8 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.*;
public class Utils {
@ -289,6 +291,16 @@ public class Utils {
return out;
}
public static byte[] taggedHash(String tag, byte[] msg) {
byte[] hash = Sha256Hash.hash(tag.getBytes(StandardCharsets.UTF_8));
ByteBuffer buffer = ByteBuffer.allocate(hash.length + hash.length + msg.length);
buffer.put(hash);
buffer.put(hash);
buffer.put(msg);
return Sha256Hash.hash(buffer.array());
}
public static class LexicographicByteArrayComparator implements Comparator<byte[]> {
@Override
public int compare(byte[] left, byte[] right) {

View file

@ -26,7 +26,6 @@ import org.slf4j.LoggerFactory;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.security.SignatureException;
@ -369,7 +368,7 @@ public class ECKey {
}
try {
byte[] sigBytes = NativeSecp256k1.schnorrSign(input.getBytes(), priv.toByteArray(), null);
byte[] sigBytes = NativeSecp256k1.schnorrSign(input.getBytes(), Utils.bigIntegerToBytes(priv, 32), new byte[32]);
return SchnorrSignature.decode(sigBytes);
} catch(NativeSecp256k1Util.AssertFailException e) {
log.error("Error signing schnorr", e);
@ -393,13 +392,28 @@ public class ECKey {
}
public ECKey getTweakedOutputKey() {
ECPoint internalKey = liftX(getPubKeyXCoord());
byte[] taggedHash = taggedHash("TapTweak", internalKey.getXCoord().getEncoded());
ECPoint outputKey = internalKey.add(ECKey.fromPrivate(taggedHash).getPubKeyPoint());
TaprootPubKey taprootPubKey = liftX(getPubKeyXCoord());
ECPoint internalKey = taprootPubKey.ecPoint;
byte[] taggedHash = Utils.taggedHash("TapTweak", internalKey.getXCoord().getEncoded());
ECKey tweakValue = ECKey.fromPrivate(taggedHash);
ECPoint outputKey = internalKey.add(tweakValue.getPubKeyPoint());
if(hasPrivKey()) {
BigInteger taprootPriv = priv;
BigInteger tweakedPrivKey = taprootPriv.add(tweakValue.getPrivKey()).mod(CURVE_PARAMS.getCurve().getOrder());
//TODO: Improve on this hack. How do we know whether to negate the private key before tweaking it?
if(!ECKey.fromPrivate(tweakedPrivKey).getPubKeyPoint().equals(outputKey)) {
taprootPriv = CURVE_PARAMS.getCurve().getOrder().subtract(priv);
tweakedPrivKey = taprootPriv.add(tweakValue.getPrivKey()).mod(CURVE_PARAMS.getCurve().getOrder());
}
return new ECKey(tweakedPrivKey, outputKey, true);
}
return ECKey.fromPublicOnly(outputKey, true);
}
private static ECPoint liftX(byte[] bytes) {
private static TaprootPubKey liftX(byte[] bytes) {
SecP256K1Curve secP256K1Curve = (SecP256K1Curve)CURVE_PARAMS.getCurve();
BigInteger x = new BigInteger(1, bytes);
BigInteger p = secP256K1Curve.getQ();
@ -413,17 +427,17 @@ public class ECKey {
throw new IllegalStateException("Calculated invalid y_sq when solving for y co-ordinate");
}
return secP256K1Curve.createPoint(x, y.and(BigInteger.ONE).equals(BigInteger.ZERO) ? y : p.subtract(y));
return y.and(BigInteger.ONE).equals(BigInteger.ZERO) ? new TaprootPubKey(secP256K1Curve.createPoint(x, y), false) : new TaprootPubKey(secP256K1Curve.createPoint(x, p.subtract(y)), true);
}
private static byte[] taggedHash(String tag, byte[] msg) {
byte[] hash = Sha256Hash.hash(tag.getBytes(StandardCharsets.UTF_8));
ByteBuffer buffer = ByteBuffer.allocate(hash.length + hash.length + msg.length);
buffer.put(hash);
buffer.put(hash);
buffer.put(msg);
private static class TaprootPubKey {
public final ECPoint ecPoint;
public final boolean negated;
return Sha256Hash.hash(buffer.array());
public TaprootPubKey(ECPoint ecPoint, boolean negated) {
this.ecPoint = ecPoint;
this.negated = negated;
}
}
/**

View file

@ -11,6 +11,7 @@ import org.slf4j.LoggerFactory;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.Objects;
/**
* Groups the two components that make up a Schnorr signature
@ -77,4 +78,21 @@ public class SchnorrSignature {
return false;
}
@Override
public boolean equals(Object o) {
if(this == o) {
return true;
}
if(o == null || getClass() != o.getClass()) {
return false;
}
SchnorrSignature that = (SchnorrSignature) o;
return r.equals(that.r) && s.equals(that.s);
}
@Override
public int hashCode() {
return Objects.hash(r, s);
}
}

View file

@ -1016,6 +1016,11 @@ public enum ScriptType {
}
},
P2TR("P2TR", "Taproot (P2TR)", "m/86'/0'/0'") {
@Override
public ECKey getOutputKey(ECKey derivedKey) {
return derivedKey.getTweakedOutputKey();
}
@Override
public Address getAddress(byte[] pubKey) {
return new P2TRAddress(pubKey);
@ -1023,7 +1028,7 @@ public enum ScriptType {
@Override
public Address getAddress(ECKey derivedKey) {
return getAddress(derivedKey.getTweakedOutputKey().getPubKeyXCoord());
return getAddress(getOutputKey(derivedKey).getPubKeyXCoord());
}
@Override
@ -1042,7 +1047,7 @@ public enum ScriptType {
@Override
public Script getOutputScript(ECKey derivedKey) {
return getOutputScript(derivedKey.getTweakedOutputKey().getPubKeyXCoord());
return getOutputScript(getOutputKey(derivedKey).getPubKeyXCoord());
}
@Override
@ -1105,7 +1110,9 @@ public enum ScriptType {
@Override
public TransactionInput addSpendingInput(Transaction transaction, TransactionOutput prevOutput, ECKey pubKey, TransactionSignature signature) {
throw new UnsupportedOperationException("Constructing Taproot inputs is not yet supported");
Script scriptSig = getScriptSig(prevOutput.getScript(), pubKey, signature);
TransactionWitness witness = new TransactionWitness(transaction, signature);
return transaction.addInput(prevOutput.getHash(), prevOutput.getIndex(), scriptSig, witness);
}
@Override
@ -1186,6 +1193,10 @@ public enum ScriptType {
return getAllowedPolicyTypes().contains(policyType);
}
public ECKey getOutputKey(ECKey derivedKey) {
return derivedKey;
}
public abstract Address getAddress(byte[] bytes);
public abstract Address getAddress(ECKey key);

View file

@ -31,6 +31,8 @@ public class Transaction extends ChildMessage {
//Default min feerate, defined in sats/vByte
public static final double DEFAULT_MIN_RELAY_FEE = 1d;
public static final byte LEAF_VERSION_TAPSCRIPT = (byte)0xc0;
private long version;
private long locktime;
private boolean segwit;
@ -609,4 +611,123 @@ public class Transaction extends ChildMessage {
return Sha256Hash.twiceOf(bos.toByteArray());
}
/**
* <p>Calculates a signature hash, that is, a hash of a simplified form of the transaction. How exactly the transaction
* is simplified is specified by the type and anyoneCanPay parameters.</p>
*
* (See BIP341: https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki)</p>
*
* @param spentUtxos the ordered list of spent UTXOs corresponding to the inputs of this transaction
* @param inputIndex input the signature is being calculated for. Tx signatures are always relative to an input.
* @param scriptPath whether we are signing for the keypath or the scriptpath
* @param script if signing for the scriptpath, the script to sign
* @param sigHash should usually be SigHash.ALL
* @param annex annex data
*/
public synchronized Sha256Hash hashForTaprootSignature(List<TransactionOutput> spentUtxos, int inputIndex, boolean scriptPath, Script script, SigHash sigHash, byte[] annex) {
return hashForTaprootSignature(spentUtxos, inputIndex, scriptPath, script, sigHash.value, annex);
}
public synchronized Sha256Hash hashForTaprootSignature(List<TransactionOutput> spentUtxos, int inputIndex, boolean scriptPath, Script script, byte sigHashType, byte[] annex) {
if(spentUtxos.size() != getInputs().size()) {
throw new IllegalArgumentException("Provided spent UTXOs length does not equal the number of transaction inputs");
}
if(inputIndex >= getInputs().size()) {
throw new IllegalArgumentException("Input index is greater than the number of transaction inputs");
}
ByteArrayOutputStream bos = new UnsafeByteArrayOutputStream(length == UNKNOWN_LENGTH ? 256 : length + 4);
try {
byte outType = sigHashType == 0x00 ? SigHash.ALL.value : (byte)(sigHashType & 0x03);
boolean anyoneCanPay = (sigHashType & SigHash.ANYONECANPAY.value) == SigHash.ANYONECANPAY.value;
bos.write(0x00);
bos.write(sigHashType);
uint32ToByteStreamLE(this.version, bos);
uint32ToByteStreamLE(this.locktime, bos);
if(!anyoneCanPay) {
ByteArrayOutputStream outpoints = new ByteArrayOutputStream();
ByteArrayOutputStream outputValues = new ByteArrayOutputStream();
ByteArrayOutputStream outputScriptPubKeys = new ByteArrayOutputStream();
ByteArrayOutputStream inputSequences = new ByteArrayOutputStream();
for(int i = 0; i < getInputs().size(); i++) {
TransactionInput input = getInputs().get(i);
input.getOutpoint().bitcoinSerializeToStream(outpoints);
Utils.uint64ToByteStreamLE(BigInteger.valueOf(spentUtxos.get(i).getValue()), outputValues);
byteArraySerialize(spentUtxos.get(i).getScriptBytes(), outputScriptPubKeys);
Utils.uint32ToByteStreamLE(input.getSequenceNumber(), inputSequences);
}
bos.write(Sha256Hash.hash(outpoints.toByteArray()));
bos.write(Sha256Hash.hash(outputValues.toByteArray()));
bos.write(Sha256Hash.hash(outputScriptPubKeys.toByteArray()));
bos.write(Sha256Hash.hash(inputSequences.toByteArray()));
}
if(outType == SigHash.ALL.value) {
ByteArrayOutputStream outputs = new ByteArrayOutputStream();
for(TransactionOutput output : getOutputs()) {
output.bitcoinSerializeToStream(outputs);
}
bos.write(Sha256Hash.hash(outputs.toByteArray()));
}
byte spendType = 0x00;
if(annex != null) {
spendType |= 0x01;
}
if(scriptPath) {
spendType |= 0x02;
}
bos.write(spendType);
if(anyoneCanPay) {
getInputs().get(inputIndex).getOutpoint().bitcoinSerializeToStream(bos);
Utils.uint32ToByteStreamLE(spentUtxos.get(inputIndex).getValue(), bos);
byteArraySerialize(spentUtxos.get(inputIndex).getScriptBytes(), bos);
Utils.uint32ToByteStreamLE(getInputs().get(inputIndex).getSequenceNumber(), bos);
} else {
Utils.uint32ToByteStreamLE(inputIndex, bos);
}
if((spendType & 0x01) != 0) {
ByteArrayOutputStream annexStream = new ByteArrayOutputStream();
byteArraySerialize(annex, annexStream);
bos.write(Sha256Hash.hash(annexStream.toByteArray()));
}
if(outType == SigHash.SINGLE.value) {
if(inputIndex < getOutputs().size()) {
bos.write(Sha256Hash.hash(getOutputs().get(inputIndex).bitcoinSerialize()));
} else {
bos.write(Sha256Hash.ZERO_HASH.getBytes());
}
}
if(scriptPath) {
ByteArrayOutputStream leafStream = new ByteArrayOutputStream();
leafStream.write(LEAF_VERSION_TAPSCRIPT);
byteArraySerialize(script.getProgram(), leafStream);
bos.write(Utils.taggedHash("TapLeaf", leafStream.toByteArray()));
bos.write(0x00);
Utils.uint32ToByteStreamLE(-1, bos);
}
byte[] msgBytes = bos.toByteArray();
long requiredLength = 175 - (anyoneCanPay ? 49 : 0) - (outType != SigHash.ALL.value && outType != SigHash.SINGLE.value ? 32 : 0) + (annex != null ? 32 : 0) + (scriptPath ? 37 : 0);
if(msgBytes.length != requiredLength) {
throw new IllegalStateException("Invalid message length, was " + msgBytes.length + " not " + requiredLength);
}
return Sha256Hash.wrap(Utils.taggedHash("TapSighash", msgBytes));
} catch (IOException e) {
throw new RuntimeException(e); // Cannot happen.
}
}
private void byteArraySerialize(byte[] bytes, OutputStream outputStream) throws IOException {
outputStream.write(new VarInt(bytes.length).encode());
outputStream.write(bytes);
}
}

View file

@ -24,7 +24,7 @@ public class TransactionSignature {
/** Constructs a signature with the given components of the given type and SIGHASH_ALL. */
public TransactionSignature(BigInteger r, BigInteger s, Type type) {
this(r, s, type, SigHash.ALL.value);
this(r, s, type, type == Type.ECDSA ? SigHash.ALL.value : SigHash.ALL_TAPROOT.value);
}
/** Constructs a transaction signature based on the ECDSA signature. */
@ -59,7 +59,7 @@ public class TransactionSignature {
return (sighashFlags & SigHash.ANYONECANPAY.value) != 0;
}
public SigHash getSigHash() {
private SigHash getSigHash() {
boolean anyoneCanPay = anyoneCanPay();
final int mode = sighashFlags & 0x1f;
if (mode == SigHash.NONE.value) {
@ -86,7 +86,7 @@ public class TransactionSignature {
throw new RuntimeException(e); // Cannot happen.
}
} else if(schnorrSignature != null) {
SigHash sigHash = getSigHash();
SigHash sigHash = getSigHash(); //Note this will return Sighash.ALL for Sighash.ALL_TAPROOT as well
ByteBuffer buffer = ByteBuffer.allocate(sigHash == SigHash.ALL ? 64 : 65);
buffer.put(schnorrSignature.encode());
if(sigHash != SigHash.ALL) {

View file

@ -14,6 +14,12 @@ import java.util.List;
public class TransactionWitness extends ChildMessage {
private List<byte[]> pushes;
public TransactionWitness(Transaction transaction, TransactionSignature signature) {
setParent(transaction);
this.pushes = new ArrayList<>();
pushes.add(signature.encodeToBitcoin());
}
public TransactionWitness(Transaction transaction, ECKey pubKey, TransactionSignature signature) {
setParent(transaction);
this.pushes = new ArrayList<>();

View file

@ -52,7 +52,7 @@ public class PSBT {
this.transaction = transaction;
for(int i = 0; i < transaction.getInputs().size(); i++) {
psbtInputs.add(new PSBTInput(transaction, i));
psbtInputs.add(new PSBTInput(this, transaction, i));
}
for(int i = 0; i < transaction.getOutputs().size(); i++) {
@ -111,12 +111,18 @@ public class PSBT {
}
Map<ECKey, KeyDerivation> derivedPublicKeys = new LinkedHashMap<>();
ECKey tapInternalKey = null;
for(Keystore keystore : wallet.getKeystores()) {
WalletNode walletNode = utxoEntry.getValue();
derivedPublicKeys.put(keystore.getPubKey(walletNode), keystore.getKeyDerivation().extend(walletNode.getDerivation()));
derivedPublicKeys.put(wallet.getScriptType().getOutputKey(keystore.getPubKey(walletNode)), keystore.getKeyDerivation().extend(walletNode.getDerivation()));
//TODO: Implement Musig for multisig wallets
if(wallet.getScriptType() == ScriptType.P2TR) {
tapInternalKey = keystore.getPubKey(walletNode);
}
}
PSBTInput psbtInput = new PSBTInput(wallet.getScriptType(), transaction, inputIndex, utxo, utxoIndex, redeemScript, witnessScript, derivedPublicKeys, Collections.emptyMap(), alwaysIncludeWitnessUtxo);
PSBTInput psbtInput = new PSBTInput(this, wallet.getScriptType(), transaction, inputIndex, utxo, utxoIndex, redeemScript, witnessScript, derivedPublicKeys, Collections.emptyMap(), tapInternalKey, alwaysIncludeWitnessUtxo);
psbtInputs.add(psbtInput);
}
@ -327,7 +333,7 @@ public class PSBT {
}
int inputIndex = this.psbtInputs.size();
PSBTInput input = new PSBTInput(inputEntries, transaction, inputIndex);
PSBTInput input = new PSBTInput(this, inputEntries, transaction, inputIndex);
if(verifySignatures) {
boolean verified = input.verifySignatures();
@ -400,7 +406,7 @@ public class PSBT {
public boolean hasSignatures() {
for(PSBTInput psbtInput : getPsbtInputs()) {
if(!psbtInput.getPartialSignatures().isEmpty() || psbtInput.getFinalScriptSig() != null || psbtInput.getFinalScriptWitness() != null) {
if(!psbtInput.getPartialSignatures().isEmpty() || psbtInput.getTapKeyPathSignature() != null || psbtInput.getFinalScriptSig() != null || psbtInput.getFinalScriptWitness() != null) {
return true;
}
}

View file

@ -4,12 +4,14 @@ import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.crypto.ECDSASignature;
import com.sparrowwallet.drongo.crypto.ECKey;
import com.sparrowwallet.drongo.crypto.SchnorrSignature;
import com.sparrowwallet.drongo.protocol.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.stream.Collectors;
import static com.sparrowwallet.drongo.protocol.ScriptType.*;
import static com.sparrowwallet.drongo.psbt.PSBTEntry.*;
@ -26,7 +28,10 @@ public class PSBTInput {
public static final byte PSBT_IN_FINAL_SCRIPTWITNESS = 0x08;
public static final byte PSBT_IN_POR_COMMITMENT = 0x09;
public static final byte PSBT_IN_PROPRIETARY = (byte)0xfc;
public static final byte PSBT_IN_TAP_KEY_SIG = 0x13;
public static final byte PSBT_IN_TAP_INTERNAL_KEY = 0x17;
private final PSBT psbt;
private Transaction nonWitnessUtxo;
private TransactionOutput witnessUtxo;
private final Map<ECKey, TransactionSignature> partialSignatures = new LinkedHashMap<>();
@ -38,20 +43,22 @@ public class PSBTInput {
private TransactionWitness finalScriptWitness;
private String porCommitment;
private final Map<String, String> proprietary = new LinkedHashMap<>();
private TransactionSignature tapKeyPathSignature;
private ECKey tapInternalKey;
private final Transaction transaction;
private final int index;
private static final Logger log = LoggerFactory.getLogger(PSBTInput.class);
PSBTInput(Transaction transaction, int index) {
PSBTInput(PSBT psbt, Transaction transaction, int index) {
this.psbt = psbt;
this.transaction = transaction;
this.index = index;
}
PSBTInput(ScriptType scriptType, Transaction transaction, int index, Transaction utxo, int utxoIndex, Script redeemScript, Script witnessScript, Map<ECKey, KeyDerivation> derivedPublicKeys, Map<String, String> proprietary, boolean alwaysAddNonWitnessTx) {
this(transaction, index);
sigHash = SigHash.ALL;
PSBTInput(PSBT psbt, ScriptType scriptType, Transaction transaction, int index, Transaction utxo, int utxoIndex, Script redeemScript, Script witnessScript, Map<ECKey, KeyDerivation> derivedPublicKeys, Map<String, String> proprietary, ECKey tapInternalKey, boolean alwaysAddNonWitnessTx) {
this(psbt, transaction, index);
if(Arrays.asList(ScriptType.WITNESS_TYPES).contains(scriptType)) {
this.witnessUtxo = utxo.getOutputs().get(utxoIndex);
@ -69,9 +76,14 @@ public class PSBTInput {
this.derivedPublicKeys.putAll(derivedPublicKeys);
this.proprietary.putAll(proprietary);
this.tapInternalKey = tapInternalKey;
this.sigHash = getDefaultSigHash();
}
PSBTInput(List<PSBTEntry> inputEntries, Transaction transaction, int index) throws PSBTParseException {
PSBTInput(PSBT psbt, List<PSBTEntry> inputEntries, Transaction transaction, int index) throws PSBTParseException {
this.psbt = psbt;
for(PSBTEntry entry : inputEntries) {
switch(entry.getKeyType()) {
case PSBT_IN_NON_WITNESS_UTXO:
@ -100,7 +112,7 @@ public class PSBTInput {
case PSBT_IN_WITNESS_UTXO:
entry.checkOneByteKey();
TransactionOutput witnessTxOutput = new TransactionOutput(null, entry.getData(), 0);
if(!P2SH.isScriptType(witnessTxOutput.getScript()) && !P2WPKH.isScriptType(witnessTxOutput.getScript()) && !P2WSH.isScriptType(witnessTxOutput.getScript())) {
if(!P2SH.isScriptType(witnessTxOutput.getScript()) && !P2WPKH.isScriptType(witnessTxOutput.getScript()) && !P2WSH.isScriptType(witnessTxOutput.getScript()) && !P2TR.isScriptType(witnessTxOutput.getScript())) {
throw new PSBTParseException("Witness UTXO provided for non-witness or unknown input");
}
this.witnessUtxo = witnessTxOutput;
@ -197,6 +209,14 @@ public class PSBTInput {
this.proprietary.put(Utils.bytesToHex(entry.getKeyData()), Utils.bytesToHex(entry.getData()));
log.debug("Found proprietary input " + Utils.bytesToHex(entry.getKeyData()) + ": " + Utils.bytesToHex(entry.getData()));
break;
case PSBT_IN_TAP_KEY_SIG:
this.tapKeyPathSignature = TransactionSignature.decodeFromBitcoin(TransactionSignature.Type.SCHNORR, entry.getData(), true);
log.debug("Found input taproot key path signature " + Utils.bytesToHex(entry.getData()));
break;
case PSBT_IN_TAP_INTERNAL_KEY:
this.tapInternalKey = ECKey.fromPublicOnly(entry.getData());
log.debug("Found input taproot internal key " + Utils.bytesToHex(entry.getData()));
break;
default:
log.warn("PSBT input not recognized key type: " + entry.getKeyType());
}
@ -210,7 +230,8 @@ public class PSBTInput {
List<PSBTEntry> entries = new ArrayList<>();
if(nonWitnessUtxo != null) {
entries.add(populateEntry(PSBT_IN_NON_WITNESS_UTXO, null, nonWitnessUtxo.bitcoinSerialize()));
//Serialize all nonWitnessUtxo fields without witness data (pre-Segwit serialization) to reduce PSBT size
entries.add(populateEntry(PSBT_IN_NON_WITNESS_UTXO, null, nonWitnessUtxo.bitcoinSerialize(false)));
}
if(witnessUtxo != null) {
@ -255,6 +276,14 @@ public class PSBTInput {
entries.add(populateEntry(PSBT_IN_PROPRIETARY, Utils.hexToBytes(entry.getKey()), Utils.hexToBytes(entry.getValue())));
}
if(tapKeyPathSignature != null) {
entries.add(populateEntry(PSBT_IN_TAP_KEY_SIG, null, tapKeyPathSignature.encodeToBitcoin()));
}
if(tapInternalKey != null) {
entries.add(populateEntry(PSBT_IN_TAP_INTERNAL_KEY, null, tapInternalKey.getPubKeyXCoord()));
}
return entries;
}
@ -288,6 +317,14 @@ public class PSBTInput {
}
proprietary.putAll(psbtInput.proprietary);
if(psbtInput.tapKeyPathSignature != null) {
tapKeyPathSignature = psbtInput.tapKeyPathSignature;
}
if(psbtInput.tapInternalKey != null) {
tapInternalKey = psbtInput.tapInternalKey;
}
}
public Transaction getNonWitnessUtxo() {
@ -384,8 +421,30 @@ public class PSBTInput {
return proprietary;
}
public TransactionSignature getTapKeyPathSignature() {
return tapKeyPathSignature;
}
public void setTapKeyPathSignature(TransactionSignature tapKeyPathSignature) {
this.tapKeyPathSignature = tapKeyPathSignature;
}
public ECKey getTapInternalKey() {
return tapInternalKey;
}
public void setTapInternalKey(ECKey tapInternalKey) {
this.tapInternalKey = tapInternalKey;
}
public boolean isTaproot() {
return getScriptType() == P2TR;
}
public boolean isSigned() {
if(!getPartialSignatures().isEmpty()) {
if(getTapKeyPathSignature() != null) {
return true;
} else if(!getPartialSignatures().isEmpty()) {
try {
//All partial sigs are already verified
int reqSigs = getSigningScript().getNumRequiredSignatures();
@ -404,29 +463,46 @@ public class PSBTInput {
return getFinalScriptWitness().getSignatures();
} else if(getFinalScriptSig() != null) {
return getFinalScriptSig().getSignatures();
} else if(getTapKeyPathSignature() != null) {
return List.of(getTapKeyPathSignature());
} else {
return getPartialSignatures().values();
}
}
private SigHash getDefaultSigHash() {
if(isTaproot()) {
return SigHash.ALL_TAPROOT;
}
return SigHash.ALL;
}
public boolean sign(ECKey privKey) {
SigHash localSigHash = getSigHash();
if(localSigHash == null) {
//Assume SigHash.ALL
localSigHash = SigHash.ALL;
localSigHash = getDefaultSigHash();
}
if(getNonWitnessUtxo() != null || getWitnessUtxo() != null) {
Script signingScript = getSigningScript();
if(signingScript != null) {
Sha256Hash hash = getHashForSignature(signingScript, localSigHash);
ECDSASignature ecdsaSignature = privKey.signEcdsa(hash);
TransactionSignature transactionSignature = new TransactionSignature(ecdsaSignature, localSigHash);
ECKey pubKey = ECKey.fromPublicOnly(privKey);
getPartialSignatures().put(pubKey, transactionSignature);
if(isTaproot()) {
SchnorrSignature schnorrSignature = privKey.signSchnorr(hash);
tapKeyPathSignature = new TransactionSignature(schnorrSignature, localSigHash);
return true;
return true;
} else {
ECDSASignature ecdsaSignature = privKey.signEcdsa(hash);
TransactionSignature transactionSignature = new TransactionSignature(ecdsaSignature, localSigHash);
ECKey pubKey = ECKey.fromPublicOnly(privKey);
getPartialSignatures().put(pubKey, transactionSignature);
return true;
}
}
}
@ -436,8 +512,7 @@ public class PSBTInput {
boolean verifySignatures() throws PSBTSignatureException {
SigHash localSigHash = getSigHash();
if(localSigHash == null) {
//Assume SigHash.ALL
localSigHash = SigHash.ALL;
localSigHash = getDefaultSigHash();
}
if(getNonWitnessUtxo() != null || getWitnessUtxo() != null) {
@ -445,10 +520,17 @@ public class PSBTInput {
if(signingScript != null) {
Sha256Hash hash = getHashForSignature(signingScript, localSigHash);
for(ECKey sigPublicKey : getPartialSignatures().keySet()) {
TransactionSignature signature = getPartialSignature(sigPublicKey);
if(!sigPublicKey.verify(hash, signature)) {
throw new PSBTSignatureException("Partial signature does not verify against provided public key");
if(isTaproot() && tapKeyPathSignature != null) {
ECKey outputKey = ScriptType.P2TR.getPublicKeyFromScript(getUtxo().getScript());
if(!outputKey.verify(hash, tapKeyPathSignature)) {
throw new PSBTSignatureException("Tweaked internal key does not verify against provided taproot keypath signature");
}
} else {
for(ECKey sigPublicKey : getPartialSignatures().keySet()) {
TransactionSignature signature = getPartialSignature(sigPublicKey);
if(!sigPublicKey.verify(hash, signature)) {
throw new PSBTSignatureException("Partial signature does not verify against provided public key");
}
}
}
@ -467,7 +549,7 @@ public class PSBTInput {
Map<ECKey, TransactionSignature> signingKeys = new LinkedHashMap<>();
if(signingScript != null) {
Sha256Hash hash = getHashForSignature(signingScript, getSigHash() == null ? SigHash.ALL : getSigHash());
Sha256Hash hash = getHashForSignature(signingScript, getSigHash() == null ? getDefaultSigHash() : getSigHash());
for(ECKey sigPublicKey : availableKeys) {
for(TransactionSignature signature : signatures) {
@ -531,6 +613,11 @@ public class PSBTInput {
}
}
if(P2TR.isScriptType(signingScript)) {
//For now, only support keypath spends and just return the ScriptPubKey
//In future return the script from PSBT_IN_TAP_LEAF_SCRIPT
}
return signingScript;
}
@ -554,13 +641,17 @@ public class PSBTInput {
witnessScript = null;
porCommitment = null;
proprietary.clear();
tapKeyPathSignature = null;
}
private Sha256Hash getHashForSignature(Script connectedScript, SigHash localSigHash) {
Sha256Hash hash;
ScriptType scriptType = getScriptType();
if(Arrays.asList(WITNESS_TYPES).contains(scriptType)) {
if(scriptType == ScriptType.P2TR) {
List<TransactionOutput> spentUtxos = psbt.getPsbtInputs().stream().map(PSBTInput::getUtxo).collect(Collectors.toList());
hash = transaction.hashForTaprootSignature(spentUtxos, index, !P2TR.isScriptType(connectedScript), connectedScript, localSigHash, null);
} else if(Arrays.asList(WITNESS_TYPES).contains(scriptType)) {
long prevValue = getUtxo().getValue();
hash = transaction.hashForWitnessSignature(index, connectedScript, prevValue, localSigHash);
} else {

View file

@ -795,7 +795,7 @@ public class Wallet extends Persistable {
for(TransactionInput txInput : signingNodes.keySet()) {
WalletNode walletNode = signingNodes.get(txInput);
Map<ECKey, Keystore> keystoreKeysForNode = getKeystores().stream().collect(Collectors.toMap(keystore -> keystore.getPubKey(walletNode), Function.identity(),
Map<ECKey, Keystore> keystoreKeysForNode = getKeystores().stream().collect(Collectors.toMap(keystore -> getScriptType().getOutputKey(keystore.getPubKey(walletNode)), Function.identity(),
(u, v) -> { throw new IllegalStateException("Duplicate keys from different keystores for node " + walletNode.getDerivationPath()); },
LinkedHashMap::new));
@ -806,7 +806,15 @@ public class Wallet extends Persistable {
TransactionOutput spentTxo = blockTransaction.getTransaction().getOutputs().get((int)txInput.getOutpoint().getIndex());
Script signingScript = getSigningScript(txInput, spentTxo);
Sha256Hash hash = txInput.hasWitness() ? transaction.hashForWitnessSignature(txInput.getIndex(), signingScript, spentTxo.getValue(), SigHash.ALL) : transaction.hashForLegacySignature(txInput.getIndex(), signingScript, SigHash.ALL);
Sha256Hash hash;
if(getScriptType() == P2TR) {
List<TransactionOutput> spentOutputs = transaction.getInputs().stream().map(input -> transactions.get(input.getOutpoint().getHash()).getTransaction().getOutputs().get((int)input.getOutpoint().getIndex())).collect(Collectors.toList());
hash = transaction.hashForTaprootSignature(spentOutputs, txInput.getIndex(), !P2TR.isScriptType(signingScript), signingScript, SigHash.ALL_TAPROOT, null);
} else if(txInput.hasWitness()) {
hash = transaction.hashForWitnessSignature(txInput.getIndex(), signingScript, spentTxo.getValue(), SigHash.ALL);
} else {
hash = transaction.hashForLegacySignature(txInput.getIndex(), signingScript, SigHash.ALL);
}
for(ECKey sigPublicKey : keystoreKeysForNode.keySet()) {
for(TransactionSignature signature : txInput.hasWitness() ? txInput.getWitness().getSignatures() : txInput.getScriptSig().getSignatures()) {
@ -887,12 +895,12 @@ public class Wallet extends Persistable {
for(PSBTInput psbtInput : signingNodes.keySet()) {
WalletNode walletNode = signingNodes.get(psbtInput);
Map<ECKey, Keystore> keystoreKeysForNode = getKeystores().stream().collect(Collectors.toMap(keystore -> keystore.getPubKey(walletNode), Function.identity(),
Map<ECKey, Keystore> keystoreKeysForNode = getKeystores().stream().collect(Collectors.toMap(keystore -> getScriptType().getOutputKey(keystore.getPubKey(walletNode)), Function.identity(),
(u, v) -> { throw new IllegalStateException("Duplicate keys from different keystores for node " + walletNode.getDerivationPath()); },
LinkedHashMap::new));
Map<ECKey, TransactionSignature> keySignatureMap;
if(psbt.isFinalized()) {
if(psbt.isFinalized() || psbtInput.isTaproot()) {
keySignatureMap = psbtInput.getSigningKeys(keystoreKeysForNode.keySet());
} else {
keySignatureMap = psbtInput.getPartialSignatures();
@ -916,7 +924,7 @@ public class Wallet extends Persistable {
for(Keystore keystore : getKeystores()) {
if(keystore.hasPrivateKey()) {
for(Map.Entry<PSBTInput, WalletNode> signingEntry : signingNodes.entrySet()) {
ECKey privKey = keystore.getKey(signingEntry.getValue());
ECKey privKey = getScriptType().getOutputKey(keystore.getKey(signingEntry.getValue()));
PSBTInput psbtInput = signingEntry.getKey();
if(!psbtInput.isSigned()) {
@ -954,13 +962,15 @@ public class Wallet extends Persistable {
}
};
if(psbtInput.getPartialSignatures().size() >= threshold && signingNode != null) {
//TODO: Handle taproot scriptpath spending
int signaturesAvailable = psbtInput.isTaproot() ? (psbtInput.getTapKeyPathSignature() != null ? 1 : 0) : psbtInput.getPartialSignatures().size();
if(signaturesAvailable >= threshold && signingNode != null) {
Transaction transaction = new Transaction();
TransactionInput finalizedTxInput;
if(getPolicyType().equals(PolicyType.SINGLE)) {
ECKey pubKey = getPubKey(signingNode);
TransactionSignature transactionSignature = psbtInput.getPartialSignature(pubKey);
TransactionSignature transactionSignature = psbtInput.isTaproot() ? psbtInput.getTapKeyPathSignature() : psbtInput.getPartialSignature(pubKey);
if(transactionSignature == null) {
throw new IllegalArgumentException("Pubkey of partial signature does not match wallet pubkey");
}

View file

@ -4,6 +4,7 @@ import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.address.InvalidAddressException;
import com.sparrowwallet.drongo.crypto.ECKey;
import com.sparrowwallet.drongo.crypto.SchnorrSignature;
import org.junit.Assert;
import org.junit.Test;
@ -479,4 +480,27 @@ public class TransactionTest {
Assert.assertEquals(spendingHex, constructedHex);
}
@Test
public void signBip340() {
ECKey privKey = ECKey.fromPrivate(Utils.hexToBytes("0000000000000000000000000000000000000000000000000000000000000003"));
Assert.assertEquals("F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9", Utils.bytesToHex(privKey.getPubKeyXCoord()).toUpperCase());
SchnorrSignature sig = privKey.signSchnorr(Sha256Hash.ZERO_HASH);
Assert.assertEquals("E907831F80848D1069A5371B402410364BDF1C5F8307B0084C55F1CE2DCA821525F66A4A85EA8B71E482A74F382D2CE5EBEEE8FDB2172F477DF4900D310536C0", Utils.bytesToHex(sig.encode()).toUpperCase());
}
@Test
public void signTaprootKeypath() {
Transaction tx = new Transaction(Utils.hexToBytes("02000000000101786ed355f998b98f8ef8ef2acf461577325cf170a9133d48a17aba957eb97ff00000000000ffffffff0100e1f50500000000220020693a94699e6e41ab302fd623a9bf5a5b2d6606cbfb35c550d1cb4300451356a102473044022004cc317c20eb9e372cb0e640f51eb2b8311616125321b11dbaa5671db5a3ca2a02207ae3d2771b565be98ae56e21045b9629c94b6ca8f4e3932260e54d4f0e2016b30121032da1692a41a61ad14f3795b31d33431abf8d6ee161b997d004c26a37bc20083500000000"));
Transaction spendingTx = new Transaction(Utils.hexToBytes("01000000011af4dca4a6bc6da092edca5390355891da9bbe76d2be1c04d067ec9c3a3d22b10000000000000000000180f0fa0200000000160014a3bcb5f272025cc66dc42e7518a5846bd60a9c9600000000"));
Sha256Hash hash = spendingTx.hashForTaprootSignature(tx.getOutputs(), 0, false, null, SigHash.ALL_TAPROOT, null);
ECKey privateKey = ECKey.fromPrivate(Utils.hexToBytes("d9bc817b92916a24b87d25dc48ef466b4fcd6c89cf90afbc17cba40eb8b91330"));
SchnorrSignature sig = privateKey.signSchnorr(hash);
Assert.assertEquals("7b04f59bc8f5c2c33c9b8acbf94743de74cc25a6052b52ff61a516f7c5ca19cc68345ba99b354f22bfaf5c04de395b9223f3bf0a5c351fc1cc68c224f4e5b202", Utils.bytesToHex(sig.encode()));
ECKey pubKey = ECKey.fromPublicOnly(privateKey);
Assert.assertTrue(pubKey.verify(hash, new TransactionSignature(sig, SigHash.ALL_TAPROOT)));
}
}