Skip to content

Commit

Permalink
Adding new v2-byzcoin requests
Browse files Browse the repository at this point in the history
The previous requests and instances were difficult to use for multi-instruction transactions.
Also, they didn't use the new Observable interface given by ByzCoin.

This PR adds a v2/ directory with some soon-to-be stable version of Darc and Coin instances.
Every instance can now be created as a BehaviorSubject that updates automatically whenever
a new block is available.
This allows to program a much more reactive programming of the user-interface: instead of
polling for new values, the UI can subscribe to the BehaviorSubjects and be updated
whenever something changes.

In the long run, other contracts should also find their way into byzcoin/v2.
  • Loading branch information
ineiti committed Oct 19, 2020
1 parent 72d0dd3 commit a38d357
Show file tree
Hide file tree
Showing 20 changed files with 1,052 additions and 3 deletions.
6 changes: 6 additions & 0 deletions external/js/cothority/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
3.6.0 - 2020 10 16
- added a new api for the instances using rxjs observables
- changed the way ByzCoinRPC.getUpdates behaves

3.5.3 - 2020 09 24
- remove buffer import in log.ts
24 changes: 24 additions & 0 deletions external/js/cothority/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion external/js/cothority/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"private": true,
"name": "@dedis/cothority",
"version": "3.5.5",
"version": "3.6.0",
"description": "A typescript implementation of the cothority",
"main": "index.js",
"browser": "bundle.min.js",
Expand Down Expand Up @@ -83,6 +83,7 @@
"prettier": "^1.19.1",
"ts-loader": "^5.3.3",
"ts-node": "^8.0.1",
"tsconfig-paths": "^3.9.0",
"tslint": "^5.12.1",
"typedoc": "^0.15.8",
"typescript": "^3.6.4",
Expand Down
78 changes: 78 additions & 0 deletions external/js/cothority/spec/helpers/bctest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { Log } from "../../src";
import { ByzCoinRPC, IStorage, LocalCache } from "../../src/byzcoin";
import { Darc, Rule } from "../../src/darc";
import { RosterWSConnection } from "../../src/network";
import { StatusRPC } from "../../src/status";
import { StatusRequest, StatusResponse } from "../../src/status/proto";
import { TransactionBuilder } from "../../src/v2/byzcoin";
import { CoinContract, DarcInst } from "../../src/v2/byzcoin/contracts";
import { BLOCK_INTERVAL, ROSTER, SIGNER, startConodes, stopConodes } from "../support/conondes";

/**
* BCTest allows for using a single ByzCoin instance for multiple tests. It should be called with
*
* const bct = await BCTest.singleton()
*
* in every test where a byzcoin-instance is used. Thereafter the test can use the genesisInst
* to create new CoinInstances and DarcInstances.
*
* Using this class reduces the time to test, as the same ByzCoin instance is used for all tests.
* But it also means that the tests need to make sure that the genesis-darc is not made
* unusable.
*/
export class BCTest {

static async singleton(): Promise<BCTest> {
if (BCTest.bct === undefined) {
BCTest.bct = await BCTest.init();
} else {
await new Promise((resolve) => setTimeout(resolve, 1000));
}

return BCTest.bct;
}
private static bct: BCTest | undefined;

private static async init(): Promise<BCTest> {
Log.lvl = 1;
const roster4 = ROSTER.slice(0, 4);

let usesDocker = true;
try {
const ws = new RosterWSConnection(roster4, StatusRPC.serviceName);
ws.setParallel(1);
await ws.send(new StatusRequest(), StatusResponse);
Log.warn("Using already running nodes for test!");
usesDocker = false;
} catch (e) {
await startConodes();
}

const cache = new LocalCache();
const genesis = ByzCoinRPC.makeGenesisDarc([SIGNER], roster4, "initial");
[CoinContract.ruleFetch, CoinContract.ruleMint, CoinContract.ruleSpawn, CoinContract.ruleStore,
CoinContract.ruleTransfer]
.forEach((rule) => genesis.addIdentity(rule, SIGNER, Rule.OR));
const rpc = await ByzCoinRPC.newByzCoinRPC(roster4, genesis, BLOCK_INTERVAL, cache);
rpc.setParallel(1);
const tx = new TransactionBuilder(rpc);
const genesisInst = await DarcInst.retrieve(rpc, genesis.getBaseID());
return new BCTest(cache, genesis, genesisInst, rpc, tx, usesDocker);
}

private constructor(
public cache: IStorage,
public genesis: Darc,
public genesisInst: DarcInst,
public rpc: ByzCoinRPC,
public tx: TransactionBuilder,
public usesDocker: boolean,
) {
}

async shutdown() {
if (this.usesDocker) {
return stopConodes();
}
}
}
96 changes: 96 additions & 0 deletions external/js/cothority/spec/support/historyObs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { Log } from "../../src";

