Commit 48ac814b authored by zeroleak's avatar zeroleak
Browse files

add scode.fee-value-percent + scode.expiration

parent 96beb870
......@@ -55,13 +55,18 @@ server.register-input.max-inputs-same-hash: max inputs with same hash (same orig
### SCodes
```
server.samourai-fees.feePayloadByScode[foo] = 12345
server.samourai-fees.feePayloadByScode[bar] = 23456
server.samourai-fees.scodes[foo].payload = 12345
server.samourai-fees.scodes[foo].fee-value-percent = 50
server.samourai-fees.scodes[foo].expiration = 1569484078 # optional
server.samourai-fees.scodes[bar].payload = 23456
server.samourai-fees.scodes[bar].fee-value-percent = 0
```
Scodes are special codes usable to enable special rules for tx0.
Each scode is mapped in configuration to a short value (-32,768 to 32,767) which will be embedded into tx0's OP_RETURN as WhirlpoolFeeData.feePayload.
Multiple scode can be mapped to same short value.
Forbidden short value is '0', which is mapped to WhirlpoolFeeData.feePayload=NULL.
SCodes are special codes usable to enable special rules for tx0.
Each SCode is mapped to a short value payload (-32,768 to 32,767) which will be embedded into tx0's OP_RETURN as WhirlpoolFeeData.feePayload.
Payload '0' is forbidden, which is mapped to WhirlpoolFeeData.feePayload=NULL.
SCode overrides standard fee-value with a percent of this value.
SCode can expire for tx0s confirmed after a specified time.
### Pool: Mix limits
```
......@@ -94,6 +99,8 @@ server.test-mode = false
For testing purpose, *server.rpc-client.mock-tx-broadcast* can be enabled to mock txs instead of broadcasting it.
When enabled, *server.test-mode* allows client to bypass tx0 checks.
java -jar -Dspring.profiles.active=test target/whirlpool-server-develop-SNAPSHOT.jar --debug --spring.config.location=classpath:application.properties,/path/to/application-default.properties
## Resources
* [whirlpool](https://github.com/Samourai-Wallet/Whirlpool)
* [whirlpool-protocol](https://github.com/Samourai-Wallet/whirlpool-protocol)
......
......@@ -15,6 +15,7 @@ import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;
import org.springframework.context.ApplicationContext;
import org.springframework.core.env.Environment;
@SpringBootApplication
@ServletComponentScan(value = "com.samourai.whirlpool.server.config.filters")
......@@ -25,6 +26,8 @@ public class Application implements ApplicationRunner {
private ApplicationArguments args;
@Autowired Environment env;
@Autowired private RpcClientService rpcClientService;
@Autowired private ApplicationContext applicationContext;
......@@ -46,6 +49,12 @@ public class Application implements ApplicationRunner {
Utils.setLoggerDebug("com.samourai.whirlpool.server");
}
try {
whirlpoolServerConfig.validate();
} catch (Exception e) {
System.err.println("ERROR: invalid server configuration: " + e.getMessage());
exit();
}
if (!rpcClientService.testConnectivity()) {
exit();
}
......@@ -58,6 +67,7 @@ public class Application implements ApplicationRunner {
for (Map.Entry<String, String> entry : whirlpoolServerConfig.getConfigInfo().entrySet()) {
log.info("config: " + entry.getKey() + ": " + entry.getValue());
}
log.info("profiles: " + Arrays.toString(env.getActiveProfiles()));
}
private void exit() {
......
......@@ -16,8 +16,9 @@ public class PoolFee {
this.feeAccept = (feeAccept != null ? feeAccept : new HashMap<>());
}
public boolean checkTx0FeePaid(long tx0FeePaid, long tx0Time) {
if (tx0FeePaid >= feeValue) {
public boolean checkTx0FeePaid(long tx0FeePaid, long tx0Time, int feeValuePercent) {
long feeToPay = computeFeeValue(feeValuePercent);
if (tx0FeePaid >= feeToPay) {
return true;
}
Long maxTxTime = feeAccept.get(tx0FeePaid);
......@@ -34,7 +35,7 @@ public class PoolFee {
+ maxTxTime);
}
}
log.warn("checkTx0FeePaid: invalid fee payment: " + tx0FeePaid + " < " + feeValue);
log.warn("checkTx0FeePaid: invalid fee payment: " + tx0FeePaid + " < " + feeToPay);
return false;
}
......@@ -46,6 +47,11 @@ public class PoolFee {
return feeAccept;
}
private long computeFeeValue(int feePercent) {
int result = Math.round(feeValue * feePercent / 100);
return result;
}
@Override
public String toString() {
return "feeValue=" + feeValue + ", feeAccept=" + feeAccept;
......
......@@ -6,6 +6,7 @@ import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import javax.validation.constraints.NotEmpty;
import org.apache.commons.lang3.StringUtils;
import org.bitcoinj.core.NetworkParameters;
import org.bitcoinj.params.MainNetParams;
import org.bitcoinj.params.TestNet3Params;
......@@ -459,7 +460,16 @@ public class WhirlpoolServerConfig {
public static class SamouraiFeeConfig {
@NotEmpty private String xpub;
private SecretWalletConfig secretWallet;
private Map<String, Short> feePayloadByScode = new HashMap<>(); // -32,768 to 32,767
private Map<String, ScodeSamouraiFeeConfig> scodes = new HashMap<>(); // -32,768 to 32,767
public void validate() throws Exception {
for (Map.Entry<String, ScodeSamouraiFeeConfig> scodeEntry : scodes.entrySet()) {
if (StringUtils.isEmpty(scodeEntry.getKey())) {
throw new Exception("Invalid scode ID: empty");
}
scodeEntry.getValue().validate();
}
}
public String getXpub() {
return xpub;
......@@ -477,12 +487,54 @@ public class WhirlpoolServerConfig {
this.secretWallet = secretWallet;
}
public Map<String, Short> getFeePayloadByScode() {
return feePayloadByScode;
public Map<String, ScodeSamouraiFeeConfig> getScodes() {
return scodes;
}
public void setScodes(Map<String, ScodeSamouraiFeeConfig> scodes) {
this.scodes = scodes;
}
}
public static class ScodeSamouraiFeeConfig {
@NotEmpty private Short payload;
@NotEmpty private Integer feeValuePercent; // 0-100
private Long expiration;
public void validate() throws Exception {
if (payload == null || payload == 0) {
throw new Exception("Invalid scode.payload");
}
if (feeValuePercent == null || feeValuePercent < 0 || feeValuePercent > 100) {
throw new Exception("Invalid scode.feeValuePercent");
}
if (expiration != null && expiration <= 0) {
throw new Exception("Invalid scode.expiration");
}
}
public Short getPayload() {
return payload;
}
public void setPayload(Short payload) {
this.payload = payload;
}
public Integer getFeeValuePercent() {
return feeValuePercent;
}
public void setFeePayloadByScode(Map<String, Short> feePayloadByScode) {
this.feePayloadByScode = feePayloadByScode;
public void setFeeValuePercent(Integer feeValuePercent) {
this.feeValuePercent = feeValuePercent;
}
public Long getExpiration() {
return expiration;
}
public void setExpiration(Long expiration) {
this.expiration = expiration;
}
}
......@@ -507,6 +559,10 @@ public class WhirlpoolServerConfig {
}
}
public void validate() throws Exception {
samouraiFees.validate();
}
public Map<String, String> getConfigInfo() {
Map<String, String> configInfo = new LinkedHashMap<>();
configInfo.put("port", String.valueOf(getPort()));
......@@ -578,10 +634,15 @@ public class WhirlpoolServerConfig {
+ "]";
configInfo.put("pools[" + poolConfig.id + "]", poolInfo);
}
for (Map.Entry<String, Short> feePayloadEntry :
samouraiFees.getFeePayloadByScode().entrySet()) {
for (Map.Entry<String, ScodeSamouraiFeeConfig> feePayloadEntry :
samouraiFees.getScodes().entrySet()) {
String scode = Utils.obfuscateString(feePayloadEntry.getKey(), 1);
configInfo.put("scode[" + scode + "]", "enabled");
ScodeSamouraiFeeConfig scodeConfig = feePayloadEntry.getValue();
String scodeInfo = "feeValue=" + scodeConfig.feeValuePercent + "%";
if (scodeConfig.expiration != null) {
scodeInfo += ", expiration=" + scodeConfig.expiration;
}
configInfo.put("scode[" + scode + "]", scodeInfo);
}
return configInfo;
}
......
......@@ -39,7 +39,8 @@ public class Tx0Controller extends AbstractRestController {
@RequestMapping(value = WhirlpoolEndpoint.REST_TX0_DATA, method = RequestMethod.GET)
public Tx0DataResponse tx0Data(@RequestParam(value = "scode", required = false) String scode) {
String feePaymentCode = feeValidationService.getFeePaymentCode();
byte[] feePayload = feeValidationService.getFeePayloadByScode(scode);
byte[] feePayload =
feeValidationService.getFeePayloadByScode(scode, System.currentTimeMillis());
Tx0DataResponse tx0DataResponse;
if (feePayload != null) {
if (log.isDebugEnabled()) {
......
......@@ -5,7 +5,6 @@ import com.samourai.wallet.hd.HD_Wallet;
import com.samourai.wallet.hd.java.HD_WalletFactoryJava;
import com.samourai.wallet.segwit.bech32.Bech32UtilGeneric;
import com.samourai.wallet.util.Callback;
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;
......@@ -33,7 +32,6 @@ public class FeeValidationService {
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private CryptoService cryptoService;
private FormatsUtilGeneric formatsUtil;
private WhirlpoolServerConfig serverConfig;
private Bech32UtilGeneric bech32Util;
private HD_WalletFactoryJava hdWalletFactory;
......@@ -44,7 +42,6 @@ public class FeeValidationService {
public FeeValidationService(
CryptoService cryptoService,
FormatsUtilGeneric formatsUtil,
WhirlpoolServerConfig serverConfig,
Bech32UtilGeneric bech32UtilGeneric,
HD_WalletFactoryJava hdWalletFactory,
......@@ -53,7 +50,6 @@ public class FeeValidationService {
WhirlpoolFee whirlpoolFee)
throws Exception {
this.cryptoService = cryptoService;
this.formatsUtil = formatsUtil;
this.serverConfig = serverConfig;
this.bech32Util = bech32UtilGeneric;
this.hdWalletFactory = hdWalletFactory;
......@@ -99,15 +95,19 @@ public class FeeValidationService {
public boolean isValidTx0(
Transaction tx0, long tx0Time, WhirlpoolFeeData feeData, PoolFee poolFee) {
// validate feePayload
if (isValidFeePayload(feeData.getFeePayload())) {
WhirlpoolServerConfig.ScodeSamouraiFeeConfig scodeConfig =
validateFeePayload(feeData.getFeePayload(), tx0Time);
int feePercent = (scodeConfig != null ? scodeConfig.getFeeValuePercent() : 100);
if (feePercent == 0) {
// no fee
return true;
} else {
// validate for feeIndice
return isTx0FeePaid(tx0, tx0Time, feeData.getFeeIndice(), poolFee);
}
// validate for feeIndice with feePercent
return isTx0FeePaid(tx0, tx0Time, feeData.getFeeIndice(), poolFee, feePercent);
}
protected boolean isTx0FeePaid(Transaction tx0, long tx0Time, int x, PoolFee poolFee) {
protected boolean isTx0FeePaid(
Transaction tx0, long tx0Time, int x, PoolFee poolFee, int feeValuePercent) {
if (x < 0) {
log.error("Invalid samouraiFee indice: " + x);
return false;
......@@ -124,7 +124,7 @@ public class FeeValidationService {
if (toAddress != null && feesAddressBech32.equals(toAddress)) {
// ok, this is the fees payment output
long feePaid = txOutput.getValue().getValue();
if (poolFee.checkTx0FeePaid(feePaid, tx0Time)) {
if (poolFee.checkTx0FeePaid(feePaid, tx0Time, feeValuePercent)) {
return true;
} else {
log.warn(
......@@ -138,7 +138,9 @@ public class FeeValidationService {
+ x
+ ", poolFee="
+ poolFee
+ ", feesAddressBech32="
+ ", feeValuePercent="
+ feeValuePercent
+ ", feesAddressBech32="
+ feesAddressBech32);
}
}
......@@ -152,6 +154,8 @@ public class FeeValidationService {
+ x
+ ", poolFee="
+ poolFee
+ ", feeValuePercent="
+ feeValuePercent
+ ", feesAddressBech32="
+ feesAddressBech32);
return false;
......@@ -204,37 +208,62 @@ public class FeeValidationService {
return null;
}
private boolean isValidFeePayload(byte[] feePayload) {
private WhirlpoolServerConfig.ScodeSamouraiFeeConfig validateFeePayload(
byte[] feePayload, long tx0Time) {
if (feePayload == null || feePayload.length != WhirlpoolFee.FEE_PAYLOAD_LENGTH) {
return false;
return null;
}
// search in configuration
return getScodeByFeePayload(feePayload) != null;
WhirlpoolServerConfig.ScodeSamouraiFeeConfig scodeConfig = getScodeByFeePayload(feePayload);
if (scodeConfig == null) {
// scode not found
return null;
}
if (!isScodeValid(scodeConfig, tx0Time)) {
// scode expired
return null;
}
return scodeConfig;
}
protected String getScodeByFeePayload(byte[] feePayload) {
public boolean isScodeValid(
WhirlpoolServerConfig.ScodeSamouraiFeeConfig scodeConfig, long tx0Time) {
// check expiration
if (scodeConfig.getExpiration() != null && tx0Time > scodeConfig.getExpiration()) {
log.warn("SCode expired: expiration=" + scodeConfig.getExpiration() + ", tx0Time=" + tx0Time);
return false;
}
return true;
}
protected WhirlpoolServerConfig.ScodeSamouraiFeeConfig getScodeByFeePayload(byte[] feePayload) {
short feePayloadAsShort = Utils.feePayloadBytesToShort(feePayload);
Optional<Entry<String, Short>> feePayloadEntry =
Optional<Entry<String, WhirlpoolServerConfig.ScodeSamouraiFeeConfig>> feePayloadEntry =
serverConfig
.getSamouraiFees()
.getFeePayloadByScode()
.getScodes()
.entrySet()
.stream()
.filter(e -> e.getValue() == feePayloadAsShort)
.filter(e -> e.getValue().getPayload() == feePayloadAsShort)
.findFirst();
if (!feePayloadEntry.isPresent()) {
// scode not found
log.warn("No SCode found for payload=" + Utils.feePayloadBytesToShort(feePayload));
return null;
}
return feePayloadEntry.get().getKey();
return feePayloadEntry.get().getValue();
}
public byte[] getFeePayloadByScode(String scode) {
Short feePayloadAsShort = serverConfig.getSamouraiFees().getFeePayloadByScode().get(scode);
if (feePayloadAsShort == null) {
public byte[] getFeePayloadByScode(String scode, long tx0Time) {
WhirlpoolServerConfig.ScodeSamouraiFeeConfig scodeConfig =
serverConfig.getSamouraiFees().getScodes().get(scode);
if (scodeConfig == null) {
return null;
}
if (!isScodeValid(scodeConfig, tx0Time)) {
return null;
}
return Utils.feePayloadShortToBytes(feePayloadAsShort);
return Utils.feePayloadShortToBytes(scodeConfig.getPayload());
}
}
......@@ -4,28 +4,31 @@ import com.samourai.wallet.segwit.SegwitAddress;
import com.samourai.wallet.segwit.bech32.Bech32UtilGeneric;
import com.samourai.whirlpool.server.beans.rpc.RpcTransaction;
import com.samourai.whirlpool.server.services.CryptoService;
import com.samourai.whirlpool.server.utils.TestUtils;
import com.samourai.whirlpool.server.utils.Utils;
import java.io.File;
import java.lang.invoke.MethodHandles;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import org.aspectj.util.FileUtil;
import org.bitcoinj.core.NetworkParameters;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.core.TransactionInput;
import org.bitcoinj.core.TransactionOutput;
import org.junit.Assert;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import wf.bitcoin.javabitcoindrpcclient.GenericRpcException;
@Service
@Profile(Utils.PROFILE_TEST)
public class MockRpcClientServiceImpl implements RpcClientService {
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private TestUtils testUtils;
private NetworkParameters params;
private Bech32UtilGeneric bech32Util;
......@@ -34,10 +37,8 @@ public class MockRpcClientServiceImpl implements RpcClientService {
public static final int MOCK_TX_CONFIRMATIONS = 99;
private static final long MOCK_TX_TIME = 900000;
public MockRpcClientServiceImpl(
TestUtils testUtils, CryptoService cryptoService, Bech32UtilGeneric bech32Util) {
public MockRpcClientServiceImpl(CryptoService cryptoService, Bech32UtilGeneric bech32Util) {
log.info("Instanciating MockRpcClientServiceImpl");
this.testUtils = testUtils;
this.params = cryptoService.getNetworkParameters();
this.bech32Util = bech32Util;
......@@ -69,7 +70,7 @@ public class MockRpcClientServiceImpl implements RpcClientService {
}
// load mock from mock file
Optional<String> rpcTxHex = testUtils.loadMockRpc(txid);
Optional<String> rpcTxHex = loadMockTx(txid);
if (!rpcTxHex.isPresent()) {
return Optional.empty();
}
......@@ -106,7 +107,7 @@ public class MockRpcClientServiceImpl implements RpcClientService {
TransactionOutput transactionOutput =
bech32Util.getTransactionOutput(addressBech32, amount, params);
transaction.addOutput(transactionOutput);
Assert.assertEquals((long) i, transactionOutput.getIndex());
Assert.isTrue(i == transactionOutput.getIndex(), "assert failed");
}
int utxoIndex = nbOuts - 1;
......@@ -127,9 +128,32 @@ public class MockRpcClientServiceImpl implements RpcClientService {
RpcTransaction rpcTransaction = new RpcTransaction(rawTxResponse, params);
TransactionOutput txOutput = rpcTransaction.getTx().getOutput(utxoIndex);
Assert.assertEquals(
addressBech32, bech32Util.getAddressFromScript(txOutput.getScriptPubKey(), params));
Assert.isTrue(
addressBech32.equals(bech32Util.getAddressFromScript(txOutput.getScriptPubKey(), params)),
"assert failed");
return rpcTransaction;
}
private Optional<String> loadMockTx(String txid) {
String mockFile = getMockFileName(txid);
try {
log.info("reading mock: " + mockFile);
String rawTx = FileUtil.readAsString(new File(mockFile));
return Optional.of(rawTx);
} catch (Exception e) {
log.info("mock not found: " + mockFile);
return Optional.empty();
}
}
private String getMockFileName(String txid) {
return "./src/test/resources/mocks/" + txid + ".txt";
}
public void writeMockRpc(String txid, String rawTxHex) throws Exception {
String fileName = getMockFileName(txid);
System.out.println("writing " + fileName + ": " + rawTxHex);
Files.write(Paths.get(fileName), rawTxHex.getBytes(), StandardOpenOption.CREATE);
}
}
......@@ -101,6 +101,8 @@ public abstract class AbstractIntegrationTest {
// enable debug
Utils.setLoggerDebug("com.samourai.whirlpool");
serverConfig.validate();
Assert.assertTrue(MockRpcClientServiceImpl.class.isAssignableFrom(rpcClientService.getClass()));
this.params = cryptoService.getNetworkParameters();
......@@ -233,4 +235,15 @@ public abstract class AbstractIntegrationTest {
throws Exception {
return createAndMockTxOutPoint(address, amount, nbConfirmations, null);
}
protected void setScodeConfig(String scode, short payload, int feeValuePercent, Long expiration) {
WhirlpoolServerConfig.ScodeSamouraiFeeConfig scodeConfig =
new WhirlpoolServerConfig.ScodeSamouraiFeeConfig();
scodeConfig.setPayload(payload);
scodeConfig.setFeeValuePercent(feeValuePercent);
if (expiration != null) {
scodeConfig.setExpiration(expiration);
}
serverConfig.getSamouraiFees().getScodes().put(scode, scodeConfig);
}
}
......@@ -34,13 +34,13 @@ public class FeeValidationServiceTest extends AbstractIntegrationTest {
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private static final long FEES_VALID = 975000;
private static final String SCODE_FOO = "foo";
private static final String SCODE_FOO_10 = "foo";
private static final short SCODE_FOO_PAYLOAD = 1234;
private static final String SCODE_BAR = "bar";
private static final String SCODE_BAR_25 = "bar";
private static final short SCODE_BAR_PAYLOAD = 5678;
private static final String SCODE_MIN = "min";
private static final String SCODE_MIN_50 = "min";
private static final short SCODE_MIN_PAYLOAD = -32768;
private static final String SCODE_MAX = "max";
private static final String SCODE_MAX_100 = "max";
private static final short SCODE_MAX_PAYLOAD = 32767;
@Override
......@@ -48,13 +48,11 @@ public class FeeValidationServiceTest extends AbstractIntegrationTest {
super.setUp();
dbService.__reset();
// feePayloadByScode
Map<String, Short> feePayloadByScode = new HashMap<>();
feePayloadByScode.put(SCODE_FOO, SCODE_FOO_PAYLOAD);
feePayloadByScode.put(SCODE_BAR, SCODE_BAR_PAYLOAD);
feePayloadByScode.put(SCODE_MIN, SCODE_MIN_PAYLOAD);
feePayloadByScode.put(SCODE_MAX, SCODE_MAX_PAYLOAD);
serverConfig.getSamouraiFees().setFeePayloadByScode(feePayloadByScode);
// scodes
setScodeConfig(SCODE_FOO_10, SCODE_FOO_PAYLOAD, 0, null);
setScodeConfig(SCODE_BAR_25, SCODE_BAR_PAYLOAD, 25, null);
setScodeConfig(SCODE_MIN_50, SCODE_MIN_PAYLOAD, 50, null);
setScodeConfig(SCODE_MAX_100, SCODE_MAX_PAYLOAD, 100, null);
}
private void assertFeeData(String txid, Integer feeIndice, byte[] feePayload) {
......@@ -104,20 +102,20 @@ public class FeeValidationServiceTest extends AbstractIntegrationTest {
String txid = "cb2fad88ae75fdabb2bcc131b2f4f0ff2c82af22b6dd804dc341900195fb6187";
// accept when paid exact fee
Assert.assertTrue(doIsTx0FeePaid(txid, 1234, FEES_VALID, 1, null));
Assert.assertTrue(doIsTx0FeePaid(txid, 1234, FEES_VALID, 1, null, 100));
// accept when paid more than fee
Assert.assertTrue(doIsTx0FeePaid(txid, 1234, FEES_VALID - 1, 1, null));
Assert.assertTrue(doIsTx0FeePaid(txid, 1234, 1, 1, null));
Assert.assertTrue(doIsTx0FeePaid(txid, 1234, FEES_VALID - 1, 1, null, 100));
Assert.assertTrue(doIsTx0FeePaid(txid, 1234, 1, 1, null, 100));
// reject when paid less than fee
Assert.assertFalse(doIsTx0FeePaid(txid, 1234, FEES_VALID + 1, 1, null));
Assert.assertFalse(doIsTx0FeePaid(txid, 1234, 1000000, 1, null));
Assert.assertFalse(doIsTx0FeePaid(txid, 1234, FEES_VALID + 1, 1, null, 100));
Assert.assertFalse(doIsTx0FeePaid(txid, 1234, 1000000, 1, null, 100));
// reject when paid to wrong xpub indice
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));
Assert.assertFalse(doIsTx0FeePaid(txid, 234, FEES_VALID, 0, null, 100));
Assert.assertFalse(doIsTx0FeePaid(txid, 234, FEES_VALID, 2, null, 100));
Assert.assertFalse(doIsTx0FeePaid(txid, 234, FEES_VALID, 10, null, 100));
}
@Test
......@@ -127,20 +125,26 @@ public class FeeValidationServiceTest extends AbstractIntegrationTest {