Commit 6e4e38fc authored by zeroleak's avatar zeroleak
Browse files

implement new liquidity rules

parent ac21a6a8
......@@ -36,16 +36,16 @@ UTXO for liquidities should be founded with *server.round.denomination*+*server.
### Round limits
```
server.round.target-must-mix
server.round.min-must-mix
server.round.must-mix-adjust-timeout
```
Round will start after *server.round.target-must-mix* mustMix register.<br/>
If this target is not met after *server.round.must-mix-adjust-timeout*, it will be gradually decreased to *server.round.min-must-mix*.
server.round.anonymity-set-target = 10
server.round.anonymity-set-min = 6
server.round.anonymity-set-max = 20
server.round.anonymity-set-adjust-timeout
### Liquidities
```
server.round.liquidity-ratio
server.round.must-mix-min
server.round.liquidity-timeout
```
If *server.round.liquidity-ratio=1*, server will use as many liquidities as mustMix.<br>
If *server.round.liquidity-ratio=0*, no liquidity will be used.<br>
Round will start when *server.round.anonymity-set-target* (mustMix + liquidities) are registered.<br/>
If this target is not met after *server.round.anonymity-set-adjust-timeout*, it will be gradually decreased to *server.round.anonymity-set-min*.<br/>
At the beginning of the round, only mustMix can register and liquidities are placed on a waiting pool.<br/>
After *server.round.liquidity-timeout*, liquidities are added as soon as *server.round.must-mix-min* is reached, up to *server.round.anonymity-set-max* inputs (mustMix + liquidities).
......@@ -11,14 +11,16 @@ public class Round {
private String roundId;
private long denomination; // in satoshis
private long fees; // in satoshis
private int targetMustMixInitial;
private int targetMustMix;
private int minMustMix;
private long mustMixAdjustTimeout; // wait X seconds for decreasing targetMustMix
private float liquidityRatio; // 1 = 1 liquidity for 1 mustMix
private int targetAnonymitySetInitial;
private int targetAnonymitySet;
private int minAnonymitySet;
private int maxAnonymitySet;
private long timeoutAdjustAnonymitySet; // wait X seconds for decreasing anonymitySet
private boolean acceptLiquidities;
private long liquidityTimeout; // wait X seconds for accepting liquidities
private RoundStatus roundStatus;
private long roundStatusTime;
private Map<String,RegisteredInput> inputsById;
private List<String> sendAddresses;
......@@ -30,18 +32,20 @@ public class Round {
private Transaction tx;
private RoundResult failReason;
public Round(String roundId, long denomination, long fees, int targetMustMix, int minMustMix, long mustMixAdjustTimeout, float liquidityRatio) {
public Round(String roundId, long denomination, long fees, int minMustMix, int targetAnonymitySet, int minAnonymitySet, int maxAnonymitySet, long timeoutAdjustAnonymitySet, long liquidityTimeout) {
this.roundId = roundId;
this.denomination = denomination;
this.fees = fees;
this.targetMustMixInitial = targetMustMix;
this.targetMustMix = targetMustMix;
this.minMustMix = minMustMix;
this.mustMixAdjustTimeout = mustMixAdjustTimeout;
this.liquidityRatio = liquidityRatio;
this.targetAnonymitySetInitial = targetAnonymitySet;
this.targetAnonymitySet = targetAnonymitySet;
this.minAnonymitySet = minAnonymitySet;
this.maxAnonymitySet = maxAnonymitySet;
this.timeoutAdjustAnonymitySet = timeoutAdjustAnonymitySet;
this.acceptLiquidities = false;
this.liquidityTimeout = liquidityTimeout;
this.roundStatus = RoundStatus.REGISTER_INPUT;
this.roundStatusTime = System.currentTimeMillis();
this.inputsById = new HashMap<>();
this.sendAddresses = new LinkedList<>();
......@@ -55,7 +59,11 @@ public class Round {
}
public Round(String roundId, Round copyRound) {
this(roundId, copyRound.getDenomination(), copyRound.getFees(), copyRound.getTargetMustMixInitial(), copyRound.getMinMustMix(), copyRound.getMustMixAdjustTimeout(), copyRound.liquidityRatio);
this(roundId, copyRound.getDenomination(), copyRound.getFees(), copyRound.getMinMustMix(), copyRound.getTargetAnonymitySetInitial(), copyRound.getMinAnonymitySet(), copyRound.getMaxAnonymitySet(), copyRound.getTimeoutAdjustAnonymitySet(), copyRound.getLiquidityTimeout());
}
public boolean hasMinMustMixReached() {
return getNbInputsMustMix() >= getMinMustMix();
}
public String getRoundId() {
......@@ -70,28 +78,44 @@ public class Round {
return fees;
}
public int getTargetMustMixInitial() {
return targetMustMixInitial;
public int getMinMustMix() {
return minMustMix;
}
public int getTargetMustMix() {
return targetMustMix;
public int getTargetAnonymitySetInitial() {
return targetAnonymitySetInitial;
}
public void setTargetMustMix(int targetMustMix) {
this.targetMustMix = targetMustMix;
public int getTargetAnonymitySet() {
return targetAnonymitySet;
}
public int getMinMustMix() {
return minMustMix;
public void setTargetAnonymitySet(int targetAnonymitySet) {
this.targetAnonymitySet = targetAnonymitySet;
}
public int getMinAnonymitySet() {
return minAnonymitySet;
}
public int getMaxAnonymitySet() {
return maxAnonymitySet;
}
public long getTimeoutAdjustAnonymitySet() {
return timeoutAdjustAnonymitySet;
}
public boolean isAcceptLiquidities() {
return acceptLiquidities;
}
public long getMustMixAdjustTimeout() {
return mustMixAdjustTimeout;
public void setAcceptLiquidities(boolean acceptLiquidities) {
this.acceptLiquidities = acceptLiquidities;
}
public int computeLiquiditiesExpected() {
return (int)Math.ceil(getTargetMustMix() * liquidityRatio);
public long getLiquidityTimeout() {
return liquidityTimeout;
}
public RoundStatus getRoundStatus() {
......
......@@ -160,10 +160,12 @@ public class WhirlpoolServerConfig {
public static class RoundConfig {
private long denomination;
private long minerFee;
private int targetMustMix;
private int minMustMix;
private long mustMixAdjustTimeout;
private float liquidityRatio;
private int mustMixMin;
private int anonymitySetTarget;
private int anonymitySetMin;
private int anonymitySetMax;
private long anonymitySetAdjustTimeout;
private long liquidityTimeout;
public long getDenomination() {
return denomination;
......@@ -181,36 +183,52 @@ public class WhirlpoolServerConfig {
this.minerFee = minerFee;
}
public int getTargetMustMix() {
return targetMustMix;
public int getMustMixMin() {
return mustMixMin;
}
public void setTargetMustMix(int targetMustMix) {
this.targetMustMix = targetMustMix;
public void setMustMixMin(int mustMixMin) {
this.mustMixMin = mustMixMin;
}
public int getMinMustMix() {
return minMustMix;
public int getAnonymitySetTarget() {
return anonymitySetTarget;
}
public void setMinMustMix(int minMustMix) {
this.minMustMix = minMustMix;
public void setAnonymitySetTarget(int anonymitySetTarget) {
this.anonymitySetTarget = anonymitySetTarget;
}
public long getMustMixAdjustTimeout() {
return mustMixAdjustTimeout;
public int getAnonymitySetMin() {
return anonymitySetMin;
}
public void setMustMixAdjustTimeout(long mustMixAdjustTimeout) {
this.mustMixAdjustTimeout = mustMixAdjustTimeout;
public void setAnonymitySetMin(int anonymitySetMin) {
this.anonymitySetMin = anonymitySetMin;
}
public float getLiquidityRatio() {
return liquidityRatio;
public int getAnonymitySetMax() {
return anonymitySetMax;
}
public void setLiquidityRatio(float liquidityRatio) {
this.liquidityRatio = liquidityRatio;
public void setAnonymitySetMax(int anonymitySetMax) {
this.anonymitySetMax = anonymitySetMax;
}
public long getAnonymitySetAdjustTimeout() {
return anonymitySetAdjustTimeout;
}
public void setAnonymitySetAdjustTimeout(long anonymitySetAdjustTimeout) {
this.anonymitySetAdjustTimeout = anonymitySetAdjustTimeout;
}
public long getLiquidityTimeout() {
return liquidityTimeout;
}
public void setLiquidityTimeout(long liquidityTimeout) {
this.liquidityTimeout = liquidityTimeout;
}
}
......
......@@ -3,6 +3,8 @@ package com.samourai.whirlpool.server.services;
import com.samourai.whirlpool.server.beans.*;
import com.samourai.whirlpool.server.config.WhirlpoolServerConfig;
import com.samourai.whirlpool.server.exceptions.RoundException;
import com.samourai.whirlpool.server.utils.timeout.ITimeoutWatcherListener;
import com.samourai.whirlpool.server.utils.timeout.TimeoutWatcher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
......@@ -19,7 +21,7 @@ public class RoundLimitsManager {
private WhirlpoolServerConfig whirlpoolServerConfig;
private Map<String, LiquidityPool> liquidityPools;
private Map<String, RoundLimitsWatcher> roundWatchers;
private Map<String, TimeoutWatcher> roundWatchers;
public RoundLimitsManager(RoundService roundService, BlameService blameService, WhirlpoolServerConfig whirlpoolServerConfig) {
this.roundService = roundService;
......@@ -30,7 +32,7 @@ public class RoundLimitsManager {
this.roundWatchers = new HashMap<>();
}
private RoundLimitsWatcher getRoundLimitsWatcher(Round round) {
private TimeoutWatcher getRoundLimitsWatcher(Round round) {
String roundId = round.getRoundId();
return roundWatchers.get(roundId);
}
......@@ -47,94 +49,118 @@ public class RoundLimitsManager {
LiquidityPool liquidityPool = new LiquidityPool();
liquidityPools.put(roundId, liquidityPool);
// create roundWatcher
RoundLimitsWatcher roundLimitsWatcher = new RoundLimitsWatcher(round, this);
this.roundWatchers.put(roundId, roundLimitsWatcher);
// wait first input registered before instanciating roundWatcher
}
public void unmanage(Round round) {
String roundId = round.getRoundId();
liquidityPools.remove(roundId);
RoundLimitsWatcher roundLimitsWatcher = getRoundLimitsWatcher(round);
TimeoutWatcher roundLimitsWatcher = getRoundLimitsWatcher(round);
if (roundLimitsWatcher != null) {
roundLimitsWatcher.stop();
roundWatchers.remove(roundId);
}
}
public long computeRoundWatcherTimeToWait(long waitSince, Round round) {
long elapsedTime = System.currentTimeMillis() - waitSince;
long timeToWait;
switch(round.getRoundStatus()) {
case REGISTER_INPUT:
// timeout before next targetMustMix adjustment
timeToWait = round.getMustMixAdjustTimeout()*1000 - elapsedTime;
break;
case REGISTER_OUTPUT:
timeToWait = whirlpoolServerConfig.getRegisterOutput().getTimeout() * 1000;
break;
case SIGNING:
timeToWait = whirlpoolServerConfig.getSigning().getTimeout() * 1000;
break;
default:
// should never use this default value
timeToWait = 10000;
break;
}
return timeToWait;
}
public void onRoundStatusChange(Round round) {
RoundLimitsWatcher roundLimitsWatcher = getRoundLimitsWatcher(round);
TimeoutWatcher roundLimitsWatcher = getRoundLimitsWatcher(round);
// reset timeout for new roundStatus
roundLimitsWatcher.resetTimeout();
}
public void onRoundWatcherTimeout(Round round, RoundLimitsWatcher roundLimitsWatcher) {
switch(round.getRoundStatus()) {
case REGISTER_INPUT:
adjustTargetMustMix(round, roundLimitsWatcher);
break;
public TimeoutWatcher computeRoundLimitsWatcher(Round round) {
ITimeoutWatcherListener listener = new ITimeoutWatcherListener() {
private long computeTimeToWaitAcceptLiquidities(long elapsedTime) {
long timeToWaitAcceptLiquidities = round.getLiquidityTimeout()*1000 - elapsedTime;
return timeToWaitAcceptLiquidities;
}
case REGISTER_OUTPUT:
roundService.goRevealOutputOrBlame(round.getRoundId());
break;
@Override
public long computeTimeToWait(TimeoutWatcher timeoutWatcher) {
long elapsedTime = timeoutWatcher.computeElapsedTime();
long timeToWait;
switch(round.getRoundStatus()) {
case REGISTER_INPUT:
// timeout before next targetAnonymitySet adjustment
long timeToWaitAnonymitySetAdjust = round.getTimeoutAdjustAnonymitySet()*1000 - elapsedTime;
// timeout before accepting liquidities
if (!round.isAcceptLiquidities()) {
long timeToWaitAcceptLiquidities = computeTimeToWaitAcceptLiquidities(elapsedTime);
timeToWait = Math.min(timeToWaitAnonymitySetAdjust, timeToWaitAcceptLiquidities);
}
else {
timeToWait = timeToWaitAnonymitySetAdjust;
}
break;
case REGISTER_OUTPUT:
timeToWait = whirlpoolServerConfig.getRegisterOutput().getTimeout() * 1000 - elapsedTime;
break;
case SIGNING:
timeToWait = whirlpoolServerConfig.getSigning().getTimeout() * 1000 - elapsedTime;
break;
default:
// should never use this default value
timeToWait = 10000;
break;
}
return timeToWait;
}
case SIGNING:
blameForSigningAndResetRound(round);
break;
}
@Override
public void onTimeout(TimeoutWatcher timeoutWatcher) {
switch(round.getRoundStatus()) {
case REGISTER_INPUT:
long elapsedTime = timeoutWatcher.computeElapsedTime();
if (!round.isAcceptLiquidities() && computeTimeToWaitAcceptLiquidities(elapsedTime) <= 0) {
// accept liquidities
round.setAcceptLiquidities(true);
addLiquidities(round);
}
else {
// adjust targetAnonymitySet
adjustTargetAnonymitySet(round, timeoutWatcher);
}
break;
case REGISTER_OUTPUT:
roundService.goRevealOutputOrBlame(round.getRoundId());
break;
case SIGNING:
blameForSigningAndResetRound(round);
break;
}
}
};
TimeoutWatcher roundLimitsWatcher = new TimeoutWatcher(listener);
return roundLimitsWatcher;
}
// REGISTER_INPUTS
private void adjustTargetMustMix(Round round, RoundLimitsWatcher roundLimitsWatcher) {
private void adjustTargetAnonymitySet(Round round, TimeoutWatcher timeoutWatcher) {
// no input registered yet => nothing to do
if (round.getNbInputs() == 0) {
return;
}
// round is ready => nothing to do
if (round.getNbInputs() >= round.getTargetMustMix() || round.getMinMustMix() >= round.getTargetMustMix()) {
if (round.getNbInputs() >= round.getTargetAnonymitySet() || round.getMinAnonymitySet() >= round.getTargetAnonymitySet()) {
return;
}
// adjust mustMix
int nextTargetMustMix = round.getTargetMustMix() - 1;
log.info(" • must-mix-adjust-timeout over, adjusting targetMustMix: "+nextTargetMustMix);
round.setTargetMustMix(nextTargetMustMix);
roundLimitsWatcher.resetTimeout();
if (!checkAddLiquidity(round)) {
// no liquidities needed => round is ready
roundService.checkRegisterInputReady(round);
}
int nextTargetAnonymitySet = round.getTargetAnonymitySet() - 1;
log.info(" • must-mix-adjust-timeout over, adjusting targetAnonymitySet: "+nextTargetAnonymitySet);
round.setTargetAnonymitySet(nextTargetAnonymitySet);
timeoutWatcher.resetTimeout();
}
public LiquidityPool getLiquidityPool(Round round) throws RoundException {
......@@ -147,52 +173,51 @@ public class RoundLimitsManager {
}
public synchronized void onInputRegistered(Round round) {
RoundLimitsWatcher roundLimitsWatcher = getRoundLimitsWatcher(round);
// first input registered => reset timeout for next targetMustMix adjustment
// first input registered => instanciate roundWatcher
if (round.getNbInputs() == 1) {
roundLimitsWatcher.resetTimeout();
String roundId = round.getRoundId();
TimeoutWatcher roundLimitsWatcher = computeRoundLimitsWatcher(round);
this.roundWatchers.put(roundId, roundLimitsWatcher);
}
// check if liquidity is needed
checkAddLiquidity(round);
if (round.isAcceptLiquidities() && round.getNbInputsLiquidities() == 0 && round.hasMinMustMixReached()) {
// we just reached minMustMix and acceptLiquidities was enabled earlier => add liquidities now
addLiquidities(round);
}
}
private boolean checkAddLiquidity(Round round) {
boolean liquiditiesAdded = false;
if (round.getNbInputs() == round.getTargetMustMix()) {
try {
int liquiditiesToAdd = round.computeLiquiditiesExpected();
if (liquiditiesToAdd > 0) {
addLiquidities(round, liquiditiesToAdd);
liquiditiesAdded = true;
}
} catch (Exception e) {
log.error("addLiquiditiesFailed", e);
}
private void addLiquidities(Round round) {
if (!round.hasMinMustMixReached()) {
// will retry to add on onInputRegistered
log.info("Cannot add liquidities yet, minMustMix not reached");
return;
}
return liquiditiesAdded;
}
private void addLiquidities(Round round, int liquiditiesToAdd) throws RoundException {
LiquidityPool liquidityPool = getLiquidityPool(round);
int liquiditiesAdded = 0;
// round needs liquidities
while (liquiditiesAdded < liquiditiesToAdd && liquidityPool.hasLiquidity()) {
log.info("Registering liquidity " + (liquiditiesAdded + 1) + "/" + liquiditiesToAdd);
RegisteredLiquidity randomLiquidity = liquidityPool.peekRandomLiquidity();
int liquiditiesToAdd = round.getTargetAnonymitySet() - round.getNbInputs();
if (liquiditiesToAdd > 0) {
// round needs liquidities
try {
roundService.addLiquidity(round, randomLiquidity);
liquiditiesAdded++;
} catch (Exception e) {
log.error("registerInput error when adding more liquidity", e);
// ignore the error and continue with more liquidity
LiquidityPool liquidityPool = getLiquidityPool(round);
int liquiditiesAdded = 0;
while (liquiditiesAdded < liquiditiesToAdd && liquidityPool.hasLiquidity()) {
log.info("Adding liquidity " + (liquiditiesAdded + 1) + "/" + liquiditiesToAdd);
RegisteredLiquidity randomLiquidity = liquidityPool.peekRandomLiquidity();
try {
roundService.addLiquidity(round, randomLiquidity);
liquiditiesAdded++;
} catch (Exception e) {
log.error("registerInput error when adding liquidity", e);
// ignore the error and continue with more liquidity
}
}
int missingLiquidities = liquiditiesToAdd - liquiditiesAdded;
if (missingLiquidities > 0) {
log.warn("Not enough liquidities to start the round now! " + missingLiquidities + " liquidities missing");
}
}
catch(Exception e) {
log.error("Unexpected exception", e);
}
}
int missingLiquidities = liquiditiesToAdd - liquiditiesAdded;
if (missingLiquidities > 0) {
log.warn("Not enough liquidities to start the round! "+missingLiquidities+" liquidities missing");
}
}
......@@ -222,7 +247,7 @@ public class RoundLimitsManager {
public void __simulateElapsedTime(Round round, long elapsedTimeSeconds) {
String roundId = round.getRoundId();
log.info("__simulateElapsedTime for roundId="+roundId);
RoundLimitsWatcher roundLimitsWatcher = roundWatchers.get(roundId);
TimeoutWatcher roundLimitsWatcher = roundWatchers.get(roundId);
roundLimitsWatcher.__simulateElapsedTime(elapsedTimeSeconds);
}
}
......@@ -55,11 +55,13 @@ public class RoundService {
String roundId = generateRoundId();
long denomination = whirlpoolServerConfig.getRound().getDenomination();
long fees = roundConfig.getMinerFee();
int targetMustMix = roundConfig.getTargetMustMix();
int minMustMix = roundConfig.getMinMustMix();
long mustMixAdjustTimeout = roundConfig.getMustMixAdjustTimeout();
float liquidityRatio = roundConfig.getLiquidityRatio();
Round round = new Round(roundId, denomination, fees, targetMustMix, minMustMix, mustMixAdjustTimeout, liquidityRatio);
int minMustMix = roundConfig.getMustMixMin();
int targetAnonymitySet = roundConfig.getAnonymitySetTarget();
int minAnonymitySet = roundConfig.getAnonymitySetMin();
int maxAnonymitySet = roundConfig.getAnonymitySetMax();
long mustMixAdjustTimeout = roundConfig.getAnonymitySetAdjustTimeout();
long liquidityTimeout = roundConfig.getLiquidityTimeout();
Round round = new Round(roundId, denomination, fees, minMustMix, targetAnonymitySet, minAnonymitySet, maxAnonymitySet, mustMixAdjustTimeout, liquidityTimeout);
this.__reset(round);
}
......@@ -77,42 +79,50 @@ public class RoundService {
}
RegisteredInput registeredInput = new RegisteredInput(username, input, pubkey, paymentCode, liquidity);
if (!liquidity) {
if (liquidity) {
if (!isRegisterLiquiditiesOpen(round) || isRoundFull(round)) {
// place liquidity on queue instead of rejecting it
queueLiquidity(round, registeredInput, signedBordereauToReply);
}
else {
// register liquidity if round opened to liquidities and not full
registerInput(round, registeredInput, signedBordereauToReply, true);
}
}
else {
/*
* user wants to mix
*/
registerInput(round, registeredInput, signedBordereauToReply, false);
}
else {
/*
* user is providing liquidity
*/
LiquidityPool liquidityPool = roundLimitsManager.getLiquidityPool(round);
if (liquidityPool.hasLiquidity(input)) {
throw new IllegalInputException("Liquidity already registered for this round");
}
}
// queue liquidity for later
RegisteredLiquidity registeredInputQueued = new RegisteredLiquidity(registeredInput, signedBordereauToReply);