support psbt taproot bip32 derivation field, fix taproot signature verification

This commit is contained in:
Craig Raw 2022-03-29 11:49:49 +02:00
parent 74e32bab3d
commit d1088fe9ee
4 changed files with 91 additions and 8 deletions

View file

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

View file

@ -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<KeyDerivation, List<Sha256Hash>> 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<Sha256Hash> 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<Sha256Hash> 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");
}
}
}

View file

@ -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<String, String> proprietary = new LinkedHashMap<>();
private TransactionSignature tapKeyPathSignature;
private Map<ECKey, Map<KeyDerivation, List<Sha256Hash>>> 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<KeyDerivation, List<Sha256Hash>> 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<ECKey, Map<KeyDerivation, List<Sha256Hash>>> 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<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;
}
@ -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;
}

View file

@ -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());