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..eba6863e0a --- /dev/null +++ b/external/js/cothority/spec/helpers/bctest.ts @@ -0,0 +1,79 @@ +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 { + // Wait for 1s in case a transaction from a previous test is still being accepted. + 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/byzcoin/client-transaction.ts b/external/js/cothority/src/byzcoin/client-transaction.ts index 661ba7d69a..07a0fdaaab 100644 --- a/external/js/cothority/src/byzcoin/client-transaction.ts +++ b/external/js/cothority/src/byzcoin/client-transaction.ts @@ -116,6 +116,8 @@ export default class ClientTransaction extends Message { } } +type InstructionType = 0 | 1 | 2; + /** * An instruction represents one action */ @@ -128,7 +130,7 @@ export class Instruction extends Message { * Get the type of the instruction * @returns the type as a number */ - get type(): number { + get type(): InstructionType { if (this.spawn) { return Instruction.typeSpawn; } 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"