diff --git a/src/main/java/com/sparrowwallet/drongo/psbt/PSBT.java b/src/main/java/com/sparrowwallet/drongo/psbt/PSBT.java index cf97b56..77ecef7 100644 --- a/src/main/java/com/sparrowwallet/drongo/psbt/PSBT.java +++ b/src/main/java/com/sparrowwallet/drongo/psbt/PSBT.java @@ -330,15 +330,16 @@ public class PSBT { int inputIndex = this.psbtInputs.size(); PSBTInput input = new PSBTInput(this, inputEntries, transaction, inputIndex); + this.psbtInputs.add(input); + } - if(verifySignatures) { + if(verifySignatures) { + for(PSBTInput input : psbtInputs) { boolean verified = input.verifySignatures(); if(!verified && input.getPartialSignatures().size() > 0) { throw new PSBTSignatureException("Unverifiable partial signatures provided"); } } - - this.psbtInputs.add(input); } } diff --git a/src/main/java/com/sparrowwallet/drongo/psbt/PSBTEntry.java b/src/main/java/com/sparrowwallet/drongo/psbt/PSBTEntry.java index cf7575b..fa4326d 100644 --- a/src/main/java/com/sparrowwallet/drongo/psbt/PSBTEntry.java +++ b/src/main/java/com/sparrowwallet/drongo/psbt/PSBTEntry.java @@ -3,6 +3,8 @@ package com.sparrowwallet.drongo.psbt; import com.sparrowwallet.drongo.KeyDerivation; import com.sparrowwallet.drongo.Utils; import com.sparrowwallet.drongo.crypto.ChildNumber; +import com.sparrowwallet.drongo.protocol.Sha256Hash; +import com.sparrowwallet.drongo.protocol.VarInt; import java.io.ByteArrayOutputStream; import java.nio.ByteBuffer; @@ -10,6 +12,7 @@ import java.nio.ByteOrder; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Map; public class PSBTEntry { private final byte[] key; @@ -55,6 +58,25 @@ public class PSBTEntry { } } + public static Map> parseTaprootKeyDerivation(byte[] data) throws PSBTParseException { + if(data.length < 1) { + throw new PSBTParseException("Invalid taproot key derivation: no bytes"); + } + VarInt varInt = new VarInt(data, 0); + int offset = varInt.getOriginalSizeInBytes(); + + if(data.length < offset + (varInt.value * 32)) { + throw new PSBTParseException("Invalid taproot key derivation: not enough bytes for leaf hashes"); + } + List leafHashes = new ArrayList<>(); + for(int i = 0; i < varInt.value; i++) { + leafHashes.add(Sha256Hash.wrap(Arrays.copyOfRange(data, offset + (i * 32), offset + (i * 32) + 32))); + } + + KeyDerivation keyDerivation = parseKeyDerivation(Arrays.copyOfRange(data, offset + (leafHashes.size() * 32), data.length)); + return Map.of(keyDerivation, leafHashes); + } + public static KeyDerivation parseKeyDerivation(byte[] data) throws PSBTParseException { if(data.length < 4) { throw new PSBTParseException("Invalid master fingerprint specified: not enough bytes"); @@ -91,6 +113,19 @@ public class PSBTEntry { return path; } + public static byte[] serializeTaprootKeyDerivation(List leafHashes, KeyDerivation keyDerivation) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + + VarInt hashesLen = new VarInt(leafHashes.size()); + baos.writeBytes(hashesLen.encode()); + for(Sha256Hash leafHash : leafHashes) { + baos.writeBytes(leafHash.getBytes()); + } + + baos.writeBytes(serializeKeyDerivation(keyDerivation)); + return baos.toByteArray(); + } + public static byte[] serializeKeyDerivation(KeyDerivation keyDerivation) { ByteArrayOutputStream baos = new ByteArrayOutputStream(); byte[] fingerprintBytes = Utils.hexToBytes(keyDerivation.getMasterFingerprint()); @@ -210,13 +245,19 @@ public class PSBTEntry { public void checkOneBytePlusXpubKey() throws PSBTParseException { if(this.getKey().length != 79) { - throw new PSBTParseException("PSBT key type must be one byte"); + throw new PSBTParseException("PSBT key type must be one byte plus xpub"); } } public void checkOneBytePlusPubKey() throws PSBTParseException { if(this.getKey().length != 34) { - throw new PSBTParseException("PSBT key type must be one byte"); + throw new PSBTParseException("PSBT key type must be one byte plus pub key"); + } + } + + public void checkOneBytePlusXOnlyPubKey() throws PSBTParseException { + if(this.getKey().length != 33) { + throw new PSBTParseException("PSBT key type must be one byte plus x only pub key"); } } } diff --git a/src/main/java/com/sparrowwallet/drongo/psbt/PSBTInput.java b/src/main/java/com/sparrowwallet/drongo/psbt/PSBTInput.java index 7b67a63..71bab98 100644 --- a/src/main/java/com/sparrowwallet/drongo/psbt/PSBTInput.java +++ b/src/main/java/com/sparrowwallet/drongo/psbt/PSBTInput.java @@ -29,6 +29,7 @@ public class PSBTInput { 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_BIP32_DERIVATION = 0x16; public static final byte PSBT_IN_TAP_INTERNAL_KEY = 0x17; private final PSBT psbt; @@ -44,6 +45,7 @@ public class PSBTInput { private String porCommitment; private final Map proprietary = new LinkedHashMap<>(); private TransactionSignature tapKeyPathSignature; + private Map>> tapDerivedPublicKeys = new LinkedHashMap<>(); private ECKey tapInternalKey; private final Transaction transaction; @@ -79,6 +81,11 @@ public class PSBTInput { this.tapInternalKey = tapInternalKey; + if(tapInternalKey != null && !derivedPublicKeys.values().isEmpty()) { + KeyDerivation tapKeyDerivation = derivedPublicKeys.values().iterator().next(); + tapDerivedPublicKeys.put(tapInternalKey, Map.of(tapKeyDerivation, Collections.emptyList())); + } + this.sigHash = getDefaultSigHash(); } @@ -121,6 +128,10 @@ public class PSBTInput { case PSBT_IN_PARTIAL_SIG: entry.checkOneBytePlusPubKey(); ECKey sigPublicKey = ECKey.fromPublicOnly(entry.getKeyData()); + if(entry.getData().length == 64 || entry.getData().length == 65) { + log.error("Schnorr signature provided as ECDSA partial signature, ignoring"); + break; + } //TODO: Verify signature TransactionSignature signature = TransactionSignature.decodeFromBitcoin(TransactionSignature.Type.ECDSA, entry.getData(), true); this.partialSignatures.put(sigPublicKey, signature); @@ -206,10 +217,23 @@ public class PSBTInput { log.debug("Found proprietary input " + Utils.bytesToHex(entry.getKeyData()) + ": " + Utils.bytesToHex(entry.getData())); break; case PSBT_IN_TAP_KEY_SIG: + entry.checkOneByteKey(); 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_BIP32_DERIVATION: + entry.checkOneBytePlusXOnlyPubKey(); + ECKey tapPublicKey = ECKey.fromPublicOnly(entry.getKeyData()); + Map> tapKeyDerivations = parseTaprootKeyDerivation(entry.getData()); + if(tapKeyDerivations.isEmpty()) { + log.warn("PSBT provided an invalid taproot key derivation"); + } else { + this.tapDerivedPublicKeys.put(tapPublicKey, tapKeyDerivations); + log.debug("Found input taproot key derivation for key " + Utils.bytesToHex(entry.getKey())); + } + break; case PSBT_IN_TAP_INTERNAL_KEY: + entry.checkOneByteKey(); this.tapInternalKey = ECKey.fromPublicOnly(entry.getData()); log.debug("Found input taproot internal key " + Utils.bytesToHex(entry.getData())); break; @@ -276,6 +300,12 @@ public class PSBTInput { entries.add(populateEntry(PSBT_IN_TAP_KEY_SIG, null, tapKeyPathSignature.encodeToBitcoin())); } + for(Map.Entry>> entry : tapDerivedPublicKeys.entrySet()) { + if(!entry.getValue().isEmpty()) { + entries.add(populateEntry(PSBT_IN_TAP_BIP32_DERIVATION, entry.getKey().getPubKeyXCoord(), serializeTaprootKeyDerivation(Collections.emptyList(), entry.getValue().keySet().iterator().next()))); + } + } + if(tapInternalKey != null) { entries.add(populateEntry(PSBT_IN_TAP_INTERNAL_KEY, null, tapInternalKey.getPubKeyXCoord())); } @@ -318,6 +348,8 @@ public class PSBTInput { tapKeyPathSignature = psbtInput.tapKeyPathSignature; } + tapDerivedPublicKeys.putAll(psbtInput.tapDerivedPublicKeys); + if(psbtInput.tapInternalKey != null) { tapInternalKey = psbtInput.tapInternalKey; } @@ -425,6 +457,14 @@ public class PSBTInput { this.tapKeyPathSignature = tapKeyPathSignature; } + public Map>> getTapDerivedPublicKeys() { + return tapDerivedPublicKeys; + } + + public void setTapDerivedPublicKeys(Map>> tapDerivedPublicKeys) { + this.tapDerivedPublicKeys = tapDerivedPublicKeys; + } + public ECKey getTapInternalKey() { return tapInternalKey; } @@ -517,7 +557,7 @@ public class PSBTInput { Sha256Hash hash = getHashForSignature(signingScript, localSigHash); if(isTaproot() && tapKeyPathSignature != null) { - ECKey outputKey = ScriptType.P2TR.getPublicKeyFromScript(getUtxo().getScript()); + ECKey outputKey = P2TR.getPublicKeyFromScript(getUtxo().getScript()); if(!outputKey.verify(hash, tapKeyPathSignature)) { throw new PSBTSignatureException("Tweaked internal key does not verify against provided taproot keypath signature"); } @@ -637,6 +677,7 @@ public class PSBTInput { witnessScript = null; porCommitment = null; proprietary.clear(); + tapDerivedPublicKeys.clear(); tapKeyPathSignature = null; } diff --git a/src/test/java/com/sparrowwallet/drongo/bip47/PaymentCodeTest.java b/src/test/java/com/sparrowwallet/drongo/bip47/PaymentCodeTest.java index 31c05a9..f8e5809 100644 --- a/src/test/java/com/sparrowwallet/drongo/bip47/PaymentCodeTest.java +++ b/src/test/java/com/sparrowwallet/drongo/bip47/PaymentCodeTest.java @@ -118,7 +118,7 @@ public class PaymentCodeTest { PaymentCode paymentCodeBob = new PaymentCode("PM8TJS2JxQ5ztXUpBBRnpTbcUXbUHy2T1abfrb3KkAAtMEGNbey4oumH7Hc578WgQJhPjBxteQ5GHHToTYHE3A1w6p7tU6KSoFmWBVbFGjKPisZDbP97"); - Wallet aliceBip47Wallet = aliceWallet.addChildWallet(paymentCodeBob, ScriptType.P2PKH); + Wallet aliceBip47Wallet = aliceWallet.addChildWallet(paymentCodeBob, ScriptType.P2PKH, "Alice"); PaymentCode paymentCodeAlice = aliceBip47Wallet.getKeystores().get(0).getPaymentCode(); Assert.assertEquals(aliceWallet.getPaymentCode(), aliceBip47Wallet.getPaymentCode()); @@ -144,7 +144,7 @@ public class PaymentCodeTest { bobWallet.getKeystores().add(Keystore.fromSeed(bobSeed, bobWallet.getScriptType().getDefaultDerivation())); bobWallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, ScriptType.P2PKH, bobWallet.getKeystores(), 1)); - Wallet bobBip47Wallet = bobWallet.addChildWallet(paymentCodeAlice, ScriptType.P2PKH); + Wallet bobBip47Wallet = bobWallet.addChildWallet(paymentCodeAlice, ScriptType.P2PKH, "Bob"); Assert.assertEquals(paymentCodeBob.toString(), bobBip47Wallet.getKeystores().get(0).getPaymentCode().toString()); Assert.assertEquals("1ChvUUvht2hUQufHBXF8NgLhW8SwE2ecGV", paymentCodeBob.getNotificationAddress().toString());