mirror of
https://github.com/sparrowwallet/drongo.git
synced 2024-11-02 18:26:43 +00:00
support psbt taproot bip32 derivation field, fix taproot signature verification
This commit is contained in:
parent
74e32bab3d
commit
d1088fe9ee
4 changed files with 91 additions and 8 deletions
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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());
|
||||
|
||||
|
|
Loading…
Reference in a new issue