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 472cbdd commit 9f1e374
Show file tree
Hide file tree
Showing 20 changed files with 1,024 additions and 3 deletions.
6 changes: 6 additions & 0 deletions external/js/cothority/CHANGELOG.md
@@ -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
@@ -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
66 changes: 66 additions & 0 deletions external/js/cothority/spec/helpers/bctest.ts
@@ -0,0 +1,66 @@
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";

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();
}
}
}
80 changes: 80 additions & 0 deletions external/js/cothority/spec/support/historyObs.ts
@@ -0,0 +1,80 @@
import { Log } from "../../src";

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
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
@@ -0,0 +1,6 @@
{
"extends": "../tsconfig",
"include": [
"./**/*"
]
}
72 changes: 72 additions & 0 deletions external/js/cothority/spec/v2/coin-inst.spec.ts
@@ -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);
});
84 changes: 84 additions & 0 deletions external/js/cothority/spec/v2/darc-inst.spec.ts
@@ -0,0 +1,84 @@
import { elementAt } from "rxjs/operators";

import { DarcInstance } from "../../src/byzcoin/contracts";
import { Darc, SignerEd25519 } from "../../src/darc";
import { DarcContract, DarcInst } from "../../src/v2/byzcoin/contracts";

import { BCTest } from "../helpers/bctest";
import { SIGNER } from "../support/conondes";
import { HistoryObs } from "../support/historyObs";

describe("DarcInst should", () => {
it("retrieve an instance from byzcoin", async () => {
const {genesis, rpc} = await BCTest.singleton();
const dbs = await DarcInst.retrieve(rpc, genesis.getBaseID());
expect(dbs.getValue().inst.id.equals(genesis.getBaseID())).toBeTruthy();
});

it("update when the darc is updated", async () => {
const {genesis, rpc, tx} = await BCTest.singleton();
const d = Darc.createBasic([SIGNER], [SIGNER], Buffer.from("new darc"));
await DarcInstance.spawn(rpc, genesis.getBaseID(), [SIGNER], d);
const dbs = await DarcInst.retrieve(rpc, d.getBaseID());
expect(dbs.getValue().inst.id.equals(d.getBaseID())).toBeTruthy();

const newDarc = dbs.pipe(elementAt(1)).toPromise();
dbs.setDescription(tx, Buffer.from("new description"));
await tx.send([[SIGNER]], 10);

expect((await newDarc).description).toEqual(Buffer.from("new description"));
});

it("update rules", async () => {
const {genesis, rpc, tx} = await BCTest.singleton();
const newDarc = Darc.createBasic([SIGNER], [SIGNER], Buffer.from("darc1"));
await DarcInstance.spawn(rpc, genesis.getBaseID(), [SIGNER], newDarc);
const dbs = await DarcInst.retrieve(rpc, newDarc.getBaseID());
const hist = new HistoryObs();

// Create updates with the description:#signers:#evolvers
dbs.subscribe((d) => {
const signLen = d.rules.getRule(DarcContract.ruleSign).getIdentities().length;
const evolveLen = d.rules.getRule(DarcContract.ruleEvolve).getIdentities().length;
hist.push(`${d.description.toString()}:${signLen}:${evolveLen}`);
});
await hist.resolve("darc1:1:1");

dbs.setDescription(tx, Buffer.from("darc2"));
await tx.send([[SIGNER]]);
await hist.resolve("darc2:1:1");

// Change the evolver and use it to evolve future darcs
const newEvolver = SignerEd25519.random();
dbs.addToRules(tx, [DarcContract.ruleEvolve, newEvolver]);
await tx.send([[SIGNER]], 10);
await hist.resolve("darc2:1:2");

// Add both signer and evolver
const newSigner = SignerEd25519.random();
const newEvolver2 = SignerEd25519.random();
dbs.addToRules(tx, [DarcContract.ruleSign, newSigner], [DarcContract.ruleEvolve, newEvolver2]);
await tx.send([[newEvolver]], 10);
await hist.resolve("darc2:2:3");

// Remove the old evolver
dbs.rmFromRules(tx, [DarcContract.ruleEvolve, newEvolver]);
await tx.send([[newEvolver2]], 10);
await hist.resolve("darc2:2:2");

// Reset the evolver
dbs.setRules(tx, [DarcContract.ruleSign, newSigner], [DarcContract.ruleEvolve, newEvolver2]);
await tx.send([[newEvolver2]], 10);
await hist.resolve("darc2:1:1");

// Reset the signer (first add, then set)
dbs.addToRules(tx, [DarcContract.ruleSign, newSigner]);
await tx.send([[newEvolver2]], 10);
await hist.resolve("darc2:2:1");

dbs.setRules(tx, [DarcContract.ruleSign, newSigner]);
await tx.send([[newEvolver2]], 10);
await hist.resolve("darc2:1:1");
expect(dbs.getValue().rules.getRule(DarcContract.ruleEvolve).getIdentities()[0]).toBe(newEvolver2.toString());
});
});
2 changes: 1 addition & 1 deletion external/js/cothority/src/bevm/bevm-instance.ts
@@ -1,4 +1,4 @@
import { randomBytes } from "crypto";
import { randomBytes } from "crypto-browserify";
import { ec } from "elliptic";
import Keccak from "keccak";
import Long from "long";
Expand Down

0 comments on commit 9f1e374

Please sign in to comment.