/**
* HistoryObs allows a test to wait for a set of changes to occur and to throw an error if a timeout occurs before that.
* To use it, the `push` method should be called for every new occurrence of the item to be observed.
* This is usually done in an observer:
*
* const ho = new HistoryObs();
* coinInstance.subscribe((c) => ho.push(coinInstance.value.toString()));
*
* After that, the test can wait for a number of occurrences on this value:
*
* await h.resolve("0", "100000");
*
* This will wait for the history to have at least two elements: "0" and "100000". If during the timeout less than
* two elements are available, the `resolve` throws an error. It also throws an error if the two first history elements
* don't correspond to the `resolve` call.
*/
export class HistoryObs {

private readonly entries: string[] = [];

constructor(private maxWait = 20) {}

push(...e: string[]) {
this.entries.push(...e);
}

async resolveInternal(newEntries: string[], complete?: boolean): Promise<void> {
await expectAsync(this.expect(newEntries, true, complete)).toBeResolved();
}

async resolve(...newEntries: string[]): Promise<void> {
return this.resolveInternal(newEntries);
}

async resolveComplete(...newEntries: string[]): Promise<void> {
return this.resolveInternal(newEntries, true);
}

async resolveAll(newEntries: string[]): Promise<void> {
let found = true;
while (found) {
try {
await this.expect(newEntries, true, false, true);
} catch (e) {
Log.lvl4(e);
found = false;
}
}
}

async reject(newEntries: string[], complete?: boolean): Promise<void> {
await expectAsync(this.expect(newEntries, false, complete)).toBeRejected();
}

async expect(newEntries: string[], succeed: boolean, complete?: boolean, silent?: boolean): Promise<void> {
return new Promise(async (res, rej) => {
try {
for (let i = 0; i < this.maxWait && this.entries.length < newEntries.length; i++) {
if (!silent) {
Log.lvl3("waiting", i, this.entries.length, newEntries.length);
}
await new Promise((resolve) => setTimeout(resolve, 200));
}
if (!silent) {
if (succeed) {
Log.lvl2("History:", this.entries, "wanted:", newEntries);
} else {
Log.lvl2("Want history:", this.entries, "to fail with:", newEntries);
}
}
if (this.entries.length < newEntries.length) {
throw new Error("not enough entries");
}
for (const e of newEntries) {
const h = this.entries.splice(0, 1)[0];
if (e !== h) {
throw new Error(`Got ${h} instead of ${e}`);
}
}
if (complete && this.entries.length !== 0) {
throw new Error(`didn't describe all history: ${this.entries}`);
}
res();
} catch (e) {
if (succeed) {
if (!silent) {
Log.error(e);
}
}
rej(e);
}
});
}
}
2 changes: 1 addition & 1 deletion external/js/cothority/spec/support/jasmine.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@
"helpers": [
"helpers/**/*"
],
"stopSpecOnExpectationFailure": false,
"stopSpecOnExpectationFailure": true,
"random": false
}
6 changes: 6 additions & 0 deletions external/js/cothority/spec/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"extends": "../tsconfig",
"include": [
"./**/*"
]
}
72 changes: 72 additions & 0 deletions external/js/cothority/spec/v2/coin-inst.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import Long from "long";

import { Log } from "../../src";
import { CoinContract, CoinInst } from "../../src/v2/byzcoin/contracts";
import { BCTest } from "../helpers/bctest";
import { SIGNER } from "../support/conondes";
import { HistoryObs } from "../support/historyObs";

describe("CoinInst should", () => {
const name = Buffer.alloc(32);

beforeAll(async () => {
name.write("coinName");
});

it("retrieve an instance from byzcoin", async () => {
const {genesisInst, tx, rpc} = await BCTest.singleton();
const coinID = genesisInst.spawnCoin(tx, name);
await tx.send([[SIGNER]], 10);
const ci = await CoinInst.retrieve(rpc, coinID);
expect(ci.getValue().name.equals(name)).toBeTruthy();
});

it("mint some coins", async () => {
const {genesisInst, tx, rpc} = await BCTest.singleton();
const coinID = genesisInst.spawnCoin(tx, name);
await tx.send([[SIGNER]], 10);

const ci = await CoinInst.retrieve(rpc, coinID);
const h = new HistoryObs();
ci.subscribe((c) => h.push(c.value.toString()));
await h.resolve("0");

ci.mint(tx, Long.fromNumber(1e6));
await tx.send([[SIGNER]], 10);
await h.resolve(1e6.toString());
});

it("transfer coins", async () => {
const {genesisInst, tx, rpc} = await BCTest.singleton();

Log.lvl2("Spawning 2 coins");
const sourceID = genesisInst.spawnCoin(tx, name);
const targetID = genesisInst.spawnCoin(tx, name);
CoinContract.mint(tx, sourceID, Long.fromNumber(1e6));
CoinContract.transfer(tx, sourceID, targetID, Long.fromNumber(1e5));
await tx.send([[SIGNER]], 10);

Log.lvl2("Getting coins and values");
const target = await CoinInst.retrieve(rpc, targetID);
const hTarget = new HistoryObs();
target.subscribe((ci) => hTarget.push(ci.value.toString()));
await hTarget.resolve(1e5.toString());

Log.lvl2("Transferring some coins from source to target");
const source = await CoinInst.retrieve(rpc, sourceID);
const hSource = new HistoryObs();
source.subscribe((ci) => hSource.push(ci.value.toString()));
source.mint(tx, Long.fromNumber(1e6));
source.transfer(tx, targetID, Long.fromNumber(2e5));
await tx.send([[SIGNER]], 10);
await hSource.resolve(9e5.toString(), 17e5.toString());
await hTarget.resolve(3e5.toString());

Log.lvl2("Using fetch and store for transfer");
source.fetch(tx, Long.fromNumber(3e5));
target.store(tx);
await tx.send([[SIGNER]], 10);
await hSource.resolve(14e5.toString());
await hTarget.resolve(6e5.toString());
}, 600000);
});
Loading

0 comments on commit a38d357

Please sign in to comment.