Commit 161cd77a authored by zeroleak's avatar zeroleak
Browse files

add configuration: pool.fee-accept

parent a80e17ef
......@@ -20,16 +20,27 @@ server.rpc-client.password = CONFIGURE-ME
The bitcoin node should be running on the same network (main or test).<br/>
The node will be used to verify UTXO and broadcast tx.
### UTXO amounts
### Pool: UTXO amounts
```
server.mix.denomination: amount in satoshis
server.mix.miner-fee-min: minimum miner-fee to pay by mustMix
server.mix.miner-fee-max: maximum miner-fee to pay
server.pools[x].denomination: amount in satoshis
server.pools[x].miner-fee-min: minimum miner-fee accepted for mustMix
server.pools[x].miner-fee-max: maximum miner-fee accepted for mustMix
server.pools[x].miner-fee-cap: "soft cap" miner-fee recommended for a new mustMix (should be <= miner-fee-max)
```
UTXO should be founded with:<br/>
for mustMix: (*server.mix.denomination* + *server.mix.miner-fee-min*) to (*server.mix.denomination* + *server.mix.miner-fee-max*)<br/>
for liquidities: (*server.mix.denomination*) to (*server.mix.denomination* + *server.mix.miner-fee-max*)
### Pool: TX0 fees
```
server.pools[x].fee-value: server fee (in satoshis) for each tx0
server.pools[x].fee-accept: alternate fee values accepted (key=fee in sats, value=maxBlockHeight)
```
Standard fee configuration is through *fee-value*.
*fee-accept* is useful when changing *fee-value*, to still accept unspent tx0s <= maxBlockHeight with previous fee-value.
### UTXO confirmations
```
server.register-input.min-confirmations-must-mix: minimum confirmations for mustMix inputs
......@@ -51,21 +62,21 @@ Each scode is mapped in configuration to a short value (-32,768 to 32,767) which
Multiple scode can be mapped to same short value.
Forbidden short value is '0', which is mapped to WhirlpoolFeeData.feePayload=NULL.
### Mix limits
### Pool: Mix limits
```
server.mix.anonymity-set-target = 10
server.mix.anonymity-set-min = 6
server.mix.anonymity-set-max = 20
server.mix.anonymity-set-adjust-timeout = 120
server.pools[x].anonymity-set-target = 10
server.pools[x].anonymity-set-min = 6
server.pools[x].anonymity-set-max = 20
server.pools[x].anonymity-set-adjust-timeout = 120
server.mix.must-mix-min = 1
server.mix.liquidity-timeout = 60
server.pools[x].must-mix-min = 1
server.pools[x].liquidity-timeout = 60
```
Mix will start when *server.mix.anonymity-set-target* (mustMix + liquidities) are registered.<br/>
If this target is not met after *server.mix.anonymity-set-adjust-timeout*, it will be gradually decreased to *server.mix.anonymity-set-min*.<br/>
Mix will start when *anonymity-set-target* (mustMix + liquidities) are registered.<br/>
If this target is not met after *anonymity-set-adjust-timeout*, it will be gradually decreased to *anonymity-set-min*.<br/>
At the beginning of the mix, only mustMix can register. Meanwhile, liquidities connecting are placed on a waiting pool.<br/>
After *server.mix.liquidity-timeout* or when current *anonymity-set-target* is reached, liquidities are added as soon as *server.mix.must-mix-min* is reached, up to *server.mix.anonymity-set-max* inputs for the mix.
After *liquidity-timeout* or when current *anonymity-set-target* is reached, liquidities are added as soon as *must-mix-min* is reached, up to *anonymity-set-max* inputs for the mix.
### Exports
Each mix success/fail is appended to a CSV file:
......
......@@ -5,7 +5,7 @@ import com.samourai.whirlpool.protocol.WhirlpoolProtocol;
public class Pool {
private String poolId;
private long denomination; // in satoshis
private long feeValue; // in satoshis
private PoolFee poolFee;
private long minerFeeMin; // in satoshis
private long minerFeeCap; // in satoshis
private long minerFeeMax; // in satoshis
......@@ -23,7 +23,7 @@ public class Pool {
public Pool(
String poolId,
long denomination,
long feeValue,
PoolFee poolFee,
long minerFeeMin,
long minerFeeCap,
long minerFeeMax,
......@@ -35,7 +35,7 @@ public class Pool {
long liquidityTimeout) {
this.poolId = poolId;
this.denomination = denomination;
this.feeValue = feeValue;
this.poolFee = poolFee;
this.minerFeeMin = minerFeeMin;
this.minerFeeCap = minerFeeCap;
this.minerFeeMax = minerFeeMax;
......@@ -90,8 +90,8 @@ public class Pool {
return denomination;
}
public long getFeeValue() {
return feeValue;
public PoolFee getPoolFee() {
return poolFee;
}
public long getMinerFeeMin() {
......
package com.samourai.whirlpool.server.beans;
import java.lang.invoke.MethodHandles;
import java.util.HashMap;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class PoolFee {
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private long feeValue; // in satoshis
private Map<Long, Long> feeAccept; // key=sats, value=maxBlockHeight
public PoolFee(long feeValue, Map<Long, Long> feeAccept) {
this.feeValue = feeValue;
this.feeAccept = (feeAccept != null ? feeAccept : new HashMap<>());
}
public boolean checkTx0FeePaid(long tx0FeePaid, long tx0BlockHeight) {
if (tx0FeePaid >= feeValue) {
return true;
}
Long maxBlockHeight = feeAccept.get(tx0FeePaid);
if (maxBlockHeight != null) {
if (tx0BlockHeight <= maxBlockHeight) {
return true;
} else {
log.warn(
"checkTx0FeePaid: invalid fee payment: feeAccept found for "
+ tx0FeePaid
+ " but tx0BlockHeight="
+ tx0BlockHeight
+ " > maxBlockHeight="
+ tx0BlockHeight);
}
}
log.warn("checkTx0FeePaid: invalid fee payment: " + tx0FeePaid + " < " + feeValue);
return false;
}
public long getFeeValue() {
return feeValue;
}
public Map<Long, Long> getFeeAccept() {
return feeAccept;
}
@Override
public String toString() {
return "feeValue=" + feeValue + ", feeAccept=" + feeAccept;
}
}
......@@ -8,6 +8,7 @@ import org.bitcoinj.core.Utils;
public class RpcTransaction {
private int confirmations;
private long blockHeight;
@JsonIgnore private Transaction tx;
......@@ -15,12 +16,17 @@ public class RpcTransaction {
// parse tx with bitcoinj
this.tx = new Transaction(params, Utils.HEX.decode(rpcRawTransaction.getHex()));
this.confirmations = rpcRawTransaction.getConfirmations();
this.blockHeight = rpcRawTransaction.getBlockHeight();
}
public int getConfirmations() {
return confirmations;
}
public long getBlockHeight() {
return blockHeight;
}
public Transaction getTx() {
return tx;
}
......
......@@ -247,6 +247,7 @@ public class WhirlpoolServerConfig {
private String id;
private long denomination;
private long feeValue;
private Map<Long, Long> feeAccept;
private long minerFeeMin;
private long minerFeeCap;
private long minerFeeMax;
......@@ -281,6 +282,14 @@ public class WhirlpoolServerConfig {
this.feeValue = feeValue;
}
public Map<Long, Long> getFeeAccept() {
return feeAccept;
}
public void setFeeAccept(Map<Long, Long> feeAccept) {
this.feeAccept = feeAccept;
}
public long getMinerFeeMin() {
return minerFeeMin;
}
......@@ -504,6 +513,8 @@ public class WhirlpoolServerConfig {
poolInfo +=
", feeValue="
+ Utils.satoshisToBtc(poolConfig.feeValue)
+ ", feeAccept="
+ (poolConfig.feeAccept != null ? poolConfig.feeAccept : null)
+ ", anonymitySet="
+ poolConfig.anonymitySetTarget
+ "["
......
......@@ -58,7 +58,7 @@ public class PoolsController extends AbstractRestController {
new PoolInfo(
pool.getPoolId(),
pool.getDenomination(),
pool.getFeeValue(),
pool.getPoolFee().getFeeValue(),
pool.computeMustMixBalanceMin(),
pool.computeMustMixBalanceCap(),
pool.computeMustMixBalanceMax(),
......
......@@ -48,7 +48,8 @@ public class StatusWebController {
Map<String, Object> poolAttributes = new HashMap<>();
poolAttributes.put("poolId", pool.getPoolId());
poolAttributes.put("denomination", pool.getDenomination());
poolAttributes.put("feeValue", pool.getFeeValue());
poolAttributes.put("feeValue", pool.getPoolFee().getFeeValue());
poolAttributes.put("feeAccept", pool.getPoolFee().getFeeAccept());
poolAttributes.put("mixStatus", mix.getMixStatus());
poolAttributes.put("targetAnonymitySet", mix.getTargetAnonymitySet());
poolAttributes.put("maxAnonymitySet", pool.getMaxAnonymitySet());
......
......@@ -97,5 +97,9 @@ public class DbService {
public void __reset() {
blames = new ArrayList<>();
mixRepository.deleteAll();
tx0WhitelistRepository.deleteAll();
mixOutputRepository.deleteAll();
mixTxidRepository.deleteAll();
}
}
......@@ -9,6 +9,7 @@ import com.samourai.wallet.util.FormatsUtilGeneric;
import com.samourai.wallet.util.TxUtil;
import com.samourai.whirlpool.protocol.fee.WhirlpoolFee;
import com.samourai.whirlpool.protocol.fee.WhirlpoolFeeData;
import com.samourai.whirlpool.server.beans.PoolFee;
import com.samourai.whirlpool.server.beans.rpc.RpcTransaction;
import com.samourai.whirlpool.server.config.WhirlpoolServerConfig;
import com.samourai.whirlpool.server.config.WhirlpoolServerConfig.SecretWalletConfig;
......@@ -16,7 +17,10 @@ import com.samourai.whirlpool.server.utils.Utils;
import java.lang.invoke.MethodHandles;
import java.util.Map.Entry;
import java.util.Optional;
import org.bitcoinj.core.*;
import org.bitcoinj.core.NetworkParameters;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.core.TransactionOutPoint;
import org.bitcoinj.core.TransactionOutput;
import org.bitcoinj.script.Script;
import org.bitcoinj.script.ScriptChunk;
import org.bitcoinj.script.ScriptOpCodes;
......@@ -92,17 +96,18 @@ public class FeeValidationService {
return secretAccountBip47.getPaymentCode();
}
public boolean isValidTx0(Transaction tx0, WhirlpoolFeeData feeData, long poolFeeValue) {
public boolean isValidTx0(
Transaction tx0, long tx0BlockHeight, WhirlpoolFeeData feeData, PoolFee poolFee) {
// validate feePayload
if (isValidFeePayload(feeData.getFeePayload())) {
return true;
} else {
// validate for feeIndice
return isTx0FeePaid(tx0, feeData.getFeeIndice(), poolFeeValue);
return isTx0FeePaid(tx0, tx0BlockHeight, feeData.getFeeIndice(), poolFee);
}
}
protected boolean isTx0FeePaid(Transaction tx0, int x, long poolFeeValue) {
protected boolean isTx0FeePaid(Transaction tx0, long tx0BlockHeight, int x, PoolFee poolFee) {
if (x < 0) {
log.error("Invalid samouraiFee indice: " + x);
return false;
......@@ -113,16 +118,42 @@ public class FeeValidationService {
// make sure tx contains an output to samourai fees
for (TransactionOutput txOutput : tx0.getOutputs()) {
if (txOutput.getValue().getValue() >= poolFeeValue) {
// is this the fees payment output?
String toAddress =
Utils.getToAddressBech32(txOutput, bech32Util, cryptoService.getNetworkParameters());
if (toAddress != null && feesAddressBech32.equals(toAddress)) {
// ok, this is the fees payment output
// is this the fees payment output?
String toAddress =
Utils.getToAddressBech32(txOutput, bech32Util, cryptoService.getNetworkParameters());
if (toAddress != null && feesAddressBech32.equals(toAddress)) {
// ok, this is the fees payment output
long feePaid = txOutput.getValue().getValue();
if (poolFee.checkTx0FeePaid(feePaid, tx0BlockHeight)) {
return true;
} else {
log.warn(
"Tx0: invalid feePaid="
+ feePaid
+ " for tx0="
+ tx0.getHashAsString()
+ ", tx0BlockHeight="
+ tx0BlockHeight
+ ", x="
+ x
+ ", poolFee="
+ poolFee
+ ", feesAddressBech32="
+ feesAddressBech32);
}
}
}
log.warn(
"Tx0: no valid fee payment found for tx0="
+ tx0.getHashAsString()
+ ", tx0BlockHeight="
+ tx0BlockHeight
+ ", x="
+ x
+ ", poolFee="
+ poolFee
+ ", feesAddressBech32="
+ feesAddressBech32);
return false;
}
......
......@@ -2,6 +2,9 @@ package com.samourai.whirlpool.server.services;
import com.samourai.wallet.util.MessageSignUtilGeneric;
import com.samourai.whirlpool.protocol.fee.WhirlpoolFeeData;
import com.samourai.whirlpool.server.beans.Pool;
import com.samourai.whirlpool.server.beans.PoolFee;
import com.samourai.whirlpool.server.beans.rpc.RpcTransaction;
import com.samourai.whirlpool.server.beans.rpc.TxOutPoint;
import com.samourai.whirlpool.server.config.WhirlpoolServerConfig;
import com.samourai.whirlpool.server.exceptions.IllegalInputException;
......@@ -36,7 +39,7 @@ public class InputValidationService {
}
public TxOutPoint validateProvenance(
TxOutPoint txOutPoint, Transaction tx, boolean liquidity, boolean testMode, long poolFeeValue)
TxOutPoint txOutPoint, RpcTransaction tx, boolean liquidity, boolean testMode, Pool pool)
throws IllegalInputException {
// provenance verification can be disabled with testMode
......@@ -46,14 +49,14 @@ public class InputValidationService {
}
// check tx0Whitelist
String txid = tx.getHashAsString();
String txid = tx.getTx().getHashAsString();
if (dbService.hasTx0Whitelist(txid)) {
log.warn("tx0 check disabled by whitelist for txid=" + txid);
return txOutPoint;
}
// verify input comes from a valid tx0 or previous mix
boolean isLiquidity = checkInputProvenance(tx, txOutPoint.getValue(), poolFeeValue);
boolean isLiquidity = checkInputProvenance(tx, txOutPoint.getValue(), pool.getPoolFee());
if (!isLiquidity && liquidity) {
throw new IllegalInputException("Input rejected: joined as liquidity but is a mustMix");
}
......@@ -63,8 +66,9 @@ public class InputValidationService {
return txOutPoint;
}
protected boolean checkInputProvenance(Transaction tx, long inputValue, long poolFeeValue)
protected boolean checkInputProvenance(RpcTransaction rpcTx, long inputValue, PoolFee poolFee)
throws IllegalInputException {
Transaction tx = rpcTx.getTx();
// is it a tx0?
WhirlpoolFeeData feeData = feeValidationService.decodeFeeData(tx);
if (feeData != null) {
......@@ -84,7 +88,8 @@ public class InputValidationService {
}
// check fees paid
if (!feeValidationService.isValidTx0(tx, feeData, poolFeeValue)) {
if (!feeValidationService.isValidTx0(
rpcTx.getTx(), rpcTx.getBlockHeight(), feeData, poolFee)) {
throw new IllegalInputException(
"Input rejected (invalid fee for tx0="
+ tx.getHashAsString()
......
......@@ -3,10 +3,7 @@ package com.samourai.whirlpool.server.services;
import com.samourai.whirlpool.protocol.WhirlpoolProtocol;
import com.samourai.whirlpool.protocol.websocket.messages.SubscribePoolResponse;
import com.samourai.whirlpool.protocol.websocket.notifications.ConfirmInputMixStatusNotification;
import com.samourai.whirlpool.server.beans.InputPool;
import com.samourai.whirlpool.server.beans.Mix;
import com.samourai.whirlpool.server.beans.Pool;
import com.samourai.whirlpool.server.beans.RegisteredInput;
import com.samourai.whirlpool.server.beans.*;
import com.samourai.whirlpool.server.beans.rpc.TxOutPoint;
import com.samourai.whirlpool.server.config.WhirlpoolServerConfig;
import com.samourai.whirlpool.server.exceptions.IllegalInputException;
......@@ -62,6 +59,7 @@ public class PoolService {
String poolId = poolConfig.getId();
long denomination = poolConfig.getDenomination();
long feeValue = poolConfig.getFeeValue();
Map<Long, Long> feeAccept = poolConfig.getFeeAccept();
long minerFeeMin = poolConfig.getMinerFeeMin();
long minerFeeCap = poolConfig.getMinerFeeCap();
long minerFeeMax = poolConfig.getMinerFeeMax();
......@@ -74,11 +72,12 @@ public class PoolService {
Assert.notNull(poolId, "Pool configuration: poolId must not be NULL");
Assert.isTrue(!pools.containsKey(poolId), "Pool configuration: poolId must not be duplicate");
PoolFee poolFee = new PoolFee(feeValue, feeAccept);
Pool pool =
new Pool(
poolId,
denomination,
feeValue,
poolFee,
minerFeeMin,
minerFeeCap,
minerFeeMax,
......
......@@ -73,9 +73,8 @@ public class RegisterInputService {
// verify input is a valid mustMix or liquidity
Pool pool = poolService.getPool(poolId);
long poolFeeValue = pool.getFeeValue();
inputValidationService.validateProvenance(
txOutPoint, rpcTransaction.getTx(), liquidity, testMode, poolFeeValue);
txOutPoint, rpcTransaction, liquidity, testMode, pool);
// register input to pool
poolService.registerInput(poolId, username, liquidity, txOutPoint, true);
......
......@@ -85,7 +85,7 @@ public class JSONRpcClientServiceImpl implements RpcClientService {
return Optional.empty();
}
RpcRawTransactionResponse rpcTxResponse =
new RpcRawTransactionResponse(rawTx.hex(), rawTx.confirmations());
new RpcRawTransactionResponse(rawTx.hex(), rawTx.confirmations(), rawTx.height());
return Optional.of(rpcTxResponse);
} catch (Exception e) {
log.error("getRawTransaction error", e);
......
......@@ -3,10 +3,12 @@ package com.samourai.whirlpool.server.services.rpc;
public class RpcRawTransactionResponse {
private String hex;
private int confirmations;
private long blockHeight;
public RpcRawTransactionResponse(String hex, Integer confirmations) {
public RpcRawTransactionResponse(String hex, Integer confirmations, Long blockHeight) {
this.hex = hex;
this.confirmations = (confirmations != null ? confirmations : 0);
this.blockHeight = (blockHeight != null ? blockHeight : 0);
}
public String getHex() {
......@@ -16,4 +18,8 @@ public class RpcRawTransactionResponse {
public int getConfirmations() {
return confirmations;
}
public long getBlockHeight() {
return blockHeight;
}
}
......@@ -52,11 +52,13 @@ public class RpcTransactionTest extends AbstractIntegrationTest {
@Test
public void testInstanciate() throws Exception {
int CONFIRMATIONS = 1234;
long BLOCK_HEIGHT = 900000;
for (Map.Entry<String, String> entry : expectedHexs.entrySet()) {
String txid = entry.getKey();
String txhex = entry.getValue();
RpcRawTransactionResponse rawTxResponse = new RpcRawTransactionResponse(txhex, CONFIRMATIONS);
RpcRawTransactionResponse rawTxResponse =
new RpcRawTransactionResponse(txhex, CONFIRMATIONS, BLOCK_HEIGHT);
// TEST
RpcTransaction rpcTransaction =
......@@ -78,6 +80,7 @@ public class RpcTransactionTest extends AbstractIntegrationTest {
@Test
public void testBitcoinj() throws Exception {
int CONFIRMATIONS = 123;
long BLOCK_HEIGHT = 900000;
for (Map.Entry<String, String> entry : expectedHexs.entrySet()) {
String txid = entry.getKey();
String txhex = entry.getValue();
......@@ -92,7 +95,8 @@ public class RpcTransactionTest extends AbstractIntegrationTest {
Assert.assertEquals(txhex, Utils.HEX.encode(tx.bitcoinSerialize()));
// verify structure
RpcRawTransactionResponse rawTxResponse = new RpcRawTransactionResponse(txhex, CONFIRMATIONS);
RpcRawTransactionResponse rawTxResponse =
new RpcRawTransactionResponse(txhex, CONFIRMATIONS, BLOCK_HEIGHT);
RpcTransaction rpcTransaction =
new RpcTransaction(rawTxResponse, cryptoService.getNetworkParameters());
......
......@@ -167,7 +167,8 @@ public abstract class AbstractIntegrationTest {
WhirlpoolServerConfig.PoolConfig poolConfig = new WhirlpoolServerConfig.PoolConfig();
poolConfig.setId(Utils.generateUniqueString());
poolConfig.setDenomination(copyPool.getDenomination());
poolConfig.setFeeValue(copyPool.getFeeValue());
poolConfig.setFeeValue(copyPool.getPoolFee().getFeeValue());
poolConfig.setFeeAccept(copyPool.getPoolFee().getFeeAccept());
poolConfig.setMinerFeeMin(copyPool.getMinerFeeMin());
poolConfig.setMinerFeeCap(copyPool.getMinerFeeCap());
poolConfig.setMinerFeeMax(copyPool.getMinerFeeMax());
......
package com.samourai.whirlpool.server.services;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.DEFINED_PORT;
import com.samourai.wallet.client.Bip84Wallet;
import com.samourai.wallet.client.indexHandler.MemoryIndexHandler;
import com.samourai.wallet.hd.HD_Wallet;
......@@ -11,15 +9,10 @@ import com.samourai.whirlpool.client.tx0.Tx0Service;
import com.samourai.whirlpool.client.whirlpool.beans.Pool;
import com.samourai.whirlpool.client.whirlpool.beans.Tx0Data;
import com.samourai.whirlpool.protocol.fee.WhirlpoolFeeData;
import com.samourai.whirlpool.server.beans.PoolFee;
import com.samourai.whirlpool.server.beans.rpc.RpcTransaction;
import com.samourai.whirlpool.server.integration.AbstractIntegrationTest;
import com.samourai.whirlpool.server.utils.Utils;
import java.lang.invoke.MethodHandles;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import org.bitcoinj.core.ECKey;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.core.TransactionOutPoint;
......@@ -31,6 +24,11 @@ import org.slf4j.LoggerFactory;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import java.lang.invoke.MethodHandles;
import java.util.*;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.DEFINED_PORT;
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = DEFINED_PORT)
public class FeeValidationServiceTest extends AbstractIntegrationTest {
......@@ -97,28 +95,46 @@ public class FeeValidationServiceTest extends AbstractIntegrationTest {
}
@Test
public void isTx0FeePaid() throws Exception {
public void isTx0FeePaid_feeValue() throws Exception {
String txid = "cb2fad88ae75fdabb2bcc131b2f4f0ff2c82af22b6dd804dc341900195fb6187";
// accept when paid exact fee
Assert.assertTrue(doIsTx0FeePaid(txid, FEES_VALID, 1));
Assert.assertTrue(doIsTx0FeePaid(txid, 1234, FEES_VALID, 1, null));
// accept when paid more than fee
Assert.assertTrue(doIsTx0FeePaid(txid, FEES_VALID - 1, 1));
Assert.assertTrue(doIsTx0FeePaid(txid, 1, 1));
Assert.assertTrue(doIsTx0FeePaid(txid, 1234, FEES_VALID - 1, 1, null));
Assert.assertTrue(doIsTx0FeePaid(txid, 1234, 1, 1, null));
// reject when paid less than fee
Assert.assertFalse(doIsTx0FeePaid(txid, FEES_VALID + 1, 1));
Assert.assertFalse(doIsTx0FeePaid(txid, 1000000, 1));
Assert.assertFalse(doIsTx0FeePaid(txid, 1234, FEES_VALID + 1, 1, null));
Assert.assertFalse(doIsTx0FeePaid(txid, 1234, 1000000, 1, null));
// reject when paid to wrong xpub indice
Assert.assertFalse(doIsTx0FeePaid(txid, FEES_VALID, 0));
Assert.assertFalse(doIsTx0FeePaid(txid, FEES_VALID, 2));
Assert.assertFalse(doIsTx0FeePaid(txid, FEES_VALID, 10));
Assert.assertFalse(doIsTx0FeePaid(txid, 234, FEES_VALID, 0, null));
Assert.assertFalse(doIsTx0FeePaid(txid, 234, FEES_VALID, 2, null));
Assert.assertFalse(doIsTx0FeePaid(txid, 234, FEES_VALID, 10, null));