From a38d357d90e5001e4531ad7cc21fc8697e5ecfa7 Mon Sep 17 00:00:00 2001 From: Linus Gasser Date: Tue, 14 Jul 2020 13:51:08 +0200 Subject: [PATCH] Adding new v2-byzcoin requests 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. --- external/js/cothority/CHANGELOG.md | 6 + external/js/cothority/package-lock.json | 24 +++ external/js/cothority/package.json | 3 +- external/js/cothority/spec/helpers/bctest.ts | 78 +++++++ .../js/cothority/spec/support/historyObs.ts | 96 +++++++++ .../js/cothority/spec/support/jasmine.json | 2 +- external/js/cothority/spec/tsconfig.json | 6 + .../js/cothority/spec/v2/coin-inst.spec.ts | 72 +++++++ .../js/cothority/spec/v2/darc-inst.spec.ts | 84 ++++++++ .../js/cothority/src/bevm/bevm-instance.ts | 2 +- external/js/cothority/src/v2/README.md | 27 +++ .../src/v2/byzcoin/contracts/coinContract.ts | 97 +++++++++ .../src/v2/byzcoin/contracts/coinInst.ts | 103 +++++++++ .../src/v2/byzcoin/contracts/darcContract.ts | 74 +++++++ .../src/v2/byzcoin/contracts/darcInsts.ts | 199 ++++++++++++++++++ .../src/v2/byzcoin/contracts/index.ts | 16 ++ external/js/cothority/src/v2/byzcoin/index.ts | 7 + .../src/v2/byzcoin/transactionBuilder.ts | 143 +++++++++++++ external/js/cothority/src/v2/byzcoin/util.ts | 15 ++ external/js/cothority/tsconfig.json | 1 + 20 files changed, 1052 insertions(+), 3 deletions(-) create mode 100644 external/js/cothority/CHANGELOG.md create mode 100644 external/js/cothority/spec/helpers/bctest.ts create mode 100644 external/js/cothority/spec/support/historyObs.ts create mode 100644 external/js/cothority/spec/tsconfig.json create mode 100644 external/js/cothority/spec/v2/coin-inst.spec.ts create mode 100644 external/js/cothority/spec/v2/darc-inst.spec.ts create mode 100644 external/js/cothority/src/v2/README.md create mode 100644 external/js/cothority/src/v2/byzcoin/contracts/coinContract.ts create mode 100644 external/js/cothority/src/v2/byzcoin/contracts/coinInst.ts create mode 100644 external/js/cothority/src/v2/byzcoin/contracts/darcContract.ts create mode 100644 external/js/cothority/src/v2/byzcoin/contracts/darcInsts.ts create mode 100644 external/js/cothority/src/v2/byzcoin/contracts/index.ts create mode 100644 external/js/cothority/src/v2/byzcoin/index.ts create mode 100644 external/js/cothority/src/v2/byzcoin/transactionBuilder.ts create mode 100644 external/js/cothority/src/v2/byzcoin/util.ts diff --git a/external/js/cothority/CHANGELOG.md b/external/js/cothority/CHANGELOG.md new file mode 100644 index 0000000000..32f79f07ba --- /dev/null +++ b/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 diff --git a/external/js/cothority/package-lock.json b/external/js/cothority/package-lock.json index 5b7dd2ab61..67874fb022 100644 --- a/external/js/cothority/package-lock.json +++ b/external/js/cothority/package-lock.json @@ -1260,6 +1260,12 @@ "integrity": "sha512-TuXyEBYWWrjBZAC4h3YY5NcrgC9HaLO5FogvkY5FOfAyjrk3wWABoH/3p7ooFfA8BHjc2BeQCDvvQBLb+9q1rA==", "dev": true }, + "@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", + "dev": true + }, "@types/lodash": { "version": "4.14.139", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.139.tgz", @@ -7428,6 +7434,12 @@ "ansi-regex": "^3.0.0" } }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + }, "strip-eof": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", @@ -7730,6 +7742,18 @@ "yn": "^2.0.0" } }, + "tsconfig-paths": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.9.0.tgz", + "integrity": "sha512-dRcuzokWhajtZWkQsDVKbWyY+jgcLC5sqJhg2PSgf4ZkH2aHPvaOY8YWGhmjb68b5qqTfasSsDO9k7RUiEmZAw==", + "dev": true, + "requires": { + "@types/json5": "^0.0.29", + "json5": "^1.0.1", + "minimist": "^1.2.0", + "strip-bom": "^3.0.0" + } + }, "tslib": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", diff --git a/external/js/cothority/package.json b/external/js/cothority/package.json index 46dde19970..32820fe68d 100644 --- a/external/js/cothority/package.json +++ b/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", @@ -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", diff --git a/external/js/cothority/spec/helpers/bctest.ts b/external/js/cothority/spec/helpers/bctest.ts new file mode 100644 index 0000000000..cd718d714e --- /dev/null +++ b/external/js/cothority/spec/helpers/bctest.ts @@ -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 { + 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 { + 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(); + } + } +} diff --git a/external/js/cothority/spec/support/historyObs.ts b/external/js/cothority/spec/support/historyObs.ts new file mode 100644 index 0000000000..34b525c9fe --- /dev/null +++ b/external/js/cothority/spec/support/historyObs.ts @@ -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 { + await expectAsync(this.expect(newEntries, true, complete)).toBeResolved(); + } + + async resolve(...newEntries: string[]): Promise { + return this.resolveInternal(newEntries); + } + + async resolveComplete(...newEntries: string[]): Promise { + return this.resolveInternal(newEntries, true); + } + + async resolveAll(newEntries: string[]): Promise { + 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 { + await expectAsync(this.expect(newEntries, false, complete)).toBeRejected(); + } + + async expect(newEntries: string[], succeed: boolean, complete?: boolean, silent?: boolean): Promise { + 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); + } + }); + } +} diff --git a/external/js/cothority/spec/support/jasmine.json b/external/js/cothority/spec/support/jasmine.json index 3444e60187..34c5d28368 100644 --- a/external/js/cothority/spec/support/jasmine.json +++ b/external/js/cothority/spec/support/jasmine.json @@ -6,6 +6,6 @@ "helpers": [ "helpers/**/*" ], - "stopSpecOnExpectationFailure": false, + "stopSpecOnExpectationFailure": true, "random": false } diff --git a/external/js/cothority/spec/tsconfig.json b/external/js/cothority/spec/tsconfig.json new file mode 100644 index 0000000000..9b49559eda --- /dev/null +++ b/external/js/cothority/spec/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../tsconfig", + "include": [ + "./**/*" + ] +} diff --git a/external/js/cothority/spec/v2/coin-inst.spec.ts b/external/js/cothority/spec/v2/coin-inst.spec.ts new file mode 100644 index 0000000000..d0e16df940 --- /dev/null +++ b/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); +}); diff --git a/external/js/cothority/spec/v2/darc-inst.spec.ts b/external/js/cothority/spec/v2/darc-inst.spec.ts new file mode 100644 index 0000000000..2f9a178b69 --- /dev/null +++ b/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()); + }); +}); diff --git a/external/js/cothority/src/bevm/bevm-instance.ts b/external/js/cothority/src/bevm/bevm-instance.ts index 21fe259d4b..5f262d9e3e 100644 --- a/external/js/cothority/src/bevm/bevm-instance.ts +++ b/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"; diff --git a/external/js/cothority/src/v2/README.md b/external/js/cothority/src/v2/README.md new file mode 100644 index 0000000000..99bcbb16e7 --- /dev/null +++ b/external/js/cothority/src/v2/README.md @@ -0,0 +1,27 @@ +# Cothority API v2 + +This is a new start for the cothority API, mostly geared towards ByzCoin. Compared to the old API, it tries to avoid +the following errors: + +- mix between definitions of services and live data + - define the constants in a central place + - have services implementations with more information, e.g., skipchain should be bound to one ID + - instance-definitions that separate correctly the data and the update +- manual updating of instances from byzcoin + - use the new `byzcoin.GetUpdats` service endpoint to get informed when new things happen + +The v1 will still be kept around, but at least the byzcoin-contracts will be reproduced in v2. +Perhaps also the services might get a copy in v2. + +## Elements of every contract + +For every contract described in v2, the following elements must be present: +- `NameStruct` to merge the `Instance` and the protobuf representation of `Name` +- `NameContract` as a namespace representing all constants needed to work with the contract + - `contractID` - as given in ByzCoin + - `attribute*` - all attributes that can be passed to an existing instance + - `command*` - all commands available in the instance + - `rule(spawn|*)` - available rules, one for `spawn`ing, and one for every command +- `NameInst` as a `BehaviorSubject` with: + - `retrieve(ByzCoinRPC, InstanceID)` + - `commands*` - as in `NameContract`, but for this instance diff --git a/external/js/cothority/src/v2/byzcoin/contracts/coinContract.ts b/external/js/cothority/src/v2/byzcoin/contracts/coinContract.ts new file mode 100644 index 0000000000..b85c53e7b9 --- /dev/null +++ b/external/js/cothority/src/v2/byzcoin/contracts/coinContract.ts @@ -0,0 +1,97 @@ +import { createHash } from "crypto-browserify"; +import * as Long from "long"; + +import { TransactionBuilder } from ".."; +import { Argument, InstanceID } from "../../../byzcoin"; + +export const contractID = "coin"; +export const commandMint = "mint"; +export const commandFetch = "fetch"; +export const commandTransfer = "transfer"; +export const commandStore = "store"; +export const argumentCoinID = "coinID"; +export const argumentDarcID = "darcID"; +export const argumentType = "type"; +export const argumentCoins = "coins"; +export const argumentDestination = "destination"; +export const ruleSpawn = "spawn:" + contractID; +export const ruleMint = rule(commandMint); +export const ruleFetch = rule(commandFetch); +export const ruleTransfer = rule(commandTransfer); +export const ruleStore = rule(commandStore); + +/** + * Generate the coin instance ID for a given darc ID + * + * @param buf Any buffer that is known to the caller + * @returns the id as a buffer + */ +export function coinIID(buf: Buffer): InstanceID { + const h = createHash("sha256"); + h.update(Buffer.from(contractID)); + h.update(buf); + return h.digest(); +} + +/** + * Mints coins on ByzCoin. For this to work, the DARC governing this coin instance needs to have a + * 'invoke.Coin.mint' rule and the instruction will need to be signed by the appropriate identity. + * + * @param tx used to collect one or more instructions + * @param coinID to mint + * @param amount positive, non-zero value to mint on this coin + */ +export function mint(tx: TransactionBuilder, coinID: InstanceID, amount: Long) { + if (amount.lessThanOrEqual(0)) { + throw new Error("cannot mint 0 or negative values"); + } + tx.invoke(coinID, + contractID, + commandMint, + [new Argument({name: argumentCoins, value: Buffer.from(amount.toBytesLE())})]); +} + +/** + * Creates an instruction to transfer coins to another account. + * + * @param tx used to collect one or more instructions that will be bundled together and sent as one transaction + * to byzcoin. + * @param src the source account to fetch coins from. + * @param dest the destination account to store the coins in. The destination must exist! + * @param amount how many coins to transfer. + */ +export function transfer(tx: TransactionBuilder, src: InstanceID, dest: InstanceID, amount: Long) { + tx.invoke(src, contractID, commandTransfer, + [new Argument({name: argumentDestination, value: dest}), + new Argument({name: argumentCoins, value: Buffer.from(amount.toBytesLE())})]); +} + +/** + * Fetches coins from a coinInstance and puts it on the ByzCoin 'stack' for use by the next instruction. + * Unused coins are discarded by all nodes and thus lost. + * + * @param tx used to collect one or more instructions that will be bundled together and sent as one transaction + * to byzcoin. + * @param src the source account to fetch coins from. + * @param amount how many coins to put on the stack + */ +export function fetch(tx: TransactionBuilder, src: InstanceID, amount: Long) { + tx.invoke(src, contractID, commandFetch, + [new Argument({name: argumentCoins, value: Buffer.from(amount.toBytesLE())})]); +} + +/** + * Stores coins from the ByzCoin 'stack' in the given coin-account. Only the coins of the stack with the same + * name are added to the destination account. + * + * @param tx used to collect one or more instructions that will be bundled together and sent as one transaction + * to byzcoin. + * @param dst where the coins to store + */ +export function store(tx: TransactionBuilder, dst: InstanceID) { + tx.invoke(dst, contractID, commandStore, []); +} + +function rule(command: string): string { + return `invoke:${contractID}.${command}`; +} diff --git a/external/js/cothority/src/v2/byzcoin/contracts/coinInst.ts b/external/js/cothority/src/v2/byzcoin/contracts/coinInst.ts new file mode 100644 index 0000000000..df144bc3cf --- /dev/null +++ b/external/js/cothority/src/v2/byzcoin/contracts/coinInst.ts @@ -0,0 +1,103 @@ +import * as Long from "long"; +import { BehaviorSubject } from "rxjs"; +import { map } from "rxjs/operators"; + +import { Argument, ByzCoinRPC, Instance as BCInstance, InstanceID } from "../../../byzcoin"; +import { Coin } from "../../../byzcoin/contracts"; +import Log from "../../../log"; + +import { TransactionBuilder } from ".."; +import { ObservableToBS } from ".."; +import { CoinContract } from "./"; + +/** + * CoinStruct merges a Coin structure with an instance. + */ +export class CoinStruct extends Coin { + constructor(readonly inst: BCInstance) { + super(Coin.decode(inst.data)); + } +} + +/** + * CoinBS represents a coin with the new interface. Instead of relying on a synchronous interface, + * this implementation allows for a more RxJS-style interface. + */ +export class CoinInst extends BehaviorSubject { + + /** + * Retrieves a coinInstance from ByzCoin and returns a BehaviorSubject that updates whenever the + * coin changes. + * + * @param bc of an initialized ByzCoinRPC instance + * @param coinID of an existing coin instance + * @return a BehaviorSubject pointing to a coinInstance that updates automatically + */ + static async retrieve(bc: ByzCoinRPC, coinID: InstanceID): + Promise { + Log.lvl3("getting coinBS"); + const coinObs = (await bc.instanceObservable(coinID)).pipe( + map((proof) => new CoinStruct(BCInstance.fromProof(coinID, proof))), + ); + return new CoinInst(await ObservableToBS(coinObs)); + } + + readonly id: InstanceID; + + constructor(coin: BehaviorSubject) { + super(coin.getValue()); + coin.subscribe(this); + this.id = coin.getValue().inst.id; + } + + /** + * Creates an instruction to transfer coins to another account. + * + * @param tx used to collect one or more instructions that will be bundled together and sent as one transaction + * to byzcoin. + * @param dest the destination account to store the coins in. The destination must exist! + * @param amount how many coins to transfer. + */ + transfer(tx: TransactionBuilder, dest: InstanceID, amount: Long) { + CoinContract.transfer(tx, this.getValue().inst.id, dest, amount); + } + + /** + * Mints coins on ByzCoin. For this to work, the DARC governing this coin instance needs to have a + * 'invoke.Coin.mint' rule and the instruction will need to be signed by the appropriate identity. + * + * @param tx used to collect one or more instructions + * @param amount positive, non-zero value to mint on this coin + * @return the coin as it will be created if the transaction is accepted - warning: other instructions in this + * transaction might change the value of the coin. + */ + mint(tx: TransactionBuilder, amount: Long): Coin { + const ci = this.getValue(); + CoinContract.mint(tx, ci.inst.id, amount); + return new Coin({name: ci.name, value: ci.value.add(amount)}); + } + + /** + * Fetches coins from a coinInstance and puts it on the ByzCoin 'stack' for use by the next instruction. + * Unused coins are discarded by all nodes and thus lost. + * + * @param tx used to collect one or more instructions that will be bundled together and sent as one transaction + * to byzcoin. + * @param amount how many coins to put on the stack + */ + fetch(tx: TransactionBuilder, amount: Long) { + tx.invoke(this.getValue().inst.id, CoinContract.contractID, CoinContract.commandFetch, + [new Argument({name: CoinContract.argumentCoins, value: Buffer.from(amount.toBytesLE())})]); + } + + /** + * Stores coins from the ByzCoin 'stack' in the given coin-account. Only the coins of the stack with the same + * name are added to the destination account. + * + * @param tx used to collect one or more instructions that will be bundled together and sent as one transaction + * to byzcoin. + */ + store(tx: TransactionBuilder) { + tx.invoke(this.getValue().inst.id, CoinContract.contractID, CoinContract.commandStore, []); + } +} diff --git a/external/js/cothority/src/v2/byzcoin/contracts/darcContract.ts b/external/js/cothority/src/v2/byzcoin/contracts/darcContract.ts new file mode 100644 index 0000000000..20fa6c91f6 --- /dev/null +++ b/external/js/cothority/src/v2/byzcoin/contracts/darcContract.ts @@ -0,0 +1,74 @@ +import { randomBytes } from "crypto-browserify"; + +import { Argument, InstanceID } from "../../../byzcoin"; +import { Darc } from "../../../darc"; + +import { TransactionBuilder } from "../transactionBuilder"; + +import { CoinContract } from "./"; +import { IDarcAttr } from "./darcInsts"; + +/** + * DarcContract represents a darc taken from an instance. It has all necessary constants to interact with a darc + * contract on byzcoin. + */ +export const contractID = "darc"; +export const commandEvolve = "evolve"; +export const commandEvolveUnrestricted = "evolve_unrestricted"; +export const argumentDarc = "darc"; +export const ruleSign = Darc.ruleSign; +export const ruleEvolve = "invoke:" + contractID + "." + commandEvolve; +export const ruleEvolveUnrestricted = "invoke:" + contractID + "." + + commandEvolveUnrestricted; + +/** + * Creates an instruction in the transaction with either an update of the description and/or an update + * of the rules. + * + * @param tx where the instruction will be appended to + * @param oldDarc that needs to be evolved + * @param updates contains a description update and/or rules to be merged + * @param unrestricted if true, will create an unrestricted evolve that allows to create new rules + * @return the new DARC as it will appear on ByzCoin if the transaction is accepted + */ +export function evolve(tx: TransactionBuilder, oldDarc: Darc, updates: IDarcAttr, unrestricted = false): Darc { + const newArgs = {...oldDarc.evolve(), ...updates}; + const newDarc = new Darc(newArgs); + const cmd = unrestricted ? commandEvolveUnrestricted : commandEvolve; + const args = [new Argument({ + name: argumentDarc, + value: Buffer.from(Darc.encode(newDarc).finish()), + })]; + tx.invoke(newDarc.getBaseID(), contractID, cmd, args); + return newDarc; +} + +/** + * Creates the instruction necessary to spawn a new coin using this darc. + * + * @param tx where the instruction will be appended to + * @param did baseID of the darc that can spawn coins + * @param name of the coin + * @param preHash used to calculate the ID of the coin, if given + */ +export function spawnCoin(tx: TransactionBuilder, did: InstanceID, name: Buffer, preHash?: Buffer): InstanceID { + if (preHash === undefined) { + preHash = randomBytes(32); + } + const args = [new Argument({name: CoinContract.argumentType, value: name}), + new Argument({name: CoinContract.argumentCoinID, value: preHash})]; + tx.spawn(did, CoinContract.contractID, args); + return CoinContract.coinIID(preHash); +} + +/** + * Spawns a new darc. The darc given by did must have a `spawn:darc` rule. + * + * @param tx where the instruction will be appended to + * @param did baseID of the darc that can spawn other darcs + * @param newDarc to be spawned + */ +export function spawnDarc(tx: TransactionBuilder, did: InstanceID, newDarc: Darc) { + tx.spawn(did, contractID, + [new Argument({name: argumentDarc, value: newDarc.toBytes()})]); +} diff --git a/external/js/cothority/src/v2/byzcoin/contracts/darcInsts.ts b/external/js/cothority/src/v2/byzcoin/contracts/darcInsts.ts new file mode 100644 index 0000000000..bdb0bb6975 --- /dev/null +++ b/external/js/cothority/src/v2/byzcoin/contracts/darcInsts.ts @@ -0,0 +1,199 @@ +import { BehaviorSubject } from "rxjs"; +import { flatMap, map } from "rxjs/operators"; + +import { Argument, ByzCoinRPC, Instance, InstanceID } from "../../../byzcoin"; +import { Darc, IIdentity, Rule, Rules } from "../../../darc"; +import IdentityDarc from "../../../darc/identity-darc"; +import Log from "../../../log"; + +import { TransactionBuilder } from ".."; +import { ObservableToBS } from ".."; + +import { DarcContract } from "./"; + +/** + * Used in DarcBS.evolve for chosing which parts of the DARC to evolve. + */ +export interface IDarcAttr { + description?: Buffer; + rules?: Rules; +} + +/** + * Update rules given an action and an identity. If it's an InstanceID, it will + * be interpreted as darc:instanceID. + */ +export type IRule = [string, IIdentity | InstanceID]; + +/** + * DarcStruct holds a darc together with the corresponding instance. This instance can be used to + * get the version. + */ +export class DarcStruct extends Darc { + constructor(readonly inst: Instance) { + super(Darc.decode(inst.data)); + } +} + +/** + * Holds a list of DARCs that will be updated individually, and whenever the list changes. + */ +export class DarcInsts extends BehaviorSubject { + + /** + * Retrieves an eventually changing list of darcs from ByzCoin. + * + * @param bc of an initialized ByzCoinRPC instance + * @param idsBS + */ + static async retrieve(bc: ByzCoinRPC, idsBS: BehaviorSubject | InstanceID[]): Promise { + Log.lvl3("getting darcsBS"); + if (idsBS instanceof Array) { + idsBS = new BehaviorSubject(idsBS); + } + const darcs = await ObservableToBS(idsBS.pipe( + flatMap((ais) => Promise.all(ais + .map((iid) => DarcInst.retrieve(bc, iid)))), + map((dbs) => dbs.filter((db) => db !== undefined)), + )); + return new DarcInsts(darcs); + } + + constructor(sbs: BehaviorSubject) { + super(sbs.getValue()); + sbs.subscribe(this); + } +} + +/** + * A DarcBS class represents a darc on byzcoin. It has methods to modify the darc by + * adding and removing rules, as well as to change the description. + */ +export class DarcInst extends BehaviorSubject { + + /** + * Retrieves a DarcBS from ByzCoin given an InstanceID. + * + * @param bc of an initialized ByzCoinRPC instance + * @param darcID a fixed InstanceID representing the baseID of the darc to retrieve + * @return a DarcBS or undefined if something went wrong (no Darc at that ID) + */ + static async retrieve(bc: ByzCoinRPC, darcID: InstanceID): + Promise { + Log.lvl3("getting darcBS"); + const instObs = (await bc.instanceObservable(darcID)).pipe( + map((proof) => (proof && proof.value && proof.value.length > 0) ? + new DarcStruct(Instance.fromProof(darcID, proof)) : undefined), + ); + const bsDarc = await ObservableToBS(instObs); + if (bsDarc.getValue() === undefined) { + throw new Error("this darc doesn't exist"); + } + return new DarcInst(bsDarc); + } + + constructor(darc: BehaviorSubject) { + super(darc.getValue()); + darc.subscribe(this); + } + + /** + * Creates an instruction in the transaction with either an update of the description and/or an update + * of the rules. + * + * @param tx where the instruction will be appended to + * @param updates contains a description update and/or rules to be merged + * @param unrestricted if true, will create an unrestricted evolve that allows to create new rules + * @return the new DARC as it will appear on ByzCoin if the transaction is accepted + */ + evolve(tx: TransactionBuilder, updates: IDarcAttr, unrestricted = false): Darc { + return DarcContract.evolve(tx, this.getValue(), updates, unrestricted); + } + + /** + * Sets the description of the DARC. + * + * @param tx where the instruction will be appended to + * @param description of the new DARC + * @return the new DARC as it will appear on ByzCoin if the transaction is accepted + */ + setDescription(tx: TransactionBuilder, description: Buffer): Darc { + return this.evolve(tx, {description}); + } + + /** + * Creates a new darc by overwriting existing rules. + * A darc with a rule for `invoke:darc.evolve_unrestricted` can also accept new rules + * + * @param tx where the instruction will be appended to + * @param newRules is a map of action to expression + * @return the new DARC as it will appear on ByzCoin if the transaction is accepted + */ + setRules(tx: TransactionBuilder, ...newRules: IRule[]): Darc { + const rules = this.getValue().rules.clone(); + newRules.forEach(([action, expression]) => rules.setRule(action, toIId(expression))); + return this.evolve(tx, {rules}); + } + + /** + * Adds a sign and evolve element to the DARC with an OR expression + * + * @param tx where the instruction will be appended to + * @param addRules is a map of actions to identities that will be ORed with the existing expression. + * @return the new DARC as it will appear on ByzCoin if the transaction is accepted + */ + addToRules(tx: TransactionBuilder, ...addRules: IRule[]): Darc { + const rules = this.getValue().rules.clone(); + addRules.forEach(([action, expression]) => rules.appendToRule(action, toIId(expression), Rule.OR)); + return this.evolve(tx, {rules}); + } + + /** + * Removes an identity in the sign and/or evolve expression. The expressions need to be pure + * OR expressions, else this will fail. + * + * @param tx where the instruction will be appended to + * @param rmRules is a map of actions to identities that will be removed from existing rules. + * @return the new DARC as it will appear on ByzCoin if the transaction is accepted + */ + rmFromRules(tx: TransactionBuilder, ...rmRules: IRule[]): Darc { + const rules = this.getValue().rules.clone(); + for (const [action, expression] of rmRules) { + try { + rules.getRule(action).remove(toIId(expression).toString()); + } catch (e) { + Log.warn("while removing identity from ", action, ":", e); + } + } + return this.evolve(tx, {rules}); + } + + /** + * Creates the instruction necessary to spawn a new coin using this darc. + * + * @param tx where the instruction will be appended to + * @param name of the coin + * @param preHash used to calculate the ID of the coin, if given + */ + spawnCoin(tx: TransactionBuilder, name: Buffer, preHash?: Buffer): InstanceID { + return DarcContract.spawnCoin(tx, this.getValue().getBaseID(), name, preHash); + } + + /** + * Spawns a new darc. The current darc must have a `spawn:darc` rule. + * + * @param tx where the instruction will be appended to + * @param newDarc to be spawned + */ + spawnDarc(tx: TransactionBuilder, newDarc: Darc) { + tx.spawn(this.getValue().getBaseID(), DarcContract.contractID, + [new Argument({name: DarcContract.argumentDarc, value: newDarc.toBytes()})]); + } +} + +function toIId(id: IIdentity | InstanceID): IIdentity { + if (Buffer.isBuffer(id)) { + return new IdentityDarc({id}); + } + return id; +} diff --git a/external/js/cothority/src/v2/byzcoin/contracts/index.ts b/external/js/cothority/src/v2/byzcoin/contracts/index.ts new file mode 100644 index 0000000000..92078abf8e --- /dev/null +++ b/external/js/cothority/src/v2/byzcoin/contracts/index.ts @@ -0,0 +1,16 @@ +import * as CoinContract from "./coinContract"; +import { CoinInst, CoinStruct } from "./coinInst"; +import * as DarcContract from "./darcContract"; +import { DarcInst, DarcInsts, DarcStruct, IDarcAttr, IRule } from "./darcInsts"; + +export { + CoinStruct, + CoinInst, + CoinContract, + DarcInst, + DarcInsts, + DarcContract, + DarcStruct, + IDarcAttr, + IRule, +}; diff --git a/external/js/cothority/src/v2/byzcoin/index.ts b/external/js/cothority/src/v2/byzcoin/index.ts new file mode 100644 index 0000000000..748a13370a --- /dev/null +++ b/external/js/cothority/src/v2/byzcoin/index.ts @@ -0,0 +1,7 @@ +import { TransactionBuilder } from "./transactionBuilder"; +import { ObservableToBS } from "./util"; + +export { + ObservableToBS, + TransactionBuilder, +}; diff --git a/external/js/cothority/src/v2/byzcoin/transactionBuilder.ts b/external/js/cothority/src/v2/byzcoin/transactionBuilder.ts new file mode 100644 index 0000000000..e4462f93be --- /dev/null +++ b/external/js/cothority/src/v2/byzcoin/transactionBuilder.ts @@ -0,0 +1,143 @@ +import { Argument, ByzCoinRPC, ClientTransaction, InstanceID, Instruction } from "../../byzcoin"; +import { AddTxResponse } from "../../byzcoin/proto/requests"; +import ISigner from "../../darc/signer"; + +/** + * TransactionBuilder handles collecting multiple instructions and signing them all + * together before sending the transaction to the chain. + * There are convenience methods to create spawn, invoke, or delete instructions. + * Once all instructions are added, the send method will contact one or more nodes + * to submit the transaction. + * After a call to the send method, the transaction is ready for new instructions. + * + * If any of the instructions in this transaction fails, all other instructions will + * fail, too. + */ +export class TransactionBuilder { + private instructions: Instruction[] = []; + + constructor(protected bc: ByzCoinRPC) { + } + + /** + * Signs all instructions and sends them to the nodes. + * The `instructions` field is only emptied if the transaction has been accepted successfully. + * If the transaction fails, it can be retried. + * + * @param signers one set of signers per instruction. If there is only one set for multiple + * instructions, always the same set of signers will be used. + * @param wait if 0, doesn't wait for inclusion. If > 0, waits for inclusion for this many blockIntervals. + */ + async send(signers: ISigner[][], wait = 0): Promise<[ClientTransaction, AddTxResponse]> { + const ctx = ClientTransaction.make(this.bc.getProtocolVersion(), ...this.instructions); + await ctx.updateCountersAndSign(this.bc, signers); + const response = await this.bc.sendTransactionAndWait(ctx, wait); + this.instructions = []; + return [ctx, response]; + } + + /** + * @return true if one or more instructions are available. + */ + hasInstructions(): boolean { + return this.instructions.length > 0; + } + + /** + * Appends a new instruction. + * + * @param inst the new instruction to append + * @return the appended instruction + */ + append(inst: Instruction): Instruction { + this.instructions.push(inst); + return inst; + } + + /** + * Prepends a new instruction + * + * @param inst the instruction to prepend + * @return the prepended instruction + */ + prepend(inst: Instruction): Instruction { + this.instructions.unshift(inst); + return inst; + } + + /** + * Appends a spawn instruction. + * + * @param iid the instance ID where the instruction is sent to + * @param contractID the contractID to spawn + * @param args arguments of the contract + * @return new instruction - can be used for deriveID + */ + spawn(iid: Buffer, contractID: string, args: Argument[]): Instruction { + return this.append(Instruction.createSpawn(iid, contractID, args)); + } + + /** + * Appends an invoke instruction. + * + * @param iid the instance ID where the instruction is sent to + * @param contractID the contractID to invoke + * @param command to be invoked on the contract + * @param args arguments of the command + * @return new instruction - can be used for deriveID + */ + invoke(iid: Buffer, contractID: string, command: string, args: Argument[]): Instruction { + return this.append(Instruction.createInvoke(iid, contractID, command, args)); + } + + /** + * Appends a delete instruction. + * + * @param iid the instance ID where the instruction is sent to + * @param contractID to be deleted - must match the actual contract + * @return new instruction - can be used for deriveID + */ + delete(iid: Buffer, contractID: string): Instruction { + return this.append(Instruction.createDelete(iid, contractID)); + } + + /** + * Returns the ID that will be produced by the given instruction + * + * @param index of the instruction + * @param name if given, will be passed to deriveID + */ + deriveID(index: number, name = ""): InstanceID { + if (index < 0 || index > this.instructions.length) { + throw new Error("instruction out of bound"); + } + return this.instructions[index].deriveId(name); + } + + /** + * returns a useful and readable representation of all instructions in this transaction. + */ + toString(): string { + return this.instructions.map((inst, i) => { + const t = ["Spawn", "Invoke", "Delete"][inst.type]; + let cid: string; + let args: Argument[]; + switch (inst.type) { + case Instruction.typeSpawn: + cid = inst.spawn.contractID; + args = inst.spawn.args; + break; + case Instruction.typeInvoke: + cid = `${inst.invoke.contractID} / ${inst.invoke.command}`; + args = inst.invoke.args; + break; + case Instruction.typeDelete: + cid = inst.delete.contractID; + args = []; + break; + } + return `${i}: ${t} ${cid}: ${inst.instanceID.toString("hex")}\n\t` + + args.map((kv) => `${kv.name}: ${kv.value.toString("hex")}`).join("\n\t"); + }).join("\n\n"); + } +} diff --git a/external/js/cothority/src/v2/byzcoin/util.ts b/external/js/cothority/src/v2/byzcoin/util.ts new file mode 100644 index 0000000000..cca1f22baa --- /dev/null +++ b/external/js/cothority/src/v2/byzcoin/util.ts @@ -0,0 +1,15 @@ +import { BehaviorSubject, Observable } from "rxjs"; + +export async function ObservableToBS(src: Observable): Promise> { + return new Promise((resolve) => { + let bs: BehaviorSubject; + src.subscribe((next) => { + if (bs === undefined) { + bs = new BehaviorSubject(next); + resolve(bs); + } else { + bs.next(next); + } + }); + }); +} diff --git a/external/js/cothority/tsconfig.json b/external/js/cothority/tsconfig.json index 32594c2a45..cea2d5843e 100644 --- a/external/js/cothority/tsconfig.json +++ b/external/js/cothority/tsconfig.json @@ -10,6 +10,7 @@ "esModuleInterop": true, "resolveJsonModule": true, "importHelpers": true, + "baseUrl": "./", "typeRoots": [ "../types", "./node_modules/@types"