Commit a6922683 authored by zeroleak's avatar zeroleak
Browse files

append each mix success/fail to a CSV file

parent c2b5b69f
......@@ -65,6 +65,13 @@ If this target is not met after *server.mix.anonymity-set-adjust-timeout*, it wi
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.
### Exports
Each mix success/fail is appended to a CSV file:
```
server.export.mixs.directory
server.export.mixs.filename
```
### Testing
```
server.rpc-client.mock-tx-broadcast = false
......
......@@ -93,6 +93,11 @@
<artifactId>jquery</artifactId>
<version>3.1.1-1</version>
</dependency>
<dependency>
<groupId>com.opencsv</groupId>
<artifactId>opencsv</artifactId>
<version>4.2</version>
</dependency>
<!-- test -->
<dependency>
<groupId>com.samouraiwallet</groupId>
......
......@@ -12,6 +12,7 @@ import java.util.*;
public class Mix {
private MixTO mixTO;
private String mixId;
private Timestamp timeStarted;
private Map<MixStatus,Timestamp> timeStatus;
......@@ -65,6 +66,10 @@ public class Mix {
return mixTO;
}
public Optional<MixTO> __getMixTO() {
return Optional.ofNullable(mixTO);
}
public boolean hasMinMustMixReached() {
return getNbInputsMustMix() >= pool.getMinMustMix();
}
......
package com.samourai.whirlpool.server.beans.export;
import com.opencsv.bean.CsvBindByPosition;
import com.samourai.whirlpool.protocol.websocket.notifications.MixStatus;
import com.samourai.whirlpool.server.beans.FailReason;
import com.samourai.whirlpool.server.persistence.to.MixTO;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import java.sql.Timestamp;
public class MixCsv {
public static final String[] HEADERS = new String[]{"id", "created", "updated", "poolId", "mixId", "denomination", "anonymitySet", "nbMustMix", "nbLiquidities", "mixStatus", "failReason", "txid", "rawTx"};
@CsvBindByPosition(position = 0)
private Long id;
@CsvBindByPosition(position = 1)
private Timestamp created;
@CsvBindByPosition(position = 2)
private Timestamp updated;
//
@CsvBindByPosition(position = 3)
private String poolId;
@CsvBindByPosition(position = 4)
private String mixId;
@CsvBindByPosition(position = 5)
private long denomination;
@CsvBindByPosition(position = 6)
private int anonymitySet;
@CsvBindByPosition(position = 7)
private int nbMustMix;
@CsvBindByPosition(position = 8)
private int nbLiquidities;
@CsvBindByPosition(position = 9)
@Enumerated(EnumType.STRING)
private MixStatus mixStatus;
@CsvBindByPosition(position = 10)
@Enumerated(EnumType.STRING)
private FailReason failReason;
//
@CsvBindByPosition(position = 11)
private String txid;
@CsvBindByPosition(position = 12)
private String rawTx;
public MixCsv(MixTO to) {
this.id = to.getId();
this.created = to.getCreated();
this.updated = to.getUpdated();
this.poolId = to.getPoolId();
this.mixId = to.getMixId();
this.denomination = to.getDenomination();
this.anonymitySet = to.getAnonymitySet();
this.nbMustMix = to.getNbMustMix();
this.nbLiquidities = to.getNbLiquidities();
this.mixStatus = to.getMixStatus();
this.failReason = to.getFailReason();
if (to.getMixLog() != null) {
this.txid = to.getMixLog().getTxid();
this.rawTx = to.getMixLog().getRawTx();
}
}
public Long getId() {
return id;
}
public Timestamp getCreated() {
return created;
}
public Timestamp getUpdated() {
return updated;
}
public String getPoolId() {
return poolId;
}
public String getMixId() {
return mixId;
}
public long getDenomination() {
return denomination;
}
public int getAnonymitySet() {
return anonymitySet;
}
public int getNbMustMix() {
return nbMustMix;
}
public int getNbLiquidities() {
return nbLiquidities;
}
public MixStatus getMixStatus() {
return mixStatus;
}
public FailReason getFailReason() {
return failReason;
}
public String getTxid() {
return txid;
}
public String getRawTx() {
return rawTx;
}
}
......@@ -23,9 +23,9 @@ public class WhirlpoolServerConfig {
private SigningConfig signing;
private RevealOutputConfig revealOutput;
private BanConfig ban;
private ExportConfig export;
private PoolConfig[] pools;
public SamouraiFeeConfig getSamouraiFees() {
return samouraiFees;
}
......@@ -102,6 +102,14 @@ public class WhirlpoolServerConfig {
this.ban = ban;
}
public ExportConfig getExport() {
return export;
}
public void setExport(ExportConfig export) {
this.export = export;
}
public PoolConfig[] getPools() {
return pools;
}
......@@ -197,6 +205,39 @@ public class WhirlpoolServerConfig {
}
}
public static class ExportConfig {
private ExportItemConfig mixs;
public ExportItemConfig getMixs() {
return mixs;
}
public void setMixs(ExportItemConfig mixs) {
this.mixs = mixs;
}
}
public static class ExportItemConfig {
private String filename;
private String directory;
public String getFilename() {
return filename;
}
public void setFilename(String filename) {
this.filename = filename;
}
public String getDirectory() {
return directory;
}
public void setDirectory(String directory) {
this.directory = directory;
}
}
public static class PoolConfig {
private String id;
private long denomination;
......
package com.samourai.whirlpool.server.services;
import com.opencsv.CSVWriter;
import com.opencsv.bean.ColumnPositionMappingStrategy;
import com.opencsv.bean.StatefulBeanToCsv;
import com.opencsv.bean.StatefulBeanToCsvBuilder;
import com.samourai.whirlpool.server.beans.Mix;
import com.samourai.whirlpool.server.beans.export.MixCsv;
import com.samourai.whirlpool.server.config.WhirlpoolServerConfig;
import com.samourai.whirlpool.server.persistence.to.MixTO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.io.File;
import java.io.FileWriter;
import java.lang.invoke.MethodHandles;
@Service
public class ExportService {
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private WhirlpoolServerConfig serverConfig;
private ExportHandler<MixCsv> exportMixs;
public ExportService(WhirlpoolServerConfig serverConfig) throws Exception {
this.serverConfig = serverConfig;
init();
}
public void exportMix(Mix mix) {
try {
MixTO mixTO = mix.__getMixTO().get();
MixCsv mixCSV = new MixCsv(mixTO);
exportMixs.write(mixCSV);
} catch(Exception e) {
log.error("unable to export mix", e);
}
}
private void init() throws Exception {
// init export: mixs
exportMixs = initExport(serverConfig.getExport().getMixs(), MixCsv.class, MixCsv.HEADERS);
}
private <T> ExportHandler<T> initExport(WhirlpoolServerConfig.ExportItemConfig exportItemConfig, Class<T> exportType, String[] headers) throws Exception {
// verify directory exists
String dirname = exportItemConfig.getDirectory();
File exportDirectory = new File(dirname);
if (!exportDirectory.isDirectory()) {
throw new Exception("export-mixs directory doesn't exist: " + dirname);
}
// create file if not exists
boolean justCreated = false;
String filename = exportItemConfig.getFilename();
File csvFile = new File(exportDirectory, filename);
if (!csvFile.exists()) {
csvFile.createNewFile();
if (!csvFile.exists()) {
throw new Exception("export-mixs file doesn't exist: " + filename + " in " + dirname);
}
justCreated = true;
}
// verify file is writable
if (!csvFile.canWrite()) {
throw new Exception("export-mixs file is not writable: " + filename + " in " + dirname);
}
log.info("Ready to export: " + exportType.getName() + " => " + csvFile.getAbsolutePath());
// map type to CSV
CSVWriter writer = new CSVWriter(new FileWriter(csvFile, true));
ColumnPositionMappingStrategy mapStrategy = new ColumnPositionMappingStrategy();
mapStrategy.setType(exportType);
StatefulBeanToCsv csv = new StatefulBeanToCsvBuilder<>(writer)
.withQuotechar(CSVWriter.NO_QUOTE_CHARACTER)
.withMappingStrategy(mapStrategy)
.withSeparator(',')
.withThrowExceptions(true)
.build();
if (justCreated) {
// write headers
writer.writeNext(headers);
writer.flush();
}
return new ExportHandler<>(writer, csv);
}
private static class ExportHandler<T> {
private CSVWriter writer;
private StatefulBeanToCsv<T> csv;
public ExportHandler(CSVWriter writer, StatefulBeanToCsv<T> csv) {
this.writer = writer;
this.csv = csv;
}
public void write(T bean) throws Exception {
csv.write(bean);
writer.flush();
}
}
}
......@@ -37,11 +37,12 @@ public class MixService {
private Bech32Util bech32Util;
private WhirlpoolServerConfig whirlpoolServerConfig;
private PoolService poolService;
private ExportService exportService;
private Map<String,Mix> currentMixs;
@Autowired
public MixService(CryptoService cryptoService, BlameService blameService, DbService dbService, RpcClientService rpcClientService, WebSocketService webSocketService, Bech32Util bech32Util, WhirlpoolServerConfig whirlpoolServerConfig, MixLimitsService mixLimitsService, PoolService poolService) {
public MixService(CryptoService cryptoService, BlameService blameService, DbService dbService, RpcClientService rpcClientService, WebSocketService webSocketService, Bech32Util bech32Util, WhirlpoolServerConfig whirlpoolServerConfig, MixLimitsService mixLimitsService, PoolService poolService, ExportService exportService) {
this.cryptoService = cryptoService;
this.blameService = blameService;
this.dbService = dbService;
......@@ -52,6 +53,7 @@ public class MixService {
mixLimitsService.setMixService(this); // avoids circular reference
this.mixLimitsService = mixLimitsService;
this.poolService = poolService;
this.exportService = exportService;
this.currentMixs = new HashMap<>();
......@@ -277,7 +279,7 @@ public class MixService {
log.info("Tx to broadcast: \n" + tx + "\nRaw: " + Utils.getRawTx(tx));
try {
rpcClientService.broadcastTransaction(tx);
changeMixStatus(mixId, MixStatus.SUCCESS);
goSuccess(mix);
}
catch(Exception e) {
log.error("Unable to broadcast tx", e);
......@@ -473,6 +475,14 @@ public class MixService {
public void goFail(Mix mix, FailReason failReason) {
mix.setFailReason(failReason);
changeMixStatus(mix.getMixId(), MixStatus.FAIL);
exportService.exportMix(mix);
}
public void goSuccess(Mix mix) {
changeMixStatus(mix.getMixId(), MixStatus.SUCCESS);
exportService.exportMix(mix);
}
public synchronized void onClientDisconnect(String username) {
......
......@@ -39,6 +39,10 @@ server.reveal-output.timeout = 60
# ban after x blames
server.ban.blames = 3
server.export.directory = CONFIGURE-ME
server.export.mixs.directory = ${server.export.directory}
server.export.mixs.filename = mixs.csv
# pool 0
server.pools[0].id = 0.5btc
server.pools[0].denomination = 50000000
......
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