Commit 2261a45d authored by zeroleak's avatar zeroleak
Browse files

add --scode

parent 1868df49
......@@ -41,6 +41,16 @@ server.register-input.min-confirmations-liquidity: minimum confirmations for liq
server.register-input.max-inputs-same-hash: max inputs with same hash (same origin tx) allowed to register to a mix
```
### SCodes
```
server.samourai-fees.feePayloadByScode[foo] = 12345
server.samourai-fees.feePayloadByScode[bar] = 23456
```
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.
### Mix limits
```
server.mix.anonymity-set-target = 10
......
......@@ -7,6 +7,7 @@ import com.samourai.wallet.util.FormatsUtilGeneric;
import com.samourai.wallet.util.MessageSignUtilGeneric;
import com.samourai.wallet.util.TxUtil;
import com.samourai.whirlpool.protocol.WhirlpoolProtocol;
import com.samourai.whirlpool.protocol.fee.WhirlpoolFee;
import com.samourai.whirlpool.server.services.CryptoService;
import java.lang.invoke.MethodHandles;
import nz.net.ultraq.thymeleaf.LayoutDialect;
......@@ -50,6 +51,11 @@ public class ServicesConfig {
return new WhirlpoolProtocol();
}
@Bean
WhirlpoolFee whirlpoolFee() {
return WhirlpoolFee.getInstance();
}
@Bean
FormatsUtilGeneric formatsUtilGeneric() {
return FormatsUtilGeneric.getInstance();
......
package com.samourai.whirlpool.server.config;
import com.samourai.whirlpool.protocol.WhirlpoolProtocol;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import javax.validation.constraints.NotEmpty;
......@@ -392,6 +393,7 @@ public class WhirlpoolServerConfig {
@NotEmpty private String xpub;
private long amount;
private SecretWalletConfig secretWallet;
private Map<String, Short> feePayloadByScode = new HashMap<>(); // -32,768 to 32,767
public String getXpub() {
return xpub;
......@@ -416,6 +418,14 @@ public class WhirlpoolServerConfig {
public void setSecretWallet(SecretWalletConfig secretWallet) {
this.secretWallet = secretWallet;
}
public Map<String, Short> getFeePayloadByScode() {
return feePayloadByScode;
}
public void setFeePayloadByScode(Map<String, Short> feePayloadByScode) {
this.feePayloadByScode = feePayloadByScode;
}
}
public static class SecretWalletConfig {
......
package com.samourai.whirlpool.server.controllers.rest;
import com.samourai.whirlpool.protocol.WhirlpoolEndpoint;
import com.samourai.whirlpool.protocol.WhirlpoolProtocol;
import com.samourai.whirlpool.protocol.rest.PoolInfo;
import com.samourai.whirlpool.protocol.rest.PoolsResponse;
import com.samourai.whirlpool.protocol.rest.RestErrorResponse;
import com.samourai.whirlpool.server.beans.Mix;
import com.samourai.whirlpool.server.beans.Pool;
import com.samourai.whirlpool.server.config.WhirlpoolServerConfig;
import com.samourai.whirlpool.server.services.FeeValidationService;
import com.samourai.whirlpool.server.services.PoolService;
import java.lang.invoke.MethodHandles;
......@@ -16,6 +18,7 @@ import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
......@@ -24,15 +27,20 @@ public class PoolsController extends AbstractRestController {
private PoolService poolService;
private FeeValidationService feeValidationService;
private WhirlpoolServerConfig serverConfig;
@Autowired
public PoolsController(PoolService poolService, FeeValidationService feeValidationService) {
public PoolsController(
PoolService poolService,
FeeValidationService feeValidationService,
WhirlpoolServerConfig serverConfig) {
this.poolService = poolService;
this.feeValidationService = feeValidationService;
this.serverConfig = serverConfig;
}
@RequestMapping(value = WhirlpoolEndpoint.REST_POOLS, method = RequestMethod.GET)
public PoolsResponse pools() {
public PoolsResponse pools(@RequestParam("scode") String scode) {
PoolInfo[] pools =
poolService
.getPools()
......@@ -40,7 +48,9 @@ public class PoolsController extends AbstractRestController {
.map(pool -> computePoolInfo(pool))
.toArray((i) -> new PoolInfo[i]);
String feePaymentCode = feeValidationService.getFeePaymentCode();
PoolsResponse poolsResponse = new PoolsResponse(pools, feePaymentCode);
byte[] feePayload = feeValidationService.getFeePayloadByScode(scode);
PoolsResponse poolsResponse =
new PoolsResponse(pools, feePaymentCode, WhirlpoolProtocol.encodeBytes(feePayload));
return poolsResponse;
}
......
......@@ -7,12 +7,14 @@ 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.WhirlpoolProtocol;
import com.samourai.whirlpool.protocol.fee.WhirlpoolFee;
import com.samourai.whirlpool.protocol.fee.WhirlpoolFeeData;
import com.samourai.whirlpool.server.beans.rpc.RpcTransaction;
import com.samourai.whirlpool.server.config.WhirlpoolServerConfig;
import com.samourai.whirlpool.server.config.WhirlpoolServerConfig.SecretWalletConfig;
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.ECKey;
import org.bitcoinj.core.Transaction;
......@@ -34,34 +36,37 @@ public class FeeValidationService {
private CryptoService cryptoService;
private FormatsUtilGeneric formatsUtil;
private WhirlpoolServerConfig whirlpoolServerConfig;
private WhirlpoolServerConfig serverConfig;
private Bech32UtilGeneric bech32Util;
private HD_WalletFactoryJava hdWalletFactory;
private BIP47Account secretAccountBip47;
private TxUtil txUtil;
private BlockchainDataService blockchainDataService;
private WhirlpoolFee whirlpoolFee;
public FeeValidationService(
CryptoService cryptoService,
FormatsUtilGeneric formatsUtil,
WhirlpoolServerConfig whirlpoolServerConfig,
WhirlpoolServerConfig serverConfig,
Bech32UtilGeneric bech32UtilGeneric,
HD_WalletFactoryJava hdWalletFactory,
TxUtil txUtil,
BlockchainDataService blockchainDataService)
BlockchainDataService blockchainDataService,
WhirlpoolFee whirlpoolFee)
throws Exception {
this.cryptoService = cryptoService;
this.formatsUtil = formatsUtil;
this.whirlpoolServerConfig = whirlpoolServerConfig;
this.serverConfig = serverConfig;
this.bech32Util = bech32UtilGeneric;
this.hdWalletFactory = hdWalletFactory;
this.secretAccountBip47 = computeSecretAccount();
this.txUtil = txUtil;
this.blockchainDataService = blockchainDataService;
this.whirlpoolFee = whirlpoolFee;
}
private BIP47Account computeSecretAccount() throws Exception {
SecretWalletConfig secretWallet = whirlpoolServerConfig.getSamouraiFees().getSecretWallet();
SecretWalletConfig secretWallet = serverConfig.getSamouraiFees().getSecretWallet();
HD_Wallet hdw =
hdWalletFactory.restoreWallet(
secretWallet.getWords(),
......@@ -73,12 +78,43 @@ public class FeeValidationService {
.getAccount(0);
}
public WhirlpoolFeeData decodeFeeData(Transaction tx) {
byte[] opReturnMaskedValue = findOpReturnValue(tx);
if (opReturnMaskedValue == null) {
return null;
}
// decode opReturnMaskedValue
TransactionOutPoint input0OutPoint = tx.getInput(0).getOutpoint();
Callback<byte[]> fetchInputOutpointScriptBytes =
computeCallbackFetchOutpointScriptBytes(input0OutPoint); // needed for P2PK
byte[] input0Pubkey = txUtil.findInputPubkey(tx, 0, fetchInputOutpointScriptBytes);
WhirlpoolFeeData feeData =
whirlpoolFee.decode(opReturnMaskedValue, secretAccountBip47, input0OutPoint, input0Pubkey);
return feeData;
}
public String getFeePaymentCode() {
return secretAccountBip47.getPaymentCode();
}
public boolean isValidTx0(Transaction tx0, WhirlpoolFeeData feeData) {
// validate feePayload
if (isValidFeePayload(feeData.getFeePayload())) {
return true;
} else {
// validate for feeIndice
return isTx0FeePaid(tx0, feeData.getFeeIndice());
}
}
protected boolean isTx0FeePaid(Transaction tx0, int x) {
long samouraiFeesMin = whirlpoolServerConfig.getSamouraiFees().getAmount();
if (x < 0) {
log.error("Invalid samouraiFee indice: " + x);
return false;
}
long samouraiFeesMin = serverConfig.getSamouraiFees().getAmount();
// find samourai payment address from xpub indice in tx payload
String feesAddressBech32 = computeFeeAddress(x);
......@@ -100,7 +136,7 @@ public class FeeValidationService {
protected String computeFeeAddress(int x) {
DeterministicKey mKey =
formatsUtil.createMasterPubKeyFromXPub(whirlpoolServerConfig.getSamouraiFees().getXpub());
formatsUtil.createMasterPubKeyFromXPub(serverConfig.getSamouraiFees().getXpub());
DeterministicKey cKey =
HDKeyDerivation.deriveChildKey(
mKey, new ChildNumber(0, false)); // assume external/receive chain
......@@ -111,23 +147,6 @@ public class FeeValidationService {
return feeAddressBech32;
}
protected Integer findFeeIndice(Transaction tx) {
byte[] opReturnMaskedValue = findOpReturnValue(tx);
if (opReturnMaskedValue == null) {
return null;
}
// decode opReturnMaskedValue
TransactionOutPoint input0OutPoint = tx.getInput(0).getOutpoint();
Callback<byte[]> fetchInputOutpointScriptBytes =
computeCallbackFetchOutpointScriptBytes(input0OutPoint); // needed for P2PK
byte[] input0Pubkey = txUtil.findInputPubkey(tx, 0, fetchInputOutpointScriptBytes);
Integer dataUnmasked =
WhirlpoolProtocol.getWhirlpoolFee()
.decode(opReturnMaskedValue, secretAccountBip47, input0OutPoint, input0Pubkey);
return dataUnmasked;
}
private Callback<byte[]> computeCallbackFetchOutpointScriptBytes(TransactionOutPoint outPoint) {
Callback<byte[]> fetchInputOutpointScriptBytes =
() -> {
......@@ -168,4 +187,38 @@ public class FeeValidationService {
}
return null;
}
private boolean isValidFeePayload(byte[] feePayload) {
if (feePayload == null || feePayload.length != WhirlpoolFee.FEE_PAYLOAD_LENGTH) {
return false;
}
// search in configuration
return getScodeByFeePayload(feePayload) != null;
}
protected String getScodeByFeePayload(byte[] feePayload) {
short feePayloadAsShort = Utils.feePayloadBytesToShort(feePayload);
Optional<Entry<String, Short>> feePayloadEntry =
serverConfig
.getSamouraiFees()
.getFeePayloadByScode()
.entrySet()
.stream()
.filter(e -> e.getValue() == feePayloadAsShort)
.findFirst();
if (!feePayloadEntry.isPresent()) {
// scode not found
return null;
}
return feePayloadEntry.get().getKey();
}
public byte[] getFeePayloadByScode(String scode) {
Short feePayloadAsShort = serverConfig.getSamouraiFees().getFeePayloadByScode().get(scode);
if (feePayloadAsShort == null) {
return null;
}
return Utils.feePayloadShortToBytes(feePayloadAsShort);
}
}
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.rpc.TxOutPoint;
import com.samourai.whirlpool.server.config.WhirlpoolServerConfig;
import com.samourai.whirlpool.server.exceptions.IllegalInputException;
import java.lang.invoke.MethodHandles;
import org.bitcoinj.core.ECKey;
import org.bitcoinj.core.Transaction;
import org.bouncycastle.util.encoders.Hex;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
......@@ -58,18 +60,45 @@ public class InputValidationService {
protected boolean checkInputProvenance(Transaction tx, long inputValue)
throws IllegalInputException {
// is it a tx0?
Integer x = feeValidationService.findFeeIndice(tx);
if (x != null) {
WhirlpoolFeeData feeData = feeValidationService.decodeFeeData(tx);
if (feeData != null) {
// this is a tx0 => mustMix
String feePayloadHex =
feeData.getFeePayload() != null ? Hex.toHexString(feeData.getFeePayload()) : "null";
if (log.isDebugEnabled()) {
log.debug(
"Validating input: txid="
+ tx.getHashAsString()
+ ", value="
+ inputValue
+ ": feeIndice="
+ feeData.getFeeIndice()
+ ", feePayloadHex="
+ feePayloadHex);
}
// check fees paid
if (!feeValidationService.isTx0FeePaid(tx, x)) {
if (!feeValidationService.isValidTx0(tx, feeData)) {
throw new IllegalInputException(
"Input rejected (invalid fee for tx0=" + tx.getHashAsString() + ", x=" + x + ")");
"Input rejected (invalid fee for tx0="
+ tx.getHashAsString()
+ ", x="
+ feeData.getFeeIndice()
+ ", feePayloadHex="
+ feePayloadHex
+ ")");
}
return false; // mustMix
} else {
// this is not a valid tx0 => liquidity coming from a previous whirlpool tx
if (log.isDebugEnabled()) {
log.debug(
"Validating input: txid="
+ tx.getHashAsString()
+ ", value="
+ inputValue
+ ": feeData=null");
}
boolean isWhirlpoolTx = isWhirlpoolTx(tx.getHashAsString(), inputValue);
if (!isWhirlpoolTx) {
......
......@@ -678,12 +678,14 @@ public class MixService {
String mixId = mix.getMixId();
// remove from confirming inputs
mix.removeConfirmingInputByUsername(username).ifPresent(confirmInput ->
log.info(" • ["
+ mixId
+ "] unregistered from confirming inputs, username="
+ username)
);
mix.removeConfirmingInputByUsername(username)
.ifPresent(
confirmInput ->
log.info(
" • ["
+ mixId
+ "] unregistered from confirming inputs, username="
+ username));
// mark registeredInput offline
List<ConfirmedInput> confirmedInputs =
......
......@@ -2,11 +2,13 @@ package com.samourai.whirlpool.server.utils;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.samourai.wallet.segwit.bech32.Bech32UtilGeneric;
import com.samourai.whirlpool.protocol.fee.WhirlpoolFee;
import com.samourai.whirlpool.server.beans.rpc.TxOutPoint;
import com.samourai.whirlpool.server.services.rpc.JSONRpcClientServiceImpl;
import com.samourai.whirlpool.server.services.rpc.RpcClientService;
import java.lang.invoke.MethodHandles;
import java.math.BigDecimal;
import java.nio.ByteBuffer;
import java.security.SecureRandom;
import java.util.Comparator;
import java.util.LinkedHashMap;
......@@ -126,4 +128,12 @@ public class Utils {
}
return null;
}
public static byte[] feePayloadShortToBytes(short feePayloadAsShort) {
return ByteBuffer.allocate(WhirlpoolFee.FEE_PAYLOAD_LENGTH).putShort(feePayloadAsShort).array();
}
public static short feePayloadBytesToShort(byte[] feePayload) {
return ByteBuffer.wrap(feePayload).getShort();
}
}
......@@ -28,9 +28,9 @@ server.register-input.min-confirmations-must-mix = 0
server.register-input.min-confirmations-liquidity = 0
server.register-input.max-inputs-same-hash = 1
server.register-output.timeout = 20
server.register-output.timeout = 200
server.signing.timeout = 40
server.reveal-output.timeout = 30
server.reveal-output.timeout = 200
# ban after x blames
server.ban.blames = 3
......
......@@ -8,12 +8,16 @@ import com.samourai.whirlpool.client.tx0.Tx0;
import com.samourai.whirlpool.client.tx0.Tx0Service;
import com.samourai.whirlpool.client.utils.Bip84Wallet;
import com.samourai.whirlpool.client.utils.indexHandler.MemoryIndexHandler;
import com.samourai.whirlpool.protocol.fee.WhirlpoolFeeData;
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.HashMap;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.function.BiFunction;
import org.bitcoinj.core.ECKey;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.core.TransactionOutPoint;
import org.junit.Assert;
import org.junit.Test;
......@@ -29,31 +33,54 @@ 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 short SCODE_FOO_PAYLOAD = 1234;
private static final String SCODE_BAR = "bar";
private static final short SCODE_BAR_PAYLOAD = 5678;
@Override
public void setUp() throws Exception {
super.setUp();
dbService.__reset();
serverConfig.getSamouraiFees().setAmount(FEES_VALID);
// feePayloadByScode
Map<String, Short> feePayloadByScode = new HashMap<>();
feePayloadByScode.put(SCODE_FOO, SCODE_FOO_PAYLOAD);
feePayloadByScode.put(SCODE_BAR, SCODE_BAR_PAYLOAD);
serverConfig.getSamouraiFees().setFeePayloadByScode(feePayloadByScode);
}
private void assertFeeData(String txid, Integer feeIndice, byte[] feePayload) {
RpcTransaction rpcTransaction =
blockchainDataService
.getRpcTransaction(txid)
.orElseThrow(() -> new NoSuchElementException());
WhirlpoolFeeData feeData = feeValidationService.decodeFeeData(rpcTransaction.getTx());
if (feeIndice == null && feePayload == null) {
Assert.assertNull(feeData);
} else {
Assert.assertEquals((int) feeIndice, feeData.getFeeIndice());
if (feePayload == null) {
Assert.assertNull(feeData.getFeePayload());
} else {
Assert.assertArrayEquals(feePayload, feeData.getFeePayload());
}
}
}
@Test
public void findFeeIndice() {
BiFunction<String, Integer, Void> test =
(String txid, Integer xpubIndiceExpected) -> {
log.info("Test: " + txid + ", " + xpubIndiceExpected);
RpcTransaction rpcTransaction =
blockchainDataService
.getRpcTransaction(txid)
.orElseThrow(() -> new NoSuchElementException());
Integer x = feeValidationService.findFeeIndice(rpcTransaction.getTx());
Assert.assertEquals(xpubIndiceExpected, x);
return null;
};
test.apply("cb2fad88ae75fdabb2bcc131b2f4f0ff2c82af22b6dd804dc341900195fb6187", 1);
test.apply("7ea75da574ebabf8d17979615b059ab53aae3011926426204e730d164a0d0f16", null);
test.apply("5369dfb71b36ed2b91ca43f388b869e617558165e4f8306b80857d88bdd624f2", null);
public void findFeeData() {
//
assertFeeData("cb2fad88ae75fdabb2bcc131b2f4f0ff2c82af22b6dd804dc341900195fb6187", null, null);
assertFeeData("7ea75da574ebabf8d17979615b059ab53aae3011926426204e730d164a0d0f16", null, null);
assertFeeData("5369dfb71b36ed2b91ca43f388b869e617558165e4f8306b80857d88bdd624f2", null, null);
assertFeeData(
"b3557587f87bcbd37e847a0fff0ded013b23026f153d85f28cb5d407d39ef2f3",
11,
Utils.feePayloadShortToBytes((short) 12345));
assertFeeData("aa77a502ca48540706c6f4a62f6c7155ee415c344a4481e0bf945fb56bbbdfdd", 12, null);
assertFeeData("604dac3fa5f83b810fc8f4e8d94d9283e4d0b53e3831d0fe6dc9ecdb15dd8dfb", 13, null);
}
@Test
......@@ -89,15 +116,19 @@ public class FeeValidationServiceTest extends AbstractIntegrationTest {
private boolean doIsTx0FeePaid(String txid, long minFees, int xpubIndice) {
serverConfig.getSamouraiFees().setAmount(minFees);
return feeValidationService.isTx0FeePaid(getTx(txid), xpubIndice);
}
private Transaction getTx(String txid) {
RpcTransaction rpcTransaction =
blockchainDataService
.getRpcTransaction(txid)
.orElseThrow(() -> new NoSuchElementException());
return feeValidationService.isTx0FeePaid(rpcTransaction.getTx(), xpubIndice);
return rpcTransaction.getTx();
}
@Test
public void isTx0FeePaidClient() throws Exception {
public void isValidTx0_feePayload1() throws Exception {
ECKey input0Key = new ECKey();
String input0OutPointAddress = new SegwitAddress(input0Key, params).getBech32AsString();
TransactionOutPoint input0OutPoint =
......@@ -111,6 +142,7 @@ public class FeeValidationServiceTest extends AbstractIntegrationTest {
String xpubSamouraiFee = serverConfig.getSamouraiFees().getXpub();
String feePaymentCode = feeValidationService.getFeePaymentCode();
int feeIndice = 123456;
byte[] feePayload = new byte[] {123, 110};
Tx0 tx0 =
new Tx0Service(params)
.tx0(
......@@ -123,10 +155,51 @@ public class FeeValidationServiceTest extends AbstractIntegrationTest {
xpubSamouraiFee,
FEES_VALID,
feePaymentCode,
feeIndice);
feeIndice,
feePayload);
WhirlpoolFeeData feeData = feeValidationService.decodeFeeData(tx0.getTx());
Assert.assertEquals(feeIndice, feeData.getFeeIndice());
Assert.assertEquals(feePayload, feeData.getFeePayload());
Assert.assertTrue(feeValidationService.isValidTx0(tx0.getTx(), feeData));
}
@Test
public void isValidTx0_feePayload2() {
// reject nofee when unknown feePayload
String txid = "b3557587f87bcbd37e847a0fff0ded013b23026f153d85f28cb5d407d39ef2f3";
int xpubIndice = feeValidationService.findFeeIndice(tx0.getTx());
Assert.assertEquals(feeIndice, xpubIndice);
Assert.assertTrue(feeValidationService.isTx0FeePaid(tx0.getTx(), xpubIndice));
Transaction tx = getTx(txid);
Assert.assertFalse(feeValidationService.isValidTx0(tx, feeValidationService.decodeFeeData(tx)));
// accept when valid feePayload
serverConfig.getSamouraiFees().getFeePayloadByScode().put("myscode", (short) 12345);
Assert.assertTrue(feeValidationService.isValidTx0(tx, feeValidationService.decodeFeeData(tx)));
}
@Test
public void getFeePayloadByScode() throws Exception {
Assert.assertEquals(
SCODE_FOO_PAYLOAD,
Utils.feePayloadBytesToShort(feeValidationService.getFeePayloadByScode(SCODE_FOO)));
Assert.assertEquals(
SCODE_BAR_PAYLOAD,
Utils.feePayloadBytesToShort(feeValidationService.getFeePayloadByScode(SCODE_BAR)));