Permalink
Join GitHub today
GitHub is home to over 28 million developers working together to host and review code, manage projects, and build software together.
Sign up
Fetching contributors…
Cannot retrieve contributors at this time.
Cannot retrieve contributors at this time
| package io.coinswap.swap; | |
| import net.minidev.json.JSONObject; | |
| import io.mappum.altcoinj.core.*; | |
| import io.mappum.altcoinj.crypto.TransactionSignature; | |
| import io.mappum.altcoinj.script.Script; | |
| import io.mappum.altcoinj.script.ScriptBuilder; | |
| import io.mappum.altcoinj.script.ScriptOpCodes; | |
| import io.mappum.altcoinj.utils.Threading; | |
| import org.slf4j.LoggerFactory; | |
| import java.io.IOException; | |
| import java.io.ObjectInputStream; | |
| import java.io.ObjectOutputStream; | |
| import java.io.Serializable; | |
| import java.util.*; | |
| import java.util.concurrent.Executor; | |
| import java.util.concurrent.locks.ReentrantLock; | |
| import static com.google.common.base.Preconditions.checkNotNull; | |
| import static com.google.common.base.Preconditions.checkState; | |
| public class AtomicSwap implements Serializable { | |
| private static final org.slf4j.Logger log = LoggerFactory.getLogger(AtomicSwap.class); | |
| protected static final ReentrantLock LOCK = Threading.lock(AtomicSwap.class.getName()); | |
| protected ReentrantLock lock = LOCK; | |
| private static final long serialVersionUID = 1; | |
| public static final int VERSION = 0; | |
| private static final int REFUND_PERIOD = 4 * 60; // in minutes | |
| private static final int REFUND_BROADCAST_DELAY = 10; // in seconds | |
| private Map<StateListener, Executor> listeners; | |
| private List<ECKey>[] keys; | |
| private byte[] x; | |
| private byte[] xHash; | |
| private Sha256Hash[] bailinHashes; | |
| private Sha256Hash[] payoutHashes; | |
| private Sha256Hash[] refundHashes; | |
| private Transaction[] bailinTxs; | |
| private byte[][][] payoutSigs; | |
| private byte[][][] refundSigs; | |
| private long time; | |
| public String id; | |
| public AtomicSwapTrade trade; | |
| public boolean switched; | |
| public enum Step { | |
| STARTING, | |
| EXCHANGING_KEYS, | |
| EXCHANGING_BAILIN_HASHES, | |
| EXCHANGING_SIGNATURES, | |
| WAITING_FOR_BAILIN, | |
| WAITING_FOR_PAYOUT, // alice-only | |
| COMPLETE, | |
| WAITING_FOR_REFUND, | |
| CANCELED | |
| } | |
| private Step step = Step.STARTING; | |
| public enum Party { | |
| ALICE, | |
| BOB, | |
| SERVER | |
| } | |
| // the state machine for a swap | |
| public AtomicSwap(String id, AtomicSwapTrade trade, long time) { | |
| this.id = checkNotNull(id); | |
| this.trade = checkNotNull(trade); | |
| this.time = time; | |
| keys = new ArrayList[3]; | |
| bailinHashes = new Sha256Hash[2]; | |
| payoutHashes = new Sha256Hash[2]; | |
| refundHashes = new Sha256Hash[2]; | |
| bailinTxs = new Transaction[2]; | |
| payoutSigs = new byte[2][4][]; | |
| refundSigs = new byte[2][3][]; | |
| listeners = new HashMap<StateListener, Executor>(); | |
| } | |
| // returns true if we are still in the setup stage, e.g. have not yet | |
| // committed to any transactions | |
| public boolean isSettingUp() { | |
| lock.lock(); | |
| try { | |
| return step.ordinal() <= Step.EXCHANGING_SIGNATURES.ordinal(); | |
| } finally { | |
| lock.unlock(); | |
| } | |
| } | |
| // returns true if we have started communicating with the other party to | |
| // start the atomic swap | |
| public boolean isStarted() { | |
| return !getStep().equals(Step.STARTING); | |
| } | |
| // returns true if we have either successfully finished the swap, or have | |
| // canceled it and recovered our funds | |
| public boolean isDone() { | |
| Step step = getStep(); | |
| return step == Step.COMPLETE || step == Step.CANCELED; | |
| } | |
| public Step getStep() { | |
| lock.lock(); | |
| try { | |
| return step; | |
| } finally { | |
| lock.unlock(); | |
| } | |
| } | |
| public void setStep(final Step step) { | |
| Step previous; | |
| lock.lock(); | |
| try { | |
| previous = this.step; | |
| this.step = step; | |
| } finally { | |
| lock.unlock(); | |
| } | |
| // trigger event | |
| if(step != previous) { | |
| final AtomicSwap parent = this; | |
| for(final StateListener listener : listeners.keySet()) { | |
| listeners.get(listener).execute(new Runnable() { | |
| @Override | |
| public void run() { | |
| listener.onStepChange(step, parent); | |
| } | |
| }); | |
| } | |
| } | |
| } | |
| public long getTime() { | |
| lock.lock(); | |
| try { | |
| return time; | |
| } finally { | |
| lock.unlock(); | |
| } | |
| } | |
| public List<ECKey> getKeys(boolean alice) { | |
| return getKeys(alice ? Party.ALICE : Party.BOB); | |
| } | |
| public List<ECKey> getKeys(Party party) { | |
| lock.lock(); | |
| try { | |
| return keys[party.ordinal()]; | |
| } finally { | |
| lock.unlock(); | |
| } | |
| } | |
| public void setKeys(boolean alice, List<ECKey> keys) { | |
| setKeys(alice ? Party.ALICE : Party.BOB, keys); | |
| } | |
| public void setKeys(Party party, List<ECKey> keys) { | |
| if(party == Party.ALICE || party == Party.BOB) { | |
| checkState(keys.size() == 3); | |
| checkNotNull(keys.get(0)); | |
| checkNotNull(keys.get(1)); | |
| checkNotNull(keys.get(2)); | |
| } else if(party == Party.SERVER) { | |
| checkState(keys.size() == 2); | |
| checkNotNull(keys.get(0)); | |
| checkNotNull(keys.get(1)); | |
| } | |
| lock.lock(); | |
| try { | |
| this.keys[party.ordinal()] = keys; | |
| } finally { | |
| lock.unlock(); | |
| } | |
| } | |
| public byte[] getXHash() { | |
| lock.lock(); | |
| try { | |
| return xHash; | |
| } finally { | |
| lock.unlock(); | |
| } | |
| } | |
| public void setXKey(ECKey key) { | |
| checkNotNull(key); | |
| byte[] x = ScriptBuilder.createOutputScript(key).getProgram(); | |
| setX(x); | |
| } | |
| public void setX(byte[] x) { | |
| checkNotNull(x); | |
| byte[] hash = Utils.Hash160(x); | |
| lock.lock(); | |
| try { | |
| this.x = x; | |
| if(xHash == null) xHash = hash; | |
| else checkState(Arrays.equals(xHash, hash)); | |
| } finally { | |
| lock.unlock(); | |
| } | |
| } | |
| public byte[] getX() { | |
| lock.lock(); | |
| try { | |
| return x; | |
| } finally { | |
| lock.unlock(); | |
| } | |
| } | |
| public void setXHash(byte[] hash) { | |
| checkNotNull(hash); | |
| lock.lock(); | |
| try { | |
| checkState(x == null); | |
| xHash = hash; | |
| } finally { | |
| lock.unlock(); | |
| } | |
| } | |
| public Sha256Hash getBailinHash(boolean alice) { | |
| lock.lock(); | |
| try { | |
| return bailinHashes[alice ? 0 : 1]; | |
| } finally { | |
| lock.unlock(); | |
| } | |
| } | |
| public void setBailinHash(boolean alice, Sha256Hash hash) { | |
| checkNotNull(hash); | |
| int a = alice ? 0 : 1; | |
| lock.lock(); | |
| try { | |
| checkState(bailinHashes[a] == null); | |
| bailinHashes[a] = hash; | |
| } finally { | |
| lock.unlock(); | |
| } | |
| } | |
| public Sha256Hash getPayoutHash(boolean alice) { | |
| lock.lock(); | |
| try { | |
| return payoutHashes[alice ? 0 : 1]; | |
| } finally { | |
| lock.unlock(); | |
| } | |
| } | |
| public void setPayoutHash(boolean alice, Sha256Hash hash) { | |
| checkNotNull(hash); | |
| int a = alice ? 0 : 1; | |
| lock.lock(); | |
| try { | |
| checkState(payoutHashes[a] == null); | |
| payoutHashes[a] = hash; | |
| } finally { | |
| lock.unlock(); | |
| } | |
| } | |
| public Sha256Hash getRefundHash(boolean alice) { | |
| lock.lock(); | |
| try { | |
| return refundHashes[alice ? 0 : 1]; | |
| } finally { | |
| lock.unlock(); | |
| } | |
| } | |
| public void setRefundHash(boolean alice, Sha256Hash hash) { | |
| checkNotNull(hash); | |
| int a = alice ? 0 : 1; | |
| lock.lock(); | |
| try { | |
| checkState(refundHashes[a] == null); | |
| refundHashes[a] = hash; | |
| } finally { | |
| lock.unlock(); | |
| } | |
| } | |
| public void setBailinTx(boolean alice, Transaction tx) { | |
| checkNotNull(tx); | |
| Sha256Hash hash = tx.getHash(); | |
| int a = alice ? 0 : 1; | |
| lock.lock(); | |
| try { | |
| checkState(bailinTxs[a] == null); | |
| bailinTxs[a] = tx; | |
| if(bailinHashes[a] == null) bailinHashes[a] = hash; | |
| else checkState(hash.compareTo(bailinHashes[a]) == 0); | |
| } finally { | |
| lock.unlock(); | |
| } | |
| } | |
| public Transaction getBailinTx(boolean alice) { | |
| int a = alice ? 0 : 1; | |
| lock.lock(); | |
| try { | |
| return checkNotNull(bailinTxs[a]); | |
| } finally { | |
| lock.unlock(); | |
| } | |
| } | |
| public void setPayoutSig(boolean aliceTx, int i, ECKey.ECDSASignature sig) { | |
| lock.lock(); | |
| try { | |
| checkState(payoutSigs[aliceTx ? 0 : 1][i] == null); | |
| payoutSigs[aliceTx ? 0 : 1][i] = sig.encodeToDER(); | |
| } finally { | |
| lock.unlock(); | |
| } | |
| } | |
| public TransactionSignature getPayoutSig(boolean aliceTx, int i) { | |
| lock.lock(); | |
| try { | |
| if(payoutSigs[aliceTx ? 0 : 1][i] == null) return null; | |
| ECKey.ECDSASignature sig = ECKey.ECDSASignature.decodeFromDER(payoutSigs[aliceTx ? 0 : 1][i]); | |
| return new TransactionSignature(sig, Transaction.SigHash.ALL, false); | |
| } finally { | |
| lock.unlock(); | |
| } | |
| } | |
| public void setRefundSig(boolean aliceTx, int i, ECKey.ECDSASignature sig) { | |
| lock.lock(); | |
| try { | |
| checkState(refundSigs[aliceTx ? 0 : 1][i] == null); | |
| refundSigs[aliceTx ? 0 : 1][i] = sig.encodeToDER(); | |
| } finally { | |
| lock.unlock(); | |
| } | |
| } | |
| public TransactionSignature getRefundSig(boolean aliceTx, int i) { | |
| lock.lock(); | |
| try { | |
| if(refundSigs[aliceTx ? 0 : 1][i] == null) return null; | |
| ECKey.ECDSASignature sig = ECKey.ECDSASignature.decodeFromDER(refundSigs[aliceTx ? 0 : 1][i]); | |
| return new TransactionSignature(sig, Transaction.SigHash.ALL, false); | |
| } finally { | |
| lock.unlock(); | |
| } | |
| } | |
| public long getLocktime(boolean alice) { | |
| int period = REFUND_PERIOD * (alice ? 1 : 2) * 60; | |
| return getTime() + period; | |
| } | |
| public long getTimeUntilRefund(boolean alice) { | |
| long secondsLeft = getLocktime(alice) - System.currentTimeMillis() / 1000; | |
| secondsLeft += REFUND_BROADCAST_DELAY; // wait some extra time to make sure we're over the locktime | |
| return secondsLeft; | |
| } | |
| public Script getMultisigRedeem(boolean alice) { | |
| lock.lock(); | |
| try { | |
| List<ECKey> multiSigKeys = new ArrayList<ECKey>(2); | |
| multiSigKeys.add(keys[0].get(0)); | |
| multiSigKeys.add(keys[1].get(0)); | |
| multiSigKeys.add(keys[2].get(alice ? 0 : 1)); | |
| return ScriptBuilder.createMultiSigOutputScript(2, multiSigKeys); | |
| } finally { | |
| lock.unlock(); | |
| } | |
| } | |
| public Script getHashlockScript(boolean alice) { | |
| lock.lock(); | |
| try { | |
| if (alice) return new Script(getX()); | |
| return new ScriptBuilder() | |
| .op(ScriptOpCodes.OP_HASH160) | |
| .data(getXHash()) | |
| .op(ScriptOpCodes.OP_EQUALVERIFY) | |
| .data(getKeys(true).get(0).getPubKey()) | |
| .op(ScriptOpCodes.OP_CHECKSIG) | |
| .build(); | |
| } finally { | |
| lock.unlock(); | |
| } | |
| } | |
| public Script getPayoutOutput(NetworkParameters params, boolean alice) { | |
| Address address = getKeys(alice).get(2).toAddress(params); | |
| return ScriptBuilder.createOutputScript(address); | |
| } | |
| public String getChannelId(boolean alice) { | |
| return "swap:" + id + ":" + (alice ? "0" : "1"); | |
| } | |
| public void addEventListener(StateListener listener) { | |
| addEventListener(listener, Threading.SAME_THREAD); | |
| } | |
| public void addEventListener(StateListener listener, Executor executor) { | |
| lock.lock(); | |
| try { | |
| this.listeners.put(listener, executor); | |
| } finally { | |
| lock.unlock(); | |
| } | |
| } | |
| public void removeEventListener(StateListener listener) { | |
| lock.lock(); | |
| try { | |
| this.listeners.remove(listener); | |
| } finally { | |
| lock.unlock(); | |
| } | |
| } | |
| public Map toJson(boolean compact) { | |
| JSONObject data = new JSONObject(); | |
| data.put("id", id); | |
| data.put("trade", trade.toJson()); | |
| data.put("time", time + ""); | |
| if(!compact) { | |
| data.put("step", step); | |
| data.put("trade", trade.toJson()); | |
| } | |
| return data; | |
| } | |
| public static AtomicSwap fromJson(Map data) { | |
| String id = (String) checkNotNull(data.get("id")); | |
| AtomicSwapTrade trade = AtomicSwapTrade.fromJson((Map) checkNotNull(data.get("trade"))); | |
| long time = Long.valueOf((String) checkNotNull(data.get("time"))); | |
| return new AtomicSwap(id, trade, time); | |
| } | |
| private void writeObject(ObjectOutputStream out) throws IOException { | |
| lock.lock(); | |
| try { | |
| out.writeObject(x); | |
| out.writeObject(xHash); | |
| out.writeObject(bailinHashes); | |
| out.writeObject(payoutHashes); | |
| out.writeObject(refundHashes); | |
| out.writeObject(bailinTxs); | |
| out.writeObject(payoutSigs); | |
| out.writeObject(refundSigs); | |
| out.writeObject(time); | |
| out.writeObject(id); | |
| out.writeObject(trade); | |
| out.writeObject(step); | |
| out.writeObject(switched); | |
| List<byte[]>[] keys = new List[2]; | |
| keys[0] = new ArrayList<byte[]>(3); | |
| if(this.keys[0] != null) | |
| for(ECKey key : this.keys[0]) keys[0].add(key.getPubKey()); | |
| keys[1] = new ArrayList<byte[]>(3); | |
| if(this.keys[1] != null) | |
| for(ECKey key : this.keys[1]) keys[1].add(key.getPubKey()); | |
| out.writeObject(keys); | |
| } finally { | |
| lock.unlock(); | |
| } | |
| } | |
| private void readObject(ObjectInputStream in) throws IOException { | |
| this.lock = LOCK; | |
| lock.lock(); | |
| try { | |
| x = (byte[]) in.readObject(); | |
| xHash = (byte[]) in.readObject(); | |
| bailinHashes = (Sha256Hash[]) in.readObject(); | |
| payoutHashes = (Sha256Hash[]) in.readObject(); | |
| refundHashes = (Sha256Hash[]) in.readObject(); | |
| bailinTxs = (Transaction[]) in.readObject(); | |
| payoutSigs = (byte[][][]) in.readObject(); | |
| refundSigs = (byte[][][]) in.readObject(); | |
| time = (long) in.readObject(); | |
| id = (String) in.readObject(); | |
| trade = (AtomicSwapTrade) in.readObject(); | |
| step = (AtomicSwap.Step) in.readObject(); | |
| switched = (boolean) in.readObject(); | |
| List<byte[]>[] keys = (List<byte[]>[]) in.readObject(); | |
| this.keys = new List[2]; | |
| this.keys[0] = new ArrayList<ECKey>(keys[0].size()); | |
| for(byte[] key : keys[0]) this.keys[0].add(ECKey.fromPublicOnly(key)); | |
| this.keys[1] = new ArrayList<ECKey>(keys[1].size()); | |
| for(byte[] key : keys[1]) this.keys[1].add(ECKey.fromPublicOnly(key)); | |
| listeners = new HashMap<StateListener, Executor>(); | |
| // hack to ensure deserialized transactions are properly initialized | |
| if(bailinTxs[0] != null) bailinTxs[0] = new Transaction(bailinTxs[0].getParams(), | |
| bailinTxs[0].bitcoinSerialize()); | |
| if(bailinTxs[1] != null) bailinTxs[1] = new Transaction(bailinTxs[1].getParams(), | |
| bailinTxs[1].bitcoinSerialize()); | |
| } catch (Exception e) { | |
| e.printStackTrace(); | |
| } finally { | |
| lock.unlock(); | |
| } | |
| } | |
| public interface StateListener { | |
| public void onStepChange(AtomicSwap.Step step, AtomicSwap swap); | |
| } | |
| } | |