Skip to content

[Protocol3] Added circuit testing support + small improvements #396

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

Merged
merged 3 commits into from
Aug 12, 2019
Merged
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
26 changes: 15 additions & 11 deletions packages/loopring_v3/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,21 @@ To understand the overall design for Loopring 3.0, including Ethereum smart cont

### Performance

| | Loopring 2.x | Loopring 3.0 <br> (w/ Data Availability) | Loopring 3.0 <br> (w/o Data Availability) |
| :----- |:-------------: |:---------------:| :-------------:|
| Trades per Ethereum Block | 26 | 5350 | 92000|
| Trades per Second | ~2 | 350 | 6150 |
| Cost per Trade | ~300,000 gas | 1500 gas | 87 gas|
| | Loopring 2.x | Loopring 3.0 <br> (w/ Data Availability) | Loopring 3.0 <br> (w/o Data Availability) |
| :------------------------ | :----------: | :--------------------------------------: | :---------------------------------------: |
| Trades per Ethereum Block | 26 | 5350 | 92000 |
| Trades per Second | ~2 | 350 | 6150 |
| Cost per Trade | ~300,000 gas | 1500 gas | 87 gas |

After Istanbul:

| | Loopring 2.x | Loopring 3.0 <br> (w/ Data Availability) | Loopring 3.0 <br> (w/o Data Availability) |
| :----- |:-------------: |:---------------:| :-------------:|
| Trades per Ethereum Block | 26 | 20800 | 140000|
| Trades per Second | ~2 | 1400 | 9350 |
| Cost per Trade | ~300,000 gas | 385 gas | 57 gas|
| | Loopring 2.x | Loopring 3.0 <br> (w/ Data Availability) | Loopring 3.0 <br> (w/o Data Availability) |
| :------------------------ | :----------: | :--------------------------------------: | :---------------------------------------: |
| Trades per Ethereum Block | 26 | 20800 | 140000 |
| Trades per Second | ~2 | 1400 | 9350 |
| Cost per Trade | ~300,000 gas | 385 gas | 57 gas |

* *Cost in USD per Trade* in the table does not cover off-chain proof generation.
- _Cost in USD per Trade_ in the table does not cover off-chain proof generation.

## Top Features

Expand All @@ -45,6 +45,7 @@ After Istanbul:
- and more...

## Challenges

- SNARKs require trusted setups

## Build
Expand All @@ -59,7 +60,10 @@ The code of our circuits is currently not open source. If you have access to the
make
```

The circuit tests can be run with `npm run test-circuits`. A single test can be run with `npm run test-circuits <test_name>`.

## Run Unit Tests

- please clone the circuits repository `https://github.com/Loopring/protocol3-circuits.git` to the same directory as this project.
- please make sure you run `npm run build` for the first time.
- run `npm run ganache` from project's root directory in terminal.
Expand Down
5 changes: 5 additions & 0 deletions packages/loopring_v3/circuit/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,9 @@ set(circuit_src_folder "../../../../protocol3-circuits/")
add_executable(dex_circuit "${circuit_src_folder}/main.cpp")
target_link_libraries(dex_circuit ethsnarks_jubjub)

file(GLOB test_filenames
"${circuit_src_folder}/test/*.cpp"
)

add_executable(dex_circuit_tests ${test_filenames})
target_link_libraries(dex_circuit_tests ethsnarks_jubjub)
2 changes: 1 addition & 1 deletion packages/loopring_v3/ethsnarks
18 changes: 10 additions & 8 deletions packages/loopring_v3/operator/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,10 @@ def cancelOrder(self, tokenID, orderID):
rootBefore = self._balancesTree._root

# Update cancelled state
filled = int(self._balancesLeafs[str(tokenID)].getTradeHistory(orderID).filled)
tradeHistory = self._balancesLeafs[str(tokenID)].getTradeHistory(orderID)
filled = int(tradeHistory.filled)
if int(tradeHistory.orderID) < orderID:
filled = 0
tradeHistoryUpdate = self._balancesLeafs[str(tokenID)].updateTradeHistory(orderID, filled, 1, orderID)

balancesAfter = copyBalanceInfo(self._balancesLeafs[str(tokenID)])
Expand Down Expand Up @@ -344,20 +347,19 @@ def checkValid(self, context, order, fillAmountS, fillAmountB):
valid = valid and (self.validSince <= context.timestamp)
valid = valid and (context.timestamp <= self.validUntil)

valid = valid and not self.hasRoundingError(fillAmountS, int(order.amountB), int(order.amountS))
valid = valid and self.checkFillRate(int(order.amountS), int(order.amountB), fillAmountS, fillAmountB)

