Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: fast exits #258

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@
"ethereumjs-util": "6.0.0",
"fork-ts-checker-webpack-plugin": "^0.4.15",
"jsbi-utils": "^1.0.0",
"leap-core": "0.38.0",
"leap-core": "0.39.0",
"mobx": "^5.0.3",
"mobx-react": "^5.2.3",
"prop-types": "^15.6.1",
Expand Down
4 changes: 2 additions & 2 deletions src/config/testnet/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
"title": "LeapDAO Testnet — The Raft release",
"description": "Plasma network running transfer-only MoreVP governed by LeapDAO",
"consensus": "poa",
"nodes": ["wss://testnet-node1.leapdao.org:1443"],
"nodes": ["https://testnet-node1.leapdao.org"],
"tokenFormUrl": "https://docs.google.com/forms/d/e/1FAIpQLSdFAezroU_uxvWWQmMxs_DWAasl5UwC_nQXIy0CtOfmgDVE2w/viewform?embedded=true",
"exitMarketMaker": "https://cok43k06u1.execute-api.eu-west-1.amazonaws.com/v0/sellExit",
"exitMarketMaker": "https://2nuxsb25he.execute-api.eu-west-1.amazonaws.com/testnet/sellExit",
"tokenFaucet": "https://jw98dxp219.execute-api.eu-west-1.amazonaws.com/testnet",
"denverFaucet": "https://jw98dxp219.execute-api.eu-west-1.amazonaws.com/testnet/address",
"txStorage": "https://57scxrw3ql.execute-api.eu-west-1.amazonaws.com/v0"
Expand Down
14 changes: 4 additions & 10 deletions src/routes/wallet/exit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,9 @@ export default class Exit extends React.Component<ExitProps, any> {
)
.map(u => {
const inputHash = bufferToHex(u.outpoint.hash);
const utxoId = u.outpoint.hex();
return {
key: u.outpoint.hex(),
key: utxoId,
value: (
<TokenValue
{...{
Expand Down Expand Up @@ -104,21 +105,14 @@ export default class Exit extends React.Component<ExitProps, any> {
⚡ Fast exit
<br />
<br />
{pendingFastExits[inputHash].sig === '' &&
{!pendingFastExits[utxoId].sent &&
'Signature required'}
{pendingFastExits[inputHash].sig !== '' && (
<Fragment>
Waiting for block{' '}
{pendingFastExits[inputHash].effectiveBlock}{' '}
to payout.
</Fragment>
)}
</Fragment>
}
>
<span>🕐 Exiting</span>
</Tooltip>
{pendingFastExits[inputHash].sig === '' && (
{!pendingFastExits[utxoId].sent && (
<Button
size="small"
style={{ marginLeft: '10px' }}
Expand Down
201 changes: 74 additions & 127 deletions src/stores/unspents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,13 @@
* found in the LICENSE file in the root directory of this source tree.
*/

import { observable, reaction, computed, when } from 'mobx';
import {
observable,
reaction,
computed,
autorun,
IObservableArray,
} from 'mobx';
import {
Unspent,
Tx,
Expand All @@ -18,19 +24,16 @@ import {
Exit,
PeriodData,
} from 'leap-core';
import { bufferToHex, toBuffer } from 'ethereumjs-util';
import { bufferToHex } from 'ethereumjs-util';
import autobind from 'autobind-decorator';
import { bi } from 'jsbi-utils';

import { CONFIG } from '../config';
import storage from '../utils/storage';
import { accountStore } from './account';
import { exitHandlerStore } from './exitHandler';
import { bridgeStore } from './bridge';
import { nodeStore } from './node';
import { operatorStore } from './operator';
import { web3PlasmaStore } from './web3/plasma';
import { tokensStore } from './tokens';
import { web3InjectedStore } from './web3/injected';

const { getYoungestInputTx, getProof } = helpers;
Expand All @@ -40,18 +43,16 @@ type UnspentWithTx = Unspent & {
pendingFastExit?: boolean;
};

const objectify = (unspent: UnspentWithTx): UnspentWithTx => {
if (!unspent.outpoint.toJSON) {
unspent.outpoint = Outpoint.fromJSON(
(unspent.outpoint as any) as OutpointJSON
);
const objectify = (outpoint: Outpoint): Outpoint => {
if (!outpoint.toJSON) {
return Outpoint.fromJSON((outpoint as any) as OutpointJSON);
}
return unspent;
return outpoint;
};

export class UnspentsStore {
@observable
public list: UnspentWithTx[] = observable.array([]);
public list: IObservableArray<UnspentWithTx> = observable.array([]);

@observable
public pendingFastExits: {};
Expand All @@ -69,21 +70,13 @@ export class UnspentsStore {
exitHandlerStore.contract.events.ExitStarted({}, this.fetchUnspents);
}
);
reaction(
() => bridgeStore.contract,
() => {
bridgeStore.contract.events.NewHeight({}, this.finalizeFastExits);
}
);
reaction(() => nodeStore.latestBlock, this.fetchUnspents);
when(
() => this.latestBlock && !!operatorStore.slots[0],
() => this.finalizeFastExits(null, {})
);

this.pendingFastExits = storage.load('pendingFastExits');
autorun(this.storePendingFastExits);
}

@autobind
private storePendingFastExits() {
storage.store('pendingFastExits', this.pendingFastExits);
}
Expand Down Expand Up @@ -176,133 +169,87 @@ export class UnspentsStore {
);
}

private postData(url = '', data = {}) {
// Default options are marked with *
return fetch(url, {
method: 'POST', // *GET, POST, PUT, DELETE, etc.
mode: 'cors', // no-cors, cors, *same-origin
cache: 'no-cache', // *default, no-cache, reload, force-cache, only-if-cached
headers: {
'Content-Type': 'application/json',
},
redirect: 'error', // manual, *follow, error
referrer: 'no-referrer', // no-referrer, *client
body: JSON.stringify(data), // body data type must match "Content-Type" header
}).then(response => response.json()); // parses response to JSON
}

@autobind
private finalizeFastExits(_, period) {
console.log('period received', period);
if (Object.keys(this.pendingFastExits).length < 1) {
return;
}
Object.values(this.pendingFastExits)
.filter(
(e: any) => e.effectiveBlock < this.periodBlocksRange[1] && e.sig !== ''
)
.forEach(this.finalizeFastExit.bind(this));
}

@autobind
private finalizeFastExit(exit) {
console.log('Finalizing fast exit', exit, operatorStore.slots);
const { signer } = operatorStore.slots[0];
const { unspent, sig, rawTx, sigHashBuff } = exit;
const vBuff = Buffer.alloc(32);
vBuff.writeInt8(sig.v, 31);
const signedData = Exit.bufferToBytes32Array(
Buffer.concat([
toBuffer(sigHashBuff),
Buffer.from(sig.r),
Buffer.from(sig.s),
vBuff,
])
);
return Promise.all([
getProof(web3PlasmaStore.instance, rawTx),
getProof(web3PlasmaStore.instance, unspent.transaction),
0,
]).then(([txProof, inputProof, inputIndex]) => {
// call api
this.postData(CONFIG.exitMarketMaker, {
inputProof,
transferProof: txProof,
inputIndex,
outputIndex: 0, // output of spending tx that we want to exit
signedData,
})
.then(rsp => {
console.log(rsp);
delete this.pendingFastExits[bufferToHex(unspent.outpoint.hash)];
this.storePendingFastExits();
})
.catch(err => {
console.log(err);
});
});
}

@autobind
public fastExitUnspent(unspent: UnspentWithTx) {
unspent = objectify(unspent);

const token = tokensStore.tokenForColor(unspent.output.color);
const outpoint = objectify(unspent.outpoint);

const amount = bi(unspent.output.value);
const exitingUtxoId = outpoint.hex();

let tx;
let sigHashBuff;
let rawTx;

const unspentHash = bufferToHex(unspent.outpoint.hash);
return token
.transfer(exitHandlerStore.address, amount)
.then(data => data.futureReceipt)
.then(txObj => {
rawTx = txObj;
tx = Tx.fromRaw(txObj.raw);
const utxoId = new Outpoint(tx.hash(), 0).getUtxoId();
sigHashBuff = Exit.sigHashBuff(utxoId, amount as any);

// create pending exit after the first sig, so that we can continue
// the process if the user mistakingly rejects the second sig or closes the browser
this.pendingFastExits[unspentHash] = {
return (Exit.fastSellUTXO(
unspent,
web3PlasmaStore.instance,
web3InjectedStore.instance,
CONFIG.exitMarketMaker
) as any)
.on('transfer', fastSellRequest => {
this.list.remove(unspent);
this.pendingFastExits[exitingUtxoId] = {
...fastSellRequest,
unspent,
sig: '',
rawTx,
effectiveBlock: Period.periodBlockRange(rawTx.blockNumber)[1],
sigHashBuff: `0x${sigHashBuff.toString('hex')}`,
};
this.storePendingFastExits();
return this.signFastExit(unspent);
})
.then(() => {
this.pendingFastExits[exitingUtxoId].sent = true;
});
}

public signFastExit(unspent: UnspentWithTx) {
const unspentHash = bufferToHex(unspent.outpoint.hash);
const sigHashBuff = this.pendingFastExits[unspentHash].sigHashBuff;
return Tx.signMessageWithWeb3(web3InjectedStore.instance, sigHashBuff).then(
sig => {
this.pendingFastExits[unspentHash].sig = sig;
this.storePendingFastExits();
}
);
const exitingUtxoId = objectify(unspent.outpoint).hex();
const fastSellRequest = this.pendingFastExits[exitingUtxoId];
fastSellRequest.sigHashBuff = Buffer.from(fastSellRequest.sigHashBuff);
return Exit.signAndSendFastSellRequest(
fastSellRequest,
web3InjectedStore.instance,
CONFIG.exitMarketMaker
).then(() => {
this.pendingFastExits[exitingUtxoId].sent = true;
});
}

public listForColor(color: number) {
return this.list
const utxos = this.list
.filter(u => u.output.color === color)
.concat(
Object.values(this.pendingFastExits)
.filter((v: any) => v.unspent.transaction.color === color)
.map((v: any) => ({ ...objectify(v.unspent), pendingFastExit: true }))
.map((v: any) => ({
...v.unspent,
outpoint: objectify(v.unspent.outpoint),
pendingFastExit: true,
}))
);

this.updatePendingFastExits(color);
return utxos;
}

public updatePendingFastExits(color: number) {
return fetch(
`${CONFIG.exitMarketMaker}/../exits/${accountStore.address}/${color}`,
{ method: 'GET', mode: 'cors' }
)
.then(response => response.json())
.then(pendingExits => {
const exitingUtxos = pendingExits.map(exit => {
const [hash, index] = exit.utxoId.split(':');
return new Outpoint(hash, Number(index)).hex();
});

Object.keys(this.pendingFastExits).forEach(utxo => {
if (
exitingUtxos.indexOf(utxo) < 0 &&
!!this.pendingFastExits[utxo].sent
) {
delete this.pendingFastExits[utxo];
}
});
});
}

@autobind
public consolidate(color: number) {
Tx.consolidateUTXOs(this.listForColor(color)).forEach(tx =>
const utxos = this.listForColor(color).filter(u => !u.pendingFastExit);
Tx.consolidateUTXOs(utxos).forEach(tx =>
tx
.signWeb3(web3InjectedStore.instance as any)
.then(signedTx =>
Expand Down
22 changes: 18 additions & 4 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3660,6 +3660,11 @@ [email protected]:
version "1.1.1"
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-1.1.1.tgz#47786bdaa087caf7b1b75e73abc5c7d540158cd0"

[email protected]:
version "3.1.2"
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.2.tgz#2d3d48f9c346698fce83a85d7d664e98535df6e7"
integrity sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==

eventemitter3@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.0.tgz#090b4d6cdbd645ed10bf750d4b5407942d7ba163"
Expand Down Expand Up @@ -5108,15 +5113,16 @@ lcid@^2.0.0:
dependencies:
invert-kv "^2.0.0"

leap-core@0.38.0:
version "0.38.0"
resolved "https://registry.yarnpkg.com/leap-core/-/leap-core-0.38.0.tgz#808ddff55bb5189771e0fbc2a086210529a17b4f"
integrity sha512-mjS3UOiJp05wRKRsURI1u8XzZ/4HmScpj3cpAGRC9ls4QShu/XAf2CpK297PWAfHT+xQTLyd3OxBNujhuTXamA==
leap-core@0.39.0:
version "0.39.0"
resolved "https://registry.yarnpkg.com/leap-core/-/leap-core-0.39.0.tgz#d9fd84f47faf555068d340be482008d12f2caa69"
integrity sha512-sw9f9PfZZlKFy/ZcCPSXsSU1V2TONebR7sZLmQ7jSA9Dah3CVUfGsg5jcl4bGJ4jB1V9WywpyagdWtSHI/IGhQ==
dependencies:
"@types/web3" "^1.0.18"
ethereumjs-util "6.0.0"
jsbi-utils "^1.0.0"
node-fetch "^2.3.0"
web3-core-promievent "^1.2.4"

less-loader@^4.1.0:
version "4.1.0"
Expand Down Expand Up @@ -8601,6 +8607,14 @@ [email protected]:
any-promise "1.3.0"
eventemitter3 "1.1.1"

web3-core-promievent@^1.2.4:
version "1.2.4"
resolved "https://registry.yarnpkg.com/web3-core-promievent/-/web3-core-promievent-1.2.4.tgz#75e5c0f2940028722cdd21ba503ebd65272df6cb"
integrity sha512-gEUlm27DewUsfUgC3T8AxkKi8Ecx+e+ZCaunB7X4Qk3i9F4C+5PSMGguolrShZ7Zb6717k79Y86f3A00O0VAZw==
dependencies:
any-promise "1.3.0"
eventemitter3 "3.1.2"

[email protected]:
version "1.0.0-beta.33"
resolved "https://registry.yarnpkg.com/web3-core-requestmanager/-/web3-core-requestmanager-1.0.0-beta.33.tgz#7a36c40354002dfb179ca2dbdb6a6012c9f719eb"
Expand Down