verify precomputed outputs match bip375 proofs
This commit is contained in:
parent
20670b9b7d
commit
e96aa0d3f8
@ -13,6 +13,7 @@ import com.sparrowwallet.drongo.protocol.*;
|
||||
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||
import com.sparrowwallet.drongo.psbt.PSBTInput;
|
||||
import com.sparrowwallet.drongo.psbt.PSBTOutput;
|
||||
import com.sparrowwallet.drongo.psbt.PSBTProofException;
|
||||
import com.sparrowwallet.drongo.silentpayments.*;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
@ -1878,12 +1879,50 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
|
||||
|
||||
public List<SilentPayment> computeSilentPaymentOutputs(PSBT psbt, Map<PSBTInput, WalletNode> signingNodes) throws InvalidSilentPaymentException {
|
||||
List<PSBTOutput> silentOutputs = psbt.getPsbtOutputs().stream().filter(psbtOutput -> psbtOutput.getSilentPaymentAddress() != null).collect(Collectors.toList());
|
||||
if(!silentOutputs.isEmpty()) {
|
||||
if(psbt.getPsbtInputs().size() != signingNodes.size()) {
|
||||
throw new InvalidSilentPaymentException("All inputs must be from wallet to calculate silent payment addresses");
|
||||
}
|
||||
if(silentOutputs.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
List<SilentPayment> silentPayments = silentOutputs.stream()
|
||||
if(psbt.getPsbtInputs().size() != signingNodes.size()) {
|
||||
throw new InvalidSilentPaymentException("All inputs must be from wallet to calculate silent payment addresses");
|
||||
}
|
||||
|
||||
List<PSBTOutput> preComputedOutputs = new ArrayList<>();
|
||||
List<PSBTOutput> computeOutputs = new ArrayList<>();
|
||||
for(PSBTOutput silentOutput : silentOutputs) {
|
||||
Script script = silentOutput.getScript();
|
||||
if(script != null && script.getToAddress() != null) {
|
||||
preComputedOutputs.add(silentOutput);
|
||||
} else {
|
||||
computeOutputs.add(silentOutput);
|
||||
}
|
||||
}
|
||||
|
||||
List<SilentPayment> results = new ArrayList<>();
|
||||
|
||||
if(!preComputedOutputs.isEmpty()) {
|
||||
Map<TransactionInput, ECKey> inputPublicKeys = new LinkedHashMap<>();
|
||||
Transaction transaction = psbt.getTransaction();
|
||||
for(int i = 0; i < psbt.getPsbtInputs().size(); i++) {
|
||||
PSBTInput psbtInput = psbt.getPsbtInputs().get(i);
|
||||
ECKey publicKey = SilentPaymentUtils.getInputPublicKey(signingNodes.get(psbtInput));
|
||||
if(publicKey == null) {
|
||||
throw new InvalidSilentPaymentException("Cannot derive input public key for silent payment verification");
|
||||
}
|
||||
inputPublicKeys.put(transaction.getInputs().get(i), publicKey);
|
||||
}
|
||||
try {
|
||||
psbt.validateSilentPayments(inputPublicKeys);
|
||||
} catch(PSBTProofException e) {
|
||||
throw new InvalidSilentPaymentException("Silent payment metadata in PSBT failed BIP-375 verification: " + e.getMessage());
|
||||
}
|
||||
for(PSBTOutput silentOutput : preComputedOutputs) {
|
||||
results.add(new SilentPayment(silentOutput.getSilentPaymentAddress(), silentOutput.getScript().getToAddress(), null, silentOutput.getAmount(), false));
|
||||
}
|
||||
}
|
||||
|
||||
if(!computeOutputs.isEmpty()) {
|
||||
List<SilentPayment> silentPayments = computeOutputs.stream()
|
||||
.map(psbtOutput -> new SilentPayment(psbtOutput.getSilentPaymentAddress(), null, psbtOutput.getAmount(), false)).collect(Collectors.toList());
|
||||
Map<HashIndex, WalletNode> utxos = signingNodes.keySet().stream()
|
||||
.collect(Collectors.toMap(psbtInput -> new HashIndex(psbtInput.getPrevTxid(), psbtInput.getPrevIndex()), signingNodes::get));
|
||||
@ -1893,8 +1932,8 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
|
||||
psbt.getSilentPaymentsDLEQProofs().put(key, ecdhShareAndProof.dleqProof());
|
||||
});
|
||||
|
||||
for(int i = 0; i < silentOutputs.size(); i++) {
|
||||
PSBTOutput silentOutput = silentOutputs.get(i);
|
||||
for(int i = 0; i < computeOutputs.size(); i++) {
|
||||
PSBTOutput silentOutput = computeOutputs.get(i);
|
||||
SilentPayment silentPayment = silentPayments.get(i);
|
||||
if(!silentPayment.isAddressComputed()) {
|
||||
throw new InvalidSilentPaymentException("Silent payment address was not calculated");
|
||||
@ -1904,10 +1943,10 @@ public class Wallet extends Persistable implements Comparable<Wallet> {
|
||||
silentOutput.setScript(outputScript);
|
||||
}
|
||||
|
||||
return silentPayments;
|
||||
results.addAll(silentPayments);
|
||||
}
|
||||
|
||||
return Collections.emptyList();
|
||||
return results;
|
||||
}
|
||||
|
||||
public void finalise(PSBT psbt) {
|
||||
|
||||
@ -1117,6 +1117,39 @@ public class SilentPaymentUtilsTest {
|
||||
Assertions.assertTrue(verified.isEmpty(), "Missing DLEQ proof must cause rejection");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testComputeRejectsInjectedSpInfo() throws Exception {
|
||||
Wallet wallet = buildVerifyWallet();
|
||||
WalletNode receiveNode = primeReceiveNode(wallet, 0);
|
||||
PSBT psbt = buildVerifySendingPsbt(receiveNode);
|
||||
|
||||
byte[] baitXOnly = new byte[32];
|
||||
new SecureRandom().nextBytes(baitXOnly);
|
||||
Script baitScript = ScriptType.P2TR.getOutputScript(baitXOnly);
|
||||
psbt.getPsbtOutputs().getFirst().setScript(baitScript);
|
||||
|
||||
Map<PSBTInput, WalletNode> signingNodes = wallet.getSigningNodes(psbt);
|
||||
Assertions.assertThrows(InvalidSilentPaymentException.class, () -> wallet.computeSilentPaymentOutputs(psbt, signingNodes),
|
||||
"Injected PSBT_OUT_SP_V0_INFO without valid BIP-375 proofs must abort signing");
|
||||
Assertions.assertArrayEquals(baitScript.getProgram(), psbt.getPsbtOutputs().get(0).getScript().getProgram(),
|
||||
"Visible output script must not be mutated when SP metadata fails verification");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testComputePreservesScriptForVerifiedPsbt() throws Exception {
|
||||
Wallet wallet = buildVerifyWallet();
|
||||
WalletNode receiveNode = primeReceiveNode(wallet, 0);
|
||||
PSBT psbt = buildVerifySendingPsbt(receiveNode);
|
||||
|
||||
Map<PSBTInput, WalletNode> signingNodes = wallet.getSigningNodes(psbt);
|
||||
wallet.computeSilentPaymentOutputs(psbt, signingNodes);
|
||||
byte[] computedScript = psbt.getPsbtOutputs().getFirst().getScript().getProgram();
|
||||
|
||||
wallet.computeSilentPaymentOutputs(psbt, signingNodes);
|
||||
Assertions.assertArrayEquals(computedScript, psbt.getPsbtOutputs().getFirst().getScript().getProgram(),
|
||||
"Already-computed and proof-backed SP output script must be preserved by a subsequent compute call");
|
||||
}
|
||||
|
||||
private static Wallet buildVerifyWallet() throws Exception {
|
||||
DeterministicSeed seed = new DeterministicSeed(VERIFY_TEST_SEED, "", 0, DeterministicSeed.Type.BIP39);
|
||||
Wallet wallet = new Wallet();
|
||||
|
||||
Loading…
Reference in New Issue
Block a user