package org.coinjoin.server;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.security.KeyPair;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import org.bitcoinj.core.BlockChain;
import org.bitcoinj.core.CheckpointManager;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.NetworkParameters;
import org.bitcoinj.core.PeerGroup;
import org.bitcoinj.core.ScriptException;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.core.TransactionInput;
import org.bitcoinj.core.TransactionOutput;
import org.bitcoinj.core.Wallet;
import org.bitcoinj.core.Wallet.SendRequest;
import org.bitcoinj.params.TestNet3Params;
import org.bitcoinj.store.BlockStore;
import org.bitcoinj.store.BlockStoreException;
import org.bitcoinj.store.SPVBlockStore;
import org.bitcoinj.store.UnreadableWalletException;
import org.coinjoin.util.RSABlindSignUtil;
public class MainServer {
public final static long CHUNK_SIZE = 1000000;
public final static int MIN_PARTICIPANTS = 2;
private final static NetworkParameters params = new TestNet3Params();
public final static Lock mutex = new ReentrantLock();
private boolean finished;
public enum TxStatus {
OPEN, PENDING, SIGNING, FAILED, BROADCAST, CLEARED
}
/*
* Data class wrapping Transactions
*/
public class TxWrapper {
public Transaction tx;
public KeyPair rsa;
public TxStatus status;
public int statusTime;
public int regOutputs;
public int signedInputs;
}
// Map TXIDs to Transactions
private HashMap<Integer, TxWrapper> transactionMap;
// Incoming HTTPS Server
private SSLListener httpsServer;
// BTC Network Interaction Classes
private PeerGroup peerGroup;
private Wallet wallet;
private BlockChain bChain;
private File walletFile;
@SuppressWarnings("deprecation")
public MainServer(int port, File walletFile, File blockFile) {
httpsServer = new SSLListener(port);
transactionMap = new HashMap<Integer, TxWrapper>();
this.walletFile = walletFile;
// Set up Local Wallet
wallet = null;
if (walletFile.exists())
try {
System.out.println("Reading Wallet from File: " + walletFile.getName());
wallet = Wallet.loadFromFile(walletFile);
} catch (UnreadableWalletException e1) {
e1.printStackTrace();
System.exit(1);
}
else {
System.out.println("Creating new wallet.");
wallet = new Wallet(params);
}
wallet.autosaveToFile(walletFile, 1, TimeUnit.MINUTES, null);
// Set Up Local SPV BlockChain and BlockStore
System.out.println("Setting up blockchain from File: " + blockFile.getName());
try {
boolean chainExistedAlready = blockFile.exists();
BlockStore blockStore = new SPVBlockStore(params, blockFile);
if (!chainExistedAlready) {
File checkpointsFile = new File("checkpoints.dat"); // Replace path to the file here.
FileInputStream stream = new FileInputStream(checkpointsFile);
CheckpointManager.checkpoint(params, stream, blockStore, wallet.getEarliestKeyCreationTime());
}
bChain = new BlockChain(params, wallet, blockStore);
} catch (BlockStoreException | IOException e) {
e.printStackTrace();
try {
wallet.saveToFile(walletFile);
} catch (IOException e1) {
e1.printStackTrace();
}
System.exit(1);
}
// Create PeerGroup to connect to BTC Network
System.out.println("Setting Up PeerGroup Connections...");
peerGroup = new PeerGroup(params, bChain);
peerGroup.addWallet(wallet);
peerGroup.startAndWait();
System.out.println("Bitcoin Initialized! Send Fee Donations to: " + wallet.currentReceiveAddress().toString());
}
public Integer currentOpenID() {
for (int txid : transactionMap.keySet()) {
if (transactionMap.get(txid).status == TxStatus.OPEN)
return txid;
}
return null;
}
/**
* Does one final verification, broadcasts the transaction, and sends it on
* its merry way!
* @param wrapper: Previously locked TxWrapper from transactionMap
* @return null on success, or error message
*/
public String broadcastTransaction(TxWrapper wrapper) {
try {
wrapper.tx.verify();
for(TransactionInput i : wrapper.tx.getInputs())
i.verify();
} catch (Exception e) {
e.printStackTrace();
return e.toString();
}
peerGroup.broadcastTransaction(wrapper.tx);
wrapper.status = TxStatus.BROADCAST;
return null;
}
/**
* Calculates the proper fee for the (already locked) transaction and adds/signs the fee.
* @param wrapper: a previously locked transaction in transactionMap
* @return: null on Success, or an Error Message
*/
public String feeTransaction(TxWrapper wrapper) {
// Calculate current fee from users.
Coin fee = wrapper.tx.getFee();
// Calculate minimum fee for transaction
long minFee = (wrapper.tx.bitcoinSerialize().length / 1000) * 10000;
ArrayList<TransactionOutput> inputsToSign = new ArrayList<TransactionOutput>();
// Loop through wallet looking for fee money
for (TransactionOutput t : wallet.calculateAllSpendCandidates(true)) {
if(minFee <= fee.value) break;
inputsToSign.add(t);
fee.add(t.getValue());
}
if (minFee > fee.value) {
// Not enough fee in wallet!
return "Cannot cover transaction fee!";
}
// Add change for fee money.
wrapper.tx.addOutput(Coin.valueOf(fee.value - minFee), wallet.currentReceiveAddress());
// Sign and Verify fee inputs
for (TransactionOutput o: inputsToSign)
try {
TransactionInput i = wrapper.tx.addSignedInput(o, wallet.findKeyFromPubHash(o.getScriptPubKey().getPubKeyHash()));
i.verify();
} catch (ScriptException e) {
e.printStackTrace();
return e.toString();
}
return null;
}
public void start() {
httpsServer.start();
finished = false;
TxWrapper currentTx = new TxWrapper();
currentTx.rsa = RSABlindSignUtil.freshRSAKeyPair();
currentTx.status = TxStatus.OPEN;
currentTx.tx = new Transaction(params);
currentTx.statusTime = 0;
currentTx.regOutputs = 0;
currentTx.signedInputs = 0;
transactionMap.put(currentTx.rsa.getPublic().hashCode(), currentTx);
// Main Server Loop, executed once per second
while (!finished) {
mutex.lock();
/*
* 1. Loop through Transactions, locking each one.
* 2. If transaction is OPEN and has at least 3 participants, mark it PENDING,
* and create a new OPEN transaction.
* 3. If transaction has been PENDING for 5 seconds, mark it FAILED.
* 4. If transaction has been SIGNING for 5 seconds, mark it FAILED.
* 5. If transaction has been FAILED for 5 seconds, erase it from memory.
* 6. If the confidence level of a BROADCAST transaction is high, erase it
* from memory.
*/
for(Object obj : transactionMap.values().toArray()) {
TxWrapper wrapper = (TxWrapper) obj;
System.out.println("Pruning Transactions!");
try {
switch(wrapper.status) {
case OPEN:
if (wrapper.tx.getInputs().size() >= MIN_PARTICIPANTS) {
System.out.println(wrapper.rsa.getPublic().hashCode() + " is now PENDING");
wrapper.status = TxStatus.PENDING;
// Create New Open Transaction
currentTx = new TxWrapper();
currentTx.rsa = RSABlindSignUtil.freshRSAKeyPair();
currentTx.status = TxStatus.OPEN;
currentTx.tx = new Transaction(params);
currentTx.statusTime = 0;
currentTx.regOutputs = 0;
currentTx.signedInputs = 0;
transactionMap.put(currentTx.rsa.getPublic().hashCode(), currentTx);
}
break;
case PENDING:
if (wrapper.statusTime >= 50)
wrapper.status = TxStatus.FAILED;
else wrapper.statusTime++;
break;
case SIGNING:
if (wrapper.statusTime >= 50)
wrapper.status = TxStatus.FAILED;
else wrapper.statusTime++;
break;
case FAILED:
if (wrapper.statusTime >= 50) {
transactionMap.remove(wrapper.rsa.getPublic().hashCode());
}
else wrapper.statusTime++;
break;
case BROADCAST:
switch(wrapper.tx.getConfidence().getConfidenceType()) {
case BUILDING: // Transaction Succeeded!
transactionMap.remove(wrapper.rsa.getPublic().hashCode());
break;
case DEAD: // Transaction Failed
wrapper.status = TxStatus.FAILED;
break;
default:
break;
}
break;
default:
break;
}
} finally {
}
}
mutex.unlock();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
* Locks transaction (to maintain thread safety) and returns it.
* @param txid: Transaction ID
* @return TxWrapper corresponding to transaction id.
*/
public TxWrapper lockTransaction(int txid) {
mutex.lock();
TxWrapper wrapper = transactionMap.get(txid);
return wrapper;
}
/**
* Unlocks transaction so it can be used by another thread.
* @param txid: Transaction ID
*/
public void releaseTransaction(TxWrapper wrapper) {
mutex.unlock();
}
@SuppressWarnings("deprecation")
public void shutdown() {
finished = true;
// Shutdown Services
try {
wallet.saveToFile(walletFile);
System.out.println("Saved wallet to File: " + walletFile.getAbsolutePath());
peerGroup.stop();
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
final MainServer server = new MainServer(4444, new File("wallet.dat"), new File("blockchain.dat"));
SSLAPI.server = server;
Runtime.getRuntime().addShutdownHook(new Thread()
{
@Override
public void run()
{
server.shutdown();
}
});
server.start();
}
}