valid = valid and not (not self.buy and self.allOrNone and fillAmountS < int(order.amountS))
valid = valid and not (self.buy and self.allOrNone and fillAmountB < int(order.amountB))
valid = valid and fillAmountS != 0
valid = valid and fillAmountB != 0

self.valid = valid

def hasRoundingError(self, value, numerator, denominator):
multiplied = value * numerator
remainder = multiplied % denominator
# Return true if the rounding error is larger than 1%
return multiplied < remainder * 100

def checkFillRate(self, amountS, amountB, fillAmountS, fillAmountB):
# Return true if the fill rate < 1% worse than the target rate
# (fillAmountS/fillAmountB) * 100 <= (amountS/amountB) * 101
return (fillAmountS * amountB * 100) < (fillAmountB * amountS * 101)

class Ring(object):
def __init__(self, orderA, orderB):
Expand Down
3 changes: 2 additions & 1 deletion packages/loopring_v3/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@
"truffle": "truffle",
"clean": "rm -rf build/contracts",
"v": "node -v",
"preinstall": "rm -rf node_modules/websocket/.git"
"preinstall": "rm -rf node_modules/websocket/.git",
"test-circuits": "./build/circuit/dex_circuit_tests"
},
"license": "ISC",
"devDependencies": {
Expand Down
41 changes: 30 additions & 11 deletions packages/loopring_v3/test/simulator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,8 @@ export class Simulator {
const newExchangeState = this.copyExchangeState(exchangeState);

const account = newExchangeState.accounts[accountID];
const tradeHistorySlot =
orderID % 2 ** constants.TREE_DEPTH_TRADING_HISTORY;

// Update balance
account.balances[feeTokenID].balance = account.balances[
Expand All @@ -361,14 +363,20 @@ export class Simulator {
account.nonce++;

// Update trade history
if (!account.balances[orderTokenID].tradeHistory[orderID]) {
account.balances[orderTokenID].tradeHistory[orderID] = {
if (!account.balances[orderTokenID].tradeHistory[tradeHistorySlot]) {
account.balances[orderTokenID].tradeHistory[tradeHistorySlot] = {
filled: new BN(0),
cancelled: false,
orderID: 0
};
}
account.balances[orderTokenID].tradeHistory[orderID].cancelled = true;
const tradeHistory =
account.balances[orderTokenID].tradeHistory[tradeHistorySlot];
if (tradeHistory.orderID < orderID) {
tradeHistory.filled = new BN(0);
}
tradeHistory.cancelled = true;
tradeHistory.orderID = orderID;

// Update operator
const operator = newExchangeState.accounts[operatorAccountID];
Expand Down Expand Up @@ -1001,7 +1009,8 @@ export class Simulator {
if (!tradeHistory) {
tradeHistory = {
filled: new BN(0),
cancelled: false
cancelled: false,
orderID: 0
};
}
// Trade history trimming
Expand Down Expand Up @@ -1083,11 +1092,16 @@ export class Simulator {
return [fee, protocolFee, rebate];
}

private hasRoundingError(value: BN, numerator: BN, denominator: BN) {
const multiplied = value.mul(numerator);
const remainder = multiplied.mod(denominator);
// Return true if the rounding error is larger than 1%
return multiplied.lt(remainder.mul(new BN(100)));
private checkFillRate(
amountS: BN,
amountB: BN,
fillAmountS: BN,
fillAmountB: BN
) {
return fillAmountS
.mul(amountB)
.mul(new BN(100))
.lt(fillAmountB.mul(amountS).mul(new BN(101)));
}

private checkValid(
Expand Down Expand Up @@ -1118,8 +1132,13 @@ export class Simulator {
valid =
valid &&
this.ensure(
!this.hasRoundingError(fillAmountS, order.amountB, order.amountS),
"rounding error"
this.checkFillRate(
order.amountS,
order.amountB,
fillAmountS,
fillAmountB
),
"invalid fill rate"
);
valid = valid && this.ensure(!fillAmountS.eq(0), "no tokens sold");
valid = valid && this.ensure(!fillAmountB.eq(0), "no tokens bought");
Expand Down
71 changes: 71 additions & 0 deletions packages/loopring_v3/test/testExchangeCancel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,77 @@ contract("Exchange", (accounts: string[]) => {
await exchangeTestUtil.verifyPendingBlocks(exchangeId);
});

it("Cancel an order with orderID > tradeHistory.orderID", async () => {
const orderID = 1987;
const ringA: RingInfo = {
orderA: {
tokenS: "WETH",
tokenB: "GTO",
amountS: new BN(web3.utils.toWei("100", "ether")),
amountB: new BN(web3.utils.toWei("200", "ether")),
orderID: 4
},
orderB: {
tokenS: "GTO",
tokenB: "WETH",
amountS: new BN(web3.utils.toWei("200", "ether")),
amountB: new BN(web3.utils.toWei("100", "ether")),
orderID
},
expected: {
orderA: {
filledFraction: 1.0,
spread: new BN(web3.utils.toWei("0", "ether"))
},
orderB: { filledFraction: 1.0 }
}
};
await exchangeTestUtil.setupRing(ringA);

const ringB: RingInfo = {
orderA: {
tokenS: "WETH",
tokenB: "GTO",
amountS: new BN(web3.utils.toWei("100", "ether")),
amountB: new BN(web3.utils.toWei("200", "ether")),
orderID: 5
},
orderB: {
tokenS: "GTO",
tokenB: "WETH",
amountS: new BN(web3.utils.toWei("400", "ether")),
amountB: new BN(web3.utils.toWei("200", "ether")),
orderID: orderID + 2 ** constants.TREE_DEPTH_TRADING_HISTORY,
accountID: ringA.orderB.accountID,
owner: ringA.orderB.owner
},
expected: {
orderA: {
filledFraction: 0.0,
spread: new BN(web3.utils.toWei("0", "ether"))
},
orderB: { filledFraction: 0.0 }
}
};
await exchangeTestUtil.setupRing(ringB);

await exchangeTestUtil.commitDeposits(exchangeId);
await exchangeTestUtil.sendRing(exchangeId, ringA);
await exchangeTestUtil.commitRings(exchangeId);

await exchangeTestUtil.cancelOrder(
ringB.orderB,
"WETH",
new BN(web3.utils.toWei("0", "ether"))
);
await exchangeTestUtil.commitCancels(exchangeId);

await exchangeTestUtil.sendRing(exchangeId, ringB);
await exchangeTestUtil.commitRings(exchangeId);

await exchangeTestUtil.verifyPendingBlocks(exchangeId);
});

it("Cancel all orders from an account by changing the account's public key", async () => {
const ring: RingInfo = {
orderA: {
Expand Down
62 changes: 62 additions & 0 deletions packages/loopring_v3/test/testExchangeRingSettlement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -841,6 +841,68 @@ contract("Exchange", (accounts: string[]) => {
await verify();
});

it("Insufficient funds available (buy order)", async () => {
const ring: RingInfo = {
orderA: {
tokenS: "ETH",
tokenB: "GTO",
amountS: new BN(web3.utils.toWei("100", "ether")),
amountB: new BN(web3.utils.toWei("10", "ether")),
balanceS: new BN(web3.utils.toWei("40", "ether")),
buy: true
},
orderB: {
tokenS: "GTO",
tokenB: "ETH",
amountS: new BN(web3.utils.toWei("20", "ether")),
amountB: new BN(web3.utils.toWei("200", "ether"))
},
expected: {
orderA: { filledFraction: 0.4, spread: new BN(0) },
orderB: { filledFraction: 0.2 }
}
};

await exchangeTestUtil.setupRing(ring);
await exchangeTestUtil.sendRing(exchangeID, ring);

await exchangeTestUtil.commitDeposits(exchangeID);
await exchangeTestUtil.commitRings(exchangeID);

await verify();
});

it("Insufficient funds available (sell order)", async () => {
const ring: RingInfo = {
orderA: {
tokenS: "ETH",
tokenB: "GTO",
amountS: new BN(web3.utils.toWei("100", "ether")),
amountB: new BN(web3.utils.toWei("10", "ether")),
balanceS: new BN(web3.utils.toWei("40", "ether")),
buy: false
},
orderB: {
tokenS: "GTO",
tokenB: "ETH",
amountS: new BN(web3.utils.toWei("20", "ether")),
amountB: new BN(web3.utils.toWei("200", "ether"))
},
expected: {
orderA: { filledFraction: 0.4, spread: new BN(0) },
orderB: { filledFraction: 0.2 }
}
};

await exchangeTestUtil.setupRing(ring);
await exchangeTestUtil.sendRing(exchangeID, ring);

await exchangeTestUtil.commitDeposits(exchangeID);
await exchangeTestUtil.commitRings(exchangeID);

await verify();
});

it("allOrNone (Buy, successful)", async () => {
const ring: RingInfo = {
orderA: {
Expand Down
Loading