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))); + } }