add psbt taproot output fields, jade wallet

This commit is contained in:
Craig Raw 2022-05-04 14:32:24 +02:00
parent 759ebb975c
commit 9cb95f2f8c
6 changed files with 119 additions and 15 deletions

View file

@ -148,10 +148,11 @@ public class PSBT {
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());
PSBTOutput externalRecipientOutput = new PSBTOutput(null, null, null, Collections.emptyMap(), Collections.emptyMap(), null);
psbtOutputs.add(externalRecipientOutput);
} else {
TransactionOutput txOutput = transaction.getOutputs().get(outputIndex);
Wallet recipientWallet = outputNode.getWallet();
//Construct dummy transaction to spend the UTXO created by this wallet's txOutput
Transaction transaction = new Transaction();
@ -168,11 +169,17 @@ public class PSBT {
}
Map<ECKey, KeyDerivation> derivedPublicKeys = new LinkedHashMap<>();
for(Keystore keystore : outputNode.getWallet().getKeystores()) {
derivedPublicKeys.put(keystore.getPubKey(outputNode), keystore.getKeyDerivation().extend(outputNode.getDerivation()));
ECKey tapInternalKey = null;
for(Keystore keystore : recipientWallet.getKeystores()) {
derivedPublicKeys.put(recipientWallet.getScriptType().getOutputKey(keystore.getPubKey(outputNode)), keystore.getKeyDerivation().extend(outputNode.getDerivation()));
//TODO: Implement Musig for multisig wallets
if(recipientWallet.getScriptType() == ScriptType.P2TR) {
tapInternalKey = keystore.getPubKey(outputNode);
}
}
PSBTOutput walletOutput = new PSBTOutput(redeemScript, witnessScript, derivedPublicKeys, Collections.emptyMap());
PSBTOutput walletOutput = new PSBTOutput(recipientWallet.getScriptType(), redeemScript, witnessScript, derivedPublicKeys, Collections.emptyMap(), tapInternalKey);
psbtOutputs.add(walletOutput);
}
}
@ -476,7 +483,8 @@ public class PSBT {
for(PSBTInput psbtInput : getPsbtInputs()) {
List<PSBTEntry> inputEntries = psbtInput.getInputEntries();
for(PSBTEntry entry : inputEntries) {
if(includeXpubs || entry.getKeyType() != PSBT_IN_BIP32_DERIVATION) {
if(includeXpubs || (entry.getKeyType() != PSBT_IN_BIP32_DERIVATION && entry.getKeyType() != PSBT_IN_PROPRIETARY
&& entry.getKeyType() != PSBT_IN_TAP_INTERNAL_KEY && entry.getKeyType() != PSBT_IN_TAP_BIP32_DERIVATION)) {
entry.serializeToStream(baos);
}
}
@ -486,7 +494,9 @@ public class PSBT {
for(PSBTOutput psbtOutput : getPsbtOutputs()) {
List<PSBTEntry> outputEntries = psbtOutput.getOutputEntries();
for(PSBTEntry entry : outputEntries) {
if(includeXpubs || (entry.getKeyType() != PSBT_OUT_REDEEM_SCRIPT && entry.getKeyType() != PSBT_OUT_WITNESS_SCRIPT && entry.getKeyType() != PSBT_OUT_BIP32_DERIVATION && entry.getKeyType() != PSBT_OUT_PROPRIETARY)) {
if(includeXpubs || (entry.getKeyType() != PSBT_OUT_REDEEM_SCRIPT && entry.getKeyType() != PSBT_OUT_WITNESS_SCRIPT
&& entry.getKeyType() != PSBT_OUT_BIP32_DERIVATION && entry.getKeyType() != PSBT_OUT_PROPRIETARY
&& entry.getKeyType() != PSBT_OUT_TAP_INTERNAL_KEY && entry.getKeyType() != PSBT_OUT_TAP_BIP32_DERIVATION)) {
entry.serializeToStream(baos);
}
}

View file

@ -229,10 +229,12 @@ public class PSBTInput {
ECKey tapPublicKey = ECKey.fromPublicOnly(entry.getKeyData());
Map<KeyDerivation, List<Sha256Hash>> tapKeyDerivations = parseTaprootKeyDerivation(entry.getData());
if(tapKeyDerivations.isEmpty()) {
log.warn("PSBT provided an invalid taproot key derivation");
log.warn("PSBT provided an invalid input taproot key derivation");
} else {
this.tapDerivedPublicKeys.put(tapPublicKey, tapKeyDerivations);
log.debug("Found input taproot key derivation for key " + Utils.bytesToHex(entry.getKey()));
for(KeyDerivation tapKeyDerivation : tapKeyDerivations.keySet()) {
log.debug("Found input taproot key derivation for key " + Utils.bytesToHex(entry.getKeyData()) + " with master fingerprint " + tapKeyDerivation.getMasterFingerprint() + " at path " + tapKeyDerivation.getDerivationPath());
}
}
break;
case PSBT_IN_TAP_INTERNAL_KEY:

View file

@ -4,26 +4,30 @@ import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.crypto.ECKey;
import com.sparrowwallet.drongo.protocol.Script;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.protocol.Sha256Hash;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.*;
import static com.sparrowwallet.drongo.protocol.ScriptType.*;
import static com.sparrowwallet.drongo.psbt.PSBTEntry.*;
public class PSBTOutput {
public static final byte PSBT_OUT_REDEEM_SCRIPT = 0x00;
public static final byte PSBT_OUT_WITNESS_SCRIPT = 0x01;
public static final byte PSBT_OUT_BIP32_DERIVATION = 0x02;
public static final byte PSBT_OUT_TAP_INTERNAL_KEY = 0x05;
public static final byte PSBT_OUT_TAP_BIP32_DERIVATION = 0x07;
public static final byte PSBT_OUT_PROPRIETARY = (byte)0xfc;
private Script redeemScript;
private Script witnessScript;
private final Map<ECKey, KeyDerivation> derivedPublicKeys = new LinkedHashMap<>();
private final Map<String, String> proprietary = new LinkedHashMap<>();
private Map<ECKey, Map<KeyDerivation, List<Sha256Hash>>> tapDerivedPublicKeys = new LinkedHashMap<>();
private ECKey tapInternalKey;
private static final Logger log = LoggerFactory.getLogger(PSBTOutput.class);
@ -31,11 +35,22 @@ public class PSBTOutput {
//empty constructor
}
PSBTOutput(Script redeemScript, Script witnessScript, Map<ECKey, KeyDerivation> derivedPublicKeys, Map<String, String> proprietary) {
PSBTOutput(ScriptType scriptType, Script redeemScript, Script witnessScript, Map<ECKey, KeyDerivation> derivedPublicKeys, Map<String, String> proprietary, ECKey tapInternalKey) {
this.redeemScript = redeemScript;
this.witnessScript = witnessScript;
this.derivedPublicKeys.putAll(derivedPublicKeys);
if(scriptType != P2TR) {
this.derivedPublicKeys.putAll(derivedPublicKeys);
}
this.proprietary.putAll(proprietary);
this.tapInternalKey = tapInternalKey;
if(tapInternalKey != null && !derivedPublicKeys.values().isEmpty()) {
KeyDerivation tapKeyDerivation = derivedPublicKeys.values().iterator().next();
tapDerivedPublicKeys.put(tapInternalKey, Map.of(tapKeyDerivation, Collections.emptyList()));
}
}
PSBTOutput(List<PSBTEntry> outputEntries) throws PSBTParseException {
@ -64,6 +79,24 @@ public class PSBTOutput {
proprietary.put(Utils.bytesToHex(entry.getKeyData()), Utils.bytesToHex(entry.getData()));
log.debug("Found proprietary output " + Utils.bytesToHex(entry.getKeyData()) + ": " + Utils.bytesToHex(entry.getData()));
break;
case PSBT_OUT_TAP_INTERNAL_KEY:
entry.checkOneByteKey();
this.tapInternalKey = ECKey.fromPublicOnly(entry.getData());
log.debug("Found output taproot internal key " + Utils.bytesToHex(entry.getData()));
break;
case PSBT_OUT_TAP_BIP32_DERIVATION:
entry.checkOneBytePlusXOnlyPubKey();
ECKey tapPublicKey = ECKey.fromPublicOnly(entry.getKeyData());
Map<KeyDerivation, List<Sha256Hash>> tapKeyDerivations = parseTaprootKeyDerivation(entry.getData());
if(tapKeyDerivations.isEmpty()) {
log.warn("PSBT provided an invalid output taproot key derivation");
} else {
this.tapDerivedPublicKeys.put(tapPublicKey, tapKeyDerivations);
for(KeyDerivation tapKeyDerivation : tapKeyDerivations.keySet()) {
log.debug("Found output taproot key derivation for key " + Utils.bytesToHex(entry.getKeyData()) + " with master fingerprint " + tapKeyDerivation.getMasterFingerprint() + " at path " + tapKeyDerivation.getDerivationPath());
}
}
break;
default:
log.warn("PSBT output not recognized key type: " + entry.getKeyType());
}
@ -89,6 +122,16 @@ public class PSBTOutput {
entries.add(populateEntry(PSBT_OUT_PROPRIETARY, Utils.hexToBytes(entry.getKey()), Utils.hexToBytes(entry.getValue())));
}
for(Map.Entry<ECKey, Map<KeyDerivation, List<Sha256Hash>>> entry : tapDerivedPublicKeys.entrySet()) {
if(!entry.getValue().isEmpty()) {
entries.add(populateEntry(PSBT_OUT_TAP_BIP32_DERIVATION, entry.getKey().getPubKeyXCoord(), serializeTaprootKeyDerivation(Collections.emptyList(), entry.getValue().keySet().iterator().next())));
}
}
if(tapInternalKey != null) {
entries.add(populateEntry(PSBT_OUT_TAP_INTERNAL_KEY, null, tapInternalKey.getPubKeyXCoord()));
}
return entries;
}
@ -103,6 +146,12 @@ public class PSBTOutput {
derivedPublicKeys.putAll(psbtOutput.derivedPublicKeys);
proprietary.putAll(psbtOutput.proprietary);
tapDerivedPublicKeys.putAll(psbtOutput.tapDerivedPublicKeys);
if(psbtOutput.tapInternalKey != null) {
tapInternalKey = psbtOutput.tapInternalKey;
}
}
public Script getRedeemScript() {
@ -132,4 +181,24 @@ public class PSBTOutput {
public Map<String, String> getProprietary() {
return proprietary;
}
public Map<ECKey, Map<KeyDerivation, List<Sha256Hash>>> getTapDerivedPublicKeys() {
return tapDerivedPublicKeys;
}
public void setTapDerivedPublicKeys(Map<ECKey, Map<KeyDerivation, List<Sha256Hash>>> tapDerivedPublicKeys) {
this.tapDerivedPublicKeys = tapDerivedPublicKeys;
}
public ECKey getTapInternalKey() {
return tapInternalKey;
}
public void setTapInternalKey(ECKey tapInternalKey) {
this.tapInternalKey = tapInternalKey;
}
public void clearNonFinalFields() {
tapDerivedPublicKeys.clear();
}
}

View file

@ -12,6 +12,7 @@ import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.*;
import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.drongo.psbt.PSBTInput;
import com.sparrowwallet.drongo.psbt.PSBTOutput;
import java.nio.charset.StandardCharsets;
import java.util.*;
@ -1557,6 +1558,8 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
psbtInput.clearNonFinalFields();
}
}
psbt.getPsbtOutputs().forEach(PSBTOutput::clearNonFinalFields);
}
public BitcoinUnit getAutoUnit() {

View file

@ -1,7 +1,7 @@
package com.sparrowwallet.drongo.wallet;
public enum WalletModel {
SEED, SPARROW, BITCOIN_CORE, ELECTRUM, TREZOR_1, TREZOR_T, COLDCARD, LEDGER_NANO_S, LEDGER_NANO_X, DIGITALBITBOX_01, KEEPKEY, SPECTER_DESKTOP, COBO_VAULT, BITBOX_02, SPECTER_DIY, PASSPORT, BLUE_WALLET, KEYSTONE, SEEDSIGNER, CARAVAN, GORDIAN_SEED_TOOL;
SEED, SPARROW, BITCOIN_CORE, ELECTRUM, TREZOR_1, TREZOR_T, COLDCARD, LEDGER_NANO_S, LEDGER_NANO_X, DIGITALBITBOX_01, KEEPKEY, SPECTER_DESKTOP, COBO_VAULT, BITBOX_02, SPECTER_DIY, PASSPORT, BLUE_WALLET, KEYSTONE, SEEDSIGNER, CARAVAN, GORDIAN_SEED_TOOL, JADE;
public static WalletModel getModel(String model) {
return valueOf(model.toUpperCase());

View file

@ -11,6 +11,9 @@ import org.junit.After;
import org.junit.Assert;
import org.junit.Test;
import java.util.List;
import java.util.Map;
public class PSBTTest {
@Test(expected = PSBTParseException.class)
@ -379,6 +382,23 @@ public class PSBTTest {
Assert.assertFalse(PSBT.isPSBT("x"));
}
@Test
public void testTaproot() throws PSBTParseException {
Network.set(Network.TESTNET);
String strSignedPsbt = "cHNidP8BAH0CAAAAAdPUSBYKaQKOqAMgU2IcGuM6z7JtbkLe69OmYZoa4UxYAAAAAADmdQAAAp4CAAAAAAAAFgAUg+/zyGWbtKbIJb8ZasbGYDtItZhgrgEAAAAAACJRIJMQKWeQsI/WiRS+lmeeyeJaKFVnlVoBtXeuGTO7XIbrAAAAAE8BBDWHzwM27GqkgAAAAO1YZge2AP67ozhdoF9wgg2hpJw1jbVEXLKfxRQUNGEIAlog/wK83w7jxD37prhWrPenLxjGAJzkJKrj6h0ZPK8WED/ZeB1WAACAAQAAgAAAAIAAAQCJAgAAAAGBuBuu5OIheS4SKtYJufScCJDTWWLopBoXtFWhPDuRWQEAAAAA/f///wKNsQEAAAAAACJRIAjXFSnHwcD+J/obgd9CxVneHsUyFKM9xU7NY5K3DgCxHwIAAAAAAAAiUSAhUe66hJMCB1esHqXtxRVJHmviQ4ZjzFIwDWDPk+1KY6VyIQABASuNsQEAAAAAACJRIAjXFSnHwcD+J/obgd9CxVneHsUyFKM9xU7NY5K3DgCxAQMEAAAAAAETQCG/ZGuefjDVqBhmgVEuV1HbdxoZKDDWWTvTUrq6MJreRzj22k/WcFni6yPn9PGZkptZSNx9waf8ouP28ogJz24hFnRZPMvN82XI+lnim7dRKwFgpHnqiDoMGnIoFoSQRX7bGQA/2XgdVgAAgAEAAIAAAACAAQAAAAIAAAABFyB0WTzLzfNlyPpZ4pu3USsBYKR56og6DBpyKBaEkEV+2wAAIQflpK6JYWolG3K2DU7FU3hlkwSdU/69bglhZxaprqSvyxkAP9l4HVYAAIABAACAAAAAgAEAAAADAAAAAQUg5aSuiWFqJRtytg1OxVN4ZZMEnVP+vW4JYWcWqa6kr8sA";
PSBT psbt = PSBT.fromString(strSignedPsbt);
Assert.assertEquals(0, psbt.getPsbtInputs().get(0).getDerivedPublicKeys().size());
Assert.assertEquals("74593ccbcdf365c8fa59e29bb7512b0160a479ea883a0c1a7228168490457edb", Utils.bytesToHex(psbt.getPsbtInputs().get(0).getTapDerivedPublicKeys().keySet().iterator().next().getPubKeyXCoord()));
Map<KeyDerivation, List<Sha256Hash>> tapInKeyDerivations = psbt.getPsbtInputs().get(0).getTapDerivedPublicKeys().values().iterator().next();
Assert.assertEquals("3fd9781d", tapInKeyDerivations.keySet().iterator().next().getMasterFingerprint());
Assert.assertEquals(0, psbt.getPsbtOutputs().get(0).getDerivedPublicKeys().size());
Assert.assertEquals("e5a4ae89616a251b72b60d4ec553786593049d53febd6e09616716a9aea4afcb", Utils.bytesToHex(psbt.getPsbtOutputs().get(1).getTapDerivedPublicKeys().keySet().iterator().next().getPubKeyXCoord()));
Map<KeyDerivation, List<Sha256Hash>> tapOutKeyDerivations = psbt.getPsbtOutputs().get(1).getTapDerivedPublicKeys().values().iterator().next();
Assert.assertEquals("3fd9781d", tapOutKeyDerivations.keySet().iterator().next().getMasterFingerprint());
}
@After
public void tearDown() throws Exception {
Network.set(null);