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(); int inputIndex = this.psbtInputs.size();
PSBTInput input = new PSBTInput(this, inputEntries, transaction, inputIndex); PSBTInput input = new PSBTInput(this, inputEntries, transaction, inputIndex);
this.psbtInputs.add(input);
}
if(verifySignatures) { if(verifySignatures) {
for(PSBTInput input : psbtInputs) {
boolean verified = input.verifySignatures(); boolean verified = input.verifySignatures();
if(!verified && input.getPartialSignatures().size() > 0) { if(!verified && input.getPartialSignatures().size() > 0) {
throw new PSBTSignatureException("Unverifiable partial signatures provided"); 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.KeyDerivation;
import com.sparrowwallet.drongo.Utils; import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.crypto.ChildNumber; import com.sparrowwallet.drongo.crypto.ChildNumber;
import com.sparrowwallet.drongo.protocol.Sha256Hash;
import com.sparrowwallet.drongo.protocol.VarInt;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
@ -10,6 +12,7 @@ import java.nio.ByteOrder;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Map;
public class PSBTEntry { public class PSBTEntry {
private final byte[] key; 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 { public static KeyDerivation parseKeyDerivation(byte[] data) throws PSBTParseException {
if(data.length < 4) { if(data.length < 4) {
throw new PSBTParseException("Invalid master fingerprint specified: not enough bytes"); throw new PSBTParseException("Invalid master fingerprint specified: not enough bytes");
@ -91,6 +113,19 @@ public class PSBTEntry {
return path; 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) { public static byte[] serializeKeyDerivation(KeyDerivation keyDerivation) {
ByteArrayOutputStream baos = new ByteArrayOutputStream(); ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] fingerprintBytes = Utils.hexToBytes(keyDerivation.getMasterFingerprint()); byte[] fingerprintBytes = Utils.hexToBytes(keyDerivation.getMasterFingerprint());
@ -210,13 +245,19 @@ public class PSBTEntry {
public void checkOneBytePlusXpubKey() throws PSBTParseException { public void checkOneBytePlusXpubKey() throws PSBTParseException {
if(this.getKey().length != 79) { 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 { public void checkOneBytePlusPubKey() throws PSBTParseException {
if(this.getKey().length != 34) { 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_POR_COMMITMENT = 0x09;
public static final byte PSBT_IN_PROPRIETARY = (byte)0xfc; 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_KEY_SIG = 0x13;
public static final byte PSBT_IN_TAP_BIP32_DERIVATION = 0x16;
public static final byte PSBT_IN_TAP_INTERNAL_KEY = 0x17; public static final byte PSBT_IN_TAP_INTERNAL_KEY = 0x17;
private final PSBT psbt; private final PSBT psbt;
@ -44,6 +45,7 @@ public class PSBTInput {
private String porCommitment; private String porCommitment;
private final Map<String, String> proprietary = new LinkedHashMap<>(); private final Map<String, String> proprietary = new LinkedHashMap<>();
private TransactionSignature tapKeyPathSignature; private TransactionSignature tapKeyPathSignature;
private Map<ECKey, Map<KeyDerivation, List<Sha256Hash>>> tapDerivedPublicKeys = new LinkedHashMap<>();
private ECKey tapInternalKey; private ECKey tapInternalKey;
private final Transaction transaction; private final Transaction transaction;
@ -79,6 +81,11 @@ public class PSBTInput {
this.tapInternalKey = tapInternalKey; 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(); this.sigHash = getDefaultSigHash();
} }
@ -121,6 +128,10 @@ public class PSBTInput {
case PSBT_IN_PARTIAL_SIG: case PSBT_IN_PARTIAL_SIG:
entry.checkOneBytePlusPubKey(); entry.checkOneBytePlusPubKey();
ECKey sigPublicKey = ECKey.fromPublicOnly(entry.getKeyData()); 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 //TODO: Verify signature
TransactionSignature signature = TransactionSignature.decodeFromBitcoin(TransactionSignature.Type.ECDSA, entry.getData(), true); TransactionSignature signature = TransactionSignature.decodeFromBitcoin(TransactionSignature.Type.ECDSA, entry.getData(), true);
this.partialSignatures.put(sigPublicKey, signature); 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())); log.debug("Found proprietary input " + Utils.bytesToHex(entry.getKeyData()) + ": " + Utils.bytesToHex(entry.getData()));
break; break;
case PSBT_IN_TAP_KEY_SIG: case PSBT_IN_TAP_KEY_SIG:
entry.checkOneByteKey();
this.tapKeyPathSignature = TransactionSignature.decodeFromBitcoin(TransactionSignature.Type.SCHNORR, entry.getData(), true); this.tapKeyPathSignature = TransactionSignature.decodeFromBitcoin(TransactionSignature.Type.SCHNORR, entry.getData(), true);
log.debug("Found input taproot key path signature " + Utils.bytesToHex(entry.getData())); log.debug("Found input taproot key path signature " + Utils.bytesToHex(entry.getData()));
break; 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: case PSBT_IN_TAP_INTERNAL_KEY:
entry.checkOneByteKey();
this.tapInternalKey = ECKey.fromPublicOnly(entry.getData()); this.tapInternalKey = ECKey.fromPublicOnly(entry.getData());
log.debug("Found input taproot internal key " + Utils.bytesToHex(entry.getData())); log.debug("Found input taproot internal key " + Utils.bytesToHex(entry.getData()));
break; break;
@ -276,6 +300,12 @@ public class PSBTInput {
entries.add(populateEntry(PSBT_IN_TAP_KEY_SIG, null, tapKeyPathSignature.encodeToBitcoin())); 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) { if(tapInternalKey != null) {
entries.add(populateEntry(PSBT_IN_TAP_INTERNAL_KEY, null, tapInternalKey.getPubKeyXCoord())); entries.add(populateEntry(PSBT_IN_TAP_INTERNAL_KEY, null, tapInternalKey.getPubKeyXCoord()));
} }
@ -318,6 +348,8 @@ public class PSBTInput {
tapKeyPathSignature = psbtInput.tapKeyPathSignature; tapKeyPathSignature = psbtInput.tapKeyPathSignature;
} }
tapDerivedPublicKeys.putAll(psbtInput.tapDerivedPublicKeys);
if(psbtInput.tapInternalKey != null) { if(psbtInput.tapInternalKey != null) {
tapInternalKey = psbtInput.tapInternalKey; tapInternalKey = psbtInput.tapInternalKey;
} }
@ -425,6 +457,14 @@ public class PSBTInput {
this.tapKeyPathSignature = tapKeyPathSignature; 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() { public ECKey getTapInternalKey() {
return tapInternalKey; return tapInternalKey;
} }
@ -517,7 +557,7 @@ public class PSBTInput {
Sha256Hash hash = getHashForSignature(signingScript, localSigHash); Sha256Hash hash = getHashForSignature(signingScript, localSigHash);
if(isTaproot() && tapKeyPathSignature != null) { if(isTaproot() && tapKeyPathSignature != null) {
ECKey outputKey = ScriptType.P2TR.getPublicKeyFromScript(getUtxo().getScript()); ECKey outputKey = P2TR.getPublicKeyFromScript(getUtxo().getScript());
if(!outputKey.verify(hash, tapKeyPathSignature)) { if(!outputKey.verify(hash, tapKeyPathSignature)) {
throw new PSBTSignatureException("Tweaked internal key does not verify against provided taproot keypath signature"); throw new PSBTSignatureException("Tweaked internal key does not verify against provided taproot keypath signature");
} }
@ -637,6 +677,7 @@ public class PSBTInput {
witnessScript = null; witnessScript = null;
porCommitment = null; porCommitment = null;
proprietary.clear(); proprietary.clear();
tapDerivedPublicKeys.clear();
tapKeyPathSignature = null; tapKeyPathSignature = null;
} }

View file

@ -118,7 +118,7 @@ public class PaymentCodeTest {
PaymentCode paymentCodeBob = new PaymentCode("PM8TJS2JxQ5ztXUpBBRnpTbcUXbUHy2T1abfrb3KkAAtMEGNbey4oumH7Hc578WgQJhPjBxteQ5GHHToTYHE3A1w6p7tU6KSoFmWBVbFGjKPisZDbP97"); 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(); PaymentCode paymentCodeAlice = aliceBip47Wallet.getKeystores().get(0).getPaymentCode();
Assert.assertEquals(aliceWallet.getPaymentCode(), aliceBip47Wallet.getPaymentCode()); Assert.assertEquals(aliceWallet.getPaymentCode(), aliceBip47Wallet.getPaymentCode());
@ -144,7 +144,7 @@ public class PaymentCodeTest {
bobWallet.getKeystores().add(Keystore.fromSeed(bobSeed, bobWallet.getScriptType().getDefaultDerivation())); bobWallet.getKeystores().add(Keystore.fromSeed(bobSeed, bobWallet.getScriptType().getDefaultDerivation()));
bobWallet.setDefaultPolicy(Policy.getPolicy(PolicyType.SINGLE, ScriptType.P2PKH, bobWallet.getKeystores(), 1)); 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(paymentCodeBob.toString(), bobBip47Wallet.getKeystores().get(0).getPaymentCode().toString());
Assert.assertEquals("1ChvUUvht2hUQufHBXF8NgLhW8SwE2ecGV", paymentCodeBob.getNotificationAddress().toString()); Assert.assertEquals("1ChvUUvht2hUQufHBXF8NgLhW8SwE2ecGV", paymentCodeBob.getNotificationAddress().toString());