diff --git a/src/main/java/com/sparrowwallet/drongo/Utils.java b/src/main/java/com/sparrowwallet/drongo/Utils.java
index d45b9b3..f749d19 100644
--- a/src/main/java/com/sparrowwallet/drongo/Utils.java
+++ b/src/main/java/com/sparrowwallet/drongo/Utils.java
@@ -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 {
@Override
public int compare(byte[] left, byte[] right) {
diff --git a/src/main/java/com/sparrowwallet/drongo/crypto/ECKey.java b/src/main/java/com/sparrowwallet/drongo/crypto/ECKey.java
index 37f9900..17f19cb 100644
--- a/src/main/java/com/sparrowwallet/drongo/crypto/ECKey.java
+++ b/src/main/java/com/sparrowwallet/drongo/crypto/ECKey.java
@@ -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;
+ }
}
/**
diff --git a/src/main/java/com/sparrowwallet/drongo/crypto/SchnorrSignature.java b/src/main/java/com/sparrowwallet/drongo/crypto/SchnorrSignature.java
index 100db0c..92ddb7b 100644
--- a/src/main/java/com/sparrowwallet/drongo/crypto/SchnorrSignature.java
+++ b/src/main/java/com/sparrowwallet/drongo/crypto/SchnorrSignature.java
@@ -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);
+ }
}
diff --git a/src/main/java/com/sparrowwallet/drongo/protocol/ScriptType.java b/src/main/java/com/sparrowwallet/drongo/protocol/ScriptType.java
index c44e70d..3d8da30 100644
--- a/src/main/java/com/sparrowwallet/drongo/protocol/ScriptType.java
+++ b/src/main/java/com/sparrowwallet/drongo/protocol/ScriptType.java
@@ -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);
diff --git a/src/main/java/com/sparrowwallet/drongo/protocol/Transaction.java b/src/main/java/com/sparrowwallet/drongo/protocol/Transaction.java
index 59f24e0..fc86c24 100644
--- a/src/main/java/com/sparrowwallet/drongo/protocol/Transaction.java
+++ b/src/main/java/com/sparrowwallet/drongo/protocol/Transaction.java
@@ -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());
}
+
+ /**
+ * 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.
+ *
+ * (See BIP341: https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki)
+ *
+ * @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 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 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);
+ }
}
diff --git a/src/main/java/com/sparrowwallet/drongo/protocol/TransactionSignature.java b/src/main/java/com/sparrowwallet/drongo/protocol/TransactionSignature.java
index 3e9fb1f..0ac8613 100644
--- a/src/main/java/com/sparrowwallet/drongo/protocol/TransactionSignature.java
+++ b/src/main/java/com/sparrowwallet/drongo/protocol/TransactionSignature.java
@@ -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) {
diff --git a/src/main/java/com/sparrowwallet/drongo/protocol/TransactionWitness.java b/src/main/java/com/sparrowwallet/drongo/protocol/TransactionWitness.java
index 34be7d6..f5cad4d 100644
--- a/src/main/java/com/sparrowwallet/drongo/protocol/TransactionWitness.java
+++ b/src/main/java/com/sparrowwallet/drongo/protocol/TransactionWitness.java
@@ -14,6 +14,12 @@ import java.util.List;
public class TransactionWitness extends ChildMessage {
private List 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<>();
diff --git a/src/main/java/com/sparrowwallet/drongo/psbt/PSBT.java b/src/main/java/com/sparrowwallet/drongo/psbt/PSBT.java
index 3e15391..ac69162 100644
--- a/src/main/java/com/sparrowwallet/drongo/psbt/PSBT.java
+++ b/src/main/java/com/sparrowwallet/drongo/psbt/PSBT.java
@@ -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 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;
}
}
diff --git a/src/main/java/com/sparrowwallet/drongo/psbt/PSBTInput.java b/src/main/java/com/sparrowwallet/drongo/psbt/PSBTInput.java
index 6d6e1cb..3421912 100644
--- a/src/main/java/com/sparrowwallet/drongo/psbt/PSBTInput.java
+++ b/src/main/java/com/sparrowwallet/drongo/psbt/PSBTInput.java
@@ -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 partialSignatures = new LinkedHashMap<>();
@@ -38,20 +43,22 @@ public class PSBTInput {
private TransactionWitness finalScriptWitness;
private String porCommitment;
private final Map 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 derivedPublicKeys, Map 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 derivedPublicKeys, Map 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 inputEntries, Transaction transaction, int index) throws PSBTParseException {
+ PSBTInput(PSBT psbt, List 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 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 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 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 {
diff --git a/src/main/java/com/sparrowwallet/drongo/wallet/Wallet.java b/src/main/java/com/sparrowwallet/drongo/wallet/Wallet.java
index 4dbb451..fb68f38 100644
--- a/src/main/java/com/sparrowwallet/drongo/wallet/Wallet.java
+++ b/src/main/java/com/sparrowwallet/drongo/wallet/Wallet.java
@@ -795,7 +795,7 @@ public class Wallet extends Persistable {
for(TransactionInput txInput : signingNodes.keySet()) {
WalletNode walletNode = signingNodes.get(txInput);
- Map keystoreKeysForNode = getKeystores().stream().collect(Collectors.toMap(keystore -> keystore.getPubKey(walletNode), Function.identity(),
+ Map 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 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 keystoreKeysForNode = getKeystores().stream().collect(Collectors.toMap(keystore -> keystore.getPubKey(walletNode), Function.identity(),
+ Map 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 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 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");
}
diff --git a/src/test/java/com/sparrowwallet/drongo/protocol/TransactionTest.java b/src/test/java/com/sparrowwallet/drongo/protocol/TransactionTest.java
index 3bce9ec..dcc266e 100644
--- a/src/test/java/com/sparrowwallet/drongo/protocol/TransactionTest.java
+++ b/src/test/java/com/sparrowwallet/drongo/protocol/TransactionTest.java
@@ -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)));
+ }
}