Commit 8f9103c5 authored by zeroleak's avatar zeroleak
Browse files

add configuration: pool.fee-accept with tx0Time limit

parent 3d62039a
......@@ -35,10 +35,10 @@ for liquidities: (*server.mix.denomination*) to (*server.mix.denomination* + *se
### 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)
server.pools[x].fee-accept: alternate fee values accepted (key=fee in sats, value=maxTx0Time)
```
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.
*fee-accept* is useful when changing *fee-value*, to still accept unspent tx0s <= maxTx0Time with previous fee-value.
### UTXO confirmations
......
......@@ -9,29 +9,29 @@ 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
private Map<Long, Long> feeAccept; // key=sats, value=maxTx0Time
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) {
public boolean checkTx0FeePaid(long tx0FeePaid, long tx0Time) {
if (tx0FeePaid >= feeValue) {
return true;
}
Long maxBlockHeight = feeAccept.get(tx0FeePaid);
if (maxBlockHeight != null) {
if (tx0BlockHeight <= maxBlockHeight) {
Long maxTxTime = feeAccept.get(tx0FeePaid);
if (maxTxTime != null) {
if (tx0Time <= maxTxTime) {
return true;
} else {
log.warn(
"checkTx0FeePaid: invalid fee payment: feeAccept found for "
+ tx0FeePaid
+ " but tx0BlockHeight="
+ tx0BlockHeight
+ " > maxBlockHeight="
+ tx0BlockHeight);
+ " but tx0Time="
+ tx0Time
+ " > maxTxTime="
+ maxTxTime);
}
}
log.warn("checkTx0FeePaid: invalid fee payment: " + tx0FeePaid + " < " + feeValue);
......
......@@ -8,7 +8,7 @@ import org.bitcoinj.core.Utils;
public class RpcTransaction {
private int confirmations;
private long blockHeight;
private long txTime;
@JsonIgnore private Transaction tx;
......@@ -16,15 +16,15 @@ 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();
this.txTime = rpcRawTransaction.getTxTime();
}
public int getConfirmations() {
return confirmations;
}
public long getBlockHeight() {
return blockHeight;
public long getTxTime() {
return txTime;
}
public Transaction getTx() {
......
......@@ -97,17 +97,17 @@ public class FeeValidationService {
}
public boolean isValidTx0(
Transaction tx0, long tx0BlockHeight, WhirlpoolFeeData feeData, PoolFee poolFee) {
Transaction tx0, long tx0Time, WhirlpoolFeeData feeData, PoolFee poolFee) {
// validate feePayload
if (isValidFeePayload(feeData.getFeePayload())) {
return true;
} else {
// validate for feeIndice
return isTx0FeePaid(tx0, tx0BlockHeight, feeData.getFeeIndice(), poolFee);
return isTx0FeePaid(tx0, tx0Time, feeData.getFeeIndice(), poolFee);
}
}
protected boolean isTx0FeePaid(Transaction tx0, long tx0BlockHeight, int x, PoolFee poolFee) {
protected boolean isTx0FeePaid(Transaction tx0, long tx0Time, int x, PoolFee poolFee) {
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, tx0BlockHeight)) {
if (poolFee.checkTx0FeePaid(feePaid, tx0Time)) {
return true;
} else {
log.warn(
......@@ -132,8 +132,8 @@ public class FeeValidationService {
+ feePaid
+ " for tx0="
+ tx0.getHashAsString()
+ ", tx0BlockHeight="
+ tx0BlockHeight
+ ", tx0Time="
+ tx0Time
+ ", x="
+ x
+ ", poolFee="
......@@ -146,8 +146,8 @@ public class FeeValidationService {
log.warn(
"Tx0: no valid fee payment found for tx0="
+ tx0.getHashAsString()
+ ", tx0BlockHeight="
+ tx0BlockHeight
+ ", tx0Time="
+ tx0Time
+ ", x="
+ x
+ ", poolFee="
......
......@@ -88,8 +88,7 @@ public class InputValidationService {
}
// check fees paid
if (!feeValidationService.isValidTx0(
rpcTx.getTx(), rpcTx.getBlockHeight(), feeData, poolFee)) {
if (!feeValidationService.isValidTx0(rpcTx.getTx(), rpcTx.getTxTime(), feeData, poolFee)) {
throw new IllegalInputException(
"Input rejected (invalid fee for tx0="
+ tx.getHashAsString()
......
......@@ -84,8 +84,9 @@ public class JSONRpcClientServiceImpl implements RpcClientService {
if (rawTx == null) {
return Optional.empty();
}
Long txTime = rawTx.time() != null ? rawTx.time().getTime() : null;
RpcRawTransactionResponse rpcTxResponse =
new RpcRawTransactionResponse(rawTx.hex(), rawTx.confirmations(), rawTx.height());
new RpcRawTransactionResponse(rawTx.hex(), rawTx.confirmations(), txTime);
return Optional.of(rpcTxResponse);
} catch (Exception e) {
log.error("getRawTransaction error", e);
......
......@@ -3,12 +3,12 @@ package com.samourai.whirlpool.server.services.rpc;
public class RpcRawTransactionResponse {
private String hex;
private int confirmations;
private long blockHeight;
private long txTime;
public RpcRawTransactionResponse(String hex, Integer confirmations, Long blockHeight) {
public RpcRawTransactionResponse(String hex, Integer confirmations, Long txTime) {
this.hex = hex;
this.confirmations = (confirmations != null ? confirmations : 0);
this.blockHeight = (blockHeight != null ? blockHeight : 0);
this.txTime = (txTime != null ? txTime : 0);
}
public String getHex() {
......@@ -19,7 +19,7 @@ public class RpcRawTransactionResponse {
return confirmations;
}
public long getBlockHeight() {
return blockHeight;
public long getTxTime() {
return txTime;
}
}
......@@ -40,25 +40,25 @@ public class RpcTransactionTest extends AbstractIntegrationTest {
expectedJson.put(
"cb2fad88ae75fdabb2bcc131b2f4f0ff2c82af22b6dd804dc341900195fb6187",
"{\"confirmations\":1234}");
"{\"confirmations\":1234,\"txTime\":900000}");
expectedJson.put(
"7ea75da574ebabf8d17979615b059ab53aae3011926426204e730d164a0d0f16",
"{\"confirmations\":1234}");
"{\"confirmations\":1234,\"txTime\":900000}");
expectedJson.put(
"96cebec97115f59339a9053b6084aab5869adeefdbdbe974b74bfdbf3b8eaac3",
"{\"confirmations\":1234}");
"{\"confirmations\":1234,\"txTime\":900000}");
}
@Test
public void testInstanciate() throws Exception {
int CONFIRMATIONS = 1234;
long BLOCK_HEIGHT = 900000;
long TX_TIME = 900000;
for (Map.Entry<String, String> entry : expectedHexs.entrySet()) {
String txid = entry.getKey();
String txhex = entry.getValue();
RpcRawTransactionResponse rawTxResponse =
new RpcRawTransactionResponse(txhex, CONFIRMATIONS, BLOCK_HEIGHT);
new RpcRawTransactionResponse(txhex, CONFIRMATIONS, TX_TIME);
// TEST
RpcTransaction rpcTransaction =
......@@ -80,7 +80,7 @@ public class RpcTransactionTest extends AbstractIntegrationTest {
@Test
public void testBitcoinj() throws Exception {
int CONFIRMATIONS = 123;
long BLOCK_HEIGHT = 900000;
long TX_TIME = 900000;
for (Map.Entry<String, String> entry : expectedHexs.entrySet()) {
String txid = entry.getKey();
String txhex = entry.getValue();
......@@ -96,7 +96,7 @@ public class RpcTransactionTest extends AbstractIntegrationTest {
// verify structure
RpcRawTransactionResponse rawTxResponse =
new RpcRawTransactionResponse(txhex, CONFIRMATIONS, BLOCK_HEIGHT);
new RpcRawTransactionResponse(txhex, CONFIRMATIONS, TX_TIME);
RpcTransaction rpcTransaction =
new RpcTransaction(rawTxResponse, cryptoService.getNetworkParameters());
......
......@@ -123,18 +123,18 @@ public class FeeValidationServiceTest extends AbstractIntegrationTest {
// reject when no feeAccept
Assert.assertFalse(doIsTx0FeePaid(txid, 1234, FEES_VALID + 10, 1, null));
// accept when tx0BlockHeight <= feeAccept.maxBlockHeight
// accept when tx0Time <= feeAccept.maxTime
Assert.assertTrue(doIsTx0FeePaid(txid, 11111110L, FEES_VALID + 10, 1, feeAccept));
Assert.assertTrue(doIsTx0FeePaid(txid, 11110L, FEES_VALID + 10, 1, feeAccept));
// reject when tx0BlockHeight > feeAccept.maxBlockHeight
// reject when tx0Time > feeAccept.maxTime
Assert.assertFalse(doIsTx0FeePaid(txid, 11111112L, FEES_VALID + 10, 1, feeAccept));
}
private boolean doIsTx0FeePaid(
String txid, long txBlockHeight, long minFees, int xpubIndice, Map<Long, Long> feeAccept) {
String txid, long txTime, long minFees, int xpubIndice, Map<Long, Long> feeAccept) {
PoolFee poolFee = new PoolFee(minFees, feeAccept);
return feeValidationService.isTx0FeePaid(getTx(txid), txBlockHeight, xpubIndice, poolFee);
return feeValidationService.isTx0FeePaid(getTx(txid), txTime, xpubIndice, poolFee);
}
private Transaction getTx(String txid) {
......
......@@ -36,7 +36,7 @@ public class MockRpcClientServiceImpl implements RpcClientService {
private Map<String, RpcRawTransactionResponse> mockTransactions;
public static final int MOCK_TX_CONFIRMATIONS = 99;
private static final long MOCK_TX_BLOCKHEIGHT = 900000;
private static final long MOCK_TX_TIME = 900000;
public MockRpcClientServiceImpl(
TestUtils testUtils, CryptoService cryptoService, Bech32UtilGeneric bech32Util) {
......@@ -78,14 +78,14 @@ public class MockRpcClientServiceImpl implements RpcClientService {
return Optional.empty();
}
RpcRawTransactionResponse rpcTxResponse =
new RpcRawTransactionResponse(rpcTxHex.get(), MOCK_TX_CONFIRMATIONS, MOCK_TX_BLOCKHEIGHT);
new RpcRawTransactionResponse(rpcTxHex.get(), MOCK_TX_CONFIRMATIONS, MOCK_TX_TIME);
return Optional.of(rpcTxResponse);
}
public void mock(String txid, String rawTxHex, int confirmations) {
log.info("mock tx: " + txid);
RpcRawTransactionResponse rawTxResponse =
new RpcRawTransactionResponse(rawTxHex, confirmations, MOCK_TX_BLOCKHEIGHT);
new RpcRawTransactionResponse(rawTxHex, confirmations, MOCK_TX_TIME);
mockTransactions.put(txid, rawTxResponse);
}
......
......@@ -25,6 +25,15 @@ public class RpcClientServiceTest extends AbstractJsonRpcClientTest {
"7ea75da574ebabf8d17979615b059ab53aae3011926426204e730d164a0d0f16",
"010000000001033dfcfe7fb293d1b6f41b8894f896b3aaceb7f9c023061f533f7321def7929b41020000006b483045022100fd69af97109ff7f5b6aa656e8401d1f00d136ec2577d20b01b2f5154ef41f5420220205a62c372bec510caf800b2a996cc7bf0f52fc0d17fc871dd5c911bb495754501210206e398443b1468e028ef785281fdb39565d8f5dd5e29b9b8cf3fe6efb93062bafdffffff2b40dc90d245e3c23e1b39bdf17b5d1010919fd4f244c9878d4ccd217eef737c000000001716001485cafa3f554071a35f571027b8834b33b82ec056fdffffffc45432e67a0adad659f7249472756293717d423360b0c9849e6809759c03da84020000006a4730440220024e6febc89c6e313f8b297f1aec87ff057128c253f7e352b1635c3c88cf504002206c51f50d1dd4fa4c689c24d2c2bb35ee1d5cb2f99602e0aea9ffdc37092c062b012102632f214738f6f7708e201f6a299d6351eb87caf6b86ce94187ea39c98d18a60bfdffffff04e947e1020000000017a9148249408a629e70e42349addd3e36888a0ea1578287cacbf505000000001976a9143cff5d8af264dcbbc84bae87a209d3efce31734388ac1008f6050000000016001493045495bc69c0d6a3c9e5285c8969f23c79cf951008f60500000000160014d798ca9c7e764f5186887f0b381a50b7122c668b00024830450221009a870dec25f0794b91e594f21a88ea68e9ae9eb8824e54ce2cedd9c9ebe2ed7202203de75af50fe318738ca0e189835b66a5bc3f392d4a307a8c2cd29f780b19e57801210376edd2a70c6eba6b32f35965db0ed9c5502c0876b0600e754f9da9511ab80bca0000000000");
Map<String, Long> expectedTimes = new HashMap<>();
expectedTimes.put(
"cb2fad88ae75fdabb2bcc131b2f4f0ff2c82af22b6dd804dc341900195fb6187",
1523482219000L);
expectedTimes.put(
"7ea75da574ebabf8d17979615b059ab53aae3011926426204e730d164a0d0f16",
1526036025000L);
for (Map.Entry<String, String> entry : expectedHexs.entrySet()) {
String txid = entry.getKey();
String txhex = entry.getValue();
......@@ -35,6 +44,9 @@ public class RpcClientServiceTest extends AbstractJsonRpcClientTest {
// VERIFY
Assert.assertEquals(txhex, rawTxResponse.getHex());
Assert.assertTrue(rawTxResponse.getConfirmations() > 80);
long expectedTime = expectedTimes.get(txid);
Assert.assertEquals(expectedTime, rawTxResponse.getTxTime());
}
}
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment