mirror of
https://github.com/sparrowwallet/drongo.git
synced 2024-11-02 18:26:43 +00:00
support taproot single key keypath spends
This commit is contained in:
parent
f1ce2ec939
commit
c71979966b
11 changed files with 368 additions and 55 deletions
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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<>();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
|
|
@ -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)));
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue