package com.greenaddress.greenbits.spv; import com.greenaddress.greenapi.Network; import com.greenaddress.greenapi.PreparedTransaction; import com.greenaddress.greenbits.GaService; import org.bitcoinj.core.Address; import org.bitcoinj.core.Coin; import org.bitcoinj.core.Transaction; import org.bitcoinj.core.TransactionInput; import org.bitcoinj.core.TransactionOutPoint; import org.bitcoinj.core.TransactionOutput; import org.bitcoinj.params.RegTestParams; import java.util.List; import java.util.Map; class Verifier { private static void feeError(final String smallLarge, final Coin feeRate, final Coin limit) { final String msg = "Verification: Fee is too " + smallLarge + " (" + feeRate.toFriendlyString() + " vs limit " + limit.toFriendlyString() + ')'; throw new IllegalArgumentException(msg); } static Coin verify(final GaService service, final Map<TransactionOutPoint, Coin> countedUtxoValues, final PreparedTransaction ptx, final Address recipient, final Coin amount, final List<Boolean> input) { final int changeIdx; if (input == null) changeIdx = -1; else if (input.get(0)) changeIdx = 0; else if (input.get(1)) changeIdx = 1; else throw new IllegalArgumentException("Verification: Change output missing."); if (input != null && input.get(0) && input.get(1)) { // Shouldn't happen really. In theory user can send money to a new change address // of themselves which they've generated manually, but it's unlikely, so for // simplicity we don't handle it. throw new IllegalArgumentException("Verification: Cannot send to a change address."); } final TransactionOutput output = ptx.mDecoded.getOutputs().get(1 - Math.abs(changeIdx)); if (recipient != null) { final Address gotAddress = output.getScriptPubKey().getToAddress(Network.NETWORK); if (!gotAddress.equals(recipient)) throw new IllegalArgumentException("Verification: Invalid recipient address."); } if (amount != null && !output.getValue().equals(amount)) throw new IllegalArgumentException("Verification: Invalid output amount."); // 3. Verify fee value Coin fee = Coin.ZERO; for (final TransactionInput in : ptx.mDecoded.getInputs()) { if (countedUtxoValues.get(in.getOutpoint()) != null) { fee = fee.add(countedUtxoValues.get(in.getOutpoint())); continue; } final Transaction prevTx = ptx.mPrevoutRawTxs.get(in.getOutpoint().getHash().toString()); if (!prevTx.getHash().equals(in.getOutpoint().getHash())) throw new IllegalArgumentException("Verification: Prev tx hash invalid"); fee = fee.add(prevTx.getOutput((int) in.getOutpoint().getIndex()).getValue()); } for (final TransactionOutput out : ptx.mDecoded.getOutputs()) fee = fee.subtract(out.getValue()); final double messageSize = ptx.mDecoded.getMessageSize(); final double satoshiPerByte = fee.value / messageSize; final double satoshiPerKiloByte = satoshiPerByte * 1000.0; final Coin feeRate = Coin.valueOf((int) satoshiPerKiloByte); final Coin minFeeRate = service.getMinFeeRate(); if (feeRate.isLessThan(minFeeRate) && Network.NETWORK != RegTestParams.get()) feeError("small", feeRate, minFeeRate); final Coin maxFeeRate = Coin.valueOf(15000 * 1000); // FIXME: Get max fee rate from server if (feeRate.isGreaterThan(maxFeeRate)) feeError("large", feeRate, maxFeeRate); return amount == null ? output.getValue() : fee; } }