From 9b117cd7f90caba5e280034dee5865a1a42d82c1 Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Thu, 16 Jul 2020 14:59:01 +0200 Subject: [PATCH] programmatic psbt creation --- .../drongo/protocol/Transaction.java | 2 +- .../drongo/protocol/TransactionInput.java | 12 ++- .../com/sparrowwallet/drongo/psbt/PSBT.java | 93 ++++++++++++++++++- .../sparrowwallet/drongo/psbt/PSBTInput.java | 36 +++++-- .../sparrowwallet/drongo/psbt/PSBTOutput.java | 26 ++++-- .../sparrowwallet/drongo/wallet/Wallet.java | 47 +++++++--- .../drongo/wallet/WalletTransaction.java | 3 +- 7 files changed, 178 insertions(+), 41 deletions(-) diff --git a/src/main/java/com/sparrowwallet/drongo/protocol/Transaction.java b/src/main/java/com/sparrowwallet/drongo/protocol/Transaction.java index 972a7e6..74fcde0 100644 --- a/src/main/java/com/sparrowwallet/drongo/protocol/Transaction.java +++ b/src/main/java/com/sparrowwallet/drongo/protocol/Transaction.java @@ -413,7 +413,7 @@ public class Transaction extends ChildMessage { for (int i = 0; i < tx.inputs.size(); i++) { TransactionInput input = tx.inputs.get(i); input.clearScriptBytes(); - input.setWitness(null); + input.clearWitness(); } // This step has no purpose beyond being synchronized with Bitcoin Core's bugs. OP_CODESEPARATOR diff --git a/src/main/java/com/sparrowwallet/drongo/protocol/TransactionInput.java b/src/main/java/com/sparrowwallet/drongo/protocol/TransactionInput.java index 8938052..82f6e8a 100644 --- a/src/main/java/com/sparrowwallet/drongo/protocol/TransactionInput.java +++ b/src/main/java/com/sparrowwallet/drongo/protocol/TransactionInput.java @@ -96,10 +96,20 @@ public class TransactionInput extends ChildMessage { return witness; } - public void setWitness(TransactionWitness witness) { + void setWitness(TransactionWitness witness) { this.witness = witness; } + public void clearWitness() { + TransactionWitness witness = getWitness(); + if(witness != null) { + if(getParent() != null) { + getParent().adjustLength(-witness.getLength()); + } + setWitness(null); + } + } + public boolean hasWitness() { return witness != null && witness.getPushCount() != 0; } diff --git a/src/main/java/com/sparrowwallet/drongo/psbt/PSBT.java b/src/main/java/com/sparrowwallet/drongo/psbt/PSBT.java index ae9c259..685c6f1 100644 --- a/src/main/java/com/sparrowwallet/drongo/psbt/PSBT.java +++ b/src/main/java/com/sparrowwallet/drongo/psbt/PSBT.java @@ -3,7 +3,9 @@ package com.sparrowwallet.drongo.psbt; import com.sparrowwallet.drongo.ExtendedKey; import com.sparrowwallet.drongo.KeyDerivation; import com.sparrowwallet.drongo.Utils; +import com.sparrowwallet.drongo.crypto.ECKey; import com.sparrowwallet.drongo.protocol.*; +import com.sparrowwallet.drongo.wallet.*; import org.bouncycastle.util.encoders.Base64; import org.bouncycastle.util.encoders.Hex; import org.slf4j.Logger; @@ -39,14 +41,93 @@ public class PSBT { private Transaction transaction = null; private Integer version = null; - private Map extendedPublicKeys = new LinkedHashMap<>(); - private Map globalProprietary = new LinkedHashMap<>(); + private final Map extendedPublicKeys = new LinkedHashMap<>(); + private final Map globalProprietary = new LinkedHashMap<>(); - private List psbtInputs = new ArrayList<>(); - private List psbtOutputs = new ArrayList<>(); + private final List psbtInputs = new ArrayList<>(); + private final List psbtOutputs = new ArrayList<>(); private static final Logger log = LoggerFactory.getLogger(PSBT.class); + public PSBT(WalletTransaction walletTransaction) { + Wallet wallet = walletTransaction.getWallet(); + + transaction = new Transaction(walletTransaction.getTransaction().bitcoinSerialize()); + for(TransactionInput input : transaction.getInputs()) { + input.clearScriptBytes(); + input.clearWitness(); + } + for(Keystore keystore : walletTransaction.getWallet().getKeystores()) { + extendedPublicKeys.put(keystore.getExtendedPublicKey(), keystore.getKeyDerivation()); + } + version = 0; + + int inputIndex = 0; + for(Iterator> iter = walletTransaction.getSelectedUtxos().entrySet().iterator(); iter.hasNext(); inputIndex++) { + Map.Entry utxoEntry = iter.next(); + Transaction utxo = wallet.getTransactions().get(utxoEntry.getKey().getHash()).getTransaction(); + int utxoIndex = (int)utxoEntry.getKey().getIndex(); + TransactionOutput utxoOutput = utxo.getOutputs().get(utxoIndex); + + TransactionInput txInput = walletTransaction.getTransaction().getInputs().get(inputIndex); + + Script redeemScript = null; + if(ScriptType.P2SH.isScriptType(utxoOutput.getScript())) { + redeemScript = txInput.getScriptSig().getFirstNestedScript(); + } + + Script witnessScript = null; + if(txInput.getWitness() != null) { + witnessScript = txInput.getWitness().getWitnessScript(); + } + + Map derivedPublicKeys = new LinkedHashMap<>(); + for(Keystore keystore : wallet.getKeystores()) { + WalletNode walletNode = utxoEntry.getValue(); + derivedPublicKeys.put(keystore.getKey(walletNode.getKeyPurpose(), walletNode.getIndex()), keystore.getKeyDerivation()); + } + + PSBTInput psbtInput = new PSBTInput(wallet.getScriptType(), transaction, inputIndex, utxo, utxoIndex, redeemScript, witnessScript, derivedPublicKeys, Collections.emptyMap()); + psbtInputs.add(psbtInput); + } + + List outputNodes = new ArrayList<>(); + outputNodes.add(wallet.getWalletAddresses().getOrDefault(walletTransaction.getRecipientAddress(), null)); + outputNodes.add(walletTransaction.getChangeNode()); + + for(int outputIndex = 0; outputIndex < outputNodes.size(); outputIndex++) { + WalletNode outputNode = outputNodes.get(outputIndex); + if(outputNode == null) { + PSBTOutput externalRecipientOutput = new PSBTOutput(null, null, Collections.emptyMap(), Collections.emptyMap()); + psbtOutputs.add(externalRecipientOutput); + } else { + TransactionOutput txOutput = walletTransaction.getTransaction().getOutputs().get(outputIndex); + + //Construct dummy transaction to spend the UTXO created by this wallet's txOutput + Transaction transaction = new Transaction(); + TransactionInput spendingInput = wallet.addDummySpendingInput(transaction, outputNode, txOutput); + + Script redeemScript = null; + if(ScriptType.P2SH.isScriptType(txOutput.getScript())) { + redeemScript = spendingInput.getScriptSig().getFirstNestedScript(); + } + + Script witnessScript = null; + if(spendingInput.getWitness() != null) { + witnessScript = spendingInput.getWitness().getWitnessScript(); + } + + Map derivedPublicKeys = new LinkedHashMap<>(); + for(Keystore keystore : wallet.getKeystores()) { + derivedPublicKeys.put(keystore.getKey(outputNode.getKeyPurpose(), outputNode.getIndex()), keystore.getKeyDerivation()); + } + + PSBTOutput walletOutput = new PSBTOutput(redeemScript, witnessScript, derivedPublicKeys, Collections.emptyMap()); + psbtOutputs.add(walletOutput); + } + } + } + public PSBT(byte[] psbt) throws PSBTParseException { this.psbtBytes = psbt; parse(); @@ -390,6 +471,10 @@ public class PSBT { return new ArrayList(extendedPublicKeys.keySet()); } + public Map getGlobalProprietary() { + return globalProprietary; + } + public String toString() { try { return Hex.toHexString(serialize()); diff --git a/src/main/java/com/sparrowwallet/drongo/psbt/PSBTInput.java b/src/main/java/com/sparrowwallet/drongo/psbt/PSBTInput.java index 8f166d6..13ccd28 100644 --- a/src/main/java/com/sparrowwallet/drongo/psbt/PSBTInput.java +++ b/src/main/java/com/sparrowwallet/drongo/psbt/PSBTInput.java @@ -29,21 +29,39 @@ public class PSBTInput { private Transaction nonWitnessUtxo; private TransactionOutput witnessUtxo; - private Map partialSignatures = new LinkedHashMap<>(); + private final Map partialSignatures = new LinkedHashMap<>(); private Transaction.SigHash sigHash; private Script redeemScript; private Script witnessScript; - private Map derivedPublicKeys = new LinkedHashMap<>(); + private final Map derivedPublicKeys = new LinkedHashMap<>(); private Script finalScriptSig; private TransactionWitness finalScriptWitness; private String porCommitment; - private Map proprietary = new LinkedHashMap<>(); + private final Map proprietary = new LinkedHashMap<>(); - private Transaction transaction; - private int index; + private final Transaction transaction; + private final int index; private static final Logger log = LoggerFactory.getLogger(PSBTInput.class); + PSBTInput(ScriptType scriptType, Transaction transaction, int index, Transaction utxo, int utxoIndex, Script redeemScript, Script witnessScript, Map derivedPublicKeys, Map proprietary) { + this.transaction = transaction; + this.index = index; + sigHash = Transaction.SigHash.ALL; + + if(Arrays.asList(ScriptType.WITNESS_TYPES).contains(scriptType)) { + this.witnessUtxo = utxo.getOutputs().get(utxoIndex); + } else { + this.nonWitnessUtxo = utxo; + } + + this.redeemScript = redeemScript; + this.witnessScript = witnessScript; + + this.derivedPublicKeys.putAll(derivedPublicKeys); + this.proprietary.putAll(proprietary); + } + PSBTInput(List inputEntries, Transaction transaction, int index) throws PSBTParseException { for(PSBTEntry entry : inputEntries) { switch(entry.getKeyType()) { @@ -226,11 +244,9 @@ public class PSBTInput { } public ECKey getKeyForSignature(TransactionSignature signature) { - if(partialSignatures != null) { - for(Map.Entry entry : partialSignatures.entrySet()) { - if(entry.getValue().equals(signature)) { - return entry.getKey(); - } + for(Map.Entry entry : partialSignatures.entrySet()) { + if(entry.getValue().equals(signature)) { + return entry.getKey(); } } diff --git a/src/main/java/com/sparrowwallet/drongo/psbt/PSBTOutput.java b/src/main/java/com/sparrowwallet/drongo/psbt/PSBTOutput.java index 5a7c8da..d259432 100644 --- a/src/main/java/com/sparrowwallet/drongo/psbt/PSBTOutput.java +++ b/src/main/java/com/sparrowwallet/drongo/psbt/PSBTOutput.java @@ -2,7 +2,6 @@ package com.sparrowwallet.drongo.psbt; import com.sparrowwallet.drongo.KeyDerivation; import com.sparrowwallet.drongo.crypto.ECKey; -import com.sparrowwallet.drongo.crypto.LazyECPoint; import com.sparrowwallet.drongo.protocol.Script; import org.bouncycastle.util.encoders.Hex; import org.slf4j.Logger; @@ -12,6 +11,8 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import static com.sparrowwallet.drongo.psbt.PSBTEntry.parseKeyDerivation; + public class PSBTOutput { public static final byte PSBT_OUT_REDEEM_SCRIPT = 0x00; public static final byte PSBT_OUT_WITNESS_SCRIPT = 0x01; @@ -20,11 +21,18 @@ public class PSBTOutput { private Script redeemScript; private Script witnessScript; - private Map derivedPublicKeys = new LinkedHashMap<>(); - private Map proprietary = new LinkedHashMap<>(); + private final Map derivedPublicKeys = new LinkedHashMap<>(); + private final Map proprietary = new LinkedHashMap<>(); private static final Logger log = LoggerFactory.getLogger(PSBTOutput.class); + PSBTOutput(Script redeemScript, Script witnessScript, Map derivedPublicKeys, Map proprietary) { + this.redeemScript = redeemScript; + this.witnessScript = witnessScript; + this.derivedPublicKeys.putAll(derivedPublicKeys); + this.proprietary.putAll(proprietary); + } + PSBTOutput(List outputEntries) throws PSBTParseException { for(PSBTEntry entry : outputEntries) { switch (entry.getKeyType()) { @@ -42,10 +50,10 @@ public class PSBTOutput { break; case PSBT_OUT_BIP32_DERIVATION: entry.checkOneBytePlusPubKey(); - LazyECPoint publicKey = new LazyECPoint(ECKey.CURVE.getCurve(), entry.getKeyData()); - KeyDerivation keyDerivation = PSBTEntry.parseKeyDerivation(entry.getData()); - this.derivedPublicKeys.put(publicKey, keyDerivation); - log.debug("Found output bip32_derivation with master fingerprint " + keyDerivation.getMasterFingerprint() + " at path " + keyDerivation.getDerivationPath() + " public key " + publicKey); + ECKey derivedPublicKey = ECKey.fromPublicOnly(entry.getKeyData()); + KeyDerivation keyDerivation = parseKeyDerivation(entry.getData()); + this.derivedPublicKeys.put(derivedPublicKey, keyDerivation); + log.debug("Found output bip32_derivation with master fingerprint " + keyDerivation.getMasterFingerprint() + " at path " + keyDerivation.getDerivationPath() + " public key " + derivedPublicKey); break; case PSBT_OUT_PROPRIETARY: proprietary.put(Hex.toHexString(entry.getKeyData()), Hex.toHexString(entry.getData())); @@ -65,11 +73,11 @@ public class PSBTOutput { return witnessScript; } - public KeyDerivation getKeyDerivation(LazyECPoint publicKey) { + public KeyDerivation getKeyDerivation(ECKey publicKey) { return derivedPublicKeys.get(publicKey); } - public Map getDerivedPublicKeys() { + public Map getDerivedPublicKeys() { return derivedPublicKeys; } diff --git a/src/main/java/com/sparrowwallet/drongo/wallet/Wallet.java b/src/main/java/com/sparrowwallet/drongo/wallet/Wallet.java index 562e607..0ecbda6 100644 --- a/src/main/java/com/sparrowwallet/drongo/wallet/Wallet.java +++ b/src/main/java/com/sparrowwallet/drongo/wallet/Wallet.java @@ -11,6 +11,7 @@ import com.sparrowwallet.drongo.protocol.*; import java.util.*; import java.util.stream.Collectors; +import java.util.stream.IntStream; import static com.sparrowwallet.drongo.protocol.Transaction.WITNESS_SCALE_FACTOR; @@ -233,6 +234,23 @@ public class Wallet { } } + public boolean isWalletAddress(Address address) { + return getWalletAddresses().containsKey(address); + } + + public Map getWalletAddresses() { + Map walletAddresses = new LinkedHashMap<>(); + getWalletAddresses(walletAddresses, getNode(KeyPurpose.RECEIVE)); + getWalletAddresses(walletAddresses, getNode(KeyPurpose.CHANGE)); + return walletAddresses; + } + + private void getWalletAddresses(Map walletAddresses, WalletNode purposeNode) { + for(WalletNode addressNode : purposeNode.getChildren()) { + walletAddresses.put(getAddress(addressNode), addressNode); + } + } + public boolean isWalletTxo(BlockTransactionHashIndex txo) { return getWalletTxos().containsKey(txo); } @@ -386,20 +404,7 @@ public class Wallet { for(Map.Entry selectedUtxo : selectedUtxos.entrySet()) { Transaction prevTx = getTransactions().get(selectedUtxo.getKey().getHash()).getTransaction(); TransactionOutput prevTxOut = prevTx.getOutputs().get((int)selectedUtxo.getKey().getIndex()); - - if(getPolicyType().equals(PolicyType.SINGLE)) { - ECKey pubKey = getPubKey(selectedUtxo.getValue()); - TransactionSignature signature = TransactionSignature.dummy(); - getScriptType().addSpendingInput(transaction, prevTxOut, pubKey, signature); - } else if(getPolicyType().equals(PolicyType.MULTI)) { - List pubKeys = getPubKeys(selectedUtxo.getValue()); - int threshold = getDefaultPolicy().getNumSignaturesRequired(); - List signatures = new ArrayList<>(threshold); - for(int i = 0; i < threshold; i++) { - signatures.add(TransactionSignature.dummy()); - } - getScriptType().addMultisigSpendingInput(transaction, prevTxOut, threshold, pubKeys, signatures); - } + addDummySpendingInput(transaction, selectedUtxo.getValue(), prevTxOut); } //Add recipient output @@ -451,6 +456,20 @@ public class Wallet { } } + public TransactionInput addDummySpendingInput(Transaction transaction, WalletNode walletNode, TransactionOutput prevTxOut) { + if(getPolicyType().equals(PolicyType.SINGLE)) { + ECKey pubKey = getPubKey(walletNode); + return getScriptType().addSpendingInput(transaction, prevTxOut, pubKey, TransactionSignature.dummy()); + } else if(getPolicyType().equals(PolicyType.MULTI)) { + List pubKeys = getPubKeys(walletNode); + int threshold = getDefaultPolicy().getNumSignaturesRequired(); + List signatures = IntStream.range(0, threshold).mapToObj(i -> TransactionSignature.dummy()).collect(Collectors.toList()); + return getScriptType().addMultisigSpendingInput(transaction, prevTxOut, threshold, pubKeys, signatures); + } else { + throw new UnsupportedOperationException("Cannot create transaction for policy type " + getPolicyType()); + } + } + private Map selectInputs(List utxoSelectors, Long targetValue, double feeRate, double longTermFeeRate, boolean groupByAddress, boolean includeMempoolChange) throws InsufficientFundsException { List utxoPool = getGroupedUtxos(feeRate, longTermFeeRate, groupByAddress); diff --git a/src/main/java/com/sparrowwallet/drongo/wallet/WalletTransaction.java b/src/main/java/com/sparrowwallet/drongo/wallet/WalletTransaction.java index e33988d..1be8a4e 100644 --- a/src/main/java/com/sparrowwallet/drongo/wallet/WalletTransaction.java +++ b/src/main/java/com/sparrowwallet/drongo/wallet/WalletTransaction.java @@ -39,8 +39,7 @@ public class WalletTransaction { } public PSBT createPSBT() { - //TODO: Create PSBT - return null; + return new PSBT(this); } public Wallet getWallet